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