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