MDL-44834 editor_atto: Delegate change event to one editor
[moodle.git] / lib / editor / atto / yui / build / moodle-editor_atto-editor / moodle-editor_atto-editor-debug.js
CommitLineData
adca7326
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/**
62467795 19 * The Atto WYSIWG pluggable editor, written for Moodle.
adca7326 20 *
62467795 21 * @module moodle-editor_atto-editor
adca7326
DW
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
62467795 25 * @main moodle-editor_atto-editor
adca7326
DW
26 */
27
28/**
62467795
AN
29 * @module moodle-editor_atto-editor
30 * @submodule editor-base
adca7326 31 */
62467795
AN
32
33var LOGNAME = 'moodle-editor_atto-editor';
34var CSS = {
35 CONTENT: 'editor_atto_content',
36 CONTENTWRAPPER: 'editor_atto_content_wrap',
37 TOOLBAR: 'editor_atto_toolbar',
38 WRAPPER: 'editor_atto',
39 HIGHLIGHT: 'highlight'
40 };
adca7326
DW
41
42/**
62467795 43 * The Atto editor for Moodle.
adca7326 44 *
62467795
AN
45 * @namespace M.editor_atto
46 * @class Editor
47 * @constructor
48 * @uses M.editor_atto.EditorClean
49 * @uses M.editor_atto.EditorFilepicker
50 * @uses M.editor_atto.EditorSelection
51 * @uses M.editor_atto.EditorStyling
52 * @uses M.editor_atto.EditorTextArea
53 * @uses M.editor_atto.EditorToolbar
54 * @uses M.editor_atto.EditorToolbarNav
adca7326 55 */
62467795
AN
56
57function Editor() {
58 Editor.superclass.constructor.apply(this, arguments);
59}
60
61Y.extend(Editor, Y.Base, {
adca7326 62
34f5867a
DW
63 /**
64 * List of known block level tags.
65 * Taken from "https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements".
66 *
62467795 67 * @property BLOCK_TAGS
34f5867a
DW
68 * @type {Array}
69 */
70 BLOCK_TAGS : [
71 'address',
72 'article',
73 'aside',
74 'audio',
75 'blockquote',
76 'canvas',
77 'dd',
78 'div',
79 'dl',
80 'fieldset',
81 'figcaption',
82 'figure',
83 'footer',
84 'form',
85 'h1',
86 'h2',
87 'h3',
88 'h4',
89 'h5',
90 'h6',
91 'header',
92 'hgroup',
93 'hr',
94 'noscript',
95 'ol',
96 'output',
97 'p',
98 'pre',
99 'section',
100 'table',
101 'tfoot',
102 'ul',
62467795
AN
103 'video'
104 ],
d321f68b 105
bed1abbc
AD
106 PLACEHOLDER_FONTNAME: 'yui-tmp',
107 ALL_NODES_SELECTOR: '[style],font[face]',
108 FONT_FAMILY: 'fontFamily',
34f5867a 109
adca7326 110 /**
62467795
AN
111 * The wrapper containing the editor.
112 *
113 * @property _wrapper
114 * @type Node
115 * @private
adca7326 116 */
62467795 117 _wrapper: null,
adca7326
DW
118
119 /**
62467795
AN
120 * A reference to the content editable Node.
121 *
122 * @property editor
123 * @type Node
adca7326 124 */
62467795 125 editor: null,
adca7326
DW
126
127 /**
62467795
AN
128 * A reference to the original text area.
129 *
130 * @property textarea
131 * @type Node
adca7326 132 */
62467795 133 textarea: null,
adca7326
DW
134
135 /**
62467795
AN
136 * A reference to the label associated with the original text area.
137 *
138 * @property textareaLabel
139 * @type Node
adca7326 140 */
62467795 141 textareaLabel: null,
adca7326
DW
142
143 /**
62467795
AN
144 * A reference to the list of plugins.
145 *
146 * @property plugins
147 * @type object
adca7326 148 */
62467795 149 plugins: null,
adca7326 150
62467795
AN
151 initializer: function() {
152 var template;
adca7326 153
62467795
AN
154 // Note - it is not safe to use a CSS selector like '#' + elementid because the id
155 // may have colons in it - e.g. quiz.
156 this.textarea = Y.one(document.getElementById(this.get('elementid')));
26f8822d 157
62467795
AN
158 if (!this.textarea) {
159 // No text area found.
160 Y.log('Text area not found - unable to setup editor for ' + this.get('elementid'),
161 'error', LOGNAME);
162 return;
163 }
26f8822d 164
62467795
AN
165 this._wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />');
166 template = Y.Handlebars.compile('<div id="{{elementid}}editable" ' +
167 'contenteditable="true" ' +
168 'role="textbox" ' +
169 'spellcheck="true" ' +
170 'aria-live="off" ' +
171 'class="{{CSS.CONTENT}}" ' +
172 '/>');
173 this.editor = Y.Node.create(template({
174 elementid: this.get('elementid'),
175 CSS: CSS
176 }));
67d3fe45 177
62467795
AN
178 // Add a labelled-by attribute to the contenteditable.
179 this.textareaLabel = Y.one('[for="' + this.get('elementid') + '"]');
180 if (this.textareaLabel) {
181 this.textareaLabel.generateID();
182 this.editor.setAttribute('aria-labelledby', this.textareaLabel.get("id"));
183 }
67d3fe45 184
62467795
AN
185 // Add everything to the wrapper.
186 this.setupToolbar();
67d3fe45 187
62467795
AN
188 // Editable content wrapper.
189 var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />');
190 content.appendChild(this.editor);
191 this._wrapper.appendChild(content);
0fa78b80 192
62467795
AN
193 // Style the editor.
194 this.editor.setStyle('minHeight', (1.2 * (this.textarea.getAttribute('rows'))) + 'em');
195 // Disable odd inline CSS styles.
196 this.disableCssStyling();
adca7326 197
62467795
AN
198 // Add the toolbar and editable zone to the page.
199 this.textarea.get('parentNode').insert(this._wrapper, this.textarea);
adca7326 200
62467795
AN
201 // Hide the old textarea.
202 this.textarea.hide();
adca7326 203
62467795
AN
204 // Copy the text to the contenteditable div.
205 this.updateFromTextArea();
adca7326 206
62467795
AN
207 // Publish the events that are defined by this editor.
208 this.publishEvents();
adca7326 209
62467795
AN
210 // Add handling for saving and restoring selections on cursor/focus changes.
211 this.setupSelectionWatchers();
adca7326 212
62467795
AN
213 // Setup plugins.
214 this.setupPlugins();
48bdf86f
DW
215 },
216
217 /**
62467795 218 * Focus on the editable area for this editor.
48bdf86f 219 *
62467795
AN
220 * @method focus
221 * @chainable
48bdf86f 222 */
62467795
AN
223 focus: function() {
224 this.editor.focus();
225
226 return this;
48bdf86f
DW
227 },
228
229 /**
62467795 230 * Publish events for this editor instance.
48bdf86f 231 *
62467795
AN
232 * @method publishEvents
233 * @private
234 * @chainable
48bdf86f 235 */
62467795
AN
236 publishEvents: function() {
237 /**
238 * Fired when changes are made within the editor.
239 *
240 * @event change
241 */
242 this.publish('change', {
243 broadcast: true,
244 preventable: true
245 });
48bdf86f 246
62467795
AN
247 /**
248 * Fired when all plugins have completed loading.
249 *
250 * @event pluginsloaded
251 */
252 this.publish('pluginsloaded', {
253 fireOnce: true
254 });
48bdf86f 255
62467795
AN
256 this.publish('atto:selectionchanged', {
257 prefix: 'atto'
258 });
48bdf86f 259
5ce4583a 260 Y.delegate(['mouseup', 'keyup', 'focus'], this._hasSelectionChanged, document.body, '#' + this.editor.get('id'), this);
3ee53a42 261
62467795 262 return this;
3ee53a42
DW
263 },
264
62467795
AN
265 setupPlugins: function() {
266 // Clear the list of plugins.
267 this.plugins = {};
268
269 var plugins = this.get('plugins');
adca7326 270
62467795
AN
271 var groupIndex,
272 group,
273 pluginIndex,
274 plugin,
275 pluginConfig;
276
277 for (groupIndex in plugins) {
278 group = plugins[groupIndex];
279 if (!group.plugins) {
280 // No plugins in this group - skip it.
281 continue;
282 }
283 for (pluginIndex in group.plugins) {
284 plugin = group.plugins[pluginIndex];
285
286 pluginConfig = Y.mix({
287 name: plugin.name,
288 group: group.group,
289 editor: this.editor,
290 toolbar: this.toolbar,
291 host: this
292 }, plugin);
293
294 // Add a reference to the current editor.
295 if (typeof Y.M['atto_' + plugin.name] === "undefined") {
296 Y.log("Plugin '" + plugin.name + "' could not be found - skipping initialisation", "warn", LOGNAME);
297 continue;
298 }
299 this.plugins[plugin.name] = new Y.M['atto_' + plugin.name].Button(pluginConfig);
300 }
adca7326 301 }
62467795
AN
302
303 // Some plugins need to perform actions once all plugins have loaded.
304 this.fire('pluginsloaded');
305
306 return this;
adca7326
DW
307 },
308
62467795
AN
309 enablePlugins: function(plugin) {
310 this._setPluginState(true, plugin);
311 },
3ee53a42 312
62467795
AN
313 disablePlugins: function(plugin) {
314 this._setPluginState(false, plugin);
3ee53a42
DW
315 },
316
62467795
AN
317 _setPluginState: function(enable, plugin) {
318 var target = 'disableButtons';
319 if (enable) {
320 target = 'enableButtons';
3ee53a42 321 }
3ee53a42 322
62467795
AN
323 if (plugin) {
324 this.plugins[plugin][target]();
325 } else {
326 Y.Object.each(this.plugins, function(currentPlugin) {
327 currentPlugin[target]();
328 }, this);
3ee53a42 329 }
62467795 330 }
3ee53a42 331
62467795
AN
332}, {
333 NS: 'editor_atto',
334 ATTRS: {
335 /**
336 * The unique identifier for the form element representing the editor.
337 *
338 * @attribute elementid
339 * @type String
340 * @writeOnce
341 */
342 elementid: {
343 value: null,
344 writeOnce: true
345 },
adca7326 346
62467795
AN
347 /**
348 * Plugins with their configuration.
349 *
350 * The plugins structure is:
351 *
352 * [
353 * {
354 * "group": "groupName",
355 * "plugins": [
356 * "pluginName": {
357 * "configKey": "configValue"
358 * },
359 * "pluginName": {
360 * "configKey": "configValue"
361 * }
362 * ]
363 * },
364 * {
365 * "group": "groupName",
366 * "plugins": [
367 * "pluginName": {
368 * "configKey": "configValue"
369 * }
370 * ]
371 * }
372 * ]
373 *
374 * @attribute plugins
375 * @type Object
376 * @writeOnce
377 */
378 plugins: {
379 value: {},
380 writeOnce: true
adca7326 381 }
62467795
AN
382 }
383});
384
385// The Editor publishes custom events that can be subscribed to.
386Y.augment(Editor, Y.EventTarget);
387
388Y.namespace('M.editor_atto').Editor = Editor;
389
390// Function for Moodle's initialisation.
391Y.namespace('M.editor_atto.Editor').init = function(config) {
392 return new Y.M.editor_atto.Editor(config);
393};
394// This file is part of Moodle - http://moodle.org/
395//
396// Moodle is free software: you can redistribute it and/or modify
397// it under the terms of the GNU General Public License as published by
398// the Free Software Foundation, either version 3 of the License, or
399// (at your option) any later version.
400//
401// Moodle is distributed in the hope that it will be useful,
402// but WITHOUT ANY WARRANTY; without even the implied warranty of
403// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
404// GNU General Public License for more details.
405//
406// You should have received a copy of the GNU General Public License
407// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
408
409/**
410 * @module moodle-editor_atto-editor
411 * @submodule textarea
412 */
adca7326 413
62467795
AN
414/**
415 * Textarea functions for the Atto editor.
416 *
417 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
418 *
419 * @namespace M.editor_atto
420 * @class EditorTextArea
421 */
422
423function EditorTextArea() {}
424
425EditorTextArea.ATTRS= {
426};
427
428EditorTextArea.prototype = {
adca7326 429 /**
62467795 430 * Copy and clean the text from the textarea into the contenteditable div.
adca7326 431 *
62467795
AN
432 * If the text is empty, provide a default paragraph tag to hold the content.
433 *
434 * @method updateFromTextArea
435 * @chainable
adca7326 436 */
62467795
AN
437 updateFromTextArea: function() {
438 // Clear it first.
439 this.editor.setHTML('');
440
441 // Copy text to editable div.
442 this.editor.append(this.textarea.get('value'));
443
444 // Clean it.
445 this.cleanEditorHTML();
446
447 // Insert a paragraph in the empty contenteditable div.
448 if (this.editor.getHTML() === '') {
449 if (Y.UA.ie && Y.UA.ie < 10) {
450 this.editor.setHTML('<p></p>');
451 } else {
452 this.editor.setHTML('<p><br></p>');
453 }
adca7326 454 }
adca7326
DW
455 },
456
457 /**
62467795
AN
458 * Copy the text from the contenteditable to the textarea which it replaced.
459 *
460 * @method updateOriginal
461 * @chainable
adca7326 462 */
62467795
AN
463 updateOriginal : function() {
464 // Insert the cleaned content.
465 this.textarea.set('value', this.getCleanHTML());
5ec54dd1 466
62467795
AN
467 // Trigger the onchange callback on the textarea, essentially to notify moodle-core-formchangechecker.
468 this.textarea.simulate('change');
5ec54dd1 469
62467795
AN
470 // Trigger handlers for this action.
471 this.fire('change');
472 }
473};
534cf7b7 474
62467795
AN
475Y.Base.mix(Y.M.editor_atto.Editor, [EditorTextArea]);
476// This file is part of Moodle - http://moodle.org/
477//
478// Moodle is free software: you can redistribute it and/or modify
479// it under the terms of the GNU General Public License as published by
480// the Free Software Foundation, either version 3 of the License, or
481// (at your option) any later version.
482//
483// Moodle is distributed in the hope that it will be useful,
484// but WITHOUT ANY WARRANTY; without even the implied warranty of
485// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
486// GNU General Public License for more details.
487//
488// You should have received a copy of the GNU General Public License
489// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
adca7326 490
62467795
AN
491/**
492 * @module moodle-editor_atto-editor
493 * @submodule clean
494 */
adca7326 495
62467795
AN
496/**
497 * Functions for the Atto editor to clean the generated content.
498 *
499 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
500 *
501 * @namespace M.editor_atto
502 * @class EditorClean
503 */
adca7326 504
62467795 505function EditorClean() {}
adca7326 506
62467795
AN
507EditorClean.ATTRS= {
508};
adca7326 509
62467795 510EditorClean.prototype = {
8951d614 511 /**
62467795
AN
512 * Clean the generated HTML content without modifying the editor content.
513 *
514 * This includes removes all YUI ids from the generated content.
8951d614 515 *
62467795 516 * @return {string} The cleaned HTML content.
8951d614 517 */
62467795
AN
518 getCleanHTML: function() {
519 // Clone the editor so that we don't actually modify the real content.
520 var editorClone = this.editor.cloneNode(true);
8951d614 521
62467795
AN
522 // Remove all YUI IDs.
523 Y.each(editorClone.all('[id^="yui"]'), function(node) {
524 node.removeAttribute('id');
525 });
8951d614 526
62467795 527 editorClone.all('.atto_control').remove(true);
8951d614 528
62467795
AN
529 // Remove any and all nasties from source.
530 return this._cleanHTML(editorClone.get('innerHTML'));
8951d614
JM
531 },
532
adca7326 533 /**
62467795
AN
534 * Clean the HTML content of the editor.
535 *
536 * @method cleanEditorHTML
537 * @chainable
adca7326 538 */
62467795
AN
539 cleanEditorHTML: function() {
540 var startValue = this.editor.get('innerHTML');
541 this.editor.set('innerHTML', this._cleanHTML(startValue));
5ec54dd1 542
62467795
AN
543 return this;
544 },
fe0d2477 545
62467795
AN
546 /**
547 * Clean the specified HTML content and remove any content which could cause issues.
548 *
549 * @method _cleanHTML
550 * @private
551 * @param {String} content The content to clean
552 * @return {String} The cleaned HTML
553 */
554 _cleanHTML: function(content) {
555 // What are we doing ?
556 // We are cleaning random HTML from all over the shop into a set of useful html suitable for content.
557 // We are allowing styles etc, but not e.g. font tags, class="MsoNormal" etc.
558
559 var rules = [
560 // Source: "http://stackoverflow.com/questions/2875027/clean-microsoft-word-pasted-text-using-javascript"
561 // Source: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work"
562
563 // Remove all HTML comments.
564 {regex: /<!--[\s\S]*?-->/gi, replace: ""},
565 // Source: "http://www.1stclassmedia.co.uk/developers/clean-ms-word-formatting.php"
566 // Remove <?xml>, <\?xml>.
567 {regex: /<\\?\?xml[^>]*>/gi, replace: ""},
568 // Remove <o:blah>, <\o:blah>.
569 {regex: /<\/?\w+:[^>]*>/gi, replace: ""}, // e.g. <o:p...
570 // Remove MSO-blah, MSO:blah (e.g. in style attributes)
571 {regex: /\s*MSO[-:][^;"']*;?/gi, replace: ""},
572 // Remove empty spans
573 {regex: /<span[^>]*>(&nbsp;|\s)*<\/span>/gi, replace: ""},
574 // Remove class="Msoblah"
575 {regex: /class="Mso[^"]*"/gi, replace: ""},
576
577 // Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"
578 // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body.
579 {regex: /<(\/?title|\/?meta|\/?style|\/?st\d|\/?head|\/?font|\/?html|\/?body|!\[)[^>]*?>/gi, replace: ""},
580
581 // Source: "http://www.tim-jarrett.com/labs_javascript_scrub_word.php"
582 // Replace extended chars with simple text.
583 {regex: new RegExp(String.fromCharCode(8220), 'gi'), replace: '"'},
584 {regex: new RegExp(String.fromCharCode(8216), 'gi'), replace: "'"},
585 {regex: new RegExp(String.fromCharCode(8217), 'gi'), replace: "'"},
586 {regex: new RegExp(String.fromCharCode(8211), 'gi'), replace: '-'},
587 {regex: new RegExp(String.fromCharCode(8212), 'gi'), replace: '--'},
588 {regex: new RegExp(String.fromCharCode(189), 'gi'), replace: '1/2'},
589 {regex: new RegExp(String.fromCharCode(188), 'gi'), replace: '1/4'},
590 {regex: new RegExp(String.fromCharCode(190), 'gi'), replace: '3/4'},
591 {regex: new RegExp(String.fromCharCode(169), 'gi'), replace: '(c)'},
592 {regex: new RegExp(String.fromCharCode(174), 'gi'), replace: '(r)'},
593 {regex: new RegExp(String.fromCharCode(8230), 'gi'), replace: '...'}
594 ];
adca7326 595
62467795
AN
596 var i = 0;
597 for (i = 0; i < rules.length; i++) {
598 content = content.replace(rules[i].regex, rules[i].replace);
adca7326
DW
599 }
600
62467795
AN
601 return content;
602 }
603};
adca7326 604
62467795
AN
605Y.Base.mix(Y.M.editor_atto.Editor, [EditorClean]);
606// This file is part of Moodle - http://moodle.org/
607//
608// Moodle is free software: you can redistribute it and/or modify
609// it under the terms of the GNU General Public License as published by
610// the Free Software Foundation, either version 3 of the License, or
611// (at your option) any later version.
612//
613// Moodle is distributed in the hope that it will be useful,
614// but WITHOUT ANY WARRANTY; without even the implied warranty of
615// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
616// GNU General Public License for more details.
617//
618// You should have received a copy of the GNU General Public License
619// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
adca7326 620
62467795
AN
621/**
622 * @module moodle-editor_atto-editor
623 * @submodule toolbar
624 */
adca7326 625
62467795
AN
626/**
627 * Toolbar functions for the Atto editor.
628 *
629 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
630 *
631 * @namespace M.editor_atto
632 * @class EditorToolbar
633 */
adca7326 634
62467795 635function EditorToolbar() {}
34f5867a 636
62467795
AN
637EditorToolbar.ATTRS= {
638};
adca7326 639
62467795 640EditorToolbar.prototype = {
adca7326 641 /**
62467795
AN
642 * A reference to the toolbar Node.
643 *
644 * @property toolbar
645 * @type Node
adca7326 646 */
62467795 647 toolbar: null,
adca7326 648
86a83e3a 649 /**
62467795 650 * Setup the toolbar on the editor.
86a83e3a 651 *
62467795
AN
652 * @method setupToolbar
653 * @chainable
86a83e3a 654 */
62467795
AN
655 setupToolbar: function() {
656 this.toolbar = Y.Node.create('<div class="' + CSS.TOOLBAR + '" role="toolbar" aria-live="off"/>');
657 this._wrapper.appendChild(this.toolbar);
86a83e3a 658
62467795
AN
659 if (this.textareaLabel) {
660 this.toolbar.setAttribute('aria-labelledby', this.textareaLabel.get("id"));
661 }
86a83e3a 662
62467795
AN
663 // Add keyboard navigation for the toolbar.
664 this.setupToolbarNavigation();
86a83e3a 665
62467795
AN
666 return this;
667 }
668};
86a83e3a 669
62467795
AN
670Y.Base.mix(Y.M.editor_atto.Editor, [EditorToolbar]);
671// This file is part of Moodle - http://moodle.org/
672//
673// Moodle is free software: you can redistribute it and/or modify
674// it under the terms of the GNU General Public License as published by
675// the Free Software Foundation, either version 3 of the License, or
676// (at your option) any later version.
677//
678// Moodle is distributed in the hope that it will be useful,
679// but WITHOUT ANY WARRANTY; without even the implied warranty of
680// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
681// GNU General Public License for more details.
682//
683// You should have received a copy of the GNU General Public License
684// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
685
686/**
687 * @module moodle-editor_atto-editor
688 * @submodule toolbarnav
689 */
690
691/**
692 * Toolbar Navigation functions for the Atto editor.
693 *
694 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
695 *
696 * @namespace M.editor_atto
697 * @class EditorToolbarNav
698 */
699
700function EditorToolbarNav() {}
701
702EditorToolbarNav.ATTRS= {
703};
704
705EditorToolbarNav.prototype = {
706 /**
707 * The current focal point for tabbing.
708 *
709 * @property _tabFocus
710 * @type Node
711 * @default null
712 * @private
713 */
714 _tabFocus: null,
715
716 /**
717 * Set up the watchers for toolbar navigation.
718 *
719 * @method setupToolbarNavigation
720 * @chainable
721 */
722 setupToolbarNavigation: function() {
723 // Listen for Arrow left and Arrow right keys.
724 this._wrapper.delegate('key',
725 this.toolbarKeyboardNavigation,
726 'down:37,39',
727 '.' + CSS.TOOLBAR,
728 this);
729
730 return this;
86a83e3a
DW
731 },
732
adca7326 733 /**
62467795
AN
734 * Implement arrow key navigation for the buttons in the toolbar.
735 *
736 * @method toolbarKeyboardNavigation
737 * @param {EventFacade} e - the keyboard event.
adca7326 738 */
62467795
AN
739 toolbarKeyboardNavigation: function(e) {
740 // Prevent the default browser behaviour.
741 e.preventDefault();
adca7326 742
62467795 743 var buttons = this.toolbar.all('button');
26f8822d 744
62467795
AN
745 // On cursor moves we loops through the buttons.
746 var found = false,
747 index = 0,
748 direction = 1,
749 checkCount = 0,
750 group,
751 current = e.target.ancestor('button', true);
752
753 // Determine which button is currently selected.
754 while (!found && index < buttons.size()) {
755 if (buttons.item(index) === current) {
756 found = true;
757 } else {
758 index++;
759 }
26f8822d 760 }
b269f635 761
62467795
AN
762 if (!found) {
763 Y.log("Unable to find this button in the list of buttons", 'debug', LOGNAME);
764 return;
765 }
adca7326 766
62467795
AN
767 if (e.keyCode === 37) {
768 // Moving left so reverse the direction.
769 direction = -1;
770 }
771
772 // Try to find the next
773 do {
774 index += direction;
775 if (index < 0) {
776 index = buttons.size() - 1;
777 } else if (index >= buttons.size()) {
778 // Handle wrapping.
779 index = 0;
780 }
781 next = buttons.item(index);
782 group = next.ancestor('.atto_group');
adca7326 783
62467795
AN
784 // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item.
785 checkCount++;
786 // Loop while:
787 // * we are not in a loop and have not already checked every button; and
788 // * we are on a different button; and
789 // * both the next button and the group it is in are not hidden.
790 } while (checkCount < buttons.size() && next !== current && (next.hasAttribute('hidden') || group.hasAttribute('hidden')));
adca7326 791
62467795
AN
792 if (next) {
793 next.focus();
794 this._setTabFocus(next);
795 }
796 },
d088a835 797
62467795
AN
798 /**
799 * Sets tab focus for the toolbar to the specified Node.
800 *
801 * @method _setTabFocus
802 * @param {Node} button The node that focus should now be set to
803 * @chainable
804 * @private
805 */
806 _setTabFocus: function(button) {
807 if (this._tabFocus) {
808 // Unset the previous entry.
809 this._tabFocus.setAttribute('tabindex', '-1');
810 }
26f8822d 811
62467795
AN
812 // Set up the new entry.
813 this._tabFocus = button;
814 this._tabFocus.setAttribute('tabindex', 0);
4c37c1f4 815
62467795
AN
816 // And update the activedescendant to point at the currently selected button.
817 this.toolbar.setAttribute('aria-activedescendant', this._tabFocus.generateID());
3ee53a42 818
62467795
AN
819 return this;
820 }
821};
67d3fe45 822
62467795
AN
823Y.Base.mix(Y.M.editor_atto.Editor, [EditorToolbarNav]);
824// This file is part of Moodle - http://moodle.org/
825//
826// Moodle is free software: you can redistribute it and/or modify
827// it under the terms of the GNU General Public License as published by
828// the Free Software Foundation, either version 3 of the License, or
829// (at your option) any later version.
830//
831// Moodle is distributed in the hope that it will be useful,
832// but WITHOUT ANY WARRANTY; without even the implied warranty of
833// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
834// GNU General Public License for more details.
835//
836// You should have received a copy of the GNU General Public License
837// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
adca7326 838
62467795
AN
839/**
840 * @module moodle-editor_atto-editor
841 * @submodule selection
842 */
adca7326 843
62467795
AN
844/**
845 * Selection functions for the Atto editor.
846 *
847 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
848 *
849 * @namespace M.editor_atto
850 * @class EditorSelection
851 */
fcb5b5c4 852
62467795 853function EditorSelection() {}
fcb5b5c4 854
62467795
AN
855EditorSelection.ATTRS= {
856};
3ee53a42 857
62467795 858EditorSelection.prototype = {
3ee53a42
DW
859
860 /**
62467795
AN
861 * List of saved selections per editor instance.
862 *
863 * @property _selections
864 * @private
3ee53a42 865 */
62467795 866 _selections: null,
adca7326
DW
867
868 /**
62467795
AN
869 * A unique identifier for the last selection recorded.
870 *
871 * @property _lastSelection
872 * @param lastselection
873 * @type string
874 * @private
adca7326 875 */
62467795 876 _lastSelection: null,
adca7326
DW
877
878 /**
62467795
AN
879 * Whether focus came from a click event.
880 *
881 * This is used to determine whether to restore the selection or not.
882 *
883 * @property _focusFromClick
884 * @type Boolean
885 * @default false
886 * @private
adca7326 887 */
62467795 888 _focusFromClick: false,
adca7326
DW
889
890 /**
62467795
AN
891 * Set up the watchers for selection save and restoration.
892 *
893 * @method setupSelectionWatchers
894 * @chainable
adca7326 895 */
62467795
AN
896 setupSelectionWatchers: function() {
897 // Save the selection when a change was made.
898 this.on('atto:selectionchanged', this.saveSelection, this);
adca7326 899
62467795 900 this.editor.on('focus', this.restoreSelection, this);
adca7326 901
62467795
AN
902 // Do not restore selection when focus is from a click event.
903 this.editor.on('mousedown', function() {
904 this._focusFromClick = true;
905 }, this);
adca7326 906
62467795
AN
907 // Copy the current value back to the textarea when focus leaves us and save the current selection.
908 this.editor.on('blur', function() {
909 // Clear the _focusFromClick value.
910 this._focusFromClick = false;
adca7326 911
62467795
AN
912 // Update the original text area.
913 this.updateOriginal();
914 }, this);
adca7326 915
62467795 916 return this;
b269f635
DW
917 },
918
adca7326 919 /**
62467795
AN
920 * Work out if the cursor is in the editable area for this editor instance.
921 *
922 * @method isActive
923 * @return {boolean}
adca7326 924 */
62467795
AN
925 isActive: function() {
926 var range = rangy.createRange(),
927 selection = rangy.getSelection();
adca7326 928
62467795
AN
929 if (!selection.rangeCount) {
930 // If there was no range count, then there is no selection.
931 return false;
932 }
adca7326 933
62467795
AN
934 // Check whether the range intersects the editor selection.
935 range.selectNode(this.editor.getDOMNode());
936 return range.intersectsRange(selection.getRangeAt(0));
adca7326
DW
937 },
938
939 /**
62467795
AN
940 * Create a cross browser selection object that represents a YUI node.
941 *
942 * @method getSelectionFromNode
943 * @param {Node} YUI Node to base the selection upon.
944 * @return {[rangy.Range]}
adca7326 945 */
62467795 946 getSelectionFromNode: function(node) {
d321f68b
DW
947 var range = rangy.createRange();
948 range.selectNode(node.getDOMNode());
949 return [range];
adca7326
DW
950 },
951
26f8822d 952 /**
62467795
AN
953 * Save the current selection to an internal property.
954 *
955 * This allows more reliable return focus, helping improve keyboard navigation.
956 *
957 * Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/restoreSelection"}}{{/crossLink}}.
958 *
959 * @method saveSelection
26f8822d 960 */
62467795
AN
961 saveSelection: function() {
962 if (this.isActive()) {
963 this._selections = this.getSelection();
26f8822d
DW
964 }
965 },
966
967 /**
62467795
AN
968 * Restore any stored selection when the editor gets focus again.
969 *
970 * Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/saveSelection"}}{{/crossLink}}.
971 *
972 * @method restoreSelection
26f8822d 973 */
62467795
AN
974 restoreSelection: function() {
975 if (!this._focusFromClick) {
976 if (this._selections) {
977 this.setSelection(this._selections);
26f8822d
DW
978 }
979 }
62467795 980 this._focusFromClick = false;
26f8822d
DW
981 },
982
adca7326 983 /**
62467795
AN
984 * Get the selection object that can be passed back to setSelection.
985 *
986 * @method getSelection
987 * @return {array} An array of rangy ranges.
adca7326 988 */
62467795 989 getSelection: function() {
d321f68b 990 return rangy.getSelection().getAllRanges();
adca7326
DW
991 },
992
993 /**
62467795
AN
994 * Check that a YUI node it at least partly contained by the current selection.
995 *
996 * @method selectionContainsNode
997 * @param {Node} The node to check.
998 * @return {boolean}
adca7326 999 */
62467795 1000 selectionContainsNode: function(node) {
d321f68b 1001 return rangy.getSelection().containsNode(node.getDOMNode(), true);
adca7326
DW
1002 },
1003
3ee53a42 1004 /**
62467795
AN
1005 * Runs a filter on each node in the selection, and report whether the
1006 * supplied selector(s) were found in the supplied Nodes.
3ee53a42 1007 *
62467795
AN
1008 * By default, all specified nodes must match the selection, but this
1009 * can be controlled with the requireall property.
1010 *
1011 * @method selectionFilterMatches
3ee53a42 1012 * @param {String} selector
62467795
AN
1013 * @param {NodeList} [selectednodes] For performance this should be passed. If not passed, this will be looked up each time.
1014 * @param {Boolean} [requireall=true] Used to specify that "any" match is good enough.
3ee53a42
DW
1015 * @return {Boolean}
1016 */
62467795
AN
1017 selectionFilterMatches: function(selector, selectednodes, requireall) {
1018 if (typeof requireall === 'undefined') {
d321f68b
DW
1019 requireall = true;
1020 }
3ee53a42
DW
1021 if (!selectednodes) {
1022 // Find this because it was not passed as a param.
62467795 1023 selectednodes = this.getSelectedNodes();
3ee53a42 1024 }
62467795
AN
1025 var allmatch = selectednodes.size() > 0,
1026 anymatch = false;
1027
1028 var editor = this.editor,
1029 stopFn = function(node) {
1030 editor.contains(node);
1031 };
1032
67d3fe45
SH
1033 selectednodes.each(function(node){
1034 // Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing.
d321f68b
DW
1035 if (requireall) {
1036 // Check for at least one failure.
62467795 1037 if (!allmatch || !node.ancestor(selector, true, stopFn)) {
d321f68b
DW
1038 allmatch = false;
1039 }
1040 } else {
1041 // Check for at least one match.
62467795 1042 if (!anymatch && node.ancestor(selector, true, stopFn)) {
d321f68b
DW
1043 anymatch = true;
1044 }
3ee53a42 1045 }
67d3fe45 1046 }, this);
d321f68b
DW
1047 if (requireall) {
1048 return allmatch;
1049 } else {
1050 return anymatch;
1051 }
3ee53a42
DW
1052 },
1053
1054 /**
67d3fe45
SH
1055 * Get the deepest possible list of nodes in the current selection.
1056 *
62467795
AN
1057 * @method getSelectedNodes
1058 * @return {NodeList}
3ee53a42 1059 */
62467795 1060 getSelectedNodes: function() {
d321f68b
DW
1061 var results = new Y.NodeList(),
1062 nodes,
1063 selection,
67d3fe45 1064 range,
d321f68b
DW
1065 node,
1066 i;
1067
1068 selection = rangy.getSelection();
1069
1070 if (selection.rangeCount) {
1071 range = selection.getRangeAt(0);
1072 } else {
1073 // Empty range.
1074 range = rangy.createRange();
67d3fe45 1075 }
d321f68b
DW
1076
1077 if (range.collapsed) {
62467795 1078 range = range.cloneRange();
d321f68b
DW
1079 range.selectNode(range.commonAncestorContainer);
1080 }
1081
1082 nodes = range.getNodes();
1083
1084 for (i = 0; i < nodes.length; i++) {
1085 node = Y.one(nodes[i]);
62467795 1086 if (this.editor.contains(node)) {
d321f68b 1087 results.push(node);
67d3fe45
SH
1088 }
1089 }
1090 return results;
3ee53a42
DW
1091 },
1092
1093 /**
62467795 1094 * Check whether the current selection has changed since this method was last called.
67d3fe45 1095 *
62467795 1096 * If the selection has changed, the atto:selectionchanged event is also fired.
67d3fe45 1097 *
62467795 1098 * @method _hasSelectionChanged
67d3fe45
SH
1099 * @private
1100 * @param {EventFacade} e
62467795 1101 * @return {Boolean}
3ee53a42 1102 */
62467795 1103 _hasSelectionChanged: function(e) {
d321f68b 1104 var selection = rangy.getSelection(),
67d3fe45 1105 range,
d321f68b
DW
1106 changed = false;
1107
1108 if (selection.rangeCount) {
1109 range = selection.getRangeAt(0);
1110 } else {
1111 // Empty range.
1112 range = rangy.createRange();
67d3fe45 1113 }
d321f68b 1114
62467795
AN
1115 if (this._lastSelection) {
1116 if (!this._lastSelection.equals(range)) {
d321f68b 1117 changed = true;
62467795 1118 return this._fireSelectionChanged(e);
d321f68b 1119 }
67d3fe45 1120 }
62467795 1121 this._lastSelection = range;
d321f68b 1122 return changed;
67d3fe45 1123 },
3ee53a42 1124
67d3fe45
SH
1125 /**
1126 * Fires the atto:selectionchanged event.
1127 *
62467795 1128 * When the selectionchanged event is fired, the following arguments are provided:
67d3fe45 1129 * - event : the original event that lead to this event being fired.
67d3fe45
SH
1130 * - selectednodes : an array containing nodes that are entirely selected of contain partially selected content.
1131 *
62467795 1132 * @method _fireSelectionChanged
67d3fe45
SH
1133 * @private
1134 * @param {EventFacade} e
1135 */
62467795 1136 _fireSelectionChanged: function(e) {
67d3fe45 1137 this.fire('atto:selectionchanged', {
62467795
AN
1138 event: e,
1139 selectedNodes: this.getSelectedNodes()
67d3fe45
SH
1140 });
1141 },
1142
adca7326 1143 /**
62467795
AN
1144 * Get the DOM node representing the common anscestor of the selection nodes.
1145 *
1146 * @method getSelectionParentNode
1147 * @return {Element|boolean} The DOM Node for this parent, or false if no seletion was made.
adca7326 1148 */
62467795 1149 getSelectionParentNode: function() {
d321f68b
DW
1150 var selection = rangy.getSelection();
1151 if (selection.rangeCount) {
1152 return selection.getRangeAt(0).commonAncestorContainer;
34f5867a 1153 }
34f5867a 1154 return false;
adca7326
DW
1155 },
1156
adca7326
DW
1157 /**
1158 * Set the current selection. Used to restore a selection.
62467795
AN
1159 *
1160 * @method selection
1161 * @param {array} ranges A list of rangy.range objects in the selection.
adca7326 1162 */
62467795 1163 setSelection: function(ranges) {
d321f68b
DW
1164 var selection = rangy.getSelection();
1165 selection.setRanges(ranges);
34f5867a
DW
1166 },
1167
1168 /**
1169 * Change the formatting for the current selection.
62467795 1170 *
34f5867a
DW
1171 * Also changes the selection to the newly formatted block (allows applying multiple styles to a block).
1172 *
62467795
AN
1173 * @method formatSelectionBlock
1174 * @param {String} [blocktag] Change the block level tag to this. Empty string, means do not change the tag.
1175 * @param {Object} [attributes] The keys and values for attributes to be added/changed in the block tag.
1176 * @return {Node|boolean} The Node that was formatted if a change was made, otherwise false.
34f5867a 1177 */
62467795 1178 formatSelectionBlock: function(blocktag, attributes) {
34f5867a 1179 // First find the nearest ancestor of the selection that is a block level element.
62467795 1180 var selectionparentnode = this.getSelectionParentNode(),
34f5867a
DW
1181 boundary,
1182 cell,
1183 nearestblock,
1184 newcontent,
1185 match,
1186 replacement;
1187
1188 if (!selectionparentnode) {
1189 // No selection, nothing to format.
62467795 1190 return false;
34f5867a
DW
1191 }
1192
62467795 1193 boundary = this.editor;
34f5867a
DW
1194
1195 selectionparentnode = Y.one(selectionparentnode);
1196
1197 // If there is a table cell in between the selectionparentnode and the boundary,
1198 // move the boundary to the table cell.
1199 // This is because we might have a table in a div, and we select some text in a cell,
1200 // want to limit the change in style to the table cell, not the entire table (via the outer div).
1201 cell = selectionparentnode.ancestor(function (node) {
1202 var tagname = node.get('tagName');
1203 if (tagname) {
1204 tagname = tagname.toLowerCase();
1205 }
1206 return (node === boundary) ||
1207 (tagname === 'td') ||
1208 (tagname === 'th');
1209 }, true);
1210
1211 if (cell) {
1212 // Limit the scope to the table cell.
1213 boundary = cell;
1214 }
1215
62467795 1216 nearestblock = selectionparentnode.ancestor(this.BLOCK_TAGS.join(', '), true);
34f5867a
DW
1217 if (nearestblock) {
1218 // Check that the block is contained by the boundary.
1219 match = nearestblock.ancestor(function (node) {
1220 return node === boundary;
1221 }, false);
1222
1223 if (!match) {
1224 nearestblock = false;
1225 }
1226 }
1227
1228 // No valid block element - make one.
1229 if (!nearestblock) {
1230 // There is no block node in the content, wrap the content in a p and use that.
1231 newcontent = Y.Node.create('<p></p>');
1232 boundary.get('childNodes').each(function (child) {
1233 newcontent.append(child.remove());
1234 });
1235 boundary.append(newcontent);
1236 nearestblock = newcontent;
1237 }
1238
1239 // Guaranteed to have a valid block level element contained in the contenteditable region.
1240 // Change the tag to the new block level tag.
1241 if (blocktag && blocktag !== '') {
1242 // Change the block level node for a new one.
1243 replacement = Y.Node.create('<' + blocktag + '></' + blocktag + '>');
1244 // Copy all attributes.
1245 replacement.setAttrs(nearestblock.getAttrs());
1246 // Copy all children.
1247 nearestblock.get('childNodes').each(function (child) {
1248 child.remove();
1249 replacement.append(child);
1250 });
1251
1252 nearestblock.replace(replacement);
1253 nearestblock = replacement;
1254 }
1255
1256 // Set the attributes on the block level tag.
1257 if (attributes) {
1258 nearestblock.setAttrs(attributes);
1259 }
1260
1261 // Change the selection to the modified block. This makes sense when we might apply multiple styles
1262 // to the block.
62467795
AN
1263 var selection = this.getSelectionFromNode(nearestblock);
1264 this.setSelection(selection);
34f5867a
DW
1265
1266 return nearestblock;
f6bef145
FM
1267 },
1268
62467795
AN
1269 /**
1270 * Inserts the given HTML into the editable content at the currently focused point.
1271 *
1272 * @method insertContentAtFocusPoint
1273 * @param {String} html
1274 */
1275 insertContentAtFocusPoint: function(html) {
1276 var selection = rangy.getSelection(),
1277 range,
1278 node = Y.Node.create(html);
1279 if (selection.rangeCount) {
1280 range = selection.getRangeAt(0);
1281 }
1282 if (range) {
1283 range.deleteContents();
1284 range.insertNode(node.getDOMNode());
1285 }
1286 }
1287
1288};
1289
1290Y.Base.mix(Y.M.editor_atto.Editor, [EditorSelection]);
1291// This file is part of Moodle - http://moodle.org/
1292//
1293// Moodle is free software: you can redistribute it and/or modify
1294// it under the terms of the GNU General Public License as published by
1295// the Free Software Foundation, either version 3 of the License, or
1296// (at your option) any later version.
1297//
1298// Moodle is distributed in the hope that it will be useful,
1299// but WITHOUT ANY WARRANTY; without even the implied warranty of
1300// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1301// GNU General Public License for more details.
1302//
1303// You should have received a copy of the GNU General Public License
1304// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1305
1306/**
1307 * @module moodle-editor_atto-editor
1308 * @submodule styling
1309 */
1310
1311/**
1312 * Editor styling functions for the Atto editor.
1313 *
1314 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1315 *
1316 * @namespace M.editor_atto
1317 * @class EditorStyling
1318 */
1319
1320function EditorStyling() {}
1321
1322EditorStyling.ATTRS= {
1323};
1324
1325EditorStyling.prototype = {
f6bef145
FM
1326 /**
1327 * Disable CSS styling.
1328 *
62467795 1329 * @method disableCssStyling
f6bef145 1330 */
62467795 1331 disableCssStyling: function() {
f6bef145
FM
1332 try {
1333 document.execCommand("styleWithCSS", 0, false);
1334 } catch (e1) {
1335 try {
1336 document.execCommand("useCSS", 0, true);
1337 } catch (e2) {
1338 try {
1339 document.execCommand('styleWithCSS', false, false);
1340 } catch (e3) {
1341 // We did our best.
1342 }
1343 }
1344 }
1345 },
1346
1347 /**
1348 * Enable CSS styling.
1349 *
62467795 1350 * @method enableCssStyling
f6bef145 1351 */
62467795 1352 enableCssStyling: function() {
f6bef145
FM
1353 try {
1354 document.execCommand("styleWithCSS", 0, true);
1355 } catch (e1) {
1356 try {
1357 document.execCommand("useCSS", 0, false);
1358 } catch (e2) {
1359 try {
1360 document.execCommand('styleWithCSS', false, true);
1361 } catch (e3) {
1362 // We did our best.
1363 }
1364 }
1365 }
2faf4c45
SH
1366 },
1367
bed1abbc
AD
1368 /**
1369 * Change the formatting for the current selection.
62467795
AN
1370 *
1371 * This will wrap the selection in span tags, adding the provided classes.
bed1abbc
AD
1372 *
1373 * If the selection covers multiple block elements, multiple spans will be inserted to preserve the original structure.
1374 *
62467795 1375 * @method toggleInlineSelectionClass
bed1abbc
AD
1376 * @param {Array} toggleclasses - Class names to be toggled on or off.
1377 */
62467795
AN
1378 toggleInlineSelectionClass: function(toggleclasses) {
1379 var selectionparentnode = this.getSelectionParentNode(),
bed1abbc
AD
1380 nodes,
1381 items = [],
1382 parentspan,
1383 currentnode,
1384 newnode,
1385 i = 0;
1386
1387 if (!selectionparentnode) {
1388 // No selection, nothing to format.
1389 return;
1390 }
1391
1392 // Add a bogus fontname as the browsers handle inserting fonts into multiple blocks correctly.
62467795
AN
1393 document.execCommand('fontname', false, this.PLACEHOLDER_FONTNAME);
1394 nodes = this.editor.all(this.ALL_NODES_SELECTOR);
bed1abbc
AD
1395
1396 // Create a list of all nodes that have our bogus fontname.
1397 nodes.each(function(node, index) {
62467795
AN
1398 if (node.getStyle(this.FONT_FAMILY) === this.PLACEHOLDER_FONTNAME) {
1399 node.setStyle(this.FONT_FAMILY, '');
1400 if (!node.compareTo(this.editor)) {
bed1abbc
AD
1401 items.push(Y.Node.getDOMNode(nodes.item(index)));
1402 }
1403 }
1404 });
1405
1406 // Replace the fontname tags with spans
1407 for (i = 0; i < items.length; i++) {
1408 currentnode = Y.one(items[i]);
adca7326 1409
bed1abbc
AD
1410 // Check for an existing span to add the nolink class to.
1411 parentspan = currentnode.ancestor('.editor_atto_content span');
1412 if (!parentspan) {
1413 newnode = Y.Node.create('<span></span>');
1414 newnode.append(items[i].innerHTML);
1415 currentnode.replace(newnode);
1416
1417 currentnode = newnode;
1418 } else {
1419 currentnode = parentspan;
1420 }
1421
1422 // Toggle the classes on the spans.
1423 for (var j = 0; j < toggleclasses.length; j++) {
1424 currentnode.toggleClass(toggleclasses[j]);
1425 }
1426 }
1427 }
adca7326 1428};
67d3fe45 1429
62467795
AN
1430Y.Base.mix(Y.M.editor_atto.Editor, [EditorStyling]);
1431// This file is part of Moodle - http://moodle.org/
1432//
1433// Moodle is free software: you can redistribute it and/or modify
1434// it under the terms of the GNU General Public License as published by
1435// the Free Software Foundation, either version 3 of the License, or
1436// (at your option) any later version.
1437//
1438// Moodle is distributed in the hope that it will be useful,
1439// but WITHOUT ANY WARRANTY; without even the implied warranty of
1440// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1441// GNU General Public License for more details.
1442//
1443// You should have received a copy of the GNU General Public License
1444// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1445
1446/**
1447 * @module moodle-editor_atto-editor
1448 * @submodule filepicker
1449 */
adca7326
DW
1450
1451/**
62467795 1452 * Filepicker options for the Atto editor.
adca7326 1453 *
62467795
AN
1454 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1455 *
1456 * @namespace M.editor_atto
1457 * @class EditorFilepicker
adca7326 1458 */
adca7326 1459
62467795 1460function EditorFilepicker() {}
adca7326 1461
62467795 1462EditorFilepicker.ATTRS= {
adca7326 1463 /**
62467795 1464 * The options for the filepicker.
adca7326 1465 *
62467795
AN
1466 * @attribute filepickeroptions
1467 * @type object
1468 * @default {}
adca7326 1469 */
62467795
AN
1470 filepickeroptions: {
1471 value: {}
adca7326 1472 }
62467795 1473};
adca7326 1474
62467795
AN
1475EditorFilepicker.prototype = {
1476 /**
1477 * Should we show the filepicker for this filetype?
1478 *
1479 * @method canShowFilepicker
1480 * @param string type The media type for the file picker.
1481 * @return {boolean}
1482 */
1483 canShowFilepicker: function(type) {
1484 return (typeof this.get('filepickeroptions')[type] !== 'undefined');
1485 },
adca7326 1486
62467795
AN
1487 /**
1488 * Show the filepicker.
1489 *
1490 * This depends on core_filepicker, and then call that modules show function.
1491 *
1492 * @method showFilepicker
1493 * @param {string} type The media type for the file picker.
1494 * @param {function} callback The callback to use when selecting an item of media.
1495 * @param {object} [context] The context from which to call the callback.
1496 */
1497 showFilepicker: function(type, callback, context) {
1498 var self = this;
1499 Y.use('core_filepicker', function (Y) {
1500 var options = Y.clone(self.get('filepickeroptions')[type], true);
1501 options.formcallback = callback;
1502 if (context) {
1503 options.magicscope = context;
1504 }
adca7326 1505
62467795
AN
1506 M.core_filepicker.show(Y, options);
1507 });
d088a835 1508 }
62467795 1509};
d088a835 1510
62467795 1511Y.Base.mix(Y.M.editor_atto.Editor, [EditorFilepicker]);
adca7326
DW
1512
1513
3ee53a42
DW
1514}, '@VERSION@', {
1515 "requires": [
1516 "node",
1517 "io",
1518 "overlay",
1519 "escape",
1520 "event",
1521 "event-simulate",
1522 "event-custom",
1523 "yui-throttle",
d321f68b 1524 "moodle-core-notification-dialogue",
62467795
AN
1525 "moodle-editor_atto-rangy",
1526 "handlebars"
3ee53a42
DW
1527 ]
1528});