Mercurial > hg > dml-open-vis
diff src/DML/MainVisBundle/Resources/assets/marionette/modules/HelpModule.js @ 0:493bcb69166c
added public content
author | Daniel Wolff |
---|---|
date | Tue, 09 Feb 2016 20:54:02 +0100 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/DML/MainVisBundle/Resources/assets/marionette/modules/HelpModule.js Tue Feb 09 20:54:02 2016 +0100 @@ -0,0 +1,418 @@ +"use strict"; + +/* + * TODO + * keep scroll position when resize + * + * fix focus outline around links in TOC in FF + * dotted border + */ +App.module("HelpModule", function(HelpModule, App, Backbone, Marionette, $, _, Logger) { + + // Prevent auto start + HelpModule.startWithParent = false; + + // Define options + var defaultModuleOptions = { + contentScrollDuration: 200, + resizeThrottleDuration: 200, + resizeDebounceDuration: 200, + scrollThrottleDuration: 200, + scrollDebounceDuration: 200, + }; + var moduleOptions; + + // Define private variables + var logger = null; + var $help = null; + var $helpBody = null; + var $helpCloser = null; + var $helpContentContainer = null; + var $helpContent = null; + var $helpContentHeaders = null; + var $helpTocContainer = null; + var $helpToc = null; + + var contentScrollSavedPosition = null; // [offset, $materialHeader] $materialHeader - where to offset from + var resizing = false; // true when window is resizing, ignore + var scrolling = false; // true when content is being scrolled for whatever reason + var scrollingTo = null; // data id of an item that is being scrolled to during an animation + + var lastShownMaterialId = null; + var pendingMaterialId = null; + + var $lastContentHeader = null; // Not last used, but last in the list (to set up bottom margin) + + // Initialization checker + var assertModuleIsInitialized = function() { + if (!$help) { + throw "HelpModule has not been initialized"; + } + }; + + // Private functions + var updateContentBottomMargin = null; + var updateContentScrollSavedPosition = null; + var restoreContentScrollSavedPosition = null; + + var updateTocCurrentItem = null; + + /** + * Module initializer + * + */ + HelpModule.addInitializer(function(options){ + + moduleOptions = _.extend(defaultModuleOptions, options); + + logger = Logger.get("HelpModule"); + //logger.setLevel(Logger.DEBUG); + + // When window is resized, bottom margin of the content should be updated + updateContentBottomMargin = function(makeBigAndLockScroll) { + if (makeBigAndLockScroll) { + $helpContentContainer + .css("overflow", "hidden"); + $helpContent + .css("border-bottom-width", 10000); + } else { + $helpContentContainer + .css("overflow", "scroll"); + $helpContent + .css('border-bottom-width', + Math.max(0, + $helpContentContainer.outerHeight() + - $helpContent.height() + + $lastContentHeader.position().top + - parseInt($helpContent.css('padding-bottom'), 10) + + parseInt($lastContentHeader.css('margin-top'), 10) + )); + } + }; + + // Looks at the current scroll position and updates + // contentScrollSavedPosition accordingly + updateContentScrollSavedPosition = function() { + var scrollTop = $helpContentContainer.scrollTop(); + var $candidateHeader = $helpContentHeaders.first(); + + for (var i = 0; i <= $helpContentHeaders.length; i++) { + var $helpContentHeader = $($helpContentHeaders[i]); + if (!$helpContentHeader.length || $helpContentHeader.position().top >= scrollTop) { + contentScrollSavedPosition = [ + Math.floor(scrollTop - $candidateHeader.position().top - parseInt($candidateHeader.css('margin-top')), 10), + $candidateHeader + ]; + updateTocCurrentItem(); + break; + } else { + $candidateHeader = $helpContentHeader; + } + } + + App.DataModule.Storage.setStrCache(HelpModule, "saved-scroll-position", contentScrollSavedPosition[0] + " " + contentScrollSavedPosition[1].attr('data-id')); + }; + + // scrolls to a saved scroll position + restoreContentScrollSavedPosition = function(animate) { + if (animate) { + scrollingTo = contentScrollSavedPosition[1].attr('data-id'); + } + $helpContentContainer + .stop(true, false) + .scrollTo(contentScrollSavedPosition[1].position().top + parseInt(contentScrollSavedPosition[1].css('margin-top'), 10) + contentScrollSavedPosition[0], { + duration: animate ? moduleOptions.contentScrollDuration : 0, + }, function() { + scrollingTo = null; + }); + }; + + updateTocCurrentItem = function() { + var newMaterialId = scrollingTo !== null ? scrollingTo : contentScrollSavedPosition[1].attr('data-id'); + + if (lastShownMaterialId !== newMaterialId) { + $helpToc + .children() + .removeClass("help__toc-element_current"); + //.setMod('help', 'toc-element', 'current', false); + $helpToc.find(_.str.sprintf("[data-id='%s']", newMaterialId)) + .addClass("help__toc-element_current"); + //.setMod('help', 'toc-element', 'current', true); + } + + if (lastShownMaterialId != newMaterialId) { + if (HelpModule.isShowing()) { + lastShownMaterialId = newMaterialId; + HelpModule.trigger("show", {"materialId": newMaterialId}); + } + } + }; + + $help = $.bem.generateBlock('help').setMod('help','state','hidden'); + $helpBody = $.bem.generateElement('help', 'body'); + $helpContentContainer = $.bem.generateElement('help', 'content-container'); + $helpContent = $.bem.generateElement('help', 'content'); + $helpTocContainer = $.bem.generateElement('help', 'toc-container'); + $helpToc = $.bem.generateElement('help', 'toc'); + $helpCloser = $.bem.generateElement('help', 'closer'); + + // Clicking outside help hides it + $help.click(function(event) { + if ($help.hasMod("help", "state_shown")) { + HelpModule.hide(); + } + event.stopPropagation(); + }); + $helpBody.click(function(event) { + event.stopPropagation(); + }); + + // Help content goes from #help-content template + // It is both an element and a block + $helpContent.addClass('help-content') + .append($(Backbone.Marionette.TemplateCache.get("#help-content")({ + Ctrl: _.str.capitalize(App.keyboardMappings.ctrlTitle) + }))); + + // Help TOC (table of contents) is populated from headers in content + var usedDataIds = []; + $helpContentHeaders = $helpContent.find('h1, h2, h3'); + $helpContentHeaders.each(function(i, helpContentHeader) { + var $helpContentHeader = $(helpContentHeader); + var title = $helpContentHeader.attr('data-toc'); + if (!title) { + title = $helpContentHeader.text(); + } + var id = $helpContentHeader.attr('data-id'); + if (_.isUndefined(id)) { + id = _.str.slugify($helpContentHeader.text()); + } + if (usedDataIds.indexOf(id) != -1) { + throw _.str.sprintf("There are more than one header with id = '%s' in help", id); + } + usedDataIds.push(id); + $helpContentHeader.attr('data-id', id); + var $currentHelpTocElement = $.bem.generateElement('a', 'help', 'toc-element') + .attr('href', id ? _.str.sprintf('#help/%s', id) : '#help') + .attr('data-id', id) + .setMod('help', 'toc-element', 'hierarchy', $helpContentHeader.prop('tagName').slice(1)) + .text(title) + .click(function(event) { + if ($.eventsugar.isAttemptToOpenInAnotherWindow(event)) { + return; + } + event.preventDefault(); + HelpModule.show({materialId: id, forceScroll: true}); + return false; + }); + + $helpToc.append($currentHelpTocElement); + $lastContentHeader = $helpContentHeader; + }); + + // Restore scroll position from cache + var rawSavedScrollPosition = App.DataModule.Storage.getStrCache(HelpModule, "saved-scroll-position"); + if (rawSavedScrollPosition) { + var i = rawSavedScrollPosition.indexOf(" "); + var offset = parseInt(rawSavedScrollPosition.slice(0,i)); + var $materialHeader = $helpContentHeaders.filter(_.str.sprintf("[data-id='%s']", rawSavedScrollPosition.slice(i+1))); + if ($materialHeader.length) { + contentScrollSavedPosition = [offset, $materialHeader]; + } + } + if (!contentScrollSavedPosition) { + contentScrollSavedPosition = [0, $helpContentHeaders.first()]; + } + lastShownMaterialId = contentScrollSavedPosition[1].attr("data-id"); + + // Help closer + //// Move to the right for Windows + if (navigator && navigator.appVersion && navigator.appVersion.indexOf("Win") != -1) { + $helpCloser.setMod('help', 'closer', 'position', 'right'); + } + //// Close help on click + $helpCloser.click(function(event) { + if ($help.hasMod("help", "state_shown")) { + HelpModule.hide(); + } + event.stopPropagation(); + }); + + // Build element hierarchy + $helpContentContainer.append($helpContent); + $helpTocContainer.append($helpToc); + $helpBody.append($helpContentContainer, $helpTocContainer); + $help.append($helpBody, $helpCloser); + + $('.app__help').append($help); + $help.setMod("help", "animating", true); + + // Window events + var $window = $(window); + + $window.on("resize", function() { + if (!HelpModule.isShowing()) { + return; + } + if (!resizing) { + resizing = true; + updateContentBottomMargin(true); + } + restoreContentScrollSavedPosition(); + }); + + $window.on("resize", _.debounce( + function(event) { + if (!HelpModule.isShowing()) { + return; + } + updateContentBottomMargin(); + resizing = false; + restoreContentScrollSavedPosition(true); + }, + moduleOptions.resizeDebounceDuration)); + + $helpContentContainer.on("scroll",_.throttle( + function(event){if (!resizing) {scrolling = true; updateContentScrollSavedPosition();}}, + moduleOptions.scrollThrottleDuration, {trailing: false})); + + $helpContentContainer.on("scroll", _.debounce( + function(event) { + scrolling = false; + updateContentScrollSavedPosition(); + if (HelpModule.isShowing() + && pendingMaterialId !== null + && pendingMaterialId != lastShownMaterialId + && pendingMaterialId != contentScrollSavedPosition[1].attr('data-id')) { + contentScrollSavedPosition = [0, $helpContentHeaders.filter(_.str.sprintf("[data-id='%s']", pendingMaterialId))]; + pendingMaterialId = null; + restoreContentScrollSavedPosition(true); + } else { + pendingMaterialId = null; + } + }, + moduleOptions.scrollDebounceDuration)); + + + /// Scroll fix for help + new ScrollFix($helpContentContainer.get(0)); + $helpContentContainer.get(0).addEventListener('touchmove', function(event){ + event.stopPropagation(); + }); + + // Embedded content + var helpContentHasVimeo = false; + $helpContent.find("iframe").each(function() { + var $iframe = $(this); + var src = $iframe.attr("src"); + if (!_.isString(src)) { + src = ""; + } + if (src.indexOf("vimeo") !== -1) { + helpContentHasVimeo = true; + $(this).attr("data-type", "vimeo"); + } + }); + // enable control of vimeo + // see http://stackoverflow.com/questions/10401819/jquery-how-to-stop-vimeo-video-when-click + if (helpContentHasVimeo) { + var scriptElement = document.createElement('script'); + scriptElement.type = 'text/javascript'; + scriptElement.async = true; + scriptElement.src = "http://a.vimeocdn.com/js/froogaloop2.min.js"; + document.getElementsByTagName('body')[0].appendChild(scriptElement); + } + }); + + /** + * Shows a specific material id (scrolls to it) + * or shows the top of the help page if no materialId is specified + * + * options + * materialId + * $dispatcher is a jquery element that triggered help page opening + * this parameter helps animate the body of the help (TODO) + * + * forceScroll + * instant + * + */ + HelpModule.show = function(options) { + + var options = $.extend({}, options); + assertModuleIsInitialized(); + + var helpIsOpening = false; + if ($help.hasMod('help', 'state_hidden') || $help.hasMod('help', 'state_pre-hidden')) { + helpIsOpening = true; + $help.toggleClass('help_animating', !options.instant); + $help.setMod('help', 'state', 'pre-shown'); + lastShownMaterialId = null; + pendingMaterialId = null; + updateContentBottomMargin(); + } + + var needToScroll = true; + var $materialHeader = $helpContent.find(_.str.sprintf("[data-id='%s']", options.materialId)); + if ($materialHeader.length) { + pendingMaterialId = options.materialId; + if (!scrolling) { + if (options.forceScroll || !contentScrollSavedPosition[1].is($materialHeader)) { + contentScrollSavedPosition = [0, $materialHeader]; + } else { + needToScroll = false; + } + } + } + if (helpIsOpening || needToScroll && (!scrolling || scrollingTo !== null)) { + restoreContentScrollSavedPosition(!helpIsOpening); + updateTocCurrentItem(); + } + + if (!needToScroll) { + scrollingTo = null; + pendingMaterialId = null; + } + + + if ($help.hasMod('help', 'state_pre-shown')) { + $help.setMod('help', 'state', 'shown'); + } + if (!!options.instant) { + setTimeout(function() { + $help.setMod('help', 'animating', true); + }, 10) + } + }; + + /** + * Hides help + */ + HelpModule.hide = function() { + assertModuleIsInitialized(); + + // disable content + //// pause vimeo + $helpContent.find("iframe[data-type=vimeo]").each(function() { + if (window.$f) { + $f(this).api("pause"); + } + }); + + $help.setMod('help', 'state', 'pre-hidden'); + HelpModule.trigger("hide"); + setTimeout(function() { + if ($help.hasMod('help', 'state_pre-hidden')) { + $help.setMod('help', 'state', 'hidden'); + }; + }, 1000); + }; + + /** + * Returns true if help is being shown or is about to be shown + * False is return when help is not visible or is about to be hidden + */ + HelpModule.isShowing = function() { + return $help.hasMod("help", "state_pre-shown") || $help.hasMod("help", "state_shown"); + }; +}, Logger);