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