MDL-54796 forms: Editor restore event created.
[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/>.
ad3f8cd1 17/* eslint-disable no-unused-vars */
adca7326
DW
18
19/**
62467795 20 * The Atto WYSIWG pluggable editor, written for Moodle.
adca7326 21 *
62467795 22 * @module moodle-editor_atto-editor
adca7326
DW
23 * @package editor_atto
24 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
62467795 26 * @main moodle-editor_atto-editor
adca7326
DW
27 */
28
29/**
62467795
AN
30 * @module moodle-editor_atto-editor
31 * @submodule editor-base
adca7326 32 */
62467795
AN
33
34var LOGNAME = 'moodle-editor_atto-editor';
35var CSS = {
36 CONTENT: 'editor_atto_content',
37 CONTENTWRAPPER: 'editor_atto_content_wrap',
38 TOOLBAR: 'editor_atto_toolbar',
39 WRAPPER: 'editor_atto',
40 HIGHLIGHT: 'highlight'
557f44d9
AN
41 },
42 rangy = window.rangy;
adca7326
DW
43
44/**
62467795 45 * The Atto editor for Moodle.
adca7326 46 *
62467795
AN
47 * @namespace M.editor_atto
48 * @class Editor
49 * @constructor
50 * @uses M.editor_atto.EditorClean
51 * @uses M.editor_atto.EditorFilepicker
52 * @uses M.editor_atto.EditorSelection
53 * @uses M.editor_atto.EditorStyling
54 * @uses M.editor_atto.EditorTextArea
55 * @uses M.editor_atto.EditorToolbar
56 * @uses M.editor_atto.EditorToolbarNav
adca7326 57 */
62467795
AN
58
59function Editor() {
60 Editor.superclass.constructor.apply(this, arguments);
61}
62
63Y.extend(Editor, Y.Base, {
adca7326 64
34f5867a
DW
65 /**
66 * List of known block level tags.
67 * Taken from "https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements".
68 *
62467795 69 * @property BLOCK_TAGS
34f5867a
DW
70 * @type {Array}
71 */
72 BLOCK_TAGS : [
73 'address',
74 'article',
75 'aside',
76 'audio',
77 'blockquote',
78 'canvas',
79 'dd',
80 'div',
81 'dl',
82 'fieldset',
83 'figcaption',
84 'figure',
85 'footer',
86 'form',
87 'h1',
88 'h2',
89 'h3',
90 'h4',
91 'h5',
92 'h6',
93 'header',
94 'hgroup',
95 'hr',
96 'noscript',
97 'ol',
98 'output',
99 'p',
100 'pre',
101 'section',
102 'table',
103 'tfoot',
104 'ul',
62467795
AN
105 'video'
106 ],
d321f68b 107
af6a2e94 108 PLACEHOLDER_CLASS: 'atto-tmp-class',
bed1abbc
AD
109 ALL_NODES_SELECTOR: '[style],font[face]',
110 FONT_FAMILY: 'fontFamily',
34f5867a 111
adca7326 112 /**
62467795
AN
113 * The wrapper containing the editor.
114 *
115 * @property _wrapper
116 * @type Node
117 * @private
adca7326 118 */
62467795 119 _wrapper: null,
adca7326
DW
120
121 /**
62467795
AN
122 * A reference to the content editable Node.
123 *
124 * @property editor
125 * @type Node
adca7326 126 */
62467795 127 editor: null,
adca7326
DW
128
129 /**
62467795
AN
130 * A reference to the original text area.
131 *
132 * @property textarea
133 * @type Node
adca7326 134 */
62467795 135 textarea: null,
adca7326
DW
136
137 /**
62467795
AN
138 * A reference to the label associated with the original text area.
139 *
140 * @property textareaLabel
141 * @type Node
adca7326 142 */
62467795 143 textareaLabel: null,
adca7326
DW
144
145 /**
62467795
AN
146 * A reference to the list of plugins.
147 *
148 * @property plugins
149 * @type object
adca7326 150 */
62467795 151 plugins: null,
adca7326 152
4dbc02f6
AN
153 /**
154 * Event Handles to clear on editor destruction.
155 *
156 * @property _eventHandles
157 * @private
158 */
159 _eventHandles: null,
160
62467795
AN
161 initializer: function() {
162 var template;
adca7326 163
62467795
AN
164 // Note - it is not safe to use a CSS selector like '#' + elementid because the id
165 // may have colons in it - e.g. quiz.
166 this.textarea = Y.one(document.getElementById(this.get('elementid')));
26f8822d 167
62467795
AN
168 if (!this.textarea) {
169 // No text area found.
170 return;
171 }
26f8822d 172
4dbc02f6
AN
173 this._eventHandles = [];
174
62467795
AN
175 this._wrapper = Y.Node.create('<div class="' + CSS.WRAPPER + '" />');
176 template = Y.Handlebars.compile('<div id="{{elementid}}editable" ' +
177 'contenteditable="true" ' +
178 'role="textbox" ' +
179 'spellcheck="true" ' +
180 'aria-live="off" ' +
181 'class="{{CSS.CONTENT}}" ' +
182 '/>');
183 this.editor = Y.Node.create(template({
184 elementid: this.get('elementid'),
185 CSS: CSS
186 }));
67d3fe45 187
6d9a83c2
DW
188 // Add a labelled-by attribute to the contenteditable.
189 this.textareaLabel = Y.one('[for="' + this.get('elementid') + '"]');
190 if (this.textareaLabel) {
191 this.textareaLabel.generateID();
192 this.editor.setAttribute('aria-labelledby', this.textareaLabel.get("id"));
193 }
67d3fe45 194
62467795
AN
195 // Add everything to the wrapper.
196 this.setupToolbar();
67d3fe45 197
62467795
AN
198 // Editable content wrapper.
199 var content = Y.Node.create('<div class="' + CSS.CONTENTWRAPPER + '" />');
200 content.appendChild(this.editor);
201 this._wrapper.appendChild(content);
0fa78b80 202
2f0a1236
FM
203 // Style the editor. According to the styles.css: 20 is the line-height, 8 is padding-top + padding-bottom.
204 this.editor.setStyle('minHeight', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px');
205
206 if (Y.UA.ie === 0) {
207 // We set a height here to force the overflow because decent browsers allow the CSS property resize.
208 this.editor.setStyle('height', ((20 * this.textarea.getAttribute('rows')) + 8) + 'px');
209 }
210
62467795
AN
211 // Disable odd inline CSS styles.
212 this.disableCssStyling();
adca7326 213
e92a39cf
DP
214 // Use paragraphs not divs.
215 if (document.queryCommandSupported('DefaultParagraphSeparator')) {
216 document.execCommand('DefaultParagraphSeparator', false, 'p');
217 }
218
62467795 219 // Add the toolbar and editable zone to the page.
369a63ac
DC
220 this.textarea.get('parentNode').insert(this._wrapper, this.textarea).
221 setAttribute('class', 'editor_atto_wrap');
adca7326 222
62467795
AN
223 // Hide the old textarea.
224 this.textarea.hide();
adca7326 225
62467795
AN
226 // Copy the text to the contenteditable div.
227 this.updateFromTextArea();
adca7326 228
62467795
AN
229 // Publish the events that are defined by this editor.
230 this.publishEvents();
adca7326 231
62467795
AN
232 // Add handling for saving and restoring selections on cursor/focus changes.
233 this.setupSelectionWatchers();
adca7326 234
4dbc02f6
AN
235 // Add polling to update the textarea periodically when typing long content.
236 this.setupAutomaticPolling();
237
62467795
AN
238 // Setup plugins.
239 this.setupPlugins();
2ba6706d
DW
240
241 // Initialize the auto-save timer.
242 this.setupAutosave();
8a5db547
DW
243 // Preload the icons for the notifications.
244 this.setupNotifications();
48bdf86f
DW
245 },
246
247 /**
62467795 248 * Focus on the editable area for this editor.
48bdf86f 249 *
62467795
AN
250 * @method focus
251 * @chainable
48bdf86f 252 */
62467795
AN
253 focus: function() {
254 this.editor.focus();
255
256 return this;
48bdf86f
DW
257 },
258
259 /**
62467795 260 * Publish events for this editor instance.
48bdf86f 261 *
62467795
AN
262 * @method publishEvents
263 * @private
264 * @chainable
48bdf86f 265 */
62467795
AN
266 publishEvents: function() {
267 /**
268 * Fired when changes are made within the editor.
269 *
270 * @event change
271 */
272 this.publish('change', {
273 broadcast: true,
274 preventable: true
275 });
48bdf86f 276
62467795
AN
277 /**
278 * Fired when all plugins have completed loading.
279 *
280 * @event pluginsloaded
281 */
282 this.publish('pluginsloaded', {
283 fireOnce: true
284 });
48bdf86f 285
62467795
AN
286 this.publish('atto:selectionchanged', {
287 prefix: 'atto'
288 });
48bdf86f 289
62467795 290 return this;
3ee53a42
DW
291 },
292
4dbc02f6
AN
293 /**
294 * Set up automated polling of the text area to update the textarea.
295 *
296 * @method setupAutomaticPolling
297 * @chainable
298 */
299 setupAutomaticPolling: function() {
a7fdadc9
EM
300 this._registerEventHandle(this.editor.on(['keyup', 'cut'], this.updateOriginal, this));
301 this._registerEventHandle(this.editor.on('paste', this.pasteCleanup, this));
4dbc02f6 302
10fae277
PN
303 // Call this.updateOriginal after dropped content has been processed.
304 this._registerEventHandle(this.editor.on('drop', this.updateOriginalDelayed, this));
305
306 return this;
307 },
308
309 /**
310 * Calls updateOriginal on a short timer to allow native event handlers to run first.
311 *
312 * @method updateOriginalDelayed
313 * @chainable
314 */
315 updateOriginalDelayed: function() {
316 Y.soon(Y.bind(this.updateOriginal, this));
317
4dbc02f6
AN
318 return this;
319 },
320
62467795
AN
321 setupPlugins: function() {
322 // Clear the list of plugins.
323 this.plugins = {};
324
325 var plugins = this.get('plugins');
adca7326 326
62467795
AN
327 var groupIndex,
328 group,
329 pluginIndex,
330 plugin,
331 pluginConfig;
332
333 for (groupIndex in plugins) {
334 group = plugins[groupIndex];
335 if (!group.plugins) {
336 // No plugins in this group - skip it.
337 continue;
338 }
339 for (pluginIndex in group.plugins) {
340 plugin = group.plugins[pluginIndex];
341
342 pluginConfig = Y.mix({
343 name: plugin.name,
344 group: group.group,
345 editor: this.editor,
346 toolbar: this.toolbar,
347 host: this
348 }, plugin);
349
350 // Add a reference to the current editor.
351 if (typeof Y.M['atto_' + plugin.name] === "undefined") {
352 continue;
353 }
354 this.plugins[plugin.name] = new Y.M['atto_' + plugin.name].Button(pluginConfig);
355 }
adca7326 356 }
62467795
AN
357
358 // Some plugins need to perform actions once all plugins have loaded.
359 this.fire('pluginsloaded');
360
361 return this;
adca7326
DW
362 },
363
62467795
AN
364 enablePlugins: function(plugin) {
365 this._setPluginState(true, plugin);
366 },
3ee53a42 367
62467795
AN
368 disablePlugins: function(plugin) {
369 this._setPluginState(false, plugin);
3ee53a42
DW
370 },
371
62467795
AN
372 _setPluginState: function(enable, plugin) {
373 var target = 'disableButtons';
374 if (enable) {
375 target = 'enableButtons';
3ee53a42 376 }
3ee53a42 377
62467795
AN
378 if (plugin) {
379 this.plugins[plugin][target]();
380 } else {
381 Y.Object.each(this.plugins, function(currentPlugin) {
382 currentPlugin[target]();
383 }, this);
3ee53a42 384 }
4dbc02f6
AN
385 },
386
387 /**
388 * Register an event handle for disposal in the destructor.
389 *
390 * @method _registerEventHandle
391 * @param {EventHandle} The Event Handle as returned by Y.on, and Y.delegate.
392 * @private
393 */
394 _registerEventHandle: function(handle) {
395 this._eventHandles.push(handle);
62467795 396 }
3ee53a42 397
62467795
AN
398}, {
399 NS: 'editor_atto',
400 ATTRS: {
401 /**
402 * The unique identifier for the form element representing the editor.
403 *
404 * @attribute elementid
405 * @type String
406 * @writeOnce
407 */
408 elementid: {
409 value: null,
410 writeOnce: true
411 },
adca7326 412
2ba6706d
DW
413 /**
414 * The contextid of the form.
415 *
416 * @attribute contextid
417 * @type Integer
418 * @writeOnce
419 */
420 contextid: {
421 value: null,
422 writeOnce: true
423 },
424
62467795
AN
425 /**
426 * Plugins with their configuration.
427 *
428 * The plugins structure is:
429 *
430 * [
431 * {
432 * "group": "groupName",
433 * "plugins": [
434 * "pluginName": {
435 * "configKey": "configValue"
436 * },
437 * "pluginName": {
438 * "configKey": "configValue"
439 * }
440 * ]
441 * },
442 * {
443 * "group": "groupName",
444 * "plugins": [
445 * "pluginName": {
446 * "configKey": "configValue"
447 * }
448 * ]
449 * }
450 * ]
451 *
452 * @attribute plugins
453 * @type Object
454 * @writeOnce
455 */
456 plugins: {
457 value: {},
458 writeOnce: true
adca7326 459 }
62467795
AN
460 }
461});
462
463// The Editor publishes custom events that can be subscribed to.
464Y.augment(Editor, Y.EventTarget);
465
466Y.namespace('M.editor_atto').Editor = Editor;
467
468// Function for Moodle's initialisation.
469Y.namespace('M.editor_atto.Editor').init = function(config) {
470 return new Y.M.editor_atto.Editor(config);
471};
472// This file is part of Moodle - http://moodle.org/
473//
474// Moodle is free software: you can redistribute it and/or modify
475// it under the terms of the GNU General Public License as published by
476// the Free Software Foundation, either version 3 of the License, or
477// (at your option) any later version.
478//
479// Moodle is distributed in the hope that it will be useful,
480// but WITHOUT ANY WARRANTY; without even the implied warranty of
481// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
482// GNU General Public License for more details.
75c86d13
DW
483//
484// You should have received a copy of the GNU General Public License
485// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
486
487/**
488 * A notify function for the Atto editor.
489 *
490 * @module moodle-editor_atto-notify
19549f8b 491 * @submodule notify
75c86d13
DW
492 * @package editor_atto
493 * @copyright 2014 Damyon Wiese
494 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
495 */
496
19549f8b
DW
497var LOGNAME_NOTIFY = 'moodle-editor_atto-editor-notify',
498 NOTIFY_INFO = 'info',
499 NOTIFY_WARNING = 'warning';
500
75c86d13
DW
501function EditorNotify() {}
502
503EditorNotify.ATTRS= {
504};
505
75c86d13
DW
506EditorNotify.prototype = {
507
508 /**
19549f8b 509 * A single Y.Node for this editor. There is only ever one - it is replaced if a new message comes in.
75c86d13
DW
510 *
511 * @property messageOverlay
19549f8b 512 * @type {Node}
75c86d13
DW
513 */
514 messageOverlay: null,
515
8a5db547
DW
516 /**
517 * A single timer object that can be used to cancel the hiding behaviour.
518 *
519 * @property hideTimer
520 * @type {timer}
521 */
522 hideTimer: null,
523
524 /**
525 * Initialize the notifications.
526 *
527 * @method setupNotifications
528 * @chainable
529 */
530 setupNotifications: function() {
531 var preload1 = new Image(),
532 preload2 = new Image();
533
534 preload1.src = M.util.image_url('i/warning', 'moodle');
535 preload2.src = M.util.image_url('i/info', 'moodle');
536
537 return this;
538 },
539
75c86d13
DW
540 /**
541 * Show a notification in a floaty overlay somewhere in the atto editor text area.
542 *
543 * @method showMessage
19549f8b
DW
544 * @param {String} message The translated message (use get_string)
545 * @param {String} type Must be either "info" or "warning"
546 * @param {Number} timeout Time in milliseconds to show this message for.
75c86d13
DW
547 * @chainable
548 */
8a5db547 549 showMessage: function(message, type, timeout) {
19549f8b
DW
550 var messageTypeIcon = '',
551 intTimeout,
552 bodyContent;
75c86d13
DW
553
554 if (this.messageOverlay === null) {
555 this.messageOverlay = Y.Node.create('<div class="editor_atto_notification"></div>');
556
f1018cd6 557 this.messageOverlay.hide(true);
3a7887ba 558 this.textarea.get('parentNode').append(this.messageOverlay);
75c86d13
DW
559
560 this.messageOverlay.on('click', function() {
f1018cd6 561 this.messageOverlay.hide(true);
75c86d13 562 }, this);
8a5db547 563 }
75c86d13 564
8a5db547
DW
565 if (this.hideTimer !== null) {
566 this.hideTimer.cancel();
75c86d13
DW
567 }
568
19549f8b 569 if (type === NOTIFY_WARNING) {
6bfd450a 570 messageTypeIcon = '<img src="' +
75c86d13
DW
571 M.util.image_url('i/warning', 'moodle') +
572 '" alt="' + M.util.get_string('warning', 'moodle') + '"/>';
19549f8b 573 } else if (type === NOTIFY_INFO) {
6bfd450a 574 messageTypeIcon = '<img src="' +
75c86d13
DW
575 M.util.image_url('i/info', 'moodle') +
576 '" alt="' + M.util.get_string('info', 'moodle') + '"/>';
8a5db547
DW
577 } else {
578 }
579
580 // Parse the timeout value.
19549f8b
DW
581 intTimeout = parseInt(timeout, 10);
582 if (intTimeout <= 0) {
583 intTimeout = 60000;
75c86d13
DW
584 }
585
8a5db547
DW
586 // Convert class to atto_info (for example).
587 type = 'atto_' + type;
588
19549f8b 589 bodyContent = Y.Node.create('<div class="' + type + '" role="alert" aria-live="assertive">' +
75c86d13
DW
590 messageTypeIcon + ' ' +
591 Y.Escape.html(message) +
592 '</div>');
593 this.messageOverlay.empty();
594 this.messageOverlay.append(bodyContent);
f1018cd6 595 this.messageOverlay.show(true);
75c86d13 596
19549f8b 597 this.hideTimer = Y.later(intTimeout, this, function() {
8a5db547 598 this.hideTimer = null;
9e12b0fa
JD
599 if (this.messageOverlay.inDoc()) {
600 this.messageOverlay.hide(true);
601 }
8a5db547
DW
602 });
603
75c86d13
DW
604 return this;
605 }
606
607};
608
609Y.Base.mix(Y.M.editor_atto.Editor, [EditorNotify]);
610// This file is part of Moodle - http://moodle.org/
611//
612// Moodle is free software: you can redistribute it and/or modify
613// it under the terms of the GNU General Public License as published by
614// the Free Software Foundation, either version 3 of the License, or
615// (at your option) any later version.
616//
617// Moodle is distributed in the hope that it will be useful,
618// but WITHOUT ANY WARRANTY; without even the implied warranty of
619// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
620// GNU General Public License for more details.
62467795
AN
621//
622// You should have received a copy of the GNU General Public License
623// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
624
625/**
626 * @module moodle-editor_atto-editor
627 * @submodule textarea
628 */
adca7326 629
62467795
AN
630/**
631 * Textarea functions for the Atto editor.
632 *
633 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
634 *
635 * @namespace M.editor_atto
636 * @class EditorTextArea
637 */
638
639function EditorTextArea() {}
640
641EditorTextArea.ATTRS= {
642};
643
644EditorTextArea.prototype = {
4dbc02f6
AN
645
646 /**
647 * Return the appropriate empty content value for the current browser.
648 *
649 * Different browsers use a different content when they are empty and
650 * we must set this reliable across the board.
651 *
652 * @method _getEmptyContent
653 * @return String The content to use representing no user-provided content
654 * @private
655 */
656 _getEmptyContent: function() {
657 if (Y.UA.ie && Y.UA.ie < 10) {
658 return '<p></p>';
659 } else {
660 return '<p><br></p>';
661 }
662 },
663
adca7326 664 /**
62467795 665 * Copy and clean the text from the textarea into the contenteditable div.
adca7326 666 *
62467795
AN
667 * If the text is empty, provide a default paragraph tag to hold the content.
668 *
669 * @method updateFromTextArea
670 * @chainable
adca7326 671 */
62467795
AN
672 updateFromTextArea: function() {
673 // Clear it first.
674 this.editor.setHTML('');
675
9029ce75
EM
676 // Copy cleaned HTML to editable div.
677 this.editor.append(this._cleanHTML(this.textarea.get('value')));
62467795
AN
678
679 // Insert a paragraph in the empty contenteditable div.
680 if (this.editor.getHTML() === '') {
4dbc02f6 681 this.editor.setHTML(this._getEmptyContent());
adca7326 682 }
9029ce75
EM
683
684 return this;
adca7326
DW
685 },
686
687 /**
62467795
AN
688 * Copy the text from the contenteditable to the textarea which it replaced.
689 *
690 * @method updateOriginal
691 * @chainable
adca7326 692 */
62467795 693 updateOriginal : function() {
4dbc02f6
AN
694 // Get the previous and current value to compare them.
695 var oldValue = this.textarea.get('value'),
696 newValue = this.getCleanHTML();
697
698 if (newValue === "" && this.isActive()) {
699 // The content was entirely empty so get the empty content placeholder.
700 newValue = this._getEmptyContent();
701 }
5ec54dd1 702
4dbc02f6
AN
703 // Only call this when there has been an actual change to reduce processing.
704 if (oldValue !== newValue) {
705 // Insert the cleaned content.
706 this.textarea.set('value', newValue);
5ec54dd1 707
4dbc02f6
AN
708 // Trigger the onchange callback on the textarea, essentially to notify moodle-core-formchangechecker.
709 this.textarea.simulate('change');
710
711 // Trigger handlers for this action.
712 this.fire('change');
713 }
714
715 return this;
62467795
AN
716 }
717};
534cf7b7 718
62467795
AN
719Y.Base.mix(Y.M.editor_atto.Editor, [EditorTextArea]);
720// This file is part of Moodle - http://moodle.org/
721//
722// Moodle is free software: you can redistribute it and/or modify
723// it under the terms of the GNU General Public License as published by
724// the Free Software Foundation, either version 3 of the License, or
725// (at your option) any later version.
726//
727// Moodle is distributed in the hope that it will be useful,
728// but WITHOUT ANY WARRANTY; without even the implied warranty of
729// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
730// GNU General Public License for more details.
731//
732// You should have received a copy of the GNU General Public License
733// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
ad3f8cd1
DP
734/* global NOTIFY_WARNING, NOTIFY_INFO */
735/* eslint-disable no-unused-vars */
adca7326 736
2ba6706d
DW
737/**
738 * A autosave function for the Atto editor.
739 *
740 * @module moodle-editor_atto-autosave
741 * @submodule autosave-base
742 * @package editor_atto
743 * @copyright 2014 Damyon Wiese
744 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
745 */
746
6bfd450a 747var SUCCESS_MESSAGE_TIMEOUT = 5000,
19549f8b
DW
748 RECOVER_MESSAGE_TIMEOUT = 60000,
749 LOGNAME_AUTOSAVE = 'moodle-editor_atto-editor-autosave';
2ba6706d
DW
750
751function EditorAutosave() {}
752
753EditorAutosave.ATTRS= {
6bfd450a
DW
754 /**
755 * Enable/Disable auto save for this instance.
756 *
757 * @attribute autosaveEnabled
758 * @type Boolean
759 * @writeOnce
760 */
761 autosaveEnabled: {
762 value: true,
763 writeOnce: true
764 },
765
766 /**
767 * The time between autosaves (in seconds).
768 *
769 * @attribute autosaveFrequency
19549f8b 770 * @type Number
6bfd450a
DW
771 * @default 60
772 * @writeOnce
773 */
774 autosaveFrequency: {
775 value: 60,
776 writeOnce: true
56579fb6
DW
777 },
778
c07f86ce
DW
779 /**
780 * Unique hash for this page instance. Calculated from $PAGE->url in php.
781 *
782 * @attribute pageHash
783 * @type String
784 * @writeOnce
785 */
786 pageHash: {
787 value: '',
788 writeOnce: true
6bfd450a 789 }
2ba6706d
DW
790};
791
792EditorAutosave.prototype = {
793
794 /**
795 * The text that was auto saved in the last request.
796 *
797 * @property lastText
798 * @type string
799 */
e924e1b3 800 lastText: "",
2ba6706d
DW
801
802 /**
803 * Autosave instance.
804 *
805 * @property autosaveInstance
806 * @type string
807 */
808 autosaveInstance: null,
809
b803df81
DW
810 /**
811 * Autosave Timer.
812 *
813 * @property autosaveTimer
814 * @type object
815 */
816 autosaveTimer: null,
817
2ba6706d
DW
818 /**
819 * Initialize the autosave process
820 *
821 * @method setupAutosave
822 * @chainable
823 */
824 setupAutosave: function() {
6bfd450a 825 var draftid = -1,
c4e2c671 826 form,
6bfd450a 827 optiontype = null,
557f44d9 828 options = this.get('filepickeroptions'),
240a9de4 829 params;
2ba6706d
DW
830
831 if (!this.get('autosaveEnabled')) {
832 // Autosave disabled for this instance.
833 return;
834 }
835
2ba6706d 836 this.autosaveInstance = Y.stamp(this);
2ba6706d
DW
837 for (optiontype in options) {
838 if (typeof options[optiontype].itemid !== "undefined") {
839 draftid = options[optiontype].itemid;
840 }
841 }
842
843 // First see if there are any saved drafts.
844 // Make an ajax request.
2ba6706d 845 params = {
2ba6706d
DW
846 contextid: this.get('contextid'),
847 action: 'resume',
2ba6706d
DW
848 draftid: draftid,
849 elementid: this.get('elementid'),
850 pageinstance: this.autosaveInstance,
c07f86ce 851 pagehash: this.get('pageHash')
2ba6706d
DW
852 };
853
240a9de4
FM
854 this.autosaveIo(params, this, {
855 success: function(response) {
856 if (response === null) {
857 // This can happen when there is nothing to resume from.
858 return;
859 } else if (!response) {
860 return;
861 }
862
863 // Revert untouched editor contents to an empty string.
864 // Check for FF and Chrome.
865 if (response.result === '<p></p>' || response.result === '<p><br></p>' ||
866 response.result === '<br>') {
867 response.result = '';
868 }
869
870 // Check for IE 9 and 10.
871 if (response.result === '<p>&nbsp;</p>' || response.result === '<p><br>&nbsp;</p>') {
872 response.result = '';
873 }
874
875 if (response.error || typeof response.result === 'undefined') {
557f44d9
AN
876 this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
877 NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
240a9de4
FM
878 } else if (response.result !== this.textarea.get('value') &&
879 response.result !== '') {
880 this.recoverText(response.result);
2ba6706d 881 }
240a9de4
FM
882 this._fireSelectionChanged();
883
884 },
885 failure: function() {
886 this.showMessage(M.util.get_string('errortextrecovery', 'editor_atto'),
887 NOTIFY_WARNING, RECOVER_MESSAGE_TIMEOUT);
19549f8b 888 }
2ba6706d
DW
889 });
890
891 // Now setup the timer for periodic saves.
6bfd450a 892 var delay = parseInt(this.get('autosaveFrequency'), 10) * 1000;
b803df81 893 this.autosaveTimer = Y.later(delay, this, this.saveDraft, false, true);
2ba6706d
DW
894
895 // Now setup the listener for form submission.
c4e2c671
AN
896 form = this.textarea.ancestor('form');
897 if (form) {
240a9de4
FM
898 this.autosaveIoOnSubmit(form, {
899 action: 'reset',
900 contextid: this.get('contextid'),
901 elementid: this.get('elementid'),
902 pageinstance: this.autosaveInstance,
903 pagehash: this.get('pageHash')
904 });
c4e2c671 905 }
2ba6706d
DW
906 return this;
907 },
908
2ba6706d 909 /**
6bfd450a 910 * Recover a previous version of this text and show a message.
2ba6706d
DW
911 *
912 * @method recoverText
913 * @param {String} text
914 * @chainable
915 */
916 recoverText: function(text) {
6bfd450a
DW
917 this.editor.setHTML(text);
918 this.saveSelection();
919 this.updateOriginal();
920 this.lastText = text;
921
557f44d9
AN
922 this.showMessage(M.util.get_string('textrecovered', 'editor_atto'),
923 NOTIFY_INFO, RECOVER_MESSAGE_TIMEOUT);
2ba6706d 924
b5594973
AG
925 // Fire an event that the editor content has changed.
926 require(['core/event'], function(event) {
927 event.notifyEditorContentRestored();
928 });
929
2ba6706d
DW
930 return this;
931 },
932
933 /**
934 * Save a single draft via ajax.
935 *
936 * @method saveDraft
937 * @chainable
938 */
939 saveDraft: function() {
557f44d9 940 var url, params;
b803df81
DW
941
942 if (!this.editor.getDOMNode()) {
943 // Stop autosaving if the editor was removed from the page.
944 this.autosaveTimer.cancel();
945 return;
946 }
3a7887ba
DW
947 // Only copy the text from the div to the textarea if the textarea is not currently visible.
948 if (!this.editor.get('hidden')) {
949 this.updateOriginal();
950 }
2ba6706d
DW
951 var newText = this.textarea.get('value');
952
953 if (newText !== this.lastText) {
954
955 // Make an ajax request.
56579fb6 956 url = M.cfg.wwwroot + this.get('autosaveAjaxScript');
2ba6706d
DW
957 params = {
958 sesskey: M.cfg.sesskey,
959 contextid: this.get('contextid'),
960 action: 'save',
961 drafttext: newText,
962 elementid: this.get('elementid'),
c07f86ce 963 pagehash: this.get('pageHash'),
2ba6706d
DW
964 pageinstance: this.autosaveInstance
965 };
19549f8b
DW
966
967 // Reusable error handler - must be passed the correct context.
240a9de4 968 var ajaxErrorFunction = function(response) {
19549f8b
DW
969 var errorDuration = parseInt(this.get('autosaveFrequency'), 10) * 1000;
970 this.showMessage(M.util.get_string('autosavefailed', 'editor_atto'), NOTIFY_WARNING, errorDuration);
971 };
2ba6706d 972
240a9de4
FM
973 this.autosaveIo(params, this, {
974 failure: ajaxErrorFunction,
975 success: function(response) {
976 if (response && response.error) {
977 Y.soon(Y.bind(ajaxErrorFunction, this, [response]));
978 } else {
979 // All working.
980 this.lastText = newText;
981 this.showMessage(M.util.get_string('autosavesucceeded', 'editor_atto'),
982 NOTIFY_INFO, SUCCESS_MESSAGE_TIMEOUT);
2ba6706d 983 }
19549f8b 984 }
2ba6706d 985 });
2ba6706d
DW
986 }
987 return this;
988 }
989};
990
991Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosave]);
992// This file is part of Moodle - http://moodle.org/
993//
994// Moodle is free software: you can redistribute it and/or modify
995// it under the terms of the GNU General Public License as published by
996// the Free Software Foundation, either version 3 of the License, or
997// (at your option) any later version.
998//
999// Moodle is distributed in the hope that it will be useful,
1000// but WITHOUT ANY WARRANTY; without even the implied warranty of
1001// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1002// GNU General Public License for more details.
1003//
1004// You should have received a copy of the GNU General Public License
1005// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1006
240a9de4
FM
1007/**
1008 * A autosave function for the Atto editor.
1009 *
1010 * @module moodle-editor_atto-autosave-io
1011 * @submodule autosave-io
1012 * @package editor_atto
1013 * @copyright 2016 Frédéric Massart
1014 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1015 */
1016
1017var EditorAutosaveIoDispatcherInstance = null;
1018
1019function EditorAutosaveIoDispatcher() {
1020 EditorAutosaveIoDispatcher.superclass.constructor.apply(this, arguments);
1021 this._submitEvents = {};
1022 this._queue = [];
1023 this._throttle = null;
1024}
1025EditorAutosaveIoDispatcher.NAME = 'EditorAutosaveIoDispatcher';
1026EditorAutosaveIoDispatcher.ATTRS = {
1027
1028 /**
1029 * The relative path to the ajax script.
1030 *
1031 * @attribute autosaveAjaxScript
1032 * @type String
1033 * @default '/lib/editor/atto/autosave-ajax.php'
1034 * @readOnly
1035 */
1036 autosaveAjaxScript: {
1037 value: '/lib/editor/atto/autosave-ajax.php',
1038 readOnly: true
1039 },
1040
1041 /**
1042 * The time buffer for the throttled requested.
1043 *
1044 * @attribute delay
1045 * @type Number
1046 * @default 50
1047 * @readOnly
1048 */
1049 delay: {
1050 value: 50,
1051 readOnly: true
1052 }
1053
1054};
1055Y.extend(EditorAutosaveIoDispatcher, Y.Base, {
1056
1057 /**
1058 * Dispatch an IO request.
1059 *
1060 * This method will put the requests in a queue in order to attempt to bulk them.
1061 *
1062 * @param {Object} params The parameters of the request.
1063 * @param {Object} context The context in which the callbacks are called.
1064 * @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
1065 * optional keys defining the callbacks to call. Success and Complete
1066 * functions will receive the response as parameter. Success and Complete
1067 * may receive an object containing the error key, use this to confirm
1068 * that no errors occured.
1069 * @return {Void}
1070 */
1071 dispatch: function(params, context, callbacks) {
1072 if (this._throttle) {
1073 this._throttle.cancel();
1074 }
1075
1076 this._throttle = Y.later(this.get('delay'), this, this._processDispatchQueue);
1077 this._queue.push([params, context, callbacks]);
1078 },
1079
1080 /**
1081 * Dispatches the requests in the queue.
1082 *
1083 * @return {Void}
1084 */
1085 _processDispatchQueue: function() {
1086 var queue = this._queue,
1087 data = {};
1088
1089 this._queue = [];
1090 if (queue.length < 1) {
1091 return;
1092 }
1093
1094 Y.Array.each(queue, function(item, index) {
1095 data[index] = item[0];
1096 });
1097
1098 Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
1099 method: 'POST',
1100 data: Y.QueryString.stringify({
1101 actions: data,
1102 sesskey: M.cfg.sesskey
1103 }),
1104 on: {
1105 start: this._makeIoEventCallback('start', queue),
1106 complete: this._makeIoEventCallback('complete', queue),
1107 failure: this._makeIoEventCallback('failure', queue),
1108 end: this._makeIoEventCallback('end', queue),
1109 success: this._makeIoEventCallback('success', queue)
1110 }
1111 });
1112 },
1113
1114 /**
1115 * Creates a function that dispatches an IO response to callbacks.
1116 *
1117 * @param {String} event The type of event.
1118 * @param {Array} queue The queue.
1119 * @return {Function}
1120 */
1121 _makeIoEventCallback: function(event, queue) {
1122 var noop = function() {};
1123 return function() {
1124 var response = arguments[1],
1125 parsed = {};
1126
1127 if ((event == 'complete' || event == 'success') && (typeof response !== 'undefined'
1128 && typeof response.responseText !== 'undefined' && response.responseText !== '')) {
1129
1130 // Success and complete events need to parse the response.
1131 parsed = JSON.parse(response.responseText) || {};
1132 }
1133
1134 Y.Array.each(queue, function(item, index) {
1135 var context = item[1],
1136 cb = (item[2] && item[2][event]) || noop,
1137 arg;
1138
1139 if (parsed && parsed.error) {
1140 // The response is an error, we send it to everyone.
1141 arg = parsed;
1142 } else if (parsed) {
1143 // The response was parsed, we only communicate the relevant portion of the response.
1144 arg = parsed[index];
1145 }
1146
1147 cb.apply(context, [arg]);
1148 });
1149 };
1150 },
1151
1152 /**
1153 * Form submit handler.
1154 *
1155 * @param {EventFacade} e The event.
1156 * @return {Void}
1157 */
1158 _onSubmit: function(e) {
1159 var data = {},
1160 id = e.currentTarget.generateID(),
1161 params = this._submitEvents[id];
1162
1163 if (!params || params.ios.length < 1) {
1164 return;
1165 }
1166
1167 Y.Array.each(params.ios, function(param, index) {
1168 data[index] = param;
1169 });
1170
1171 Y.io(M.cfg.wwwroot + this.get('autosaveAjaxScript'), {
1172 method: 'POST',
1173 data: Y.QueryString.stringify({
1174 actions: data,
1175 sesskey: M.cfg.sesskey
1176 }),
1177 sync: true
1178 });
1179 },
1180
1181 /**
1182 * Registers a request to be made on form submission.
1183 *
1184 * @param {Node} node The forum node we will listen to.
1185 * @param {Object} params Parameters for the IO request.
1186 * @return {Void}
1187 */
1188 whenSubmit: function(node, params) {
1189 if (typeof this._submitEvents[node.generateID()] === 'undefined') {
1190 this._submitEvents[node.generateID()] = {
1191 event: node.on('submit', this._onSubmit, this),
1192 ios: []
1193 };
1194 }
1195 this._submitEvents[node.get('id')].ios.push([params]);
1196 }
1197
1198});
1199EditorAutosaveIoDispatcherInstance = new EditorAutosaveIoDispatcher();
1200
1201
1202function EditorAutosaveIo() {}
1203EditorAutosaveIo.prototype = {
1204
1205 /**
1206 * Dispatch an IO request.
1207 *
1208 * This method will put the requests in a queue in order to attempt to bulk them.
1209 *
1210 * @param {Object} params The parameters of the request.
1211 * @param {Object} context The context in which the callbacks are called.
1212 * @param {Object} callbacks Object with 'success', 'complete', 'end', 'failure' and 'start' as
1213 * optional keys defining the callbacks to call. Success and Complete
1214 * functions will receive the response as parameter. Success and Complete
1215 * may receive an object containing the error key, use this to confirm
1216 * that no errors occured.
1217 * @return {Void}
1218 */
1219 autosaveIo: function(params, context, callbacks) {
1220 EditorAutosaveIoDispatcherInstance.dispatch(params, context, callbacks);
1221 },
1222
1223 /**
1224 * Registers a request to be made on form submission.
1225 *
1226 * @param {Node} form The forum node we will listen to.
1227 * @param {Object} params Parameters for the IO request.
1228 * @return {Void}
1229 */
1230 autosaveIoOnSubmit: function(form, params) {
1231 EditorAutosaveIoDispatcherInstance.whenSubmit(form, params);
1232 }
1233
1234};
1235Y.Base.mix(Y.M.editor_atto.Editor, [EditorAutosaveIo]);
1236// This file is part of Moodle - http://moodle.org/
1237//
1238// Moodle is free software: you can redistribute it and/or modify
1239// it under the terms of the GNU General Public License as published by
1240// the Free Software Foundation, either version 3 of the License, or
1241// (at your option) any later version.
1242//
1243// Moodle is distributed in the hope that it will be useful,
1244// but WITHOUT ANY WARRANTY; without even the implied warranty of
1245// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1246// GNU General Public License for more details.
1247//
1248// You should have received a copy of the GNU General Public License
1249// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
ad3f8cd1 1250/* global LOGNAME */
240a9de4 1251
62467795
AN
1252/**
1253 * @module moodle-editor_atto-editor
1254 * @submodule clean
1255 */
adca7326 1256
62467795
AN
1257/**
1258 * Functions for the Atto editor to clean the generated content.
1259 *
1260 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1261 *
1262 * @namespace M.editor_atto
1263 * @class EditorClean
1264 */
adca7326 1265
62467795 1266function EditorClean() {}
adca7326 1267
62467795
AN
1268EditorClean.ATTRS= {
1269};
adca7326 1270
62467795 1271EditorClean.prototype = {
8951d614 1272 /**
62467795
AN
1273 * Clean the generated HTML content without modifying the editor content.
1274 *
1275 * This includes removes all YUI ids from the generated content.
8951d614 1276 *
62467795 1277 * @return {string} The cleaned HTML content.
8951d614 1278 */
62467795
AN
1279 getCleanHTML: function() {
1280 // Clone the editor so that we don't actually modify the real content.
9389ff57
FM
1281 var editorClone = this.editor.cloneNode(true),
1282 html;
8951d614 1283
62467795
AN
1284 // Remove all YUI IDs.
1285 Y.each(editorClone.all('[id^="yui"]'), function(node) {
1286 node.removeAttribute('id');
1287 });
8951d614 1288
62467795 1289 editorClone.all('.atto_control').remove(true);
9389ff57
FM
1290 html = editorClone.get('innerHTML');
1291
1292 // Revert untouched editor contents to an empty string.
1293 if (html === '<p></p>' || html === '<p><br></p>') {
1294 return '';
1295 }
8951d614 1296
62467795 1297 // Remove any and all nasties from source.
9389ff57 1298 return this._cleanHTML(html);
8951d614
JM
1299 },
1300
adca7326 1301 /**
62467795
AN
1302 * Clean the HTML content of the editor.
1303 *
1304 * @method cleanEditorHTML
1305 * @chainable
adca7326 1306 */
62467795
AN
1307 cleanEditorHTML: function() {
1308 var startValue = this.editor.get('innerHTML');
1309 this.editor.set('innerHTML', this._cleanHTML(startValue));
5ec54dd1 1310
62467795
AN
1311 return this;
1312 },
fe0d2477 1313
62467795
AN
1314 /**
1315 * Clean the specified HTML content and remove any content which could cause issues.
1316 *
1317 * @method _cleanHTML
1318 * @private
1319 * @param {String} content The content to clean
1320 * @return {String} The cleaned HTML
1321 */
1322 _cleanHTML: function(content) {
3ef96361 1323 // Removing limited things that can break the page or a disallowed, like unclosed comments, style blocks, etc.
62467795
AN
1324
1325 var rules = [
3ef96361
EM
1326 // Remove any style blocks. Some browsers do not work well with them in a contenteditable.
1327 // Plus style blocks are not allowed in body html, except with "scoped", which most browsers don't support as of 2015.
1328 // Reference: "http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work"
1329 {regex: /<style[^>]*>[\s\S]*?<\/style>/gi, replace: ""},
1330
a7fdadc9
EM
1331 // Remove any open HTML comment opens that are not followed by a close. This can completely break page layout.
1332 {regex: /<!--(?![\s\S]*?-->)/gi, replace: ""},
62467795
AN
1333
1334 // Source: "http://www.codinghorror.com/blog/2006/01/cleaning-words-nasty-html.html"
a7fdadc9 1335 // Remove forbidden tags for content, title, meta, style, st0-9, head, font, html, body, link.
d784f5ed 1336 {regex: /<\/?(?:title|meta|style|st\d|head|font|html|body|link)[^>]*?>/gi, replace: ""}
62467795 1337 ];
adca7326 1338
3ef96361
EM
1339 return this._filterContentWithRules(content, rules);
1340 },
1341
1342 /**
1343 * Take the supplied content and run on the supplied regex rules.
1344 *
1345 * @method _filterContentWithRules
1346 * @private
1347 * @param {String} content The content to clean
1348 * @param {Array} rules An array of structures: [ {regex: /something/, replace: "something"}, {...}, ...]
1349 * @return {String} The cleaned content
1350 */
1351 _filterContentWithRules: function(content, rules) {
62467795
AN
1352 var i = 0;
1353 for (i = 0; i < rules.length; i++) {
1354 content = content.replace(rules[i].regex, rules[i].replace);
adca7326
DW
1355 }
1356
62467795 1357 return content;
a7fdadc9
EM
1358 },
1359
1360 /**
1361 * Intercept and clean html paste events.
1362 *
1363 * @method pasteCleanup
1364 * @param {Object} sourceEvent The YUI EventFacade object
1365 * @return {Boolean} True if the passed event should continue, false if not.
1366 */
1367 pasteCleanup: function(sourceEvent) {
1368 // We only expect paste events, but we will check anyways.
1369 if (sourceEvent.type === 'paste') {
1370 // The YUI event wrapper doesn't provide paste event info, so we need the underlying event.
1371 var event = sourceEvent._event;
1372 // Check if we have a valid clipboardData object in the event.
1373 // IE has a clipboard object at window.clipboardData, but as of IE 11, it does not provide HTML content access.
73e14433
EM
1374 if (event && event.clipboardData && event.clipboardData.getData && event.clipboardData.types) {
1375 // Check if there is HTML type to be pasted, if we can get it, we want to scrub before insert.
a7fdadc9
EM
1376 var types = event.clipboardData.types;
1377 var isHTML = false;
73e14433
EM
1378 // Different browsers use different containers to hold the types, so test various functions.
1379 if (typeof types.contains === 'function') {
a7fdadc9
EM
1380 isHTML = types.contains('text/html');
1381 } else if (typeof types.indexOf === 'function') {
1382 isHTML = (types.indexOf('text/html') > -1);
a7fdadc9
EM
1383 }
1384
1385 if (isHTML) {
1386 // Get the clipboard content.
1387 var content;
1388 try {
1389 content = event.clipboardData.getData('text/html');
1390 } catch (error) {
1391 // Something went wrong. Fallback.
1392 this.fallbackPasteCleanupDelayed();
1393 return true;
1394 }
1395
1396 // Stop the original paste.
1397 sourceEvent.preventDefault();
1398
1399 // Scrub the paste content.
3ef96361 1400 content = this._cleanPasteHTML(content);
a7fdadc9
EM
1401
1402 // Save the current selection.
1403 // Using saveSelection as it produces a more consistent experience.
1404 var selection = window.rangy.saveSelection();
1405
1406 // Insert the content.
1407 this.insertContentAtFocusPoint(content);
1408
1409 // Restore the selection, and collapse to end.
1410 window.rangy.restoreSelection(selection);
1411 window.rangy.getSelection().collapseToEnd();
1412
1413 // Update the text area.
1414 this.updateOriginal();
1415 return false;
1416 } else {
73e14433
EM
1417 // Due to poor cross browser clipboard compatibility, the failure to find html doesn't mean it isn't there.
1418 // Wait for the clipboard event to finish then fallback clean the entire editor.
1419 this.fallbackPasteCleanupDelayed();
a7fdadc9
EM
1420 return true;
1421 }
1422 } else {
1423 // If we reached a here, this probably means the browser has limited (or no) clipboard support.
73e14433 1424 // Wait for the clipboard event to finish then fallback clean the entire editor.
a7fdadc9
EM
1425 this.fallbackPasteCleanupDelayed();
1426 return true;
1427 }
1428 }
1429
1430 // We should never get here - we must have received a non-paste event for some reason.
1431 // Um, just call updateOriginalDelayed() - it's safe.
1432 this.updateOriginalDelayed();
1433 return true;
1434 },
1435
1436 /**
1437 * Cleanup code after a paste event if we couldn't intercept the paste content.
1438 *
1439 * @method fallbackPasteCleanup
1440 * @chainable
1441 */
1442 fallbackPasteCleanup: function() {
1443
1444 // Save the current selection (cursor position).
1445 var selection = window.rangy.saveSelection();
1446
1447 // Get, clean, and replace the content in the editable.
1448 var content = this.editor.get('innerHTML');
3ef96361 1449 this.editor.set('innerHTML', this._cleanPasteHTML(content));
a7fdadc9
EM
1450
1451 // Update the textarea.
1452 this.updateOriginal();
1453
1454 // Restore the selection (cursor position).
1455 window.rangy.restoreSelection(selection);
1456
1457 return this;
1458 },
1459
1460 /**
1461 * Calls fallbackPasteCleanup on a short timer to allow the paste event handlers to complete.
1462 *
1463 * @method fallbackPasteCleanupDelayed
1464 * @chainable
1465 */
1466 fallbackPasteCleanupDelayed: function() {
1467 Y.soon(Y.bind(this.fallbackPasteCleanup, this));
1468
1469 return this;
3ef96361
EM
1470 },
1471
1472 /**
1473 * Cleanup html that comes from WYSIWYG paste events. These are more likely to contain messy code that we should strip.
1474 *
1475 * @method _cleanPasteHTML
1476 * @private
1477 * @param {String} content The html content to clean
1478 * @return {String} The cleaned HTML
1479 */
1480 _cleanPasteHTML: function(content) {
1481 // Return an empty string if passed an invalid or empty object.
1482 if (!content || content.length === 0) {
1483 return "";
1484 }
1485
1486 // Rules that get rid of the real-nasties and don't care about normalize code (correct quotes, white spaces, etc).
1487 var rules = [
6ea68e23 1488 // Stuff that is specifically from MS Word and similar office packages.
cfb32192
DM
1489 // Remove all garbage after closing html tag.
1490 {regex: /<\s*\/html\s*>([\s\S]+)$/gi, replace: ""},
6ea68e23
EM
1491 // Remove if comment blocks.
1492 {regex: /<!--\[if[\s\S]*?endif\]-->/gi, replace: ""},
1493 // Remove start and end fragment comment blocks.
1494 {regex: /<!--(Start|End)Fragment-->/gi, replace: ""},
3ef96361
EM
1495 // Remove any xml blocks.
1496 {regex: /<xml[^>]*>[\s\S]*?<\/xml>/gi, replace: ""},
1497 // Remove any <?xml><\?xml> blocks.
1498 {regex: /<\?xml[^>]*>[\s\S]*?<\\\?xml>/gi, replace: ""},
1499 // Remove <o:blah>, <\o:blah>.
df7a9fd4 1500 {regex: /<\/?\w+:[^>]*>/gi, replace: ""}
3ef96361
EM
1501 ];
1502
1503 // Apply the first set of harsher rules.
1504 content = this._filterContentWithRules(content, rules);
1505
1506 // Apply the standard rules, which mainly cleans things like headers, links, and style blocks.
1507 content = this._cleanHTML(content);
1508
1509 // Check if the string is empty or only contains whitespace.
1510 if (content.length === 0 || !content.match(/\S/)) {
1511 return content;
1512 }
1513
1514 // Now we let the browser normalize the code by loading it into the DOM and then get the html back.
1515 // This gives us well quoted, well formatted code to continue our work on. Word may provide very poorly formatted code.
1516 var holder = document.createElement('div');
1517 holder.innerHTML = content;
1518 content = holder.innerHTML;
1519 // Free up the DOM memory.
1520 holder.innerHTML = "";
1521
1522 // Run some more rules that care about quotes and whitespace.
1523 rules = [
8e202bd8
EM
1524 // Get all class attributes so we can work on them.
1525 {regex: /(<[^>]*?class\s*?=\s*?")([^>"]*)(")/gi, replace: function(match, group1, group2, group3) {
1526 // Remove MSO classes.
1527 group2 = group2.replace(/(?:^|[\s])[\s]*MSO[_a-zA-Z0-9\-]*/gi,"");
1528 // Remove Apple- classes.
1529 group2 = group2.replace(/(?:^|[\s])[\s]*Apple-[_a-zA-Z0-9\-]*/gi,"");
1530 return group1 + group2 + group3;
1531 }},
3ef96361 1532 // Remove OLE_LINK# anchors that may litter the code.
1b6ce030 1533 {regex: /<a [^>]*?name\s*?=\s*?"OLE_LINK\d*?"[^>]*?>\s*?<\/a>/gi, replace: ""}
3ef96361
EM
1534 ];
1535
91445c63
DW
1536 // Clean all style attributes from the text.
1537 content = this._cleanStyles(content);
1538
3ef96361
EM
1539 // Apply the rules.
1540 content = this._filterContentWithRules(content, rules);
1541
1542 // Reapply the standard cleaner to the content.
1543 content = this._cleanHTML(content);
1544
1b6ce030
EM
1545 // Clean unused spans out of the content.
1546 content = this._cleanSpans(content);
1547
3ef96361 1548 return content;
1b6ce030
EM
1549 },
1550
91445c63
DW
1551 /**
1552 * Clean all inline styles from pasted text.
1553 *
1554 * This code intentionally doesn't use YUI Nodes. YUI was quite a bit slower at this, so using raw DOM objects instead.
1555 *
1556 * @method _cleanStyles
1557 * @private
1558 * @param {String} content The content to clean
1559 * @return {String} The cleaned HTML
1560 */
1561 _cleanStyles: function(content) {
1562 var holder = document.createElement('div');
1563 holder.innerHTML = content;
1564 var elementsWithStyle = holder.querySelectorAll('[style]');
1565 var i = 0;
1566
1567 for (i = 0; i < elementsWithStyle.length; i++) {
1568 elementsWithStyle[i].removeAttribute('style');
1569 }
1570
1571 var elementsWithClass = holder.querySelectorAll('[class]');
1572 for (i = 0; i < elementsWithClass.length; i++) {
1573 elementsWithClass[i].removeAttribute('class');
1574 }
1575
1576 return holder.innerHTML;
1577 },
1b6ce030
EM
1578 /**
1579 * Clean empty or un-unused spans from passed HTML.
1580 *
1581 * This code intentionally doesn't use YUI Nodes. YUI was quite a bit slower at this, so using raw DOM objects instead.
1582 *
1583 * @method _cleanSpans
1584 * @private
1585 * @param {String} content The content to clean
1586 * @return {String} The cleaned HTML
1587 */
1588 _cleanSpans: function(content) {
1589 // Return an empty string if passed an invalid or empty object.
1590 if (!content || content.length === 0) {
1591 return "";
1592 }
1593 // Check if the string is empty or only contains whitespace.
1594 if (content.length === 0 || !content.match(/\S/)) {
1595 return content;
1596 }
1597
1598 var rules = [
1599 // Remove unused class, style, or id attributes. This will make empty tag detection easier later.
1600 {regex: /(<[^>]*?)(?:[\s]*(?:class|style|id)\s*?=\s*?"\s*?")+/gi, replace: "$1"}
1601 ];
1602 // Apply the rules.
1603 content = this._filterContentWithRules(content, rules);
1604
1605 // Reference: "http://stackoverflow.com/questions/8131396/remove-nested-span-without-id"
1606
1607 // This is better to run detached from the DOM, so the browser doesn't try to update on each change.
1608 var holder = document.createElement('div');
1609 holder.innerHTML = content;
1610 var spans = holder.getElementsByTagName('span');
1611
1612 // Since we will be removing elements from the list, we should copy it to an array, making it static.
1613 var spansarr = Array.prototype.slice.call(spans, 0);
1614
1615 spansarr.forEach(function(span) {
1616 if (!span.hasAttributes()) {
1617 // If no attributes (id, class, style, etc), this span is has no effect.
1618 // Move each child (if they exist) to the parent in place of this span.
1619 while (span.firstChild) {
1620 span.parentNode.insertBefore(span.firstChild, span);
1621 }
1622
1623 // Remove the now empty span.
1624 span.parentNode.removeChild(span);
1625 }
1626 });
1627
1628 return holder.innerHTML;
62467795
AN
1629 }
1630};
adca7326 1631
62467795
AN
1632Y.Base.mix(Y.M.editor_atto.Editor, [EditorClean]);
1633// This file is part of Moodle - http://moodle.org/
1634//
1635// Moodle is free software: you can redistribute it and/or modify
1636// it under the terms of the GNU General Public License as published by
1637// the Free Software Foundation, either version 3 of the License, or
1638// (at your option) any later version.
1639//
1640// Moodle is distributed in the hope that it will be useful,
1641// but WITHOUT ANY WARRANTY; without even the implied warranty of
1642// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1643// GNU General Public License for more details.
1644//
1645// You should have received a copy of the GNU General Public License
1646// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
adca7326 1647
dc9ca4ac
DT
1648/**
1649 * @module moodle-editor_atto-editor
1650 * @submodule commands
1651 */
1652
1653/**
1654 * Selection functions for the Atto editor.
1655 *
1656 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1657 *
1658 * @namespace M.editor_atto
1659 * @class EditorCommand
1660 */
1661
1662function EditorCommand() {}
1663
1664EditorCommand.ATTRS= {
1665};
1666
1667EditorCommand.prototype = {
1668 /**
1669 * Applies a callback method to editor if selection is uncollapsed or waits for input to select first.
1670 * @method applyFormat
1671 * @param e EventTarget Event to be passed to callback if selection is uncollapsed
1672 * @param method callback A callback method which changes editor when text is selected.
1673 * @param object context Context to be used for callback method
1674 * @param array args Array of arguments to pass to callback
1675 */
1676 applyFormat: function(e, callback, context, args) {
1677 function handleInsert(e, callback, context, args, anchorNode, anchorOffset) {
1678 // After something is inputed, select it and apply the formating function.
1679 Y.soon(Y.bind(function(e, callback, context, args, anchorNode, anchorOffset) {
1680 var selection = window.rangy.getSelection();
1681
1682 // Set the start of the selection to where it was when the method was first called.
1683 var range = selection.getRangeAt(0);
1684 range.setStart(anchorNode, anchorOffset);
1685 selection.setSingleRange(range);
1686
1687 // Now apply callback to the new text that is selected.
1688 callback.apply(context, [e, args]);
1689
1690 // Collapse selection so cursor is at end of inserted material.
1691 selection.collapseToEnd();
c8719db1
DT
1692
1693 // Save save selection and editor contents.
1694 this.saveSelection();
1695 this.updateOriginal();
dc9ca4ac
DT
1696 }, this, e, callback, context, args, anchorNode, anchorOffset));
1697 }
1698
1699 // Set default context for the method.
1700 context = context || this;
1701
1702 // Check whether range is collapsed.
1703 var selection = window.rangy.getSelection();
1704
1705 if (selection.isCollapsed) {
1706 // Selection is collapsed so listen for input into editor.
1707 var handle = this.editor.once('input', handleInsert, this, callback, context, args,
1708 selection.anchorNode, selection.anchorOffset);
1709
1710 // Cancel if selection changes before input.
1711 this.editor.onceAfter(['click', 'selectstart'], handle.detach, handle);
1712
1713 return;
1714 }
1715
1716 // The range is not collapsed; so apply callback method immediately.
1717 callback.apply(context, [e, args]);
1718
c8719db1
DT
1719 // Save save selection and editor contents.
1720 this.saveSelection();
1721 this.updateOriginal();
dc9ca4ac
DT
1722 },
1723
1724 /**
1725 * Replaces all the tags in a node list with new type.
1726 * @method replaceTags
1727 * @param NodeList nodelist
1728 * @param String tag
1729 */
1730 replaceTags: function(nodelist, tag) {
1731 // We mark elements in the node list for iterations.
1732 nodelist.setAttribute('data-iterate', true);
1733 var node = this.editor.one('[data-iterate="true"]');
1734 while (node) {
1735 var clone = Y.Node.create('<' + tag + ' />')
1736 .setAttrs(node.getAttrs())
1737 .removeAttribute('data-iterate');
1738 // Copy class and style if not blank.
1739 if (node.getAttribute('style')) {
1740 clone.setAttribute('style', node.getAttribute('style'));
1741 }
1742 if (node.getAttribute('class')) {
1743 clone.setAttribute('class', node.getAttribute('class'));
1744 }
1745 // We use childNodes here because we are interested in both type 1 and 3 child nodes.
1746 var children = node.getDOMNode().childNodes, child;
1747 child = children[0];
1748 while (typeof child !== "undefined") {
1749 clone.append(child);
1750 child = children[0];
1751 }
1752 node.replace(clone);
1753 node = this.editor.one('[data-iterate="true"]');
1754 }
1755 },
1756
1757 /**
1758 * Change all tags with given type to a span with CSS class attribute.
1759 * @method changeToCSS
1760 * @param String tag Tag type to be changed to span
1761 * @param String markerClass CSS class that corresponds to desired tag
1762 */
1763 changeToCSS: function(tag, markerClass) {
1764 // Save the selection.
1765 var selection = window.rangy.saveSelection();
1766
1767 // Remove display:none from rangy markers so browser doesn't delete them.
1768 this.editor.all('.rangySelectionBoundary').setStyle('display', null);
1769
1770 // Replace tags with CSS classes.
1771 this.editor.all(tag).addClass(markerClass);
1772 this.replaceTags(this.editor.all('.' + markerClass), 'span');
1773
1774 // Restore selection and toggle class.
1775 window.rangy.restoreSelection(selection);
1776 },
1777
1778 /**
1779 * Change spans with CSS classes in editor into elements with given tag.
1780 * @method changeToCSS
1781 * @param String markerClass CSS class that corresponds to desired tag
1782 * @param String tag New tag type to be created
1783 */
1784 changeToTags: function(markerClass, tag) {
1785 // Save the selection.
1786 var selection = window.rangy.saveSelection();
1787
1788 // Remove display:none from rangy markers so browser doesn't delete them.
1789 this.editor.all('.rangySelectionBoundary').setStyle('display', null);
1790
1791 // Replace spans with given tag.
1792 this.replaceTags(this.editor.all('span[class="' + markerClass + '"]'), tag);
1793 this.editor.all(tag + '[class="' + markerClass + '"]').removeAttribute('class');
1794 this.editor.all('.' + markerClass).each(function(n) {
1795 n.wrap('<' + tag + '/>');
1796 n.removeClass(markerClass);
1797 });
1798
1799 // Remove CSS classes.
1800 this.editor.all('[class="' + markerClass + '"]').removeAttribute('class');
1801 this.editor.all(tag).removeClass(markerClass);
1802
1803 // Restore selection.
1804 window.rangy.restoreSelection(selection);
1805 }
1806};
1807
1808Y.Base.mix(Y.M.editor_atto.Editor, [EditorCommand]);
1809// This file is part of Moodle - http://moodle.org/
1810//
1811// Moodle is free software: you can redistribute it and/or modify
1812// it under the terms of the GNU General Public License as published by
1813// the Free Software Foundation, either version 3 of the License, or
1814// (at your option) any later version.
1815//
1816// Moodle is distributed in the hope that it will be useful,
1817// but WITHOUT ANY WARRANTY; without even the implied warranty of
1818// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1819// GNU General Public License for more details.
1820//
1821// You should have received a copy of the GNU General Public License
1822// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
1823
62467795
AN
1824/**
1825 * @module moodle-editor_atto-editor
1826 * @submodule toolbar
1827 */
adca7326 1828
62467795
AN
1829/**
1830 * Toolbar functions for the Atto editor.
1831 *
1832 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1833 *
1834 * @namespace M.editor_atto
1835 * @class EditorToolbar
1836 */
adca7326 1837
62467795 1838function EditorToolbar() {}
34f5867a 1839
62467795
AN
1840EditorToolbar.ATTRS= {
1841};
adca7326 1842
62467795 1843EditorToolbar.prototype = {
adca7326 1844 /**
62467795
AN
1845 * A reference to the toolbar Node.
1846 *
1847 * @property toolbar
1848 * @type Node
adca7326 1849 */
62467795 1850 toolbar: null,
adca7326 1851
c63f9053
AN
1852 /**
1853 * A reference to any currently open menus in the toolbar.
1854 *
1855 * @property openMenus
1856 * @type Array
1857 */
1858 openMenus: null,
1859
86a83e3a 1860 /**
62467795 1861 * Setup the toolbar on the editor.
86a83e3a 1862 *
62467795
AN
1863 * @method setupToolbar
1864 * @chainable
86a83e3a 1865 */
62467795
AN
1866 setupToolbar: function() {
1867 this.toolbar = Y.Node.create('<div class="' + CSS.TOOLBAR + '" role="toolbar" aria-live="off"/>');
c63f9053 1868 this.openMenus = [];
62467795 1869 this._wrapper.appendChild(this.toolbar);
86a83e3a 1870
62467795
AN
1871 if (this.textareaLabel) {
1872 this.toolbar.setAttribute('aria-labelledby', this.textareaLabel.get("id"));
1873 }
86a83e3a 1874
62467795
AN
1875 // Add keyboard navigation for the toolbar.
1876 this.setupToolbarNavigation();
86a83e3a 1877
62467795
AN
1878 return this;
1879 }
1880};
86a83e3a 1881
62467795
AN
1882Y.Base.mix(Y.M.editor_atto.Editor, [EditorToolbar]);
1883// This file is part of Moodle - http://moodle.org/
1884//
1885// Moodle is free software: you can redistribute it and/or modify
1886// it under the terms of the GNU General Public License as published by
1887// the Free Software Foundation, either version 3 of the License, or
1888// (at your option) any later version.
1889//
1890// Moodle is distributed in the hope that it will be useful,
1891// but WITHOUT ANY WARRANTY; without even the implied warranty of
1892// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1893// GNU General Public License for more details.
1894//
1895// You should have received a copy of the GNU General Public License
1896// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
ad3f8cd1 1897/* global LOGNAME */
62467795
AN
1898
1899/**
1900 * @module moodle-editor_atto-editor
1901 * @submodule toolbarnav
1902 */
1903
1904/**
1905 * Toolbar Navigation functions for the Atto editor.
1906 *
1907 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
1908 *
1909 * @namespace M.editor_atto
1910 * @class EditorToolbarNav
1911 */
1912
1913function EditorToolbarNav() {}
1914
1915EditorToolbarNav.ATTRS= {
1916};
1917
1918EditorToolbarNav.prototype = {
1919 /**
1920 * The current focal point for tabbing.
1921 *
1922 * @property _tabFocus
1923 * @type Node
1924 * @default null
1925 * @private
1926 */
1927 _tabFocus: null,
1928
1929 /**
1930 * Set up the watchers for toolbar navigation.
1931 *
1932 * @method setupToolbarNavigation
1933 * @chainable
1934 */
1935 setupToolbarNavigation: function() {
1936 // Listen for Arrow left and Arrow right keys.
1937 this._wrapper.delegate('key',
1938 this.toolbarKeyboardNavigation,
1939 'down:37,39',
1940 '.' + CSS.TOOLBAR,
1941 this);
c63f9053
AN
1942 this._wrapper.delegate('focus',
1943 function(e) {
1944 this._setTabFocus(e.currentTarget);
1945 }, '.' + CSS.TOOLBAR + ' button', this);
62467795
AN
1946
1947 return this;
86a83e3a
DW
1948 },
1949
adca7326 1950 /**
62467795
AN
1951 * Implement arrow key navigation for the buttons in the toolbar.
1952 *
1953 * @method toolbarKeyboardNavigation
1954 * @param {EventFacade} e - the keyboard event.
adca7326 1955 */
62467795
AN
1956 toolbarKeyboardNavigation: function(e) {
1957 // Prevent the default browser behaviour.
1958 e.preventDefault();
adca7326 1959
62467795 1960 // On cursor moves we loops through the buttons.
b9d065ed 1961 var buttons = this.toolbar.all('button'),
62467795 1962 direction = 1,
af31595b 1963 button,
62467795
AN
1964 current = e.target.ancestor('button', true);
1965
b9d065ed
FM
1966 if (e.keyCode === 37) {
1967 // Moving left so reverse the direction.
1968 direction = -1;
26f8822d 1969 }
b269f635 1970
b9d065ed
FM
1971 button = this._findFirstFocusable(buttons, current, direction);
1972 if (button) {
1973 button.focus();
5e543b4f 1974 this._setTabFocus(button);
b9d065ed 1975 } else {
62467795 1976 }
b9d065ed 1977 },
adca7326 1978
b9d065ed
FM
1979 /**
1980 * Find the first focusable button.
1981 *
1982 * @param {NodeList} buttons A list of nodes.
1983 * @param {Node} startAt The node in the list to start the search from.
1984 * @param {Number} direction The direction in which to search (1 or -1).
1985 * @return {Node | Undefined} The Node or undefined.
1986 * @method _findFirstFocusable
1987 * @private
1988 */
1989 _findFirstFocusable: function(buttons, startAt, direction) {
1990 var checkCount = 0,
1991 group,
1992 candidate,
1993 button,
1994 index;
1995
1996 // Determine which button to start the search from.
1997 index = buttons.indexOf(startAt);
1998 if (index < -1) {
1999 index = 0;
62467795
AN
2000 }
2001
b9d065ed 2002 // Try to find the next.
af31595b 2003 while (checkCount < buttons.size()) {
62467795
AN
2004 index += direction;
2005 if (index < 0) {
2006 index = buttons.size() - 1;
2007 } else if (index >= buttons.size()) {
2008 // Handle wrapping.
2009 index = 0;
2010 }
af31595b
FM
2011
2012 candidate = buttons.item(index);
adca7326 2013
62467795
AN
2014 // Add a counter to ensure we don't get stuck in a loop if there's only one visible menu item.
2015 checkCount++;
af31595b 2016
62467795 2017 // Loop while:
af31595b
FM
2018 // * we haven't checked every button;
2019 // * the button is hidden or disabled;
2020 // * the group is hidden.
2021 if (candidate.hasAttribute('hidden') || candidate.hasAttribute('disabled')) {
2022 continue;
2023 }
2024 group = candidate.ancestor('.atto_group');
2025 if (group.hasAttribute('hidden')) {
2026 continue;
2027 }
2028
2029 button = candidate;
2030 break;
62467795 2031 }
af31595b 2032
b9d065ed
FM
2033 return button;
2034 },
2035
2036 /**
2037 * Check the tab focus.
2038 *
2039 * When we disable or hide a button, we should call this method to ensure that the
2040 * focus is not currently set on an inaccessible button, otherwise tabbing to the toolbar
2041 * would be impossible.
2042 *
2043 * @method checkTabFocus
2044 * @chainable
2045 */
2046 checkTabFocus: function() {
2047 if (this._tabFocus) {
2048 if (this._tabFocus.hasAttribute('disabled') || this._tabFocus.hasAttribute('hidden')
2049 || this._tabFocus.ancestor('.atto_group').hasAttribute('hidden')) {
2050 // Find first available button.
557f44d9 2051 var button = this._findFirstFocusable(this.toolbar.all('button'), this._tabFocus, -1);
b9d065ed
FM
2052 if (button) {
2053 if (this._tabFocus.compareTo(document.activeElement)) {
2054 // We should also move the focus, because the inaccessible button also has the focus.
2055 button.focus();
2056 }
2057 this._setTabFocus(button);
2058 }
2059 }
62467795 2060 }
b9d065ed 2061 return this;
62467795 2062 },
d088a835 2063
62467795
AN
2064 /**
2065 * Sets tab focus for the toolbar to the specified Node.
2066 *
2067 * @method _setTabFocus
2068 * @param {Node} button The node that focus should now be set to
2069 * @chainable
2070 * @private
2071 */
2072 _setTabFocus: function(button) {
2073 if (this._tabFocus) {
2074 // Unset the previous entry.
2075 this._tabFocus.setAttribute('tabindex', '-1');
2076 }
26f8822d 2077
62467795
AN
2078 // Set up the new entry.
2079 this._tabFocus = button;
2080 this._tabFocus.setAttribute('tabindex', 0);
4c37c1f4 2081
62467795
AN
2082 // And update the activedescendant to point at the currently selected button.
2083 this.toolbar.setAttribute('aria-activedescendant', this._tabFocus.generateID());
3ee53a42 2084
62467795
AN
2085 return this;
2086 }
2087};
67d3fe45 2088
62467795
AN
2089Y.Base.mix(Y.M.editor_atto.Editor, [EditorToolbarNav]);
2090// This file is part of Moodle - http://moodle.org/
2091//
2092// Moodle is free software: you can redistribute it and/or modify
2093// it under the terms of the GNU General Public License as published by
2094// the Free Software Foundation, either version 3 of the License, or
2095// (at your option) any later version.
2096//
2097// Moodle is distributed in the hope that it will be useful,
2098// but WITHOUT ANY WARRANTY; without even the implied warranty of
2099// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2100// GNU General Public License for more details.
2101//
2102// You should have received a copy of the GNU General Public License
2103// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
ad3f8cd1 2104/* global rangy */
adca7326 2105
62467795
AN
2106/**
2107 * @module moodle-editor_atto-editor
2108 * @submodule selection
2109 */
adca7326 2110
62467795
AN
2111/**
2112 * Selection functions for the Atto editor.
2113 *
2114 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
2115 *
2116 * @namespace M.editor_atto
2117 * @class EditorSelection
2118 */
fcb5b5c4 2119
62467795 2120function EditorSelection() {}
fcb5b5c4 2121
62467795
AN
2122EditorSelection.ATTRS= {
2123};
3ee53a42 2124
62467795 2125EditorSelection.prototype = {
3ee53a42
DW
2126
2127 /**
62467795
AN
2128 * List of saved selections per editor instance.
2129 *
2130 * @property _selections
2131 * @private
3ee53a42 2132 */
62467795 2133 _selections: null,
adca7326
DW
2134
2135 /**
62467795
AN
2136 * A unique identifier for the last selection recorded.
2137 *
2138 * @property _lastSelection
2139 * @param lastselection
2140 * @type string
2141 * @private
adca7326 2142 */
62467795 2143 _lastSelection: null,
adca7326
DW
2144
2145 /**
62467795
AN
2146 * Whether focus came from a click event.
2147 *
2148 * This is used to determine whether to restore the selection or not.
2149 *
2150 * @property _focusFromClick
2151 * @type Boolean
2152 * @default false
2153 * @private
adca7326 2154 */
62467795 2155 _focusFromClick: false,
adca7326 2156
90902953
DM
2157 /**
2158 * Whether if the last gesturemovestart event target was contained in this editor or not.
2159 *
2160 * @property _gesturestartededitor
2161 * @type Boolean
2162 * @default false
2163 * @private
2164 */
2165 _gesturestartededitor: false,
2166
adca7326 2167 /**
62467795
AN
2168 * Set up the watchers for selection save and restoration.
2169 *
2170 * @method setupSelectionWatchers
2171 * @chainable
adca7326 2172 */
62467795
AN
2173 setupSelectionWatchers: function() {
2174 // Save the selection when a change was made.
2175 this.on('atto:selectionchanged', this.saveSelection, this);
adca7326 2176
62467795 2177 this.editor.on('focus', this.restoreSelection, this);
adca7326 2178
62467795
AN
2179 // Do not restore selection when focus is from a click event.
2180 this.editor.on('mousedown', function() {
2181 this._focusFromClick = true;
2182 }, this);
adca7326 2183
62467795
AN
2184 // Copy the current value back to the textarea when focus leaves us and save the current selection.
2185 this.editor.on('blur', function() {
2186 // Clear the _focusFromClick value.
2187 this._focusFromClick = false;
adca7326 2188
62467795
AN
2189 // Update the original text area.
2190 this.updateOriginal();
2191 }, this);
adca7326 2192
8bca3609 2193 this.editor.on(['keyup', 'focus'], function(e) {
ee376395 2194 Y.soon(Y.bind(this._hasSelectionChanged, this, e));
8bca3609 2195 }, this);
e3050641 2196
90902953
DM
2197 Y.one(document.body).on('gesturemovestart', function(e) {
2198 if (this._wrapper.contains(e.target._node)) {
2199 this._gesturestartededitor = true;
2200 } else {
2201 this._gesturestartededitor = false;
2202 }
2203 }, null, this);
923589d7 2204
90902953
DM
2205 Y.one(document.body).on('gesturemoveend', function(e) {
2206 if (!this._gesturestartededitor) {
2207 // Ignore the event if movestart target was not contained in the editor.
923589d7
DM
2208 return;
2209 }
2210 Y.soon(Y.bind(this._hasSelectionChanged, this, e));
1f0d77f7
EM
2211 }, {
2212 // Standalone will make sure all editors receive the end event.
2213 standAlone: true
2214 }, this);
e3050641 2215
62467795 2216 return this;
b269f635
DW
2217 },
2218
adca7326 2219 /**
62467795
AN
2220 * Work out if the cursor is in the editable area for this editor instance.
2221 *
2222 * @method isActive
2223 * @return {boolean}
adca7326 2224 */
62467795
AN
2225 isActive: function() {
2226 var range = rangy.createRange(),
2227 selection = rangy.getSelection();
adca7326 2228
62467795
AN
2229 if (!selection.rangeCount) {
2230 // If there was no range count, then there is no selection.
2231 return false;
2232 }
adca7326 2233
1ce04361 2234 // We can't be active if the editor doesn't have focus at the moment.
9b9a3abf
DT
2235 if (!document.activeElement ||
2236 !(this.editor.compareTo(document.activeElement) || this.editor.contains(document.activeElement))) {
1ce04361
AN
2237 return false;
2238 }
2239
62467795
AN
2240 // Check whether the range intersects the editor selection.
2241 range.selectNode(this.editor.getDOMNode());
2242 return range.intersectsRange(selection.getRangeAt(0));
adca7326
DW
2243 },
2244
2245 /**
62467795
AN
2246 * Create a cross browser selection object that represents a YUI node.
2247 *
2248 * @method getSelectionFromNode
2249 * @param {Node} YUI Node to base the selection upon.
2250 * @return {[rangy.Range]}
adca7326 2251 */
62467795 2252 getSelectionFromNode: function(node) {
d321f68b
DW
2253 var range = rangy.createRange();
2254 range.selectNode(node.getDOMNode());
2255 return [range];
adca7326
DW
2256 },
2257
26f8822d 2258 /**
62467795
AN
2259 * Save the current selection to an internal property.
2260 *
2261 * This allows more reliable return focus, helping improve keyboard navigation.
2262 *
2263 * Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/restoreSelection"}}{{/crossLink}}.
2264 *
2265 * @method saveSelection
26f8822d 2266 */
62467795 2267 saveSelection: function() {
36265972
AN
2268 if (this.isActive()) {
2269 this._selections = this.getSelection();
2270 }
26f8822d
DW
2271 },
2272
2273 /**
62467795
AN
2274 * Restore any stored selection when the editor gets focus again.
2275 *
2276 * Should be used in combination with {{#crossLink "M.editor_atto.EditorSelection/saveSelection"}}{{/crossLink}}.
2277 *
2278 * @method restoreSelection
26f8822d 2279 */
62467795
AN
2280 restoreSelection: function() {
2281 if (!this._focusFromClick) {
2282 if (this._selections) {
2283 this.setSelection(this._selections);
26f8822d
DW
2284 }
2285 }
62467795 2286 this._focusFromClick = false;
26f8822d
DW
2287 },
2288
adca7326 2289 /**
62467795
AN
2290 * Get the selection object that can be passed back to setSelection.
2291 *
2292 * @method getSelection
2293 * @return {array} An array of rangy ranges.
adca7326 2294 */
62467795 2295 getSelection: function() {
d321f68b 2296 return rangy.getSelection().getAllRanges();
adca7326
DW
2297 },
2298
2299 /**
62467795
AN
2300 * Check that a YUI node it at least partly contained by the current selection.
2301 *
2302 * @method selectionContainsNode
2303 * @param {Node} The node to check.
2304 * @return {boolean}
adca7326 2305 */
62467795 2306 selectionContainsNode: function(node) {
d321f68b 2307 return rangy.getSelection().containsNode(node.getDOMNode(), true);
adca7326
DW
2308 },
2309
3ee53a42 2310 /**
62467795
AN
2311 * Runs a filter on each node in the selection, and report whether the
2312 * supplied selector(s) were found in the supplied Nodes.
3ee53a42 2313 *
62467795
AN
2314 * By default, all specified nodes must match the selection, but this
2315 * can be controlled with the requireall property.
2316 *
2317 * @method selectionFilterMatches
3ee53a42 2318 * @param {String} selector
62467795
AN
2319 * @param {NodeList} [selectednodes] For performance this should be passed. If not passed, this will be looked up each time.
2320 * @param {Boolean} [requireall=true] Used to specify that "any" match is good enough.
3ee53a42
DW
2321 * @return {Boolean}
2322 */
62467795
AN
2323 selectionFilterMatches: function(selector, selectednodes, requireall) {
2324 if (typeof requireall === 'undefined') {
d321f68b
DW
2325 requireall = true;
2326 }
3ee53a42
DW
2327 if (!selectednodes) {
2328 // Find this because it was not passed as a param.
62467795 2329 selectednodes = this.getSelectedNodes();
3ee53a42 2330 }
62467795
AN
2331 var allmatch = selectednodes.size() > 0,
2332 anymatch = false;
2333
2334 var editor = this.editor,
2335 stopFn = function(node) {
e76ae807
FM
2336 // The function getSelectedNodes only returns nodes within the editor, so this test is safe.
2337 return node === editor;
62467795
AN
2338 };
2339
e76ae807
FM
2340 // If we do not find at least one match in the editor, no point trying to find them in the selection.
2341 if (!editor.one(selector)) {
2342 return false;
2343 }
2344
67d3fe45
SH
2345 selectednodes.each(function(node){
2346 // Check each node, if it doesn't match the tags AND is not within the specified tags then fail this thing.
d321f68b
DW
2347 if (requireall) {
2348 // Check for at least one failure.
62467795 2349 if (!allmatch || !node.ancestor(selector, true, stopFn)) {
d321f68b
DW
2350 allmatch = false;
2351 }
2352 } else {
2353 // Check for at least one match.
62467795 2354 if (!anymatch && node.ancestor(selector, true, stopFn)) {
d321f68b
DW
2355 anymatch = true;
2356 }
3ee53a42 2357 }
67d3fe45 2358 }, this);
d321f68b
DW
2359 if (requireall) {
2360 return allmatch;
2361 } else {
2362 return anymatch;
2363 }
3ee53a42
DW
2364 },
2365
2366 /**
67d3fe45
SH
2367 * Get the deepest possible list of nodes in the current selection.
2368 *
62467795
AN
2369 * @method getSelectedNodes
2370 * @return {NodeList}
3ee53a42 2371 */
62467795 2372 getSelectedNodes: function() {
d321f68b
DW
2373 var results = new Y.NodeList(),
2374 nodes,
2375 selection,
67d3fe45 2376 range,
d321f68b
DW
2377 node,
2378 i;
2379
2380 selection = rangy.getSelection();
2381
2382 if (selection.rangeCount) {
2383 range = selection.getRangeAt(0);
2384 } else {
2385 // Empty range.
2386 range = rangy.createRange();
67d3fe45 2387 }
d321f68b
DW
2388
2389 if (range.collapsed) {
e9883836
FM
2390 // We do not want to select all the nodes in the editor if we managed to
2391 // have a collapsed selection directly in the editor.
17f253fa
AN
2392 // It's also possible for the commonAncestorContainer to be the document, which selectNode does not handle
2393 // so we must filter that out here too.
2394 if (range.commonAncestorContainer !== this.editor.getDOMNode()
2395 && range.commonAncestorContainer !== Y.config.doc) {
e9883836
FM
2396 range = range.cloneRange();
2397 range.selectNode(range.commonAncestorContainer);
2398 }
d321f68b
DW
2399 }
2400
2401 nodes = range.getNodes();
2402
2403 for (i = 0; i < nodes.length; i++) {
2404 node = Y.one(nodes[i]);
62467795 2405 if (this.editor.contains(node)) {
d321f68b 2406 results.push(node);
67d3fe45
SH
2407 }
2408 }
2409 return results;
3ee53a42
DW
2410 },
2411
2412 /**
62467795 2413 * Check whether the current selection has changed since this method was last called.
67d3fe45 2414 *
62467795 2415 * If the selection has changed, the atto:selectionchanged event is also fired.
67d3fe45 2416 *
62467795 2417 * @method _hasSelectionChanged
67d3fe45
SH
2418 * @private
2419 * @param {EventFacade} e
62467795 2420 * @return {Boolean}
3ee53a42 2421 */
62467795 2422 _hasSelectionChanged: function(e) {
d321f68b 2423 var selection = rangy.getSelection(),
67d3fe45 2424 range,
d321f68b
DW
2425 changed = false;
2426
2427 if (selection.rangeCount) {
2428 range = selection.getRangeAt(0);
2429 } else {
2430 // Empty range.
2431 range = rangy.createRange();
67d3fe45 2432 }
d321f68b 2433
62467795
AN
2434 if (this._lastSelection) {
2435 if (!this._lastSelection.equals(range)) {
d321f68b 2436 changed = true;
62467795 2437 return this._fireSelectionChanged(e);
d321f68b 2438 }
67d3fe45 2439 }
62467795 2440 this._lastSelection = range;
d321f68b 2441 return changed;
67d3fe45 2442 },
3ee53a42 2443
67d3fe45
SH
2444 /**
2445 * Fires the atto:selectionchanged event.
2446 *
62467795 2447 * When the selectionchanged event is fired, the following arguments are provided:
67d3fe45 2448 * - event : the original event that lead to this event being fired.
67d3fe45
SH
2449 * - selectednodes : an array containing nodes that are entirely selected of contain partially selected content.
2450 *
62467795 2451 * @method _fireSelectionChanged
67d3fe45
SH
2452 * @private
2453 * @param {EventFacade} e
2454 */
62467795 2455 _fireSelectionChanged: function(e) {
67d3fe45 2456 this.fire('atto:selectionchanged', {
62467795
AN
2457 event: e,
2458 selectedNodes: this.getSelectedNodes()
67d3fe45
SH
2459 });
2460 },
2461
adca7326 2462 /**
62467795
AN
2463 * Get the DOM node representing the common anscestor of the selection nodes.
2464 *
2465 * @method getSelectionParentNode
2466 * @return {Element|boolean} The DOM Node for this parent, or false if no seletion was made.
adca7326 2467 */
62467795 2468 getSelectionParentNode: function() {
d321f68b
DW
2469 var selection = rangy.getSelection();
2470 if (selection.rangeCount) {
2471 return selection.getRangeAt(0).commonAncestorContainer;
34f5867a 2472 }
34f5867a 2473 return false;
adca7326
DW
2474 },
2475
adca7326
DW
2476 /**
2477 * Set the current selection. Used to restore a selection.
62467795
AN
2478 *
2479 * @method selection
2480 * @param {array} ranges A list of rangy.range objects in the selection.
adca7326 2481 */
62467795 2482 setSelection: function(ranges) {
d321f68b
DW
2483 var selection = rangy.getSelection();
2484 selection.setRanges(ranges);
34f5867a
DW
2485 },
2486
62467795
AN
2487 /**
2488 * Inserts the given HTML into the editable content at the currently focused point.
2489 *
2490 * @method insertContentAtFocusPoint
2491 * @param {String} html
39c6f62d 2492 * @return {Node} The YUI Node object added to the DOM.
62467795
AN
2493 */
2494 insertContentAtFocusPoint: function(html) {
2495 var selection = rangy.getSelection(),
2496 range,
2497 node = Y.Node.create(html);
2498 if (selection.rangeCount) {
2499 range = selection.getRangeAt(0);
2500 }
2501 if (range) {
2502 range.deleteContents();
2503 range.insertNode(node.getDOMNode());
2504 }
39c6f62d 2505 return node;
62467795
AN
2506 }
2507
2508};
2509
2510Y.Base.mix(Y.M.editor_atto.Editor, [EditorSelection]);
2511// This file is part of Moodle - http://moodle.org/
2512//
2513// Moodle is free software: you can redistribute it and/or modify
2514// it under the terms of the GNU General Public License as published by
2515// the Free Software Foundation, either version 3 of the License, or
2516// (at your option) any later version.
2517//
2518// Moodle is distributed in the hope that it will be useful,
2519// but WITHOUT ANY WARRANTY; without even the implied warranty of
2520// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2521// GNU General Public License for more details.
2522//
2523// You should have received a copy of the GNU General Public License
2524// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
ad3f8cd1 2525/* global rangy */
62467795
AN
2526
2527/**
2528 * @module moodle-editor_atto-editor
2529 * @submodule styling
2530 */
2531
2532/**
2533 * Editor styling functions for the Atto editor.
2534 *
2535 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
2536 *
2537 * @namespace M.editor_atto
2538 * @class EditorStyling
2539 */
2540
2541function EditorStyling() {}
2542
2543EditorStyling.ATTRS= {
2544};
2545
2546EditorStyling.prototype = {
f6bef145
FM
2547 /**
2548 * Disable CSS styling.
2549 *
62467795 2550 * @method disableCssStyling
f6bef145 2551 */
62467795 2552 disableCssStyling: function() {
f6bef145
FM
2553 try {
2554 document.execCommand("styleWithCSS", 0, false);
2555 } catch (e1) {
2556 try {
2557 document.execCommand("useCSS", 0, true);
2558 } catch (e2) {
2559 try {
2560 document.execCommand('styleWithCSS', false, false);
2561 } catch (e3) {
2562 // We did our best.
2563 }
2564 }
2565 }
2566 },
2567
2568 /**
2569 * Enable CSS styling.
2570 *
62467795 2571 * @method enableCssStyling
f6bef145 2572 */
62467795 2573 enableCssStyling: function() {
f6bef145
FM
2574 try {
2575 document.execCommand("styleWithCSS", 0, true);
2576 } catch (e1) {
2577 try {
2578 document.execCommand("useCSS", 0, false);
2579 } catch (e2) {
2580 try {
2581 document.execCommand('styleWithCSS', false, true);
2582 } catch (e3) {
2583 // We did our best.
2584 }
2585 }
2586 }
2faf4c45
SH
2587 },
2588
bed1abbc
AD
2589 /**
2590 * Change the formatting for the current selection.
62467795
AN
2591 *
2592 * This will wrap the selection in span tags, adding the provided classes.
bed1abbc
AD
2593 *
2594 * If the selection covers multiple block elements, multiple spans will be inserted to preserve the original structure.
2595 *
62467795 2596 * @method toggleInlineSelectionClass
bed1abbc
AD
2597 * @param {Array} toggleclasses - Class names to be toggled on or off.
2598 */
62467795 2599 toggleInlineSelectionClass: function(toggleclasses) {
af6a2e94 2600 var classname = toggleclasses.join(" ");
b3c10f9f 2601 var cssApplier = rangy.createClassApplier(classname, {normalize: true});
af6a2e94
DW
2602
2603 cssApplier.toggleSelection();
af6a2e94
DW
2604 },
2605
2606 /**
2607 * Change the formatting for the current selection.
2608 *
2609 * This will set inline styles on the current selection.
2610 *
7bb2069b 2611 * @method formatSelectionInlineStyle
af6a2e94
DW
2612 * @param {Array} styles - Style attributes to set on the nodes.
2613 */
2614 formatSelectionInlineStyle: function(styles) {
2615 var classname = this.PLACEHOLDER_CLASS;
b3c10f9f 2616 var cssApplier = rangy.createClassApplier(classname, {normalize: true});
af6a2e94
DW
2617
2618 cssApplier.applyToSelection();
2619
2620 this.editor.all('.' + classname).each(function (node) {
2621 node.removeClass(classname).setStyles(styles);
2622 }, this);
2623
af6a2e94
DW
2624 },
2625
2626 /**
2627 * Change the formatting for the current selection.
2628 *
2629 * Also changes the selection to the newly formatted block (allows applying multiple styles to a block).
2630 *
2631 * @method formatSelectionBlock
2632 * @param {String} [blocktag] Change the block level tag to this. Empty string, means do not change the tag.
2633 * @param {Object} [attributes] The keys and values for attributes to be added/changed in the block tag.
2634 * @return {Node|boolean} The Node that was formatted if a change was made, otherwise false.
2635 */
2636 formatSelectionBlock: function(blocktag, attributes) {
2637 // First find the nearest ancestor of the selection that is a block level element.
62467795 2638 var selectionparentnode = this.getSelectionParentNode(),
af6a2e94
DW
2639 boundary,
2640 cell,
2641 nearestblock,
2642 newcontent,
2643 match,
2644 replacement;
bed1abbc
AD
2645
2646 if (!selectionparentnode) {
2647 // No selection, nothing to format.
af6a2e94 2648 return false;
bed1abbc
AD
2649 }
2650
af6a2e94 2651 boundary = this.editor;
bed1abbc 2652
af6a2e94
DW
2653 selectionparentnode = Y.one(selectionparentnode);
2654
2655 // If there is a table cell in between the selectionparentnode and the boundary,
2656 // move the boundary to the table cell.
2657 // This is because we might have a table in a div, and we select some text in a cell,
2658 // want to limit the change in style to the table cell, not the entire table (via the outer div).
2659 cell = selectionparentnode.ancestor(function (node) {
2660 var tagname = node.get('tagName');
2661 if (tagname) {
2662 tagname = tagname.toLowerCase();
bed1abbc 2663 }
af6a2e94
DW
2664 return (node === boundary) ||
2665 (tagname === 'td') ||
2666 (tagname === 'th');
2667 }, true);
bed1abbc 2668
af6a2e94
DW
2669 if (cell) {
2670 // Limit the scope to the table cell.
2671 boundary = cell;
2672 }
adca7326 2673
af6a2e94
DW
2674 nearestblock = selectionparentnode.ancestor(this.BLOCK_TAGS.join(', '), true);
2675 if (nearestblock) {
2676 // Check that the block is contained by the boundary.
2677 match = nearestblock.ancestor(function (node) {
2678 return node === boundary;
2679 }, false);
bed1abbc 2680
af6a2e94
DW
2681 if (!match) {
2682 nearestblock = false;
bed1abbc 2683 }
af6a2e94 2684 }
bed1abbc 2685
af6a2e94
DW
2686 // No valid block element - make one.
2687 if (!nearestblock) {
2688 // There is no block node in the content, wrap the content in a p and use that.
2689 newcontent = Y.Node.create('<p></p>');
2690 boundary.get('childNodes').each(function (child) {
2691 newcontent.append(child.remove());
2692 });
2693 boundary.append(newcontent);
2694 nearestblock = newcontent;
2695 }
2696
2697 // Guaranteed to have a valid block level element contained in the contenteditable region.
2698 // Change the tag to the new block level tag.
2699 if (blocktag && blocktag !== '') {
2700 // Change the block level node for a new one.
2701 replacement = Y.Node.create('<' + blocktag + '></' + blocktag + '>');
2702 // Copy all attributes.
2703 replacement.setAttrs(nearestblock.getAttrs());
2704 // Copy all children.
2705 nearestblock.get('childNodes').each(function (child) {
2706 child.remove();
2707 replacement.append(child);
2708 });
2709
2710 nearestblock.replace(replacement);
2711 nearestblock = replacement;
bed1abbc 2712 }
af6a2e94
DW
2713
2714 // Set the attributes on the block level tag.
2715 if (attributes) {
2716 nearestblock.setAttrs(attributes);
2717 }
2718
2719 // Change the selection to the modified block. This makes sense when we might apply multiple styles
2720 // to the block.
2721 var selection = this.getSelectionFromNode(nearestblock);
2722 this.setSelection(selection);
2723
2724 return nearestblock;
bed1abbc 2725 }
af6a2e94 2726
adca7326 2727};
67d3fe45 2728
62467795
AN
2729Y.Base.mix(Y.M.editor_atto.Editor, [EditorStyling]);
2730// This file is part of Moodle - http://moodle.org/
2731//
2732// Moodle is free software: you can redistribute it and/or modify
2733// it under the terms of the GNU General Public License as published by
2734// the Free Software Foundation, either version 3 of the License, or
2735// (at your option) any later version.
2736//
2737// Moodle is distributed in the hope that it will be useful,
2738// but WITHOUT ANY WARRANTY; without even the implied warranty of
2739// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2740// GNU General Public License for more details.
2741//
2742// You should have received a copy of the GNU General Public License
2743// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
2744
2745/**
2746 * @module moodle-editor_atto-editor
2747 * @submodule filepicker
2748 */
adca7326
DW
2749
2750/**
62467795 2751 * Filepicker options for the Atto editor.
adca7326 2752 *
62467795
AN
2753 * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
2754 *
2755 * @namespace M.editor_atto
2756 * @class EditorFilepicker
adca7326 2757 */
adca7326 2758
62467795 2759function EditorFilepicker() {}
adca7326 2760
62467795 2761EditorFilepicker.ATTRS= {
adca7326 2762 /**
62467795 2763 * The options for the filepicker.
adca7326 2764 *
62467795
AN
2765 * @attribute filepickeroptions
2766 * @type object
2767 * @default {}
adca7326 2768 */
62467795
AN
2769 filepickeroptions: {
2770 value: {}
adca7326 2771 }
62467795 2772};
adca7326 2773
62467795
AN
2774EditorFilepicker.prototype = {
2775 /**
2776 * Should we show the filepicker for this filetype?
2777 *
2778 * @method canShowFilepicker
2779 * @param string type The media type for the file picker.
2780 * @return {boolean}
2781 */
2782 canShowFilepicker: function(type) {
2783 return (typeof this.get('filepickeroptions')[type] !== 'undefined');
2784 },
adca7326 2785
62467795
AN
2786 /**
2787 * Show the filepicker.
2788 *
2789 * This depends on core_filepicker, and then call that modules show function.
2790 *
2791 * @method showFilepicker
2792 * @param {string} type The media type for the file picker.
2793 * @param {function} callback The callback to use when selecting an item of media.
2794 * @param {object} [context] The context from which to call the callback.
2795 */
2796 showFilepicker: function(type, callback, context) {
2797 var self = this;
2798 Y.use('core_filepicker', function (Y) {
2799 var options = Y.clone(self.get('filepickeroptions')[type], true);
2800 options.formcallback = callback;
2801 if (context) {
2802 options.magicscope = context;
2803 }
adca7326 2804
62467795
AN
2805 M.core_filepicker.show(Y, options);
2806 });
d088a835 2807 }
62467795 2808};
d088a835 2809
62467795 2810Y.Base.mix(Y.M.editor_atto.Editor, [EditorFilepicker]);
adca7326
DW
2811
2812
3ee53a42
DW
2813}, '@VERSION@', {
2814 "requires": [
2815 "node",
f1018cd6 2816 "transition",
3ee53a42
DW
2817 "io",
2818 "overlay",
2819