view src/DML/MainVisBundle/Resources/assets/marionette/modules/HelpModule.js @ 1:f38015048f48 tip

Added GPL
author Daniel Wolff
date Sat, 13 Feb 2016 20:43:38 +0100
parents 493bcb69166c
children
line wrap: on
line source
"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);