UNPKG

@essetwide/material-walkthrough

Version:

A material tour (eg Inbox from Google) for your site, enhancing the UX.

404 lines (349 loc) 15.2 kB
/** * Copyright 2017 Esset Software LTD. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var _logenv = { MSG: false, WALK_CONTENT: true, WALK_CONTENT_TOP: false, WALK_LOCK: false, WALK_SCROLL: true, ALL: true }; function _log(context, message) { if(!!_logenv[context] || _logenv.ALL) console.log(context +': '+ message); } /** * @license Apache License 2.0. * @copyright Esset Software LTD. */ (function($) { var WALK_DEFAULT_COLOR = '#2196F3'; var WALK_TRANSITION_DURATION = 500; var WALK_MIN_SIZE = 60; var WALK_PADDING = 20; var WALK_COMPONENT = "<div id='walk-bounds'><div id='walk-wrapper'>" + "<div id='walk-content-wrapper'>" + "<div id='walk-content'></div>" + "<button id='walk-action'></button>" + "</div>" + "</div></div>"; var walkWrapper = {}; var walkContentWrapper = {}; var walkContent = {}; var walkActionButton = {}; var walkUpdateHandler = null; /** * A Object that configures an walkpoint. * @typedef {object} WalkPoint * @property {string} target A jQuery selector of the element that the walk will focus; * @property {string} content A HTML code that will be inserted on the walk-content container; * @property {string} [color] A CSS (rgb, rgba, hex, etc.) color specification that will paint the walk. #2196F3 is default; * @property {string} [acceptText] The text of the accept button of the walk; * @property {function} [onSet] A function that will be called when the walk content is setted; * @property {function} [onClose] A function that will be called when the walk is accepted; */ /*** * Open the walker to a walkpoint. * @param {WalkPoint} walkPoint The configuration of the walkpoint */ $.fn.walk = function (walkPoint) { var target = this; disableScroll(); walkWrapper.removeClass('closed'); setWalker(target, walkPoint); }; /*** * Set the opened walker to a target with the properties from walkPoint. * @param {string|HTMLElement|JQueryElement} target A query or a Element to target the walker * @param {WalkPoint} walkPoint The properties for this walker */ function setWalker(target, walkPoint) { target = $(target); _log('MSG', '-------------------------------------'); _log('MSG', 'Setting a walk to #' + target[0].id); _log('WALK_SETUP', 'Properties:\n' + JSON.stringify(walkPoint, null, 2)); setupListeners(target, walkPoint.onClose); walkContentWrapper.css('display', 'none'); locateTarget(target, function () { setProperties(walkPoint.content, walkPoint.color, walkPoint.acceptText); walkWrapper.css('display', 'block'); renderFrame(target, function () { walkWrapper.addClass('opened'); renderContent(target, function() { walkContentWrapper.css('display', ''); }); }); }); _log('MSG', 'Walk created. Calling onSet() (if exists)'); if (!!walkPoint.onSet) walkPoint.onSet(walkContent); } /*** * Close the walker and flush its Listeners. */ function closeWalker() { _log('MSG', 'Closing Walker'); flushListeners(); enableScroll(); walkWrapper.css({marginTop: '-500px', marginLeft: '-500px'}); walkWrapper.addClass('closed'); setTimeout(function () { walkWrapper.css('display', 'none'); walkWrapper.removeClass('opened'); }, WALK_TRANSITION_DURATION); _log('MSG', 'Walker Closed!'); } /*** * Set the properties for the walk. * @param {string} content The content that will be displayed in the walk * @param {string} color A CSS valid color * @param {string} acceptText The text that will be displayed in the accept button */ function setProperties(content, color, acceptText) { color = !!color ? color : WALK_DEFAULT_COLOR; walkContent.html(content); walkWrapper.css('border-color', color); walkActionButton.text(acceptText); } /*** * Create the function that updates the walker to a target * @param {JQueryElement} target The target to set the update function * @returns {function} Update handler to call in the listeners */ function createUpdateHandler(target) { _log('WALK_UPDATE', 'Creating UpdateHandler for #' +target[0].id); var updateHandler = function () { _log('MSG', 'Updating and rendering'); locateTarget(target, function () { renderFrame(target, function () { renderContent(target); }); }); }; updateHandler.toString = function () { return 'updateHandler -> #' + target[0].id; }; return updateHandler; } /*** * Setup the update listeners (onResize, MutationObserver) and the close callback. * @param {JQueryElement} target The target to set the listeners * @param {function} onClose Close callback */ function setupListeners(target, onClose) { if(!!walkUpdateHandler) flushListeners(); walkUpdateHandler = createUpdateHandler(target); $(window).on('resize', walkUpdateHandler); $.walk._mutationObserver = new MutationObserver(walkUpdateHandler); $.walk._mutationObserver.observe(document.body, { childList: true, subtree: true }); walkActionButton.on('click', function actionCallback(){ if (!!onClose) onClose(); if (!!$.walk._points && !!$.walk._points[$.walk._currentIndex + 1]) { $.walk._currentIndex++; setWalker($.walk._points[$.walk._currentIndex].target, $.walk._points[$.walk._currentIndex]); } else { $.walk._currentIndex = 0; $.walk._points = null; if($.walk._callback) $.walk._callback(); $.walk._callback = null; closeWalker(); } walkActionButton.off('click', actionCallback); }); } /*** * Clean the listeners with the actual updateHandler */ function flushListeners() { _log('WALK_UPDATER', 'Flushing handlers\n' + walkUpdateHandler); if(!!$.walk._mutationObserver) $.walk._mutationObserver.disconnect(); $.walk._mutationObserver = null; $(window).off('resize', walkUpdateHandler); } /*** * Move the Walker to a target * @param {JQueryElement} target */ function locateTarget(target, locateCallback) { var position = target.offset(); var positionMode = window.getComputedStyle(target[0])['position']; var windowHeight = $(window).height(); var documentHeight = $(document.body).height(); var positionOutOfBounds = (positionMode == 'absolute' || positionMode == 'relative') && parseInt(window.getComputedStyle(target[0])['top']) > documentHeight; // Test if the position of the target is out of the document height by a forced position; _log('WALK_LOCK', 'Moving Walker to:\n' + JSON.stringify(position, null, 2)); _log('WALK_SCROLL', 'documentHeight: ' +documentHeight); var scrollTo = (position.top - (windowHeight / 2)); _log('WALK_LOCK', 'Trying to centralize the target in the screen: \n ' + JSON.stringify({ scrollTo: scrollTo, targetY: position.top, windowHeightPer2: windowHeight / 2, targetPositionMode: positionMode, positionOutOfBounds: positionOutOfBounds }, null, 2)); if (scrollTo > 0 && positionMode != 'fixed') { _log('WALK_LOCK', 'Scrolling to ' +scrollTo); _log('WALK_SCROLL', 'scrollTo + windowHeight: ' +(scrollTo + windowHeight)); if (scrollTo + windowHeight > documentHeight && !positionOutOfBounds) scrollTo = documentHeight - windowHeight; // Setting the scroll limit by the document's height _log('WALK_LOCK', 'Corrected scroll amount: ' +scrollTo); $('body,html').animate({ scrollTop: scrollTo }, WALK_TRANSITION_DURATION, function () { locateCallback(); }); } else { _log('WALK_LOCK', 'Resetting scroll'); $('body,html').animate({ scrollTop: 0 }, WALK_TRANSITION_DURATION, function () { locateCallback(); }); } } function renderFrame(target, renderCallback) { var position = target.offset(); var height = target.outerHeight(); var width = target.outerWidth(); var holeSize = height > width ? height : width; // Catch the biggest measure if (holeSize < WALK_MIN_SIZE) holeSize = WALK_MIN_SIZE; // Adjust with default min measure if it not higher than it _log('WALK_LOCK', 'Walk hole size ' +holeSize+ 'px'); walkWrapper.css({ 'height': (holeSize + WALK_PADDING) + 'px', 'width': (holeSize + WALK_PADDING) + 'px', 'margin-left': -((holeSize + WALK_PADDING) / 2) + 'px', 'margin-top': -((holeSize + WALK_PADDING) / 2) + 'px', 'left': (position.left + (width / 2)) + 'px', 'top': (position.top + (height / 2)) + 'px', }); setTimeout(function () { renderCallback(); }, 250); } function renderContent(target, renderCallback) { var position = target.offset(); var itCanBeRenderedInRight = position.left + (walkWrapper.outerWidth() - WALK_PADDING) + walkContentWrapper.outerWidth() < $(window).outerWidth(); var itCanBeRenderedInLeft = (position.left - WALK_PADDING) - walkContentWrapper.outerWidth() > 0; var itCanBeRenderedInTop = walkWrapper[0].getBoundingClientRect().top - walkContentWrapper.outerHeight() > 0; var itCanBeRenderedInBottom = walkWrapper[0].getBoundingClientRect().top + walkWrapper.outerHeight() + walkContentWrapper.outerHeight() < $(window).outerHeight(); _log('WALK_CONTENT', 'itCanBeRenderedInRight: ' +itCanBeRenderedInRight); _log('WALK_CONTENT', 'itCanBeRenderedInLeft: ' +itCanBeRenderedInLeft); _log('WALK_CONTENT', 'itCanBeRenderedInTop: ' +itCanBeRenderedInTop); _log('WALK_CONTENT', 'itCanBeRenderedInBottom: ' +itCanBeRenderedInBottom); var positionLeft = '100%'; var positionTop = '100%'; var marginTop = 0; var marginLeft = 0; var textAlign = 'left'; if (!itCanBeRenderedInRight) { positionLeft = itCanBeRenderedInLeft ? '-'+ walkContentWrapper.outerWidth() +'px': (itCanBeRenderedInBottom ? '0%': '25%'); textAlign = itCanBeRenderedInLeft ? 'right' : 'center'; marginTop = itCanBeRenderedInLeft ? 0 : (itCanBeRenderedInBottom ? '20px' : '-20px'); } if (!itCanBeRenderedInBottom) { positionTop = itCanBeRenderedInTop ? '-'+ walkContentWrapper.outerHeight() +'px': walkWrapper.outerHeight() / 2 - walkContentWrapper.outerHeight() / 2 + 'px'; marginLeft = itCanBeRenderedInTop ? 0 : (!itCanBeRenderedInRight ? '-20px' : '20px'); } walkContentWrapper.css({ 'left': positionLeft, 'top': positionTop, 'text-align': textAlign, 'margin-top': marginTop, 'margin-left': marginLeft }); if(renderCallback) renderCallback(); } $.walk = function (walkPoints, callback) { $.walk._points = walkPoints; $.walk._currentIndex = 0; $.walk._callback = callback; $(walkPoints[0].target).walk(walkPoints[0]); }; /** * Global variable that holds the current walk configuration. * @type {WalkPoint[]} */ $.walk._points = null; /** * Global variable that holds the current point index in _walkPoints array. * @type {number} */ $.walk._currentIndex = 0; /** * Global variable that holds the MutationObserver that listen body modifications. * @type {MutationObserver} */ $.walk._mutationObserver = null; function init() { $('body').append(WALK_COMPONENT); walkWrapper = $('#walk-wrapper'); walkContentWrapper = $('#walk-content-wrapper'); walkContent = $('#walk-content'); walkActionButton = $('#walk-action'); } init(); //Locking scroll //Thanks to @galambalazs // left: 37, up: 38, right: 39, down: 40, // spacebar: 32, pageup: 33, pagedown: 34, end: 35, home: 36 var keys = { 37: 1, 38: 1, 39: 1, 40: 1, 32: 1, 33: 1, 34: 1 }; function preventDefault(e) { e = e || window.event; if (e.preventDefault) e.preventDefault(); e.returnValue = false; } function preventDefaultForScrollKeys(e) { if (keys[e.keyCode]) { preventDefault(e); return false; } } function disableScroll() { $('html').css({ 'height': '100vh', 'overflow': 'hidden' }); if (window.addEventListener) // older FF window.addEventListener('DOMMouseScroll', preventDefault, false); window.onwheel = preventDefault; // modern standard window.onmousewheel = document.onmousewheel = preventDefault; // older browsers, IE window.ontouchmove = preventDefault; // mobile document.onkeydown = preventDefaultForScrollKeys; } function enableScroll() { $('html').css({ 'height': '', 'overflow': 'auto' }); if (window.removeEventListener) window.removeEventListener('DOMMouseScroll', preventDefault, false); window.onmousewheel = document.onmousewheel = null; window.onwheel = null; window.ontouchmove = null; document.onkeydown = null; } window.enableScroll = enableScroll; })(window.$);