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