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