Merge branch 'MDL-50658' of https://github.com/spvickers/moodle
[moodle.git] / lib / editor / atto / yui / src / editor / js / commands.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * @module moodle-editor_atto-editor
18  * @submodule commands
19  */
21 /**
22  * Selection functions for the Atto editor.
23  *
24  * See {{#crossLink "M.editor_atto.Editor"}}{{/crossLink}} for details.
25  *
26  * @namespace M.editor_atto
27  * @class EditorCommand
28  */
30 function EditorCommand() {}
32 EditorCommand.ATTRS= {
33 };
35 EditorCommand.prototype = {
36     /**
37      * Applies a callback method to editor if selection is uncollapsed or waits for input to select first.
38      * @method applyFormat
39      * @param e EventTarget Event to be passed to callback if selection is uncollapsed
40      * @param method callback A callback method which changes editor when text is selected.
41      * @param object context Context to be used for callback method
42      * @param array args Array of arguments to pass to callback
43      */
44     applyFormat: function(e, callback, context, args) {
45         function handleInsert(e, callback, context, args, anchorNode, anchorOffset) {
46             // After something is inputed, select it and apply the formating function.
47             Y.soon(Y.bind(function(e, callback, context, args, anchorNode, anchorOffset) {
48                 var selection = window.rangy.getSelection();
50                 // Set the start of the selection to where it was when the method was first called.
51                 var range = selection.getRangeAt(0);
52                 range.setStart(anchorNode, anchorOffset);
53                 selection.setSingleRange(range);
55                 // Now apply callback to the new text that is selected.
56                 callback.apply(context, [e, args]);
58                 // Collapse selection so cursor is at end of inserted material.
59                 selection.collapseToEnd();
60             }, this, e, callback, context, args, anchorNode, anchorOffset));
61         }
63         // Set default context for the method.
64         context = context || this;
66         // Check whether range is collapsed.
67         var selection = window.rangy.getSelection();
69         if (selection.isCollapsed) {
70             // Selection is collapsed so listen for input into editor.
71             var handle = this.editor.once('input', handleInsert, this, callback, context, args,
72                     selection.anchorNode, selection.anchorOffset);
74             // Cancel if selection changes before input.
75             this.editor.onceAfter(['click', 'selectstart'], handle.detach, handle);
77             return;
78         }
80         // The range is not collapsed; so apply callback method immediately.
81         callback.apply(context, [e, args]);
83     },
85     /**
86      * Replaces all the tags in a node list with new type.
87      * @method replaceTags
88      * @param NodeList nodelist
89      * @param String tag
90      */
91     replaceTags: function(nodelist, tag) {
92         // We mark elements in the node list for iterations.
93         nodelist.setAttribute('data-iterate', true);
94         var node = this.editor.one('[data-iterate="true"]');
95         while (node) {
96             var clone = Y.Node.create('<' + tag + ' />')
97                 .setAttrs(node.getAttrs())
98                 .removeAttribute('data-iterate');
99             // Copy class and style if not blank.
100             if (node.getAttribute('style')) {
101                 clone.setAttribute('style', node.getAttribute('style'));
102             }
103             if (node.getAttribute('class')) {
104                 clone.setAttribute('class', node.getAttribute('class'));
105             }
106             // We use childNodes here because we are interested in both type 1 and 3 child nodes.
107             var children = node.getDOMNode().childNodes, child;
108             child = children[0];
109             while (typeof child !== "undefined") {
110                 clone.append(child);
111                 child = children[0];
112             }
113             node.replace(clone);
114             node = this.editor.one('[data-iterate="true"]');
115         }
116     },
118     /**
119      * Change all tags with given type to a span with CSS class attribute.
120      * @method changeToCSS
121      * @param String tag Tag type to be changed to span
122      * @param String markerClass CSS class that corresponds to desired tag
123      */
124     changeToCSS: function(tag, markerClass) {
125         // Save the selection.
126         var selection = window.rangy.saveSelection();
128         // Remove display:none from rangy markers so browser doesn't delete them.
129         this.editor.all('.rangySelectionBoundary').setStyle('display', null);
131         // Replace tags with CSS classes.
132         this.editor.all(tag).addClass(markerClass);
133         this.replaceTags(this.editor.all('.' + markerClass), 'span');
135         // Restore selection and toggle class.
136         window.rangy.restoreSelection(selection);
137     },
139     /**
140      * Change spans with CSS classes in editor into elements with given tag.
141      * @method changeToCSS
142      * @param String markerClass CSS class that corresponds to desired tag
143      * @param String tag New tag type to be created
144      */
145     changeToTags: function(markerClass, tag) {
146         // Save the selection.
147         var selection = window.rangy.saveSelection();
149         // Remove display:none from rangy markers so browser doesn't delete them.
150         this.editor.all('.rangySelectionBoundary').setStyle('display', null);
152         // Replace spans with given tag.
153         this.replaceTags(this.editor.all('span[class="' + markerClass + '"]'), tag);
154         this.editor.all(tag + '[class="' + markerClass + '"]').removeAttribute('class');
155         this.editor.all('.' + markerClass).each(function(n) {
156             n.wrap('<' + tag + '/>');
157             n.removeClass(markerClass);
158         });
160         // Remove CSS classes.
161         this.editor.all('[class="' + markerClass + '"]').removeAttribute('class');
162         this.editor.all(tag).removeClass(markerClass);
164         // Restore selection.
165         window.rangy.restoreSelection(selection);
166     }
167 };
169 Y.Base.mix(Y.M.editor_atto.Editor, [EditorCommand]);