Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
YUI.add('moodle-editor_atto-plugin', function (Y, NAME) {
2
 
3
// This file is part of Moodle - http://moodle.org/
4
//
5
// Moodle is free software: you can redistribute it and/or modify
6
// it under the terms of the GNU General Public License as published by
7
// the Free Software Foundation, either version 3 of the License, or
8
// (at your option) any later version.
9
//
10
// Moodle is distributed in the hope that it will be useful,
11
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
// GNU General Public License for more details.
14
//
15
// You should have received a copy of the GNU General Public License
16
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17
 
18
/**
19
 * Atto editor plugin.
20
 *
21
 * @module moodle-editor_atto-plugin
22
 * @submodule plugin-base
23
 * @package    editor_atto
24
 * @copyright  2014 Andrew Nicols
25
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26
 */
27
 
28
/**
29
 * A Plugin for the Atto Editor used in Moodle.
30
 *
31
 * This class should not be directly instantiated, and all Editor plugins
32
 * should extend this class.
33
 *
34
 * @namespace M.editor_atto
35
 * @class EditorPlugin
36
 * @main
37
 * @constructor
38
 * @uses M.editor_atto.EditorPluginButtons
39
 * @uses M.editor_atto.EditorPluginDialogue
40
 */
41
 
42
function EditorPlugin() {
43
    EditorPlugin.superclass.constructor.apply(this, arguments);
44
}
45
 
46
var GROUPSELECTOR = '.atto_group.',
47
    GROUP = '_group';
48
 
49
Y.extend(EditorPlugin, Y.Base, {
50
    /**
51
     * The name of the current plugin.
52
     *
53
     * @property name
54
     * @type string
55
     */
56
    name: null,
57
 
58
    /**
59
     * A Node reference to the editor.
60
     *
61
     * @property editor
62
     * @type Node
63
     */
64
    editor: null,
65
 
66
    /**
67
     * A Node reference to the editor toolbar.
68
     *
69
     * @property toolbar
70
     * @type Node
71
     */
72
    toolbar: null,
73
 
74
    initializer: function(config) {
75
        // Set the references to configuration parameters.
76
        this.name = config.name;
77
        this.toolbar = config.toolbar;
78
        this.editor = config.editor;
79
 
80
        // Set up the prototypal properties.
81
        // These must be set up here becuase prototypal arrays and objects are copied across instances.
82
        this.buttons = {};
83
        this.buttonNames = [];
84
        this.buttonStates = {};
85
        this.menus = {};
86
        this._primaryKeyboardShortcut = [];
87
        this._buttonHandlers = [];
88
        this._menuHideHandlers = [];
89
        this._highlightQueue = {};
90
    },
91
 
92
    /**
93
     * Mark the content ediable content as having been changed.
94
     *
95
     * This is a convenience function and passes through to
96
     * {{#crossLink "M.editor_atto.EditorTextArea/updateOriginal"}}updateOriginal{{/crossLink}}.
97
     *
98
     * @method markUpdated
99
     */
100
    markUpdated: function() {
101
        // Save selection after changes to the DOM. If you don't do this here,
102
        // subsequent calls to restoreSelection() will fail expecting the
103
        // previous DOM state.
104
        this.get('host').saveSelection();
105
 
106
        return this.get('host').updateOriginal();
107
    }
108
}, {
109
    NAME: 'editorPlugin',
110
    ATTRS: {
111
        /**
112
         * The editor instance that this plugin was instantiated by.
113
         *
114
         * @attribute host
115
         * @type M.editor_atto.Editor
116
         * @writeOnce
117
         */
118
        host: {
119
            writeOnce: true
120
        },
121
 
122
        /**
123
         * The toolbar group that this button belongs to.
124
         *
125
         * When setting, the name of the group should be specified.
126
         *
127
         * When retrieving, the Node for the toolbar group is returned. If
128
         * the group doesn't exist yet, then it is created first.
129
         *
130
         * @attribute group
131
         * @type Node
132
         * @writeOnce
133
         */
134
        group: {
135
            writeOnce: true,
136
            getter: function(groupName) {
137
                var group = this.toolbar.one(GROUPSELECTOR + groupName + GROUP);
138
                if (!group) {
139
                    group = Y.Node.create('<div class="atto_group ' +
140
                            groupName + GROUP + '"></div>');
141
                    this.toolbar.append(group);
142
                }
143
 
144
                return group;
145
            }
146
        }
147
    }
148
});
149
 
150
Y.namespace('M.editor_atto').EditorPlugin = EditorPlugin;
151
// This file is part of Moodle - http://moodle.org/
152
//
153
// Moodle is free software: you can redistribute it and/or modify
154
// it under the terms of the GNU General Public License as published by
155
// the Free Software Foundation, either version 3 of the License, or
156
// (at your option) any later version.
157
//
158
// Moodle is distributed in the hope that it will be useful,
159
// but WITHOUT ANY WARRANTY; without even the implied warranty of
160
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
161
// GNU General Public License for more details.
162
//
163
// You should have received a copy of the GNU General Public License
164
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
165
 
166
/**
167
 * @module moodle-editor_atto-plugin
168
 * @submodule buttons
169
 */
170
 
171
/**
172
 * Button functions for an Atto Plugin.
173
 *
174
 * See {{#crossLink "M.editor_atto.EditorPlugin"}}{{/crossLink}} for details.
175
 *
176
 * @namespace M.editor_atto
177
 * @class EditorPluginButtons
178
 */
179
 
180
var MENUTEMPLATE = '' +
181
        '<button class="{{buttonClass}} atto_hasmenu" ' +
182
            'id="{{id}}" ' +
183
            'tabindex="-1" ' +
184
            'title="{{title}}" ' +
185
            'aria-label="{{title}}" ' +
186
            'type="button" ' +
187
            'aria-haspopup="true" ' +
188
            'aria-controls="{{id}}_menu">' +
189
            '<span class="editor_atto_menu_icon"></span>' +
190
            '<span class="editor_atto_menu_expand"></span>' +
191
        '</button>';
192
 
193
var DISABLED = 'disabled',
194
    HIGHLIGHT = 'highlight',
195
    LOGNAME = 'moodle-editor_atto-editor-plugin',
196
    CSS = {
197
        EDITORWRAPPER: '.editor_atto_content',
198
        MENUICON: '.editor_atto_menu_icon',
199
        MENUEXPAND: '.editor_atto_menu_expand'
200
    };
201
 
202
function EditorPluginButtons() {}
203
 
204
EditorPluginButtons.ATTRS = {
205
};
206
 
207
EditorPluginButtons.prototype = {
208
    /**
209
     * All of the buttons that belong to this plugin instance.
210
     *
211
     * Buttons are stored by button name.
212
     *
213
     * @property buttons
214
     * @type object
215
     */
216
    buttons: null,
217
 
218
    /**
219
     * A list of each of the button names.
220
     *
221
     * @property buttonNames
222
     * @type array
223
     */
224
    buttonNames: null,
225
 
226
    /**
227
     * A read-only view of the current state for each button. Mappings are stored by name.
228
     *
229
     * Possible states are:
230
     * <ul>
231
     * <li>{{#crossLink "M.editor_atto.EditorPluginButtons/ENABLED:property"}}{{/crossLink}}; and</li>
232
     * <li>{{#crossLink "M.editor_atto.EditorPluginButtons/DISABLED:property"}}{{/crossLink}}.</li>
233
     * </ul>
234
     *
235
     * @property buttonStates
236
     * @type object
237
     */
238
    buttonStates: null,
239
 
240
    /**
241
     * The menus belonging to this plugin instance.
242
     *
243
     * @property menus
244
     * @type object
245
     */
246
    menus: null,
247
 
248
    /**
249
     * The state for a disabled button.
250
     *
251
     * @property DISABLED
252
     * @type Number
253
     * @static
254
     * @value 0
255
     */
256
    DISABLED: 0,
257
 
258
    /**
259
     * The state for an enabled button.
260
     *
261
     * @property ENABLED
262
     * @type Number
263
     * @static
264
     * @value 1
265
     */
266
    ENABLED: 1,
267
 
268
    /**
269
     * The list of Event Handlers for buttons.
270
     *
271
     * @property _buttonHandlers
272
     * @protected
273
     * @type array
274
     */
275
    _buttonHandlers: null,
276
 
277
    /**
278
     * Hide handlers which are cancelled when the menu is hidden.
279
     *
280
     * @property _menuHideHandlers
281
     * @protected
282
     * @type array
283
     */
284
    _menuHideHandlers: null,
285
 
286
    /**
287
     * A textual description of the primary keyboard shortcut for this
288
     * plugin.
289
     *
290
     * This will be null if no keyboard shortcut has been registered.
291
     *
292
     * @property _primaryKeyboardShortcut
293
     * @protected
294
     * @type String
295
     * @default null
296
     */
297
    _primaryKeyboardShortcut: null,
298
 
299
    /**
300
     * An list of objects returned by Y.soon().
301
     *
302
     * The keys will be the buttonName of the button, and the value the Y.soon() object.
303
     *
304
     * @property _highlightQueue
305
     * @protected
306
     * @type Object
307
     * @default null
308
     */
309
    _highlightQueue: null,
310
 
311
    /**
312
     * Add a button for this plugin to the toolbar.
313
     *
314
     * @method addButton
315
     * @param {object} config The configuration for this button
316
     * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead.
317
     * @param {string} [config.icon] The icon identifier.
318
     * @param {string} [config.iconComponent='core'] The icon component.
319
     * @param {string} [config.keys] The shortcut key that can call this plugin from the keyboard.
320
     * @param {string} [config.keyDescription] An optional description for the keyboard shortcuts.
321
     * If not specified, this is automatically generated based on config.keys.
322
     * If multiple key bindings are supplied to config.keys, then only the first is used.
323
     * If set to false, then no description is added to the title.
324
     * @param {string} [config.tags] The tags that trigger this button to be highlighted.
325
     * @param {boolean} [config.tagMatchRequiresAll=true] Working in combination with the tags parameter, when true
326
     * every tag of the selection has to match. When false, only one match is needed. Only set this to false when
327
     * necessary as it is much less efficient.
328
     * See {{#crossLink "M.editor_atto.EditorSelection/selectionFilterMatches:method"}}{{/crossLink}} for more information.
329
     * @param {string} [config.title=this.name] The string identifier in the plugin's language file.
330
     * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if
331
     * specified, in the class for the button.
332
     * @param {function} config.callback A callback function to call when the button is clicked.
333
     * @param {object} [config.callbackArgs] Any arguments to pass to the callback.
334
     * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed.
335
     * @return {Node} The Node representing the newly created button.
336
     */
337
    addButton: function(config) {
338
        var group = this.get('group'),
339
            pluginname = this.name,
340
            buttonClass = 'atto_' + pluginname + '_button',
341
            button,
342
            host = this.get('host');
343
 
344
        if (config.exec) {
345
            buttonClass = buttonClass + '_' + config.exec;
346
        }
347
 
348
        if (!config.buttonName) {
349
            // Set a default button name - this is used as an identifier in the button object.
350
            config.buttonName = config.exec || pluginname;
351
        } else {
352
            buttonClass = buttonClass + '_' + config.buttonName;
353
        }
354
        config.buttonClass = buttonClass;
355
 
356
        // Normalize icon configuration.
357
        config = this._normalizeIcon(config);
358
 
359
        if (!config.title) {
360
            config.title = 'pluginname';
361
        }
362
        var title = M.util.get_string(config.title, 'atto_' + pluginname);
363
 
364
        // Create the actual button.
365
        button = Y.Node.create('<button type="button" class="' + buttonClass + '"' +
366
                'tabindex="-1"></button>');
367
        button.setAttribute('title', title);
368
        button.setAttribute('aria-label', title);
369
        window.require(['core/templates'], function(Templates) {
370
            // The button already has title and label, so no need to set them again on the icon.
371
            Templates.renderPix(config.icon, config.iconComponent, '').then(function(iconhtml) {
372
                button.append(iconhtml);
373
            });
374
        });
375
 
376
        // Append it to the group.
377
        group.append(button);
378
 
379
        var currentfocus = this.toolbar.getAttribute('aria-activedescendant');
380
        if (!currentfocus) {
381
            // Initially set the first button in the toolbar to be the default on keyboard focus.
382
            button.setAttribute('tabindex', '0');
383
            this.toolbar.setAttribute('aria-activedescendant', button.generateID());
384
            this.get('host')._tabFocus = button;
385
        }
386
 
387
        // Normalize the callback parameters.
388
        config = this._normalizeCallback(config);
389
 
390
        // Add the standard click handler to the button.
391
        this._buttonHandlers.push(
392
            this.toolbar.delegate('click', config.callback, '.' + buttonClass, this)
393
        );
394
 
395
        // Handle button click via shortcut key.
396
        if (config.keys) {
397
            if (typeof config.keyDescription !== 'undefined') {
398
                // A keyboard shortcut description was specified - use it.
399
                this._primaryKeyboardShortcut[buttonClass] = config.keyDescription;
400
            }
401
            this._addKeyboardListener(config.callback, config.keys, buttonClass);
402
 
403
            if (this._primaryKeyboardShortcut[buttonClass]) {
404
                // If we have a valid keyboard shortcut description, then set it with the title.
405
                title = M.util.get_string('plugin_title_shortcut', 'editor_atto', {
406
                    title: title,
407
                    shortcut: this._primaryKeyboardShortcut[buttonClass]
408
                });
409
                button.setAttribute('title', title);
410
                button.setAttribute('aria-label', title);
411
            }
412
        }
413
 
414
        // Handle highlighting of the button.
415
        if (config.tags) {
416
            var tagMatchRequiresAll = true;
417
            if (typeof config.tagMatchRequiresAll === 'boolean') {
418
                tagMatchRequiresAll = config.tagMatchRequiresAll;
419
            }
420
            this._buttonHandlers.push(
421
                host.on(['atto:selectionchanged', 'change'], function(e) {
422
                    if (typeof this._highlightQueue[config.buttonName] !== 'undefined') {
423
                        this._highlightQueue[config.buttonName].cancel();
424
                    }
425
                    // Async the highlighting.
426
                    this._highlightQueue[config.buttonName] = Y.soon(Y.bind(function(e) {
427
                        if (host.selectionFilterMatches(config.tags, e.selectedNodes, tagMatchRequiresAll)) {
428
                            this.highlightButtons(config.buttonName);
429
                        } else {
430
                            this.unHighlightButtons(config.buttonName);
431
                        }
432
                    }, this, e));
433
                }, this)
434
            );
435
        }
436
 
437
        // Add the button reference to the buttons array for later reference.
438
        this.buttonNames.push(config.buttonName);
439
        this.buttons[config.buttonName] = button;
440
        this.buttonStates[config.buttonName] = this.ENABLED;
441
        return button;
442
    },
443
 
444
    /**
445
     * Add a basic button which ties into the execCommand.
446
     *
447
     * See {{#crossLink "M.editor_atto.EditorPluginButtons/addButton:method"}}addButton{{/crossLink}}
448
     * for full details of the optional parameters.
449
     *
450
     * @method addBasicButton
451
     * @param {object} config The button configuration
452
     * @param {string} config.exec The execCommand to call on the document.
453
     * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead.
454
     * @param {string} [config.icon] The icon identifier.
455
     * @param {string} [config.iconComponent='core'] The icon component.
456
     * @param {string} [config.keys] The shortcut key that can call this plugin from the keyboard.
457
     * @param {string} [config.tags] The tags that trigger this button to be highlighted.
458
     * @param {boolean} [config.tagMatchRequiresAll=false] Working in combination with the tags parameter, highlight
459
     * this button when any match is good enough.
460
     *
461
     * See {{#crossLink "M.editor_atto.EditorSelection/selectionFilterMatches:method"}}{{/crossLink}} for more information.
462
     * @param {string} [config.title=this.name] The string identifier in the plugin's language file.
463
     * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if
464
     * specified, in the class for the button.
465
     * @return {Node} The Node representing the newly created button.
466
     */
467
    addBasicButton: function(config) {
468
        if (!config.exec) {
469
            return null;
470
        }
471
 
472
        // The default icon - true for most core plugins.
473
        if (!config.icon) {
474
            config.icon = 'e/' + config.exec;
475
        }
476
 
477
        // The default callback.
478
        config.callback = function() {
479
            document.execCommand(config.exec, false, null);
480
 
481
            // And mark the text area as updated.
482
            this.markUpdated();
483
        };
484
 
485
        // Return the newly created button.
486
        return this.addButton(config);
487
    },
488
 
489
    /**
490
     * Add a menu for this plugin to the editor toolbar.
491
     *
492
     * @method addToolbarMenu
493
     * @param {object} config The configuration for this button
494
     * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead.
495
     * @param {string} [config.icon] The icon identifier.
496
     * @param {string} [config.iconComponent='core'] The icon component.
497
     * @param {string} [config.title=this.name] The string identifier in the plugin's language file.
498
     * @param {string} [config.buttonName=this.name] The name of the button. This is used in the buttons object, and if
499
     * specified, in the class for the button.
500
     * @param {function} config.callback A callback function to call when the button is clicked.
501
     * @param {object} [config.callbackArgs] Any arguments to pass to the callback.
502
     * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed.
503
     * @param {array} config.entries List of menu entries with the string (entry.text) and the handlers (entry.handler).
504
     * @param {number} [config.overlayWidth=14] The width of the menu. This will be suffixed with the 'em' unit.
505
     * @param {string} [config.menuColor] menu icon background color
506
     * @return {Node} The Node representing the newly created button.
507
     */
508
    addToolbarMenu: function(config) {
509
        var group = this.get('group'),
510
            pluginname = this.name,
511
            buttonClass = 'atto_' + pluginname + '_button',
512
            button,
513
            currentFocus;
514
 
515
        if (!config.buttonName) {
516
            // Set a default button name - this is used as an identifier in the button object.
517
            config.buttonName = pluginname;
518
        } else {
519
            buttonClass = buttonClass + '_' + config.buttonName;
520
        }
521
        config.buttonClass = buttonClass;
522
 
523
        // Normalize icon configuration.
524
        config = this._normalizeIcon(config);
525
 
526
        if (!config.title) {
527
            config.title = 'pluginname';
528
        }
529
        var title = M.util.get_string(config.title, 'atto_' + pluginname);
530
 
531
        if (!config.menuColor) {
532
            config.menuColor = 'transparent';
533
        }
534
 
535
        // Create the actual button.
536
        var id = 'atto_' + pluginname + '_menubutton_' + Y.stamp(this);
537
        var template = Y.Handlebars.compile(MENUTEMPLATE);
538
        button = Y.Node.create(template({
539
            buttonClass: buttonClass,
540
            config: config,
541
            title: title,
542
            id: id
543
        }));
544
 
545
        // Add this button id to the config. It will be used in the menu later.
546
        config.buttonId = id;
547
 
548
        window.require(['core/templates'], function(Templates) {
549
            // The button already has title and label, so no need to set them again on the icon.
550
            Templates.renderPix(config.icon, config.iconComponent, '').then(function(iconhtml) {
551
                button.one(CSS.MENUICON).append(iconhtml);
552
            });
553
            Templates.renderPix('t/expanded', 'core', '').then(function(iconhtml) {
554
                button.one(CSS.MENUEXPAND).append(iconhtml);
555
            });
556
        });
557
 
558
        // Append it to the group.
559
        group.append(button);
560
        group.append(Y.Node.create('<div class="menuplaceholder" id="' + id + '_menu"></div>'));
561
        config.attachmentPoint = '#' + id + '_menu';
562
 
563
        currentFocus = this.toolbar.getAttribute('aria-activedescendant');
564
        if (!currentFocus) {
565
            // Initially set the first button in the toolbar to be the default on keyboard focus.
566
            button.setAttribute('tabindex', '0');
567
            this.toolbar.setAttribute('aria-activedescendant', button.generateID());
568
        }
569
 
570
        // Add the standard click handler to the menu.
571
        this._buttonHandlers.push(
572
            this.toolbar.delegate('click', this._showToolbarMenu, '.' + buttonClass, this, config),
573
            this.toolbar.delegate('key', this._showToolbarMenuAndFocus, '40, 32, enter', '.' + buttonClass, this, config)
574
        );
575
 
576
        // Add the button reference to the buttons array for later reference.
577
        this.buttonNames.push(config.buttonName);
578
        this.buttons[config.buttonName] = button;
579
        this.buttonStates[config.buttonName] = this.ENABLED;
580
 
581
        return button;
582
    },
583
 
584
    /**
585
     * Display a toolbar menu.
586
     *
587
     * @method _showToolbarMenu
588
     * @param {EventFacade} e
589
     * @param {object} config The configuration for the whole toolbar.
590
     * @param {Number} [config.overlayWidth=14] The width of the menu
591
     * @private
592
     */
593
    _showToolbarMenu: function(e, config) {
594
        // Prevent default primarily to prevent arrow press changes.
595
        e.preventDefault();
596
 
597
        if (!this.isEnabled()) {
598
            // Exit early if the plugin is disabled.
599
            return;
600
        }
601
 
602
        // Ensure menu button was clicked, and isn't itself disabled.
603
        var menuButton = e.currentTarget.ancestor('button', true);
604
        if (menuButton === null || menuButton.hasAttribute(DISABLED)) {
605
            return;
606
        }
607
 
608
        var menuDialogue;
609
 
610
        if (!this.menus[config.buttonClass]) {
611
            if (!config.overlayWidth) {
612
                config.overlayWidth = '14';
613
            }
614
 
615
            if (!config.innerOverlayWidth) {
616
                config.innerOverlayWidth = parseInt(config.overlayWidth, 10) - 2 + 'em';
617
            }
618
            config.overlayWidth = parseInt(config.overlayWidth, 10) + 'em';
619
 
620
            this.menus[config.buttonClass] = new Y.M.editor_atto.Menu(config);
621
 
622
            this.menus[config.buttonClass].get('contentBox').delegate('click',
623
                    this._chooseMenuItem, '.atto_menuentry a', this, config);
624
        }
625
 
626
        // Clear the focusAfterHide for any other menus which may be open.
627
        Y.Array.each(this.get('host').openMenus, function(menu) {
628
            menu.set('focusAfterHide', null);
629
        });
630
 
631
        // Ensure that we focus on this button next time.
632
        var creatorButton = this.buttons[config.buttonName];
633
        creatorButton.focus();
634
        this.get('host')._setTabFocus(creatorButton);
635
 
636
        // Get a reference to the menu dialogue.
637
        menuDialogue = this.menus[config.buttonClass];
638
 
639
        // Focus on the button by default after hiding this menu.
640
        menuDialogue.set('focusAfterHide', creatorButton);
641
 
642
        // Display the menu.
643
        menuDialogue.show();
644
 
645
        // Indicate that the menu is expanded.
646
        menuButton.setAttribute("aria-expanded", true);
647
 
648
        // Position it next to the button which opened it.
649
        menuDialogue.align(this.buttons[config.buttonName], [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
650
 
651
        this.get('host').openMenus = [menuDialogue];
652
    },
653
 
654
    /**
655
     * Display a toolbar menu and focus upon the first item.
656
     *
657
     * @method _showToolbarMenuAndFocus
658
     * @param {EventFacade} e
659
     * @param {object} config The configuration for the whole toolbar.
660
     * @param {Number} [config.overlayWidth=14] The width of the menu
661
     * @private
662
     */
663
    _showToolbarMenuAndFocus: function(e, config) {
664
        this._showToolbarMenu(e, config);
665
 
666
        // Focus on the first element in the menu.
667
        this.menus[config.buttonClass].get('boundingBox').one('a').focus();
668
    },
669
 
670
    /**
671
     * Select a menu item and call the appropriate callbacks.
672
     *
673
     * @method _chooseMenuItem
674
     * @param {EventFacade} e
675
     * @param {object} config
676
     * @param {M.core.dialogue} menuDialogue The Dialogue to hide.
677
     * @private
678
     */
679
    _chooseMenuItem: function(e, config, menuDialogue) {
680
        // Get the index from the clicked anchor.
681
        var index = e.target.ancestor('a', true).getData('index'),
682
 
683
            // And the normalized callback configuration.
684
            buttonConfig = this._normalizeCallback(config.items[index], config.globalItemConfig);
685
 
686
            menuDialogue = this.menus[config.buttonClass];
687
 
688
        // Prevent the dialogue to be closed because of some browser weirdness.
689
        menuDialogue.set('preventHideMenu', true);
690
 
691
        // Call the callback for this button.
692
        buttonConfig.callback(e, buttonConfig._callback, buttonConfig.callbackArgs);
693
 
694
        // Cancel the hide menu prevention.
695
        menuDialogue.set('preventHideMenu', false);
696
 
697
        // Set the focus after hide so that focus is returned to the editor and changes are made correctly.
698
        menuDialogue.set('focusAfterHide', this.get('host').editor);
699
        menuDialogue.hide(e);
700
    },
701
 
702
    /**
703
     * Normalize and sanitize the configuration variables relating to callbacks.
704
     *
705
     * @method _normalizeCallback
706
     * @param {object} config
707
     * @param {function} config.callback A callback function to call when the button is clicked.
708
     * @param {object} [config.callbackArgs] Any arguments to pass to the callback.
709
     * @param {boolean} [config.inlineFormat] Delay callback for text input if selection is collapsed.
710
     * @param {object} [inheritFrom] A parent configuration that this configuration may inherit from.
711
     * @return {object} The normalized configuration
712
     * @private
713
     */
714
    _normalizeCallback: function(config, inheritFrom) {
715
        if (config._callbackNormalized) {
716
            // Return early if the callback has already been normalized.
717
            return config;
718
        }
719
 
720
        if (!inheritFrom) {
721
            // Create an empty inheritFrom to make life easier below.
722
            inheritFrom = {};
723
        }
724
 
725
 
726
        // First we wrap the callback in function to handle formating of text inserted into collapsed selection.
727
        config.inlineFormat = config.inlineFormat || inheritFrom.inlineFormat;
728
        config._inlineCallback = config.callback || inheritFrom.callback;
729
        config._callback = config.callback || inheritFrom.callback;
730
        if (config.inlineFormat && typeof config._inlineCallback === 'function') {
731
            config._callback = function(e, args) {
732
                this.get('host').applyFormat(e, config._inlineCallback, this, args);
733
            };
734
        }
735
        // We wrap the callback in function to prevent the default action, check whether the editor is
736
        // active and focus it, and then mark the field as updated.
737
        config.callback = Y.rbind(this._callbackWrapper, this, config._callback, config.callbackArgs);
738
 
739
        config._callbackNormalized = true;
740
 
741
        return config;
742
    },
743
 
744
    /**
745
     * Normalize and sanitize the configuration variables relating to icons.
746
     *
747
     * @method _normalizeIcon
748
     * @param {object} config
749
     * @param {string} [config.iconurl] The URL for the icon. If not specified, then the icon and component will be used instead.
750
     * @param {string} [config.icon] The icon identifier.
751
     * @param {string} [config.iconComponent='core'] The icon component.
752
     * @return {object} The normalized configuration
753
     * @private
754
     */
755
    _normalizeIcon: function(config) {
756
        if (!config.iconurl) {
757
            // The default icon component.
758
            if (!config.iconComponent || config.iconComponent == 'moodle') {
759
                config.iconComponent = 'core';
760
            }
761
            config.iconurl = M.util.image_url(config.icon, config.iconComponent);
762
        }
763
 
764
        return config;
765
    },
766
 
767
    /**
768
     * A wrapper in which to run the callbacks.
769
     *
770
     * This handles common functionality such as:
771
     * <ul>
772
     *  <li>preventing the default action; and</li>
773
     *  <li>focusing the editor if relevant.</li>
774
     * </ul>
775
     *
776
     * @method _callbackWrapper
777
     * @param {EventFacade} e
778
     * @param {Function} callback The function to call which makes the relevant changes.
779
     * @param {Array} [callbackArgs] The arguments passed to this callback.
780
     * @return {Mixed} The value returned by the callback.
781
     * @private
782
     */
783
    _callbackWrapper: function(e, callback, callbackArgs) {
784
        e.preventDefault();
785
 
786
        if (!this.isEnabled()) {
787
            // Exit early if the plugin is disabled.
788
            return;
789
        }
790
 
791
        var creatorButton = e.currentTarget.ancestor('button', true);
792
 
793
        if (creatorButton && creatorButton.hasAttribute(DISABLED)) {
794
            // Exit early if the clicked button was disabled.
795
            return;
796
        }
797
 
798
        if (!(YUI.Env.UA.android || this.get('host').isActive())) {
799
            // We must not focus for Android here, even if the editor is not active because the keyboard auto-completion
800
            // changes the cursor position.
801
            // If we save that change, then when we restore the change later we get put in the wrong place.
802
            // Android is fine to save the selection without the editor being in focus.
803
            this.get('host').focus();
804
        }
805
 
806
        // Save the selection.
807
        this.get('host').saveSelection();
808
 
809
        // Ensure that we focus on this button next time.
810
        if (creatorButton) {
811
            this.get('host')._setTabFocus(creatorButton);
812
        }
813
 
814
        // Build the arguments list, but remove the callback we're calling.
815
        var args = [e, callbackArgs];
816
 
817
        // Restore selection before making changes.
818
        this.get('host').restoreSelection();
819
 
820
        // Actually call the callback now.
821
        return callback.apply(this, args);
822
    },
823
 
824
    /**
825
     * Add a keyboard listener to call the callback.
826
     *
827
     * The keyConfig will take either an array of keyConfigurations, in
828
     * which case _addKeyboardListener is called multiple times; an object
829
     * containing an optional eventtype, optional container, and a set of
830
     * keyCodes, or just a string containing the keyCodes. When keyConfig is
831
     * not an object, it is wrapped around a function that ensures that
832
     * only the expected key modifiers were used. For instance, it checks
833
     * that space+ctrl is not triggered when the user presses ctrl+shift+space.
834
     * When using an object, the developer should check that manually.
835
     *
836
     * @method _addKeyboardListener
837
     * @param {function} callback
838
     * @param {array|object|string} keyConfig
839
     * @param {string} [keyConfig.eventtype=key] The type of event
840
     * @param {string} [keyConfig.container=.editor_atto_content] The containing element.
841
     * @param {string} keyConfig.keyCodes The keycodes to user for the event.
842
     * @private
843
     *
844
     */
845
    _addKeyboardListener: function(callback, keyConfig, buttonName) {
846
        var eventtype = 'key',
847
            container = CSS.EDITORWRAPPER,
848
            keys,
849
            handler,
850
            modifier;
851
 
852
        if (Y.Lang.isArray(keyConfig)) {
853
            // If an Array was specified, call the add function for each element.
854
            Y.Array.each(keyConfig, function(config) {
855
                this._addKeyboardListener(callback, config);
856
            }, this);
857
 
858
            return this;
859
 
860
        } else if (typeof keyConfig === "object") {
861
            if (keyConfig.eventtype) {
862
                eventtype = keyConfig.eventtype;
863
            }
864
 
865
            if (keyConfig.container) {
866
                container = keyConfig.container;
867
            }
868
 
869
            // Must be specified.
870
            keys = keyConfig.keyCodes;
871
            handler = callback;
872
 
873
        } else {
874
            modifier = this._getDefaultMetaKey();
875
            keys = this._getKeyEvent() + keyConfig + '+' + modifier;
876
            if (typeof this._primaryKeyboardShortcut[buttonName] === 'undefined') {
877
                this._primaryKeyboardShortcut[buttonName] = this._getDefaultMetaKeyDescription(keyConfig);
878
            }
879
 
880
            // Wrap the callback into a handler to check if it uses the specified modifiers, not more.
881
            handler = Y.bind(function(modifiers, e) {
882
                if (this._eventUsesExactKeyModifiers(modifiers, e)) {
883
                    callback.apply(this, [e]);
884
                }
885
            }, this, [modifier]);
886
        }
887
 
888
        this._buttonHandlers.push(
889
            this.editor.delegate(
890
                eventtype,
891
                handler,
892
                keys,
893
                container,
894
                this
895
            )
896
        );
897
 
898
    },
899
 
900
    /**
901
     * Checks if a key event was strictly defined for the modifiers passed.
902
     *
903
     * @method _eventUsesExactKeyModifiers
904
     * @param  {Array} modifiers List of key modifiers to check for (alt, ctrl, meta or shift).
905
     * @param  {EventFacade} e The event facade.
906
     * @return {Boolean} True if the event was stricly using the modifiers specified.
907
     */
908
    _eventUsesExactKeyModifiers: function(modifiers, e) {
909
        var exactMatch = true,
910
            hasKey;
911
 
912
        if (e.type !== 'key') {
913
            return false;
914
        }
915
 
916
        hasKey = Y.Array.indexOf(modifiers, 'alt') > -1;
917
        exactMatch = exactMatch && ((e.altKey && hasKey) || (!e.altKey && !hasKey));
918
        hasKey = Y.Array.indexOf(modifiers, 'ctrl') > -1;
919
        exactMatch = exactMatch && ((e.ctrlKey && hasKey) || (!e.ctrlKey && !hasKey));
920
        hasKey = Y.Array.indexOf(modifiers, 'meta') > -1;
921
        exactMatch = exactMatch && ((e.metaKey && hasKey) || (!e.metaKey && !hasKey));
922
        hasKey = Y.Array.indexOf(modifiers, 'shift') > -1;
923
        exactMatch = exactMatch && ((e.shiftKey && hasKey) || (!e.shiftKey && !hasKey));
924
 
925
        return exactMatch;
926
    },
927
 
928
    /**
929
     * Determine if this plugin is enabled, based upon the state of it's buttons.
930
     *
931
     * @method isEnabled
932
     * @return {boolean}
933
     */
934
    isEnabled: function() {
935
        // The first instance of an undisabled button will make this return true.
936
        var found = Y.Object.some(this.buttonStates, function(button) {
937
            return (button === this.ENABLED);
938
        }, this);
939
 
940
        return found;
941
    },
942
 
943
    /**
944
     * Enable one button, or all buttons relating to this Plugin.
945
     *
946
     * If no button is specified, all buttons are disabled.
947
     *
948
     * @method disableButtons
949
     * @param {String} [button] The name of a specific plugin to enable.
950
     * @chainable
951
     */
952
    disableButtons: function(button) {
953
        return this._setButtonState(false, button);
954
    },
955
 
956
    /**
957
     * Enable one button, or all buttons relating to this Plugin.
958
     *
959
     * If no button is specified, all buttons are enabled.
960
     *
961
     * @method enableButtons
962
     * @param {String} [button] The name of a specific plugin to enable.
963
     * @chainable
964
     */
965
    enableButtons: function(button) {
966
        return this._setButtonState(true, button);
967
    },
968
 
969
    /**
970
     * Set the button state for one button, or all buttons associated with this plugin.
971
     *
972
     * @method _setButtonState
973
     * @param {Boolean} enable Whether to enable this button.
974
     * @param {String} [button] The name of a specific plugin to set state for.
975
     * @chainable
976
     * @private
977
     */
978
    _setButtonState: function(enable, button) {
979
        var attributeChange = 'setAttribute';
980
        if (enable) {
981
            attributeChange = 'removeAttribute';
982
        }
983
        if (button) {
984
            if (this.buttons[button]) {
985
                this.buttons[button][attributeChange](DISABLED, DISABLED);
986
                this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED;
987
            }
988
        } else {
989
            Y.Array.each(this.buttonNames, function(button) {
990
                this.buttons[button][attributeChange](DISABLED, DISABLED);
991
                this.buttonStates[button] = enable ? this.ENABLED : this.DISABLED;
992
            }, this);
993
        }
994
 
995
        this.get('host').checkTabFocus();
996
        return this;
997
    },
998
 
999
    /**
1000
     * Highlight a button, or buttons in the toolbar.
1001
     *
1002
     * If no button is specified, all buttons are highlighted.
1003
     *
1004
     * @method highlightButtons
1005
     * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight.
1006
     * @chainable
1007
     */
1008
    highlightButtons: function(button) {
1009
        return this._changeButtonHighlight(true, button);
1010
    },
1011
 
1012
    /**
1013
     * Un-highlight a button, or buttons in the toolbar.
1014
     *
1015
     * If no button is specified, all buttons are un-highlighted.
1016
     *
1017
     * @method unHighlightButtons
1018
     * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight.
1019
     * @chainable
1020
     */
1021
    unHighlightButtons: function(button) {
1022
        return this._changeButtonHighlight(false, button);
1023
    },
1024
 
1025
    /**
1026
     * Highlight a button, or buttons in the toolbar.
1027
     *
1028
     * @method _changeButtonHighlight
1029
     * @param {boolean} highlight true
1030
     * @param {string} [button] If a plugin has multiple buttons, the specific button to highlight.
1031
     * @protected
1032
     * @chainable
1033
     */
1034
    _changeButtonHighlight: function(highlight, button) {
1035
        var method = 'addClass';
1036
 
1037
        if (!highlight) {
1038
            method = 'removeClass';
1039
        }
1040
        if (button) {
1041
            if (this.buttons[button]) {
1042
                this.buttons[button][method](HIGHLIGHT);
1043
                this.buttons[button].setAttribute('aria-pressed', highlight ? 'true' : 'false');
1044
                this._buttonHighlightToggled(button, highlight);
1045
            }
1046
        } else {
1047
            Y.Object.each(this.buttons, function(button) {
1048
                button[method](HIGHLIGHT);
1049
                button.setAttribute('aria-pressed', highlight ? 'true' : 'false');
1050
                this._buttonHighlightToggled(button, highlight);
1051
            }, this);
1052
        }
1053
 
1054
        return this;
1055
    },
1056
 
1057
    /**
1058
     * Fires a custom event that notifies listeners that a button's highlight has been toggled.
1059
     *
1060
     * @param {String} buttonName The button name.
1061
     * @param {Boolean} highlight True when the button was highlighted. False, otherwise.
1062
     * @private
1063
     */
1064
    _buttonHighlightToggled: function(buttonName, highlight) {
1065
        var toggledButton = this.buttons[buttonName];
1066
        if (toggledButton) {
1067
            // Fire an event that the button highlight was toggled.
1068
            require(['editor_atto/events'], function(attoEvents) {
1069
                attoEvents.notifyButtonHighlightToggled(toggledButton.getDOMNode(), buttonName, highlight);
1070
            });
1071
        }
1072
    },
1073
 
1074
    /**
1075
     * Get the default meta key to use with keyboard events.
1076
     *
1077
     * On a Mac, this will be the 'meta' key for Command; otherwise it will
1078
     * be the Control key.
1079
     *
1080
     * @method _getDefaultMetaKey
1081
     * @return {string}
1082
     * @private
1083
     */
1084
    _getDefaultMetaKey: function() {
1085
        if (Y.UA.os === 'macintosh') {
1086
            return 'meta';
1087
        } else {
1088
            return 'ctrl';
1089
        }
1090
    },
1091
 
1092
    /**
1093
     * Get the user-visible description of the meta key to use with keyboard events.
1094
     *
1095
     * On a Mac, this will be 'Command' ; otherwise it will be 'Control'.
1096
     *
1097
     * @method _getDefaultMetaKeyDescription
1098
     * @return {string}
1099
     * @private
1100
     */
1101
    _getDefaultMetaKeyDescription: function(keyCode) {
1102
        if (Y.UA.os === 'macintosh') {
1103
            return M.util.get_string('editor_command_keycode', 'editor_atto', String.fromCharCode(keyCode).toLowerCase());
1104
        } else {
1105
            return M.util.get_string('editor_control_keycode', 'editor_atto', String.fromCharCode(keyCode).toLowerCase());
1106
        }
1107
    },
1108
 
1109
    /**
1110
     * Get the standard key event to use for keyboard events.
1111
     *
1112
     * @method _getKeyEvent
1113
     * @return {string}
1114
     * @private
1115
     */
1116
    _getKeyEvent: function() {
1117
        return 'down:';
1118
    }
1119
};
1120
 
1121
Y.Base.mix(Y.M.editor_atto.EditorPlugin, [EditorPluginButtons]);
1122
// This file is part of Moodle - http://moodle.org/
1123
//
1124
// Moodle is free software: you can redistribute it and/or modify
1125
// it under the terms of the GNU General Public License as published by
1126
// the Free Software Foundation, either version 3 of the License, or
1127
// (at your option) any later version.
1128
//
1129
// Moodle is distributed in the hope that it will be useful,
1130
// but WITHOUT ANY WARRANTY; without even the implied warranty of
1131
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1132
// GNU General Public License for more details.
1133
//
1134
// You should have received a copy of the GNU General Public License
1135
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
1136
 
1137
/**
1138
 * @module moodle-editor_atto-plugin
1139
 * @submodule dialogue
1140
 */
1141
 
1142
/**
1143
 * Dialogue functions for an Atto Plugin.
1144
 *
1145
 * See {{#crossLink "M.editor_atto.EditorPlugin"}}{{/crossLink}} for details.
1146
 *
1147
 * @namespace M.editor_atto
1148
 * @class EditorPluginDialogue
1149
 */
1150
 
1151
function EditorPluginDialogue() {}
1152
 
1153
EditorPluginDialogue.ATTRS = {
1154
};
1155
 
1156
EditorPluginDialogue.prototype = {
1157
    /**
1158
     * A reference to the instantiated dialogue.
1159
     *
1160
     * @property _dialogue
1161
     * @private
1162
     * @type M.core.Dialogue
1163
     */
1164
    _dialogue: null,
1165
 
1166
    /**
1167
     * Fetch the instantiated dialogue. If a dialogue has not yet been created, instantiate one.
1168
     *
1169
     * <em><b>Note:</b> Only one dialogue is supported through this interface.</em>
1170
     *
1171
     * For a full list of options, see documentation for {{#crossLink "M.core.dialogue"}}{{/crossLink}}.
1172
     *
1173
     * A sensible default is provided for the focusAfterHide attribute.
1174
     *
1175
     * @method getDialogue
1176
     * @param {object} config
1177
     * @param {boolean|string|Node} [config.focusAfterHide=undefined] Set the focusAfterHide setting to the
1178
     * specified Node according to the following values:
1179
     * <ul>
1180
     * <li>If true was passed, the first button for this plugin will be used instead; or</li>
1181
     * <li>If a String was passed, the named button for this plugin will be used instead; or</li>
1182
     * <li>If a Node was passed, that Node will be used instead.</li>
1183
     *
1184
     * This setting is checked each time that getDialogue is called.
1185
     *
1186
     * @return {M.core.dialogue}
1187
     */
1188
    getDialogue: function(config) {
1189
        // Config is an optional param - define a default.
1190
        config = config || {};
1191
 
1192
        var focusAfterHide = false;
1193
        if (config.focusAfterHide) {
1194
            // Remove the focusAfterHide because we may pass it a non-node value.
1195
            focusAfterHide = config.focusAfterHide;
1196
            delete config.focusAfterHide;
1197
        }
1198
 
1199
        if (!this._dialogue) {
1200
            // Merge the default configuration with any provided configuration.
1201
            var dialogueConfig = Y.merge({
1202
                    visible: false,
1203
                    modal: true,
1204
                    close: true,
1205
                    draggable: true
1206
                }, config);
1207
 
1208
            // Instantiate the dialogue.
1209
            this._dialogue = new M.core.dialogue(dialogueConfig);
1210
        }
1211
 
1212
        if (focusAfterHide !== false) {
1213
            if (focusAfterHide === true) {
1214
                this._dialogue.set('focusAfterHide', this.buttons[this.buttonNames[0]]);
1215
 
1216
            } else if (typeof focusAfterHide === 'string') {
1217
                this._dialogue.set('focusAfterHide', this.buttons[focusAfterHide]);
1218
 
1219
            } else {
1220
                this._dialogue.set('focusAfterHide', focusAfterHide);
1221
 
1222
            }
1223
        }
1224
 
1225
        return this._dialogue;
1226
    }
1227
};
1228
 
1229
Y.Base.mix(Y.M.editor_atto.EditorPlugin, [EditorPluginDialogue]);
1230
 
1231
 
1232
}, '@VERSION@', {
1233
    "requires": [
1234
        "node",
1235
        "base",
1236
        "escape",
1237
        "event",
1238
        "event-outside",
1239
        "handlebars",
1240
        "event-custom",
1241
        "timers",
1242
        "moodle-editor_atto-menu"
1243
    ]
1244
});