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.js
CommitLineData
c90641fa
DW
1YUI.add('moodle-editor_atto-editor', 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 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 */
26M.editor_atto = M.editor_atto || {
27 /**
28 * List of attached button handlers to prevent duplicates.
29 */
30 buttonhandlers : {},
31
32 /**
33 * List of YUI overlays for custom menus.
34 */
35 menus : {},
36
37 /**
38 * List of attached menu handlers to prevent duplicates.
39 */
40 menuhandlers : {},
41
42 /**
43 * List of file picker options for specific editor instances.
44 */
45 filepickeroptions : {},
46
47 /**
48 * List of buttons and menus that have been added to the toolbar.
49 */
50 widgets : {},
51
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];
21f6c529 61 var menu = overlay.get('bodyContent');
c90641fa
DW
62 if (overlay.get('visible') || disabled) {
63 overlay.hide();
21f6c529 64 menu.detach('clickoutside');
c90641fa 65 } else {
21f6c529 66 menu.on('clickoutside', function(ev) {
c1f10ffb 67 if ((ev.target.ancestor() !== this) && (ev.target !== this)) {
21f6c529
JF
68 if (overlay.get('visible')) {
69 menu.detach('clickoutside');
70 overlay.hide();
71 }
72 }
73 }, this);
c90641fa 74 overlay.show();
bb76233d 75 overlay.bodyNode.one('a').focus();
c90641fa 76 }
c90641fa
DW
77 },
78
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];
88
89 if (overlay) {
90 overlay.hide();
91 }
92
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 },
99
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');
107
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');
118
119 if (element) {
120 element.setAttribute('disabled', 'true');
121 }
122 }
123 },
124
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');
132
133 if (element) {
134 element.removeAttribute('disabled');
135 }
136 },
137
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');
146
147 if (element) {
148 element.removeAttribute('disabled');
149 }
150 }
151 },
152
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
36973d70 158 * @param string groupname - the group the button should be appended to.
c90641fa
DW
159 * @handler function handler- A function to call when the button is clicked.
160 */
36973d70
JF
161 add_toolbar_menu : function(elementid, plugin, icon, groupname, entries) {
162 var toolbar = Y.one('#' + elementid + '_toolbar'),
6f9cf841
DW
163 group = Y.one('#' + elementid + '_toolbar .atto_group.' + groupname + '_group'),
164 currentfocus,
165 button;
166
36973d70
JF
167 if (!group) {
168 group = Y.Node.create('<div class="atto_group ' + groupname + '_group"></div>');
169 toolbar.append(group);
170 }
6f9cf841 171 button = Y.Node.create('<button class="atto_' + plugin + '_button atto_hasmenu" ' +
c90641fa 172 'data-editor="' + Y.Escape.html(elementid) + '" ' +
6f9cf841 173 'tabindex="-1" ' +
c90641fa
DW
174 'data-menu="' + plugin + '_' + elementid + '" >' +
175 icon +
176 '</button>');
177
36973d70 178 group.append(button);
c90641fa 179
6f9cf841
DW
180 currentfocus = toolbar.getAttribute('aria-activedescendant');
181 if (!currentfocus) {
182 button.setAttribute('tabindex', '0');
183 toolbar.setAttribute('aria-activedescendant', button.generateID());
184 }
185
c90641fa
DW
186 // Save the name of the plugin.
187 M.editor_atto.widgets[plugin] = plugin;
188
189 var menu = Y.Node.create('<div class="atto_' + plugin + '_menu' +
190 ' atto_menu" data-editor="' + Y.Escape.html(elementid) + '"></div>');
c90641fa
DW
191 var i = 0, entry = {};
192
193 for (i = 0; i < entries.length; i++) {
194 entry = entries[i];
195
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);
bb76233d 206 Y.one('body').delegate('key', M.editor_atto.buttonclicked_handler, 'space,enter', '.atto_' + plugin + '_action_' + i);
c90641fa
DW
207 M.editor_atto.buttonhandlers[plugin + '_action_' + i] = entry.handler;
208 }
209 }
210
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 }
215
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,
4fd8adab 223 centered : false,
c90641fa
DW
224 align: {node: button, points: [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]}
225 });
226
227 M.editor_atto.menus[plugin + '_' + elementid] = overlay;
228 overlay.render();
229 overlay.hide();
230 overlay.headerNode.hide();
231 },
232
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.
36973d70
JF
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.
c90641fa
DW
239 * @handler function handler- A function to call when the button is clicked.
240 */
36973d70
JF
241 add_toolbar_button : function(elementid, plugin, icon, groupname, handler) {
242 var toolbar = Y.one('#' + elementid + '_toolbar'),
6f9cf841
DW
243 group = Y.one('#' + elementid + '_toolbar .atto_group.' + groupname + '_group'),
244 button,
245 currentfocus;
246
36973d70
JF
247 if (!group) {
248 group = Y.Node.create('<div class="atto_group ' + groupname +'_group"></div>');
249 toolbar.append(group);
250 }
6f9cf841
DW
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>');
c90641fa 258
36973d70 259 group.append(button);
c90641fa 260
6f9cf841
DW
261 currentfocus = toolbar.getAttribute('aria-activedescendant');
262 if (!currentfocus) {
263 button.setAttribute('tabindex', '0');
264 toolbar.setAttribute('aria-activedescendant', button.generateID());
265 }
266
c90641fa
DW
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 }
272
273 // Save the name of the plugin.
274 M.editor_atto.widgets[plugin] = plugin;
275
276 },
277
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();
285
286 if (selection.length) {
287 selection = selection.pop();
288 }
289
290 var node = null;
291 if (selection.parentElement) {
292 node = Y.one(selection.parentElement());
293 } else {
294 node = Y.one(selection.startContainer);
295 }
296
297 return node && node.ancestor('#' + elementid + 'editable') !== null;
298 },
299
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 },
307
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 = '';
6f9cf841 319 var toolbar = Y.Node.create('<div class="editor_atto_toolbar" id="' + params.elementid + '_toolbar" role="toolbar"/>');
c90641fa
DW
320
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 }
ceaef9a9 328 atto.setStyle('minHeight', (1.2 * (textarea.getAttribute('rows') - 1)) + 'em');
c90641fa
DW
329
330 // Copy text to editable div.
331 atto.append(textarea.get('value'));
332
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);
08a95d50
DW
337 atto.setStyle('color', textarea.getStyle('color'));
338 atto.setStyle('lineHeight', textarea.getStyle('lineHeight'));
339 atto.setStyle('fontSize', textarea.getStyle('fontSize'));
c90641fa
DW
340 // Hide the old textarea.
341 textarea.hide();
342
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 });
347
6f9cf841
DW
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);
355
c90641fa
DW
356 // Save the file picker options for later.
357 M.editor_atto.filepickeroptions[params.elementid] = params.filepickeroptions;
358 },
359
6f9cf841
DW
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;
370
371 e.preventDefault();
372
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');
380
381 currentindex = buttons.indexOf(current);
382
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 }
396
397 current = buttons.item(currentindex);
398 current.setAttribute('tabindex', '0');
399 current.focus();
400 Y.one('#' + elementid + '_toolbar').setAttribute('aria-activedescendant', current.generateID());
401 },
402
c90641fa
DW
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];
412
413 options.formcallback = callback;
414 options.editor_target = Y.one(elementid);
415
416 M.core_filepicker.show(Y, options);
417 });
418 },
419
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;
427
428 if (window.getSelection) {
429 range = document.createRange();
430
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 },
441
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 },
462
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 },
473
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 },
484
485 /**
486 * Set the current selection. Used to restore a selection.
487 */
488 set_selection : function(selection) {
489 var sel, i;
490
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 }
504
505};
506
507
6f9cf841 508}, '@VERSION@', {"requires": ["node", "io", "overlay", "escape", "event-key", "moodle-core-notification"]});