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