Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * A Menu for the Atto editor.
 *
 * @module     moodle-editor_atto-menu
 * @submodule  menu-base
 * @package    editor_atto
 * @copyright  2013 Damyon Wiese
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

var LOGNAME = 'moodle-editor_atto-menu';
var MENUDIALOGUE = '' +
        '<div class="open {{config.buttonClass}} atto_menu" ' +
            'style="min-width:{{config.innerOverlayWidth}};">' +
            '<ul class="dropdown-menu" role="menu" id="{{config.buttonId}}_menu" aria-labelledby="{{config.buttonId}}">' +
                '{{#each config.items}}' +
                    '<li role="none" class="atto_menuentry">' +
                        '<a href="#" role="menuitem" data-index="{{@index}}" {{#each data}}data-{{@key}}="{{this}}"{{/each}}>' +
                            '{{{text}}}' +
                        '</a>' +
                    '</li>' +
                '{{/each}}' +
            '</ul>' +
        '</div>';

/**
 * A Menu for the Atto editor used in Moodle.
 *
 * This is a drop down list of buttons triggered (and aligned to) a
 * location.
 *
 * @namespace M.editor_atto
 * @class Menu
 * @main
 * @constructor
 * @extends M.core.dialogue
 */
var Menu = function() {
    Menu.superclass.constructor.apply(this, arguments);
};

Y.extend(Menu, M.core.dialogue, {

    /**
     * A list of the menu handlers which have been attached here.
     *
     * @property _menuHandlers
     * @type Array
     * @private
     */
    _menuHandlers: null,

    /**
     * The menu button that controls this menu.
     */
    _menuButton: null,

    initializer: function(config) {
        var headerText,
            bb;

        this._menuHandlers = [];

        this._menuButton = document.getElementById(config.buttonId);

        // Create the actual button.
        var template = Y.Handlebars.compile(MENUDIALOGUE),
            menu = Y.Node.create(template({
                config: config
            }));
        this.set('bodyContent', menu);

        bb = this.get('boundingBox');
        bb.addClass('editor_atto_controlmenu');
        bb.addClass('editor_atto_menu');

        // Get the dialogue container for this menu.
        var content = bb.one('.moodle-dialogue-wrap');
        content.removeClass('moodle-dialogue-wrap')
            .addClass('moodle-dialogue-content');
        // Remove the dialog role attribute.
        content.removeAttribute('role');
        // Remove aria-labelledby in the container. The aria-labelledby attribute is properly set in the menu's template.
        content.removeAttribute('aria-labelledby');

        // Render heading if necessary.
        headerText = this.get('headerText').trim();
        if (headerText) {
            var heading = Y.Node.create('<h3/>')
                .addClass('accesshide')
                .setHTML(headerText);
            this.get('bodyContent').prepend(heading);
        }

        // Hide the header and footer node entirely.
        this.headerNode.hide();
        this.footerNode.hide();

        this._setupHandlers();
    },

    /**
     * Setup the Event handlers.
     *
     * @method _setupHandlers
     * @private
     */
    _setupHandlers: function() {
        var contentBox = this.get('contentBox');
        // Handle menu item selection.
        this._menuHandlers.push(
            // Select the menu item on space, and enter.
            contentBox.delegate('key', this._chooseMenuItem, '32, enter', '.atto_menuentry', this),

            // Move up and down the menu on up/down.
            contentBox.delegate('key', this._handleKeyboardEvent, 'down:38,40', '.dropdown-menu', this),

            // Hide the menu when clicking outside of it.
            contentBox.on('focusoutside', this.hide, this),

            // Hide the menu on left/right, and escape keys.
            contentBox.delegate('key', this.hide, 'down:37,39,esc', '.dropdown-menu', this)
        );
    },

    /**
     * Simulate other types of menu selection.
     *
     * @method _chooseMenuItem
     * @param {EventFacade} e
     */
    _chooseMenuItem: function(e) {
        e.target.simulate('click');
        e.preventDefault();
    },

    /**
     * Hide a menu, removing all of the event handlers which trigger the hide.
     *
     * @method hide
     * @param {EventFacade} e
     */
    hide: function(e) {
        if (this.get('preventHideMenu') === true) {
            return;
        }

        // We must prevent the default action (left/right/escape) because
        // there are other listeners on the toolbar which will focus on the
        // editor.
        if (e) {
            e.preventDefault();
        }

        // Remove menu button's aria-expanded attribute when this menu is hidden.
        if (this._menuButton) {
            this._menuButton.removeAttribute('aria-expanded');
        }

        return Menu.superclass.hide.call(this, arguments);
    },

    /**
     * Implement arrow-key navigation for the items in a toolbar menu.
     *
     * @method _handleKeyboardEvent
     * @param {EventFacade} e The keyboard event.
     * @static
     */
    _handleKeyboardEvent: function(e) {
        // Prevent the default browser behaviour.
        e.preventDefault();

        // Get a list of all buttons in the menu.
        var buttons = e.currentTarget.all('a[role="menuitem"]');

        // On cursor moves we loops through the buttons.
        var found = false,
            index = 0,
            direction = 1,
            checkCount = 0,
            current = e.target.ancestor('a[role="menuitem"]', true),
            next;

        // Determine which button is currently selected.
        while (!found && index < buttons.size()) {
            if (buttons.item(index) === current) {
                found = true;
            } else {
                index++;
            }
        }

        if (!found) {
            Y.log("Unable to find this menu item in the menu", 'debug', LOGNAME);
            return;
        }

        if (e.keyCode === 38) {
            // Moving up so reverse the direction.
            direction = -1;
        }

        // Try to find the next
        do {
            index += direction;
            if (index < 0) {
                index = buttons.size() - 1;
            } else if (index >= buttons.size()) {
                // Handle wrapping.
                index = 0;
            }
            next = buttons.item(index);

            // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item.
            checkCount++;
            // Loop while:
            // * we are not in a loop and have not already checked every button; and
            // * we are on a different button; and
            // * the next menu item is not hidden.
        } while (checkCount < buttons.size() && next !== current && next.hasAttribute('hidden'));

        if (next) {
            next.focus();
        }

        e.preventDefault();
        e.stopImmediatePropagation();
    }
}, {
    NAME: "menu",
    ATTRS: {
        /**
         * The header for the drop down (only accessible to screen readers).
         *
         * @attribute headerText
         * @type String
         * @default ''
         */
        headerText: {
            value: ''
        }

    }
});

Y.Base.modifyAttrs(Menu, {
    /**
     * The width for this menu.
     *
     * @attribute width
     * @default 'auto'
     */
    width: {
        value: 'auto'
    },

    /**
     * When to hide this menu.
     *
     * By default, this attribute consists of:
     * <ul>
     * <li>an object which will cause the menu to hide when the user clicks outside of the menu</li>
     * </ul>
     *
     * @attribute hideOn
     */
    hideOn: {
        value: [
            {
                eventName: 'clickoutside'
            }
        ]
    },

    /**
     * The default list of extra classes for this menu.
     *
     * @attribute extraClasses
     * @type Array
     * @default editor_atto_menu
     */
    extraClasses: {
        value: [
            'editor_atto_menu'
        ]
    },

    /**
     * Override the responsive nature of the core dialogues.
     *
     * @attribute responsive
     * @type boolean
     * @default false
     */
    responsive: {
        value: false
    },

    /**
     * The default visibility of the menu.
     *
     * @attribute visible
     * @type boolean
     * @default false
     */
    visible: {
        value: false
    },

    /**
     * Whether to centre the menu.
     *
     * @attribute center
     * @type boolean
     * @default false
     */
    center: {
        value: false
    },

    /**
     * Hide the close button.
     * @attribute closeButton
     * @type boolean
     * @default false
     */
    closeButton: {
        value: false
    }
});

Y.namespace('M.editor_atto').Menu = Menu;