712262ff11bd53d395de4ca47edcad811fe9c7f5
[moodle.git] / lib / editor / atto / yui / src / editor / js / editor.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  * Atto editor main class.
18  * Common functions required by editor plugins.
19  *
20  * @package    editor-atto
21  * @copyright  2013 Damyon Wiese  <damyon@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 M.editor_atto = M.editor_atto || {
25     /**
26      * List of attached button handlers to prevent duplicates.
27      */
28     buttonhandlers : {},
30     /**
31      * List of YUI overlays for custom menus.
32      */
33     menus : {},
35     /**
36      * List of attached menu handlers to prevent duplicates.
37      */
38     menuhandlers : {},
40     /**
41      * List of file picker options for specific editor instances.
42      */
43     filepickeroptions : {},
45     /**
46      * List of buttons and menus that have been added to the toolbar.
47      */
48     widgets : {},
50     /**
51      * Toggle a menu.
52      * @param event e
53      */
54     showhide_menu_handler : function(e) {
55         e.preventDefault();
56         var disabled = this.getAttribute('disabled');
57         var overlayid = this.getAttribute('data-menu');
58         var overlay = M.editor_atto.menus[overlayid];
59         var menu = overlay.get('bodyContent');
60         if (overlay.get('visible') || disabled) {
61             overlay.hide();
62             menu.detach('clickoutside');
63         } else {
64             menu.on('clickoutside', function(ev) {
65                 if (ev.target.ancestor() !== this) {
66                     if (overlay.get('visible')) {
67                         menu.detach('clickoutside');
68                         overlay.hide();
69                     }
70                 }
71             }, this);
72             overlay.show();
73         }
74     },
76     /**
77      * Handle clicks on editor buttons.
78      * @param event e
79      */
80     buttonclicked_handler : function(e) {
81         var elementid = this.getAttribute('data-editor');
82         var plugin = this.getAttribute('data-plugin');
83         var handler = this.getAttribute('data-handler');
84         var overlay = M.editor_atto.menus[plugin + '_' + elementid];
86         if (overlay) {
87             overlay.hide();
88         }
90         if (M.editor_atto.is_enabled(elementid, plugin)) {
91             // Pass it on.
92             handler = M.editor_atto.buttonhandlers[handler];
93             return handler(e, elementid);
94         }
95     },
97     /**
98      * Determine if the specified toolbar button/menu is enabled.
99      * @param string elementid, the element id of this editor.
100      * @param string plugin, the plugin that created the button/menu.
101      */
102     is_enabled : function(elementid, plugin) {
103         var element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
105         return !element.hasAttribute('disabled');
106     },
107     /**
108      * Disable all buttons and menus in the toolbar.
109      * @param string elementid, the element id of this editor.
110      */
111     disable_all_widgets : function(elementid) {
112         var plugin, element;
113         for (plugin in M.editor_atto.widgets) {
114             element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
116             if (element) {
117                 element.setAttribute('disabled', 'true');
118             }
119         }
120     },
122     /**
123      * Enable a single widget in the toolbar.
124      * @param string elementid, the element id of this editor.
125      * @param string plugin, the name of the plugin that created the widget.
126      */
127     enable_widget : function(elementid, plugin) {
128         var element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
130         if (element) {
131             element.removeAttribute('disabled');
132         }
133     },
135     /**
136      * Enable all buttons and menus in the toolbar.
137      * @param string elementid, the element id of this editor.
138      */
139     enable_all_widgets : function(elementid) {
140         var plugin, element;
141         for (plugin in M.editor_atto.widgets) {
142             element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
144             if (element) {
145                 element.removeAttribute('disabled');
146             }
147         }
148     },
150     /**
151      * Add a button to the toolbar belonging to the editor for element with id "elementid".
152      * @param string elementid - the id of the textarea we created this editor from.
153      * @param string plugin - the plugin defining the button
154      * @param string icon - the html used for the content of the button
155      * @handler function handler- A function to call when the button is clicked.
156      */
157     add_toolbar_menu : function(elementid, plugin, icon, entries) {
158         var toolbar = Y.one('#' + elementid + '_toolbar');
159         var button = Y.Node.create('<button class="atto_' + plugin + '_button atto_hasmenu" ' +
160                                     'data-editor="' + Y.Escape.html(elementid) + '" ' +
161                                     'data-menu="' + plugin + '_' + elementid + '" >' +
162                                     icon +
163                                     '</button>');
165         toolbar.append(button);
167         // Save the name of the plugin.
168         M.editor_atto.widgets[plugin] = plugin;
170         var menu = Y.Node.create('<div class="atto_' + plugin + '_menu' +
171                                  ' atto_menu" data-editor="' + Y.Escape.html(elementid) + '"></div>');
172         var i = 0, entry = {};
174         for (i = 0; i < entries.length; i++) {
175             entry = entries[i];
177             menu.append(Y.Node.create('<div class="atto_menuentry">' +
178                                        '<a href="#" class="atto_' + plugin + '_action_' + i + '" ' +
179                                        'data-editor="' + Y.Escape.html(elementid) + '" ' +
180                                        'data-plugin="' + Y.Escape.html(plugin) + '" ' +
181                                        'data-handler="' + Y.Escape.html(plugin + '_action_' + i) + '">' +
182                                        entry.text +
183                                        '</a>' +
184                                        '</div>'));
185             if (!M.editor_atto.buttonhandlers[plugin + '_action_' + i]) {
186                 Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_action_' + i);
187                 M.editor_atto.buttonhandlers[plugin + '_action_' + i] = entry.handler;
188             }
189         }
191         if (!M.editor_atto.buttonhandlers[plugin]) {
192             Y.one('body').delegate('click', M.editor_atto.showhide_menu_handler, '.atto_' + plugin + '_button');
193             M.editor_atto.buttonhandlers[plugin] = true;
194         }
196         var overlay = new M.core.dialogue({
197             bodyContent : menu,
198             visible : false,
199             width: '14em',
200             zindex: 100,
201             lightbox: false,
202             closeButton: false,
203             centered : false,
204             align: {node: button, points: [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]}
205         });
207         M.editor_atto.menus[plugin + '_' + elementid] = overlay;
208         overlay.render();
209         overlay.hide();
210         overlay.headerNode.hide();
211     },
213     /**
214      * Add a button to the toolbar belonging to the editor for element with id "elementid".
215      * @param string elementid - the id of the textarea we created this editor from.
216      * @param string plugin - the plugin defining the button
217      * @param string icon - the html used for the content of the button
218      * @handler function handler- A function to call when the button is clicked.
219      */
220     add_toolbar_button : function(elementid, plugin, icon, handler) {
221         var toolbar = Y.one('#' + elementid + '_toolbar');
222         var button = Y.Node.create('<button class="atto_' + plugin + '_button" ' +
223                                    'data-editor="' + Y.Escape.html(elementid) + '" ' +
224                                    'data-plugin="' + Y.Escape.html(plugin) + '" ' +
225                                    'data-handler="' + Y.Escape.html(plugin) + '">' +
226                                     icon +
227                                     '</button>');
229         toolbar.append(button);
231         // We only need to attach this once.
232         if (!M.editor_atto.buttonhandlers[plugin]) {
233             Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_button');
234             M.editor_atto.buttonhandlers[plugin] = handler;
235         }
237         // Save the name of the plugin.
238         M.editor_atto.widgets[plugin] = plugin;
240     },
242     /**
243      * Work out if the cursor is in the editable area for this editor instance.
244      * @param string elementid of this editor
245      * @return bool
246      */
247     is_active : function(elementid) {
248         var selection = M.editor_atto.get_selection();
250         if (selection.length) {
251             selection = selection.pop();
252         }
254         var node = null;
255         if (selection.parentElement) {
256             node = Y.one(selection.parentElement());
257         } else {
258             node = Y.one(selection.startContainer);
259         }
261         return node && node.ancestor('#' + elementid + 'editable') !== null;
262     },
264     /**
265      * Focus on the editable area for this editor.
266      * @param string elementid of this editor
267      */
268     focus : function(elementid) {
269         Y.one('#' + elementid + 'editable').focus();
270     },
272     /**
273      * Initialise the editor
274      * @param object params for this editor instance.
275      */
276     init : function(params) {
277         var textarea = Y.one('#' +params.elementid);
278         var atto = Y.Node.create('<div id="' + params.elementid + 'editable" ' +
279                                             'contenteditable="true" ' +
280                                             'spellcheck="true" ' +
281                                             'class="editor_atto"/>');
282         var cssfont = '';
283         var toolbar = Y.Node.create('<div class="editor_atto_toolbar" id="' + params.elementid + '_toolbar"/>');
285         // Bleh - why are we sent a url and not the css to apply directly?
286         var css = Y.io(params.content_css, { sync: true });
287         var pos = css.responseText.indexOf('font:');
288         if (pos) {
289             cssfont = css.responseText.substring(pos + 'font:'.length, css.responseText.length - 1);
290             atto.setStyle('font', cssfont);
291         }
292         atto.setStyle('minHeight', (1.2 * (textarea.getAttribute('rows') - 1)) + 'em');
294         // Copy text to editable div.
295         atto.append(textarea.get('value'));
297         // Add the toolbar to the page.
298         textarea.get('parentNode').insert(toolbar, textarea);
299         // Add the editable div to the page.
300         textarea.get('parentNode').insert(atto, textarea);
301         atto.setStyle('color', textarea.getStyle('color'));
302         atto.setStyle('lineHeight', textarea.getStyle('lineHeight'));
303         atto.setStyle('fontSize', textarea.getStyle('fontSize'));
304         // Hide the old textarea.
305         textarea.hide();
307         // Copy the current value back to the textarea when focus leaves us.
308         atto.on('blur', function() {
309             textarea.set('value', atto.getHTML());
310         });
312         // Save the file picker options for later.
313         M.editor_atto.filepickeroptions[params.elementid] = params.filepickeroptions;
314     },
316     /**
317      * Show the filepicker.
318      * @param string elementid for this editor instance.
319      * @param string type The media type for the file picker
320      * @param function callback
321      */
322     show_filepicker : function(elementid, type, callback) {
323         Y.use('core_filepicker', function (Y) {
324             var options = M.editor_atto.filepickeroptions[elementid][type];
326             options.formcallback = callback;
327             options.editor_target = Y.one(elementid);
329             M.core_filepicker.show(Y, options);
330         });
331     },
333     /**
334      * Create a cross browser selection object that represents a yui node.
335      * @param Node yui node for the selection
336      * @return range (browser dependent)
337      */
338     get_selection_from_node: function(node) {
339         var range;
341         if (window.getSelection) {
342             range = document.createRange();
344             range.setStartBefore(node.getDOMNode());
345             range.setEndAfter(node.getDOMNode());
346             return [range];
347         } else if (document.selection) {
348             range = document.body.createTextRange();
349             range.moveToElementText(node.getDOMNode());
350             return range;
351         }
352         return false;
353     },
355     /**
356      * Get the selection object that can be passed back to set_selection.
357      * @return range (browser dependent)
358      */
359     get_selection : function() {
360         if (window.getSelection) {
361             var sel = window.getSelection();
362             var ranges = [], i = 0;
363             for (i = 0; i < sel.rangeCount; i++) {
364                 ranges.push(sel.getRangeAt(i));
365             }
366             return ranges;
367         } else if (document.selection) {
368             // IE < 9
369             if (document.selection.createRange) {
370                 return document.selection.createRange();
371             }
372         }
373         return false;
374     },
376     /**
377      * Get the dom node representing the common anscestor of the selection nodes.
378      * @return DOMNode
379      */
380     get_selection_parent_node : function() {
381         var selection = M.editor_atto.get_selection();
382         if (selection.length > 0) {
383             return selection[0].commonAncestorContainer;
384         }
385     },
387     /**
388      * Get the list of child nodes of the selection.
389      * @return DOMNode[]
390      */
391     get_selection_text : function() {
392         var selection = M.editor_atto.get_selection();
393         if (selection.length > 0 && selection[0].cloneContents) {
394             return selection[0].cloneContents();
395         }
396     },
398     /**
399      * Set the current selection. Used to restore a selection.
400      */
401     set_selection : function(selection) {
402         var sel, i;
404         if (window.getSelection) {
405             sel = window.getSelection();
406             sel.removeAllRanges();
407             for (i = 0; i < selection.length; i++) {
408                 sel.addRange(selection[i]);
409             }
410         } else if (document.selection) {
411             // IE < 9
412             if (selection.select) {
413                 selection.select();
414             }
415         }
416     }
418 };