MDL-42026 Atto: Remove the styles from the headings menu + focus when opened
[moodle.git] / lib / editor / atto / yui / build / moodle-editor_atto-editor / moodle-editor_atto-editor-debug.js
1 YUI.add('moodle-editor_atto-editor', function (Y, NAME) {
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/>.
18 /**
19  * Atto editor main class.
20  * Common functions required by editor plugins.
21  *
22  * @package    editor-atto
23  * @copyright  2013 Damyon Wiese  <damyon@moodle.com>
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
26 M.editor_atto = M.editor_atto || {
27     /**
28      * List of attached button handlers to prevent duplicates.
29      */
30     buttonhandlers : {},
32     /**
33      * List of YUI overlays for custom menus.
34      */
35     menus : {},
37     /**
38      * List of attached menu handlers to prevent duplicates.
39      */
40     menuhandlers : {},
42     /**
43      * List of file picker options for specific editor instances.
44      */
45     filepickeroptions : {},
47     /**
48      * List of buttons and menus that have been added to the toolbar.
49      */
50     widgets : {},
52     /**
53      * Toggle a menu.
54      * @param event e
55      */
56     showhide_menu_handler : function(e) {
57         e.preventDefault();
58         var disabled = this.getAttribute('disabled');
59         var overlayid = this.getAttribute('data-menu');
60         var overlay = M.editor_atto.menus[overlayid];
61         var menu = overlay.get('bodyContent');
62         if (overlay.get('visible') || disabled) {
63             overlay.hide();
64             menu.detach('clickoutside');
65         } else {
66             menu.on('clickoutside', function(ev) {
67                 if ((ev.target.ancestor() !== this) && (ev.target !== this)) {
68                     if (overlay.get('visible')) {
69                         menu.detach('clickoutside');
70                         overlay.hide();
71                     }
72                 }
73             }, this);
74             overlay.show();
75             overlay.bodyNode.one('a').focus();
76         }
77     },
79     /**
80      * Handle clicks on editor buttons.
81      * @param event e
82      */
83     buttonclicked_handler : function(e) {
84         var elementid = this.getAttribute('data-editor');
85         var plugin = this.getAttribute('data-plugin');
86         var handler = this.getAttribute('data-handler');
87         var overlay = M.editor_atto.menus[plugin + '_' + elementid];
89         if (overlay) {
90             overlay.hide();
91         }
93         if (M.editor_atto.is_enabled(elementid, plugin)) {
94             // Pass it on.
95             handler = M.editor_atto.buttonhandlers[handler];
96             return handler(e, elementid);
97         }
98     },
100     /**
101      * Determine if the specified toolbar button/menu is enabled.
102      * @param string elementid, the element id of this editor.
103      * @param string plugin, the plugin that created the button/menu.
104      */
105     is_enabled : function(elementid, plugin) {
106         var element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
108         return !element.hasAttribute('disabled');
109     },
110     /**
111      * Disable all buttons and menus in the toolbar.
112      * @param string elementid, the element id of this editor.
113      */
114     disable_all_widgets : function(elementid) {
115         var plugin, element;
116         for (plugin in M.editor_atto.widgets) {
117             element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
119             if (element) {
120                 element.setAttribute('disabled', 'true');
121             }
122         }
123     },
125     /**
126      * Enable a single widget in the toolbar.
127      * @param string elementid, the element id of this editor.
128      * @param string plugin, the name of the plugin that created the widget.
129      */
130     enable_widget : function(elementid, plugin) {
131         var element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
133         if (element) {
134             element.removeAttribute('disabled');
135         }
136     },
138     /**
139      * Enable all buttons and menus in the toolbar.
140      * @param string elementid, the element id of this editor.
141      */
142     enable_all_widgets : function(elementid) {
143         var plugin, element;
144         for (plugin in M.editor_atto.widgets) {
145             element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
147             if (element) {
148                 element.removeAttribute('disabled');
149             }
150         }
151     },
153     /**
154      * Add a button to the toolbar belonging to the editor for element with id "elementid".
155      * @param string elementid - the id of the textarea we created this editor from.
156      * @param string plugin - the plugin defining the button
157      * @param string icon - the html used for the content of the button
158      * @param string groupname - the group the button should be appended to.
159      * @handler function handler- A function to call when the button is clicked.
160      */
161     add_toolbar_menu : function(elementid, plugin, icon, groupname, entries) {
162         var toolbar = Y.one('#' + elementid + '_toolbar'),
163             group = Y.one('#' + elementid + '_toolbar .atto_group.' + groupname + '_group'),
164             currentfocus,
165             button;
167         if (!group) {
168             group = Y.Node.create('<div class="atto_group ' + groupname + '_group"></div>');
169             toolbar.append(group);
170         }
171         button = Y.Node.create('<button class="atto_' + plugin + '_button atto_hasmenu" ' +
172                                     'data-editor="' + Y.Escape.html(elementid) + '" ' +
173                                     'tabindex="-1" ' +
174                                     'data-menu="' + plugin + '_' + elementid + '" >' +
175                                     icon +
176                                     '</button>');
178         group.append(button);
180         currentfocus = toolbar.getAttribute('aria-activedescendant');
181         if (!currentfocus) {
182             button.setAttribute('tabindex', '0');
183             toolbar.setAttribute('aria-activedescendant', button.generateID());
184         }
186         // Save the name of the plugin.
187         M.editor_atto.widgets[plugin] = plugin;
189         var menu = Y.Node.create('<div class="atto_' + plugin + '_menu' +
190                                  ' atto_menu" data-editor="' + Y.Escape.html(elementid) + '"></div>');
191         var i = 0, entry = {};
193         for (i = 0; i < entries.length; i++) {
194             entry = entries[i];
196             menu.append(Y.Node.create('<div class="atto_menuentry">' +
197                                        '<a href="#" class="atto_' + plugin + '_action_' + i + '" ' +
198                                        'data-editor="' + Y.Escape.html(elementid) + '" ' +
199                                        'data-plugin="' + Y.Escape.html(plugin) + '" ' +
200                                        'data-handler="' + Y.Escape.html(plugin + '_action_' + i) + '">' +
201                                        entry.text +
202                                        '</a>' +
203                                        '</div>'));
204             if (!M.editor_atto.buttonhandlers[plugin + '_action_' + i]) {
205                 Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_action_' + i);
206                 Y.one('body').delegate('key', M.editor_atto.buttonclicked_handler, 'space,enter', '.atto_' + plugin + '_action_' + i);
207                 M.editor_atto.buttonhandlers[plugin + '_action_' + i] = entry.handler;
208             }
209         }
211         if (!M.editor_atto.buttonhandlers[plugin]) {
212             Y.one('body').delegate('click', M.editor_atto.showhide_menu_handler, '.atto_' + plugin + '_button');
213             M.editor_atto.buttonhandlers[plugin] = true;
214         }
216         var overlay = new M.core.dialogue({
217             bodyContent : menu,
218             visible : false,
219             width: '14em',
220             zindex: 100,
221             lightbox: false,
222             closeButton: false,
223             centered : false,
224             align: {node: button, points: [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]}
225         });
227         M.editor_atto.menus[plugin + '_' + elementid] = overlay;
228         overlay.render();
229         overlay.hide();
230         overlay.headerNode.hide();
231     },
233     /**
234      * Add a button to the toolbar belonging to the editor for element with id "elementid".
235      * @param string elementid - the id of the textarea we created this editor from.
236      * @param string plugin - the plugin defining the button.
237      * @param string icon - the html used for the content of the button.
238      * @param string groupname - the group the button should be appended to.
239      * @handler function handler- A function to call when the button is clicked.
240      */
241     add_toolbar_button : function(elementid, plugin, icon, groupname, handler) {
242         var toolbar = Y.one('#' + elementid + '_toolbar'),
243             group = Y.one('#' + elementid + '_toolbar .atto_group.' + groupname + '_group'),
244             button,
245             currentfocus;
247         if (!group) {
248             group = Y.Node.create('<div class="atto_group ' + groupname +'_group"></div>');
249             toolbar.append(group);
250         }
251         button = Y.Node.create('<button class="atto_' + plugin + '_button" ' +
252                                'data-editor="' + Y.Escape.html(elementid) + '" ' +
253                                'data-plugin="' + Y.Escape.html(plugin) + '" ' +
254                                'tabindex="-1" ' +
255                                'data-handler="' + Y.Escape.html(plugin) + '">' +
256                                icon +
257                                '</button>');
259         group.append(button);
261         currentfocus = toolbar.getAttribute('aria-activedescendant');
262         if (!currentfocus) {
263             button.setAttribute('tabindex', '0');
264             toolbar.setAttribute('aria-activedescendant', button.generateID());
265         }
267         // We only need to attach this once.
268         if (!M.editor_atto.buttonhandlers[plugin]) {
269             Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_button');
270             M.editor_atto.buttonhandlers[plugin] = handler;
271         }
273         // Save the name of the plugin.
274         M.editor_atto.widgets[plugin] = plugin;
276     },
278     /**
279      * Work out if the cursor is in the editable area for this editor instance.
280      * @param string elementid of this editor
281      * @return bool
282      */
283     is_active : function(elementid) {
284         var selection = M.editor_atto.get_selection();
286         if (selection.length) {
287             selection = selection.pop();
288         }
290         var node = null;
291         if (selection.parentElement) {
292             node = Y.one(selection.parentElement());
293         } else {
294             node = Y.one(selection.startContainer);
295         }
297         return node && node.ancestor('#' + elementid + 'editable') !== null;
298     },
300     /**
301      * Focus on the editable area for this editor.
302      * @param string elementid of this editor
303      */
304     focus : function(elementid) {
305         Y.one('#' + elementid + 'editable').focus();
306     },
308     /**
309      * Initialise the editor
310      * @param object params for this editor instance.
311      */
312     init : function(params) {
313         var textarea = Y.one('#' +params.elementid);
314         var atto = Y.Node.create('<div id="' + params.elementid + 'editable" ' +
315                                             'contenteditable="true" ' +
316                                             'spellcheck="true" ' +
317                                             'class="editor_atto"/>');
318         var cssfont = '';
319         var toolbar = Y.Node.create('<div class="editor_atto_toolbar" id="' + params.elementid + '_toolbar" role="toolbar"/>');
321         // Bleh - why are we sent a url and not the css to apply directly?
322         var css = Y.io(params.content_css, { sync: true });
323         var pos = css.responseText.indexOf('font:');
324         if (pos) {
325             cssfont = css.responseText.substring(pos + 'font:'.length, css.responseText.length - 1);
326             atto.setStyle('font', cssfont);
327         }
328         atto.setStyle('minHeight', (1.2 * (textarea.getAttribute('rows') - 1)) + 'em');
330         // Copy text to editable div.
331         atto.append(textarea.get('value'));
333         // Add the toolbar to the page.
334         textarea.get('parentNode').insert(toolbar, textarea);
335         // Add the editable div to the page.
336         textarea.get('parentNode').insert(atto, textarea);
337         atto.setStyle('color', textarea.getStyle('color'));
338         atto.setStyle('lineHeight', textarea.getStyle('lineHeight'));
339         atto.setStyle('fontSize', textarea.getStyle('fontSize'));
340         // Hide the old textarea.
341         textarea.hide();
343         // Copy the current value back to the textarea when focus leaves us.
344         atto.on('blur', function() {
345             textarea.set('value', atto.getHTML());
346         });
348         // Listen for Arrow left and Arrow right keys.
349         Y.one(Y.config.doc.body).delegate('key',
350                                           this.keyboard_navigation,
351                                           'down:37,39',
352                                           '#' + params.elementid + '_toolbar',
353                                           this,
354                                           params.elementid);
356         // Save the file picker options for later.
357         M.editor_atto.filepickeroptions[params.elementid] = params.filepickeroptions;
358     },
360     /**
361      * Implement arrow key navigation for the buttons in the toolbar.
362      * @param Event e - the keyboard event.
363      * @param string elementid - the id of the textarea we created this editor from.
364      */
365     keyboard_navigation : function(e, elementid) {
366         var buttons,
367             current,
368             currentid,
369             currentindex;
371         e.preventDefault();
373         buttons = Y.all('#' + elementid + '_toolbar button');
374         currentid = Y.one('#' + elementid + '_toolbar').getAttribute('aria-activedescendant');
375         if (!currentid) {
376             return;
377         }
378         current = Y.one('#' + currentid);
379         current.setAttribute('tabindex', '-1');
381         currentindex = buttons.indexOf(current);
383         if (e.keyCode === 37) {
384             // Left
385             currentindex--;
386             if (currentindex < 0) {
387                 currentindex = buttons.size()-1;
388             }
389         } else {
390             // Right
391             currentindex++;
392             if (currentindex >= buttons.size()) {
393                 currentindex = 0;
394             }
395         }
397         current = buttons.item(currentindex);
398         current.setAttribute('tabindex', '0');
399         current.focus();
400         Y.one('#' + elementid + '_toolbar').setAttribute('aria-activedescendant', current.generateID());
401     },
403     /**
404      * Show the filepicker.
405      * @param string elementid for this editor instance.
406      * @param string type The media type for the file picker
407      * @param function callback
408      */
409     show_filepicker : function(elementid, type, callback) {
410         Y.use('core_filepicker', function (Y) {
411             var options = M.editor_atto.filepickeroptions[elementid][type];
413             options.formcallback = callback;
414             options.editor_target = Y.one(elementid);
416             M.core_filepicker.show(Y, options);
417         });
418     },
420     /**
421      * Create a cross browser selection object that represents a yui node.
422      * @param Node yui node for the selection
423      * @return range (browser dependent)
424      */
425     get_selection_from_node: function(node) {
426         var range;
428         if (window.getSelection) {
429             range = document.createRange();
431             range.setStartBefore(node.getDOMNode());
432             range.setEndAfter(node.getDOMNode());
433             return [range];
434         } else if (document.selection) {
435             range = document.body.createTextRange();
436             range.moveToElementText(node.getDOMNode());
437             return range;
438         }
439         return false;
440     },
442     /**
443      * Get the selection object that can be passed back to set_selection.
444      * @return range (browser dependent)
445      */
446     get_selection : function() {
447         if (window.getSelection) {
448             var sel = window.getSelection();
449             var ranges = [], i = 0;
450             for (i = 0; i < sel.rangeCount; i++) {
451                 ranges.push(sel.getRangeAt(i));
452             }
453             return ranges;
454         } else if (document.selection) {
455             // IE < 9
456             if (document.selection.createRange) {
457                 return document.selection.createRange();
458             }
459         }
460         return false;
461     },
463     /**
464      * Get the dom node representing the common anscestor of the selection nodes.
465      * @return DOMNode
466      */
467     get_selection_parent_node : function() {
468         var selection = M.editor_atto.get_selection();
469         if (selection.length > 0) {
470             return selection[0].commonAncestorContainer;
471         }
472     },
474     /**
475      * Get the list of child nodes of the selection.
476      * @return DOMNode[]
477      */
478     get_selection_text : function() {
479         var selection = M.editor_atto.get_selection();
480         if (selection.length > 0 && selection[0].cloneContents) {
481             return selection[0].cloneContents();
482         }
483     },
485     /**
486      * Set the current selection. Used to restore a selection.
487      */
488     set_selection : function(selection) {
489         var sel, i;
491         if (window.getSelection) {
492             sel = window.getSelection();
493             sel.removeAllRanges();
494             for (i = 0; i < selection.length; i++) {
495                 sel.addRange(selection[i]);
496             }
497         } else if (document.selection) {
498             // IE < 9
499             if (selection.select) {
500                 selection.select();
501             }
502         }
503     }
505 };
508 }, '@VERSION@', {"requires": ["node", "io", "overlay", "escape", "event-key", "moodle-core-notification"]});