- Improved "top nav" UX

This commit is contained in:
Mark Friedrich
2019-08-03 16:16:22 +02:00
parent e32bc21445
commit a69c9f78f1
16 changed files with 440 additions and 180 deletions

View File

@@ -314,7 +314,6 @@ define([
}
});
});
}
});
};
@@ -406,13 +405,13 @@ define([
* init scrollSpy for navigation bar
*/
let initScrollSpy = () => {
// init scrollspy
let scrollElement = window;
let timeout;
// show elements that are currently in the viewport
let showVisibleElements = () => {
// find all elements that should be animated
let visibleElements = $('.' + config.animateElementClass).isInViewport();
let visibleElements = Util.findInViewport($('.' + config.animateElementClass));
$(visibleElements).removeClass( config.animateElementClass );
$(visibleElements).velocity('transition.flipXIn', {
@@ -431,16 +430,23 @@ define([
});
};
$( window ).scroll(() => {
// check for new visible elements
showVisibleElements();
});
let scrollHandler = () => {
// If there's a timer, cancel it
if(timeout){
window.cancelAnimationFrame(timeout);
}
timeout = window.requestAnimationFrame(showVisibleElements);
};
scrollElement.addEventListener('scroll', scrollHandler, false);
// initial check for visible elements
showVisibleElements();
// event listener for navigation links
Util.initPageScroll('#' + config.navigationElementId);
Util.initScrollSpy(document.getElementById(config.navigationElementId), scrollElement, {
offset: 150
});
};
/**

View File

@@ -13,6 +13,10 @@ define([
let config = {
splashOverlayClass: 'pf-splash', // class for "splash" overlay
// navigation
navigationElementId: 'pf-navbar', // id for navbar element
webSocketStatsId: 'pf-setup-webSocket-stats', // id for webSocket "stats" panel
webSocketRefreshStatsId: 'pf-setup-webSocket-stats-refresh' // class for "reload stats" button
};
@@ -64,7 +68,9 @@ define([
let body = $('body');
// navigation (scroll) ----------------------------------------------------------------------------------------
Util.initPageScroll(body);
Util.initScrollSpy(document.getElementById(config.navigationElementId), window, {
offset: 300
});
// collapse ---------------------------------------------------------------------------------------------------
setCollapseObserver(body, '[data-toggle="collapse"]');
@@ -188,8 +194,8 @@ define([
* @param data
*/
let updateWebSocketPanel = (data) => {
let badgeSocketWarning = $('.navbar a[data-anchor="#pf-setup-socket"] .txt-color-warning');
let badgeSocketDanger = $('.navbar a[data-anchor="#pf-setup-socket"] .txt-color-danger');
let badgeSocketWarning = $('.navbar a[data-target="pf-setup-socket"] .txt-color-warning');
let badgeSocketDanger = $('.navbar a[data-target="pf-setup-socket"] .txt-color-danger');
let socketWarningCount = parseInt(badgeSocketWarning.text()) || 0;
let socketDangerCount = parseInt(badgeSocketDanger.text()) || 0;

View File

@@ -26,7 +26,6 @@ define([
$.fn.showMapManual = function(){
requirejs(['text!templates/dialog/map_manual.html', 'mustache'], (template, Mustache) => {
let data = {
dialogNavigationClass: config.dialogNavigationClass,
dialogNavLiClass: config.dialogNavigationListItemClass,
@@ -67,8 +66,7 @@ define([
let scrollspyElement = $('#' + config.mapManualScrollspyId);
let whileScrolling = function(){
let whileScrolling = () => {
if(disableOnScrollEvent === false){
for(let i = 0; i < scrollBreakpointElements.length; i++){
let offset = $(scrollBreakpointElements[i]).offset().top;
@@ -124,11 +122,9 @@ define([
let mainNavigationLiElement = $(this).parent('.' + config.dialogNavigationListItemClass);
whileScrolling();
// if link is a main navigation link (not an anchor link)
if(mainNavigationLiElement.length > 0){
// remove all active classes
scrollNavLiElements.removeClass('active');
@@ -138,7 +134,6 @@ define([
}
});
},
onScroll: function(){
disableOnScrollEvent = false;

View File

@@ -398,40 +398,6 @@ define([
return valid;
};
/**
* check multiple element if they are currently visible in viewport
* @returns {Array}
*/
$.fn.isInViewport = function(){
let visibleElement = [];
this.each(function(){
let element = $(this)[0];
let top = element.offsetTop;
let left = element.offsetLeft;
let width = element.offsetWidth;
let height = element.offsetHeight;
while(element.offsetParent){
element = element.offsetParent;
top += element.offsetTop;
left += element.offsetLeft;
}
if(
top < (window.pageYOffset + window.innerHeight) &&
left < (window.pageXOffset + window.innerWidth) &&
(top + height) > window.pageYOffset &&
(left + width) > window.pageXOffset
){
visibleElement.push(this);
}
});
return visibleElement;
};
/**
* init the map-update-counter as "easy-pie-chart"
*/
@@ -1024,17 +990,172 @@ define([
};
/**
*
* @param element
* filter elements from elements array that are not within viewport
* @param elements
* @returns {[]}
*/
let initPageScroll = (element) => {
$(element).on('click', '.page-scroll', function(){
// scroll to ancor element
$($(this).attr('data-anchor')).velocity('scroll', {
duration: 300,
easing: 'swing'
});
});
let findInViewport = elements => {
let visibleElement = [];
for(let element of elements){
if(!(element instanceof HTMLElement)){
console.warn('findInViewport() expects Array() of %O; %o given', HTMLElement, element);
continue;
}
let top = element.offsetTop;
let left = element.offsetLeft;
let width = element.offsetWidth;
let height = element.offsetHeight;
let origElement = element;
while(element.offsetParent){
element = element.offsetParent;
top += element.offsetTop;
left += element.offsetLeft;
}
if(
top < (window.pageYOffset + window.innerHeight) &&
left < (window.pageXOffset + window.innerWidth) &&
(top + height) > window.pageYOffset &&
(left + width) > window.pageXOffset
){
visibleElement.push(origElement);
}
}
return visibleElement;
};
/**
* "Scroll Spy" implementation
* @see https://github.com/cferdinandi/gumshoe/blob/master/src/js/gumshoe/gumshoe.js
* @param navElement
* @param scrollElement
* @param settings
*/
let initScrollSpy = (navElement, scrollElement = window, settings = {}) => {
let timeout, current;
let contents = Array.from(navElement.querySelectorAll('.page-scroll')).map(link => ({
link: link,
content: document.getElementById(link.getAttribute('data-target'))
}));
let getOffset = settings => {
if(typeof settings.offset === 'function'){
return parseFloat(settings.offset());
}
// Otherwise, return it as-is
return parseFloat(settings.offset);
};
let getDocumentHeight = () => {
return Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight
);
};
let activate = item => {
if(!item) return;
// Get the parent list item
let li = item.link.closest('li');
if(!li) return;
// Add the active class to li
li.classList.add('active');
};
let deactivate = item => {
if(!item) return;
// remove focus
if(document.activeElement === item.link){
document.activeElement.blur();
}
// Get the parent list item
let li = item.link.closest('li');
if(!li) return;
// Remove the active class from li
li.classList.remove('active');
};
let isInView = (elem, settings, bottom) => {
let bounds = elem.getBoundingClientRect();
let offset = getOffset(settings);
if(bottom){
return parseInt(bounds.bottom, 10) < (window.innerHeight || document.documentElement.clientHeight);
}
return parseInt(bounds.top, 10) <= offset;
};
let isAtBottom = () => {
return window.innerHeight + window.pageYOffset >= getDocumentHeight();
};
let useLastItem = (item, settings) => {
return !!(isAtBottom() && isInView(item.content, settings, true));
};
let getActive = (contents, settings) => {
let last = contents[contents.length - 1];
if(useLastItem(last, settings)) return last;
for(let i = contents.length - 1; i >= 0; i--){
if(isInView(contents[i].content, settings)) return contents[i];
}
};
let detect = () => {
let active = getActive(contents, settings);
// if there's no active content, deactivate and bail
if(!active){
if(current){
deactivate(current);
current = null;
}
return;
}
// If the active content is the one currently active, do nothing
if (current && active.content === current.content) return;
// Deactivate the current content and activate the new content
deactivate(current);
activate(active);
// Update the currently active content
current = active;
};
let scrollHandler = () => {
// If there's a timer, cancel it
if(timeout){
window.cancelAnimationFrame(timeout);
}
timeout = window.requestAnimationFrame(detect);
};
// Find the currently active content
detect();
scrollElement.addEventListener('scroll', scrollHandler, false);
// set click observer for links
let clickHandler = function(e){
e.preventDefault();
this.content.scrollIntoView({behavior: 'smooth'});
};
for(let item of contents){
$(item.link).on('click', clickHandler.bind(item));
}
};
/**
@@ -3287,7 +3408,8 @@ define([
getCurrentLocationData: getCurrentLocationData,
getCurrentUserInfo: getCurrentUserInfo,
getCurrentCharacterLog: getCurrentCharacterLog,
initPageScroll: initPageScroll,
findInViewport: findInViewport,
initScrollSpy: initScrollSpy,
convertXEditableOptionsToSelect2: convertXEditableOptionsToSelect2,
flattenXEditableSelectArray: flattenXEditableSelectArray,
getCharacterDataBySystemId: getCharacterDataBySystemId,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -314,7 +314,6 @@ define([
}
});
});
}
});
};
@@ -406,13 +405,13 @@ define([
* init scrollSpy for navigation bar
*/
let initScrollSpy = () => {
// init scrollspy
let scrollElement = window;
let timeout;
// show elements that are currently in the viewport
let showVisibleElements = () => {
// find all elements that should be animated
let visibleElements = $('.' + config.animateElementClass).isInViewport();
let visibleElements = Util.findInViewport($('.' + config.animateElementClass));
$(visibleElements).removeClass( config.animateElementClass );
$(visibleElements).velocity('transition.flipXIn', {
@@ -431,16 +430,23 @@ define([
});
};
$( window ).scroll(() => {
// check for new visible elements
showVisibleElements();
});
let scrollHandler = () => {
// If there's a timer, cancel it
if(timeout){
window.cancelAnimationFrame(timeout);
}
timeout = window.requestAnimationFrame(showVisibleElements);
};
scrollElement.addEventListener('scroll', scrollHandler, false);
// initial check for visible elements
showVisibleElements();
// event listener for navigation links
Util.initPageScroll('#' + config.navigationElementId);
Util.initScrollSpy(document.getElementById(config.navigationElementId), scrollElement, {
offset: 150
});
};
/**

View File

@@ -13,6 +13,10 @@ define([
let config = {
splashOverlayClass: 'pf-splash', // class for "splash" overlay
// navigation
navigationElementId: 'pf-navbar', // id for navbar element
webSocketStatsId: 'pf-setup-webSocket-stats', // id for webSocket "stats" panel
webSocketRefreshStatsId: 'pf-setup-webSocket-stats-refresh' // class for "reload stats" button
};
@@ -64,7 +68,9 @@ define([
let body = $('body');
// navigation (scroll) ----------------------------------------------------------------------------------------
Util.initPageScroll(body);
Util.initScrollSpy(document.getElementById(config.navigationElementId), window, {
offset: 300
});
// collapse ---------------------------------------------------------------------------------------------------
setCollapseObserver(body, '[data-toggle="collapse"]');
@@ -188,8 +194,8 @@ define([
* @param data
*/
let updateWebSocketPanel = (data) => {
let badgeSocketWarning = $('.navbar a[data-anchor="#pf-setup-socket"] .txt-color-warning');
let badgeSocketDanger = $('.navbar a[data-anchor="#pf-setup-socket"] .txt-color-danger');
let badgeSocketWarning = $('.navbar a[data-target="pf-setup-socket"] .txt-color-warning');
let badgeSocketDanger = $('.navbar a[data-target="pf-setup-socket"] .txt-color-danger');
let socketWarningCount = parseInt(badgeSocketWarning.text()) || 0;
let socketDangerCount = parseInt(badgeSocketDanger.text()) || 0;

View File

@@ -26,7 +26,6 @@ define([
$.fn.showMapManual = function(){
requirejs(['text!templates/dialog/map_manual.html', 'mustache'], (template, Mustache) => {
let data = {
dialogNavigationClass: config.dialogNavigationClass,
dialogNavLiClass: config.dialogNavigationListItemClass,
@@ -67,8 +66,7 @@ define([
let scrollspyElement = $('#' + config.mapManualScrollspyId);
let whileScrolling = function(){
let whileScrolling = () => {
if(disableOnScrollEvent === false){
for(let i = 0; i < scrollBreakpointElements.length; i++){
let offset = $(scrollBreakpointElements[i]).offset().top;
@@ -124,11 +122,9 @@ define([
let mainNavigationLiElement = $(this).parent('.' + config.dialogNavigationListItemClass);
whileScrolling();
// if link is a main navigation link (not an anchor link)
if(mainNavigationLiElement.length > 0){
// remove all active classes
scrollNavLiElements.removeClass('active');
@@ -138,7 +134,6 @@ define([
}
});
},
onScroll: function(){
disableOnScrollEvent = false;

View File

@@ -398,40 +398,6 @@ define([
return valid;
};
/**
* check multiple element if they are currently visible in viewport
* @returns {Array}
*/
$.fn.isInViewport = function(){
let visibleElement = [];
this.each(function(){
let element = $(this)[0];
let top = element.offsetTop;
let left = element.offsetLeft;
let width = element.offsetWidth;
let height = element.offsetHeight;
while(element.offsetParent){
element = element.offsetParent;
top += element.offsetTop;
left += element.offsetLeft;
}
if(
top < (window.pageYOffset + window.innerHeight) &&
left < (window.pageXOffset + window.innerWidth) &&
(top + height) > window.pageYOffset &&
(left + width) > window.pageXOffset
){
visibleElement.push(this);
}
});
return visibleElement;
};
/**
* init the map-update-counter as "easy-pie-chart"
*/
@@ -1024,17 +990,172 @@ define([
};
/**
*
* @param element
* filter elements from elements array that are not within viewport
* @param elements
* @returns {[]}
*/
let initPageScroll = (element) => {
$(element).on('click', '.page-scroll', function(){
// scroll to ancor element
$($(this).attr('data-anchor')).velocity('scroll', {
duration: 300,
easing: 'swing'
});
});
let findInViewport = elements => {
let visibleElement = [];
for(let element of elements){
if(!(element instanceof HTMLElement)){
console.warn('findInViewport() expects Array() of %O; %o given', HTMLElement, element);
continue;
}
let top = element.offsetTop;
let left = element.offsetLeft;
let width = element.offsetWidth;
let height = element.offsetHeight;
let origElement = element;
while(element.offsetParent){
element = element.offsetParent;
top += element.offsetTop;
left += element.offsetLeft;
}
if(
top < (window.pageYOffset + window.innerHeight) &&
left < (window.pageXOffset + window.innerWidth) &&
(top + height) > window.pageYOffset &&
(left + width) > window.pageXOffset
){
visibleElement.push(origElement);
}
}
return visibleElement;
};
/**
* "Scroll Spy" implementation
* @see https://github.com/cferdinandi/gumshoe/blob/master/src/js/gumshoe/gumshoe.js
* @param navElement
* @param scrollElement
* @param settings
*/
let initScrollSpy = (navElement, scrollElement = window, settings = {}) => {
let timeout, current;
let contents = Array.from(navElement.querySelectorAll('.page-scroll')).map(link => ({
link: link,
content: document.getElementById(link.getAttribute('data-target'))
}));
let getOffset = settings => {
if(typeof settings.offset === 'function'){
return parseFloat(settings.offset());
}
// Otherwise, return it as-is
return parseFloat(settings.offset);
};
let getDocumentHeight = () => {
return Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight
);
};
let activate = item => {
if(!item) return;
// Get the parent list item
let li = item.link.closest('li');
if(!li) return;
// Add the active class to li
li.classList.add('active');
};
let deactivate = item => {
if(!item) return;
// remove focus
if(document.activeElement === item.link){
document.activeElement.blur();
}
// Get the parent list item
let li = item.link.closest('li');
if(!li) return;
// Remove the active class from li
li.classList.remove('active');
};
let isInView = (elem, settings, bottom) => {
let bounds = elem.getBoundingClientRect();
let offset = getOffset(settings);
if(bottom){
return parseInt(bounds.bottom, 10) < (window.innerHeight || document.documentElement.clientHeight);
}
return parseInt(bounds.top, 10) <= offset;
};
let isAtBottom = () => {
return window.innerHeight + window.pageYOffset >= getDocumentHeight();
};
let useLastItem = (item, settings) => {
return !!(isAtBottom() && isInView(item.content, settings, true));
};
let getActive = (contents, settings) => {
let last = contents[contents.length - 1];
if(useLastItem(last, settings)) return last;
for(let i = contents.length - 1; i >= 0; i--){
if(isInView(contents[i].content, settings)) return contents[i];
}
};
let detect = () => {
let active = getActive(contents, settings);
// if there's no active content, deactivate and bail
if(!active){
if(current){
deactivate(current);
current = null;
}
return;
}
// If the active content is the one currently active, do nothing
if (current && active.content === current.content) return;
// Deactivate the current content and activate the new content
deactivate(current);
activate(active);
// Update the currently active content
current = active;
};
let scrollHandler = () => {
// If there's a timer, cancel it
if(timeout){
window.cancelAnimationFrame(timeout);
}
timeout = window.requestAnimationFrame(detect);
};
// Find the currently active content
detect();
scrollElement.addEventListener('scroll', scrollHandler, false);
// set click observer for links
let clickHandler = function(e){
e.preventDefault();
this.content.scrollIntoView({behavior: 'smooth'});
};
for(let item of contents){
$(item.link).on('click', clickHandler.bind(item));
}
};
/**
@@ -3287,7 +3408,8 @@ define([
getCurrentLocationData: getCurrentLocationData,
getCurrentUserInfo: getCurrentUserInfo,
getCurrentCharacterLog: getCurrentCharacterLog,
initPageScroll: initPageScroll,
findInViewport: findInViewport,
initScrollSpy: initScrollSpy,
convertXEditableOptionsToSelect2: convertXEditableOptionsToSelect2,
flattenXEditableSelectArray: flattenXEditableSelectArray,
getCharacterDataBySystemId: getCharacterDataBySystemId,

View File

@@ -22,23 +22,23 @@
</p>
<ul class="nav navbar-nav navbar-right" role="tablist">
<li class="hide-before"><a class="pf-navbar-manual" href="#">Manual</a></li>
<li><a class="pf-navbar-manual" href="#">Manual</a></li>
</ul>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right" role="tablist">
<li class="hide-before"><a class="page-scroll" data-anchor="#pf-landing-top" href="#">Login</a></li>
<li><a class="page-scroll" data-target="pf-landing-top" href="#pf-landing-top">Login</a></li>
<check if="{{ @PATHFINDER.SHOW_COMPLETE_LOGIN_PAGE}}">
<li class="hide-before"> <a class="page-scroll" data-anchor="#pf-landing-gallery" href="#">Features</a></li>
<li> <a class="page-scroll" data-target="pf-landing-gallery" href="#pf-landing-gallery-thumb-container">Features</a></li>
</check>
<li class="hide-before"> <a class="page-scroll" data-anchor="#pf-landing-maps" href="#">Maps</a></li>
<li> <a class="page-scroll" data-target="pf-landing-maps" href="#pf-landing-maps">Maps</a></li>
<check if="{{ @PATHFINDER.SHOW_COMPLETE_LOGIN_PAGE}}">
<li class="hide-before"> <a class="page-scroll" data-anchor="#pf-landing-admin" href="#">Admin</a></li>
<li class="hide-before"> <a class="page-scroll" data-anchor="#pf-landing-install" href="#">Install</a></li>
<li class="hide-before"> <a class="page-scroll" data-anchor="#pf-landing-about" href="#">About</a></li>
<li> <a class="page-scroll" data-target="pf-landing-admin" href="#pf-landing-admin">Admin</a></li>
<li> <a class="page-scroll" data-target="pf-landing-install" href="#pf-landing-install">Install</a></li>
<li> <a class="page-scroll" data-target="pf-landing-about" href="#pf-landing-about">About</a></li>
</check>
<li class="hide-before"> <a class="page-scroll" data-anchor="#pf-landing-faq" href="#">FAQ</a></li>
<li class="hide-before"> <a class="pf-navbar-license" href="#">License</a></li>
<li> <a class="page-scroll" data-target="pf-landing-faq" href="#pf-landing-faq">FAQ</a></li>
<li> <a class="pf-navbar-license" href="#">License</a></li>
</ul>
</div>
</div>

View File

@@ -1297,8 +1297,8 @@
<div class="navbar-collapse">
<ul class="nav navbar-nav navbar-right" role="tablist">
<repeat group="{{ @tplNavigation }}" key="{{ @navKey }}" value="{{ @navConfig }}">
<li class="hide-before">
<a class="page-scroll" data-anchor="#pf-setup-{{ @navKey }}" href="#"><i class="fas fa-fw {{ @navConfig.icon }}"></i>&nbsp;{{ lcfirst(@navKey) }}
<li>
<a class="page-scroll" data-target="pf-setup-{{ @navKey }}" href="#pf-setup-{{ @navKey }}"><i class="fas fa-fw {{ @navConfig.icon }}"></i>&nbsp;{{ lcfirst(@navKey) }}
<span class="badge txt-color txt-color-warning" title="warning" data-placement="bottom">{{ @tplCounter('get', @navKey . '_warning') }}</span>
<span class="badge txt-color txt-color-danger" title="error" data-placement="bottom">{{ @tplCounter('get', @navKey . '_danger') }}</span>
</a>

View File

@@ -205,11 +205,36 @@
}
// navigation link active/hover indicator =========================================================
@mixin navigation-active-indicator($position) {
content: '';
position: absolute;
background-color: $green;
opacity: 0;
will-change: opacity, $position ;
@include transition( $position 0.15s ease-out, opacity 0.15s ease-out );
@mixin pf-navigation-indicator($position){
&:not(.disabled){
position: relative; // otherwise :before indicator gets to height
&:before {
content: '';
position: absolute;
#{$position}: 0;
background-color: $green;
opacity: 0;
@include transition( $position 0.15s ease-out, opacity 0.15s ease-out );
will-change: opacity, $position;
@if $position == 'left' {
width: 2px;
height: 100%;
} @else if $position == 'top' {
width: 100%;
height: 2px;
} @else if $position == 'bottom' {
width: 100%;
height: 2px;
}
}
&:hover, &.active {
&:before {
#{$position}: -4px;
opacity: 1;
}
}
}
}

View File

@@ -333,7 +333,6 @@
padding: 10px 10px 5px 10px;
min-width: 155px;
min-height: 184px;
overflow: visible; // overwrite default
@include border-radius(10px);
@include box-shadow(0 4px 10px rgba(0,0,0, 0.4));

View File

@@ -664,24 +664,16 @@ table{
width: 100%;
}
}
.navbar-nav {
li:not(.disabled):not(.hide-before){
&:hover, &.active{
&:before{
top: -4px;
opacity: 1;
}
}
&:before{
@include navigation-active-indicator(top);
width: 100%;
height: 2px;
top: 0;
}
.modal .navbar-nav {
li {
@include pf-navigation-indicator(top);
}
}
.navbar-fixed-top .navbar-nav {
li {
@include pf-navigation-indicator(bottom);
}
}

View File

@@ -883,21 +883,7 @@ $mapBubbleWidth: 30px;
// top menu
&[role] > li{
&:not(.disabled){
position: relative; // otherwise :before indicator gets to height
&:before{
@include navigation-active-indicator(left);
width: 2px;
height: 100%;
left: 0;
}
&:hover:before{
left: -4px;
opacity: 1;
}
}
@include pf-navigation-indicator(left);
}
& > li {