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