Commit | Line | Data |
---|---|---|
c90641fa DW |
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/>. | |
15 | ||
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 : {}, | |
29 | ||
30 | /** | |
31 | * List of YUI overlays for custom menus. | |
32 | */ | |
33 | menus : {}, | |
34 | ||
35 | /** | |
36 | * List of attached menu handlers to prevent duplicates. | |
37 | */ | |
38 | menuhandlers : {}, | |
39 | ||
40 | /** | |
41 | * List of file picker options for specific editor instances. | |
42 | */ | |
43 | filepickeroptions : {}, | |
44 | ||
45 | /** | |
46 | * List of buttons and menus that have been added to the toolbar. | |
47 | */ | |
48 | widgets : {}, | |
49 | ||
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]; | |
21f6c529 | 59 | var menu = overlay.get('bodyContent'); |
c90641fa DW |
60 | if (overlay.get('visible') || disabled) { |
61 | overlay.hide(); | |
21f6c529 | 62 | menu.detach('clickoutside'); |
c90641fa | 63 | } else { |
21f6c529 | 64 | menu.on('clickoutside', function(ev) { |
c1f10ffb | 65 | if ((ev.target.ancestor() !== this) && (ev.target !== this)) { |
21f6c529 JF |
66 | if (overlay.get('visible')) { |
67 | menu.detach('clickoutside'); | |
68 | overlay.hide(); | |
69 | } | |
70 | } | |
71 | }, this); | |
c90641fa DW |
72 | overlay.show(); |
73 | } | |
c90641fa DW |
74 | }, |
75 | ||
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]; | |
85 | ||
86 | if (overlay) { | |
87 | overlay.hide(); | |
88 | } | |
89 | ||
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 | }, | |
96 | ||
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'); | |
104 | ||
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'); | |
115 | ||
116 | if (element) { | |
117 | element.setAttribute('disabled', 'true'); | |
118 | } | |
119 | } | |
120 | }, | |
121 | ||
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'); | |
129 | ||
130 | if (element) { | |
131 | element.removeAttribute('disabled'); | |
132 | } | |
133 | }, | |
134 | ||
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'); | |
143 | ||
144 | if (element) { | |
145 | element.removeAttribute('disabled'); | |
146 | } | |
147 | } | |
148 | }, | |
149 | ||
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 | |
36973d70 | 155 | * @param string groupname - the group the button should be appended to. |
c90641fa DW |
156 | * @handler function handler- A function to call when the button is clicked. |
157 | */ | |
36973d70 JF |
158 | add_toolbar_menu : function(elementid, plugin, icon, groupname, entries) { |
159 | var toolbar = Y.one('#' + elementid + '_toolbar'), | |
160 | group = Y.one('#' + elementid + '_toolbar .atto_group.' + groupname + '_group'); | |
161 | if (!group) { | |
162 | group = Y.Node.create('<div class="atto_group ' + groupname + '_group"></div>'); | |
163 | toolbar.append(group); | |
164 | } | |
c90641fa DW |
165 | var button = Y.Node.create('<button class="atto_' + plugin + '_button atto_hasmenu" ' + |
166 | 'data-editor="' + Y.Escape.html(elementid) + '" ' + | |
167 | 'data-menu="' + plugin + '_' + elementid + '" >' + | |
168 | icon + | |
169 | '</button>'); | |
170 | ||
36973d70 | 171 | group.append(button); |
c90641fa DW |
172 | |
173 | // Save the name of the plugin. | |
174 | M.editor_atto.widgets[plugin] = plugin; | |
175 | ||
176 | var menu = Y.Node.create('<div class="atto_' + plugin + '_menu' + | |
177 | ' atto_menu" data-editor="' + Y.Escape.html(elementid) + '"></div>'); | |
c90641fa DW |
178 | var i = 0, entry = {}; |
179 | ||
180 | for (i = 0; i < entries.length; i++) { | |
181 | entry = entries[i]; | |
182 | ||
183 | menu.append(Y.Node.create('<div class="atto_menuentry">' + | |
184 | '<a href="#" class="atto_' + plugin + '_action_' + i + '" ' + | |
185 | 'data-editor="' + Y.Escape.html(elementid) + '" ' + | |
186 | 'data-plugin="' + Y.Escape.html(plugin) + '" ' + | |
187 | 'data-handler="' + Y.Escape.html(plugin + '_action_' + i) + '">' + | |
188 | entry.text + | |
189 | '</a>' + | |
190 | '</div>')); | |
191 | if (!M.editor_atto.buttonhandlers[plugin + '_action_' + i]) { | |
192 | Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_action_' + i); | |
193 | M.editor_atto.buttonhandlers[plugin + '_action_' + i] = entry.handler; | |
194 | } | |
195 | } | |
196 | ||
197 | if (!M.editor_atto.buttonhandlers[plugin]) { | |
198 | Y.one('body').delegate('click', M.editor_atto.showhide_menu_handler, '.atto_' + plugin + '_button'); | |
199 | M.editor_atto.buttonhandlers[plugin] = true; | |
200 | } | |
201 | ||
202 | var overlay = new M.core.dialogue({ | |
203 | bodyContent : menu, | |
204 | visible : false, | |
205 | width: '14em', | |
206 | zindex: 100, | |
207 | lightbox: false, | |
208 | closeButton: false, | |
4fd8adab | 209 | centered : false, |
c90641fa DW |
210 | align: {node: button, points: [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]} |
211 | }); | |
212 | ||
213 | M.editor_atto.menus[plugin + '_' + elementid] = overlay; | |
214 | overlay.render(); | |
215 | overlay.hide(); | |
216 | overlay.headerNode.hide(); | |
217 | }, | |
218 | ||
219 | /** | |
220 | * Add a button to the toolbar belonging to the editor for element with id "elementid". | |
221 | * @param string elementid - the id of the textarea we created this editor from. | |
36973d70 JF |
222 | * @param string plugin - the plugin defining the button. |
223 | * @param string icon - the html used for the content of the button. | |
224 | * @param string groupname - the group the button should be appended to. | |
c90641fa DW |
225 | * @handler function handler- A function to call when the button is clicked. |
226 | */ | |
36973d70 JF |
227 | add_toolbar_button : function(elementid, plugin, icon, groupname, handler) { |
228 | var toolbar = Y.one('#' + elementid + '_toolbar'), | |
229 | group = Y.one('#' + elementid + '_toolbar .atto_group.' + groupname + '_group'); | |
230 | if (!group) { | |
231 | group = Y.Node.create('<div class="atto_group ' + groupname +'_group"></div>'); | |
232 | toolbar.append(group); | |
233 | } | |
c90641fa DW |
234 | var button = Y.Node.create('<button class="atto_' + plugin + '_button" ' + |
235 | 'data-editor="' + Y.Escape.html(elementid) + '" ' + | |
236 | 'data-plugin="' + Y.Escape.html(plugin) + '" ' + | |
237 | 'data-handler="' + Y.Escape.html(plugin) + '">' + | |
238 | icon + | |
239 | '</button>'); | |
240 | ||
36973d70 | 241 | group.append(button); |
c90641fa DW |
242 | |
243 | // We only need to attach this once. | |
244 | if (!M.editor_atto.buttonhandlers[plugin]) { | |
245 | Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_button'); | |
246 | M.editor_atto.buttonhandlers[plugin] = handler; | |
247 | } | |
248 | ||
249 | // Save the name of the plugin. | |
250 | M.editor_atto.widgets[plugin] = plugin; | |
251 | ||
252 | }, | |
253 | ||
254 | /** | |
255 | * Work out if the cursor is in the editable area for this editor instance. | |
256 | * @param string elementid of this editor | |
257 | * @return bool | |
258 | */ | |
259 | is_active : function(elementid) { | |
260 | var selection = M.editor_atto.get_selection(); | |
261 | ||
262 | if (selection.length) { | |
263 | selection = selection.pop(); | |
264 | } | |
265 | ||
266 | var node = null; | |
267 | if (selection.parentElement) { | |
268 | node = Y.one(selection.parentElement()); | |
269 | } else { | |
270 | node = Y.one(selection.startContainer); | |
271 | } | |
272 | ||
273 | return node && node.ancestor('#' + elementid + 'editable') !== null; | |
274 | }, | |
275 | ||
276 | /** | |
277 | * Focus on the editable area for this editor. | |
278 | * @param string elementid of this editor | |
279 | */ | |
280 | focus : function(elementid) { | |
281 | Y.one('#' + elementid + 'editable').focus(); | |
282 | }, | |
283 | ||
284 | /** | |
285 | * Initialise the editor | |
286 | * @param object params for this editor instance. | |
287 | */ | |
288 | init : function(params) { | |
289 | var textarea = Y.one('#' +params.elementid); | |
290 | var atto = Y.Node.create('<div id="' + params.elementid + 'editable" ' + | |
291 | 'contenteditable="true" ' + | |
292 | 'spellcheck="true" ' + | |
293 | 'class="editor_atto"/>'); | |
294 | var cssfont = ''; | |
295 | var toolbar = Y.Node.create('<div class="editor_atto_toolbar" id="' + params.elementid + '_toolbar"/>'); | |
296 | ||
297 | // Bleh - why are we sent a url and not the css to apply directly? | |
298 | var css = Y.io(params.content_css, { sync: true }); | |
299 | var pos = css.responseText.indexOf('font:'); | |
300 | if (pos) { | |
301 | cssfont = css.responseText.substring(pos + 'font:'.length, css.responseText.length - 1); | |
302 | atto.setStyle('font', cssfont); | |
303 | } | |
ceaef9a9 | 304 | atto.setStyle('minHeight', (1.2 * (textarea.getAttribute('rows') - 1)) + 'em'); |
c90641fa DW |
305 | |
306 | // Copy text to editable div. | |
307 | atto.append(textarea.get('value')); | |
308 | ||
309 | // Add the toolbar to the page. | |
310 | textarea.get('parentNode').insert(toolbar, textarea); | |
311 | // Add the editable div to the page. | |
312 | textarea.get('parentNode').insert(atto, textarea); | |
08a95d50 DW |
313 | atto.setStyle('color', textarea.getStyle('color')); |
314 | atto.setStyle('lineHeight', textarea.getStyle('lineHeight')); | |
315 | atto.setStyle('fontSize', textarea.getStyle('fontSize')); | |
c90641fa DW |
316 | // Hide the old textarea. |
317 | textarea.hide(); | |
318 | ||
319 | // Copy the current value back to the textarea when focus leaves us. | |
320 | atto.on('blur', function() { | |
321 | textarea.set('value', atto.getHTML()); | |
322 | }); | |
323 | ||
324 | // Save the file picker options for later. | |
325 | M.editor_atto.filepickeroptions[params.elementid] = params.filepickeroptions; | |
326 | }, | |
327 | ||
328 | /** | |
329 | * Show the filepicker. | |
330 | * @param string elementid for this editor instance. | |
331 | * @param string type The media type for the file picker | |
332 | * @param function callback | |
333 | */ | |
334 | show_filepicker : function(elementid, type, callback) { | |
335 | Y.use('core_filepicker', function (Y) { | |
336 | var options = M.editor_atto.filepickeroptions[elementid][type]; | |
337 | ||
338 | options.formcallback = callback; | |
339 | options.editor_target = Y.one(elementid); | |
340 | ||
341 | M.core_filepicker.show(Y, options); | |
342 | }); | |
343 | }, | |
344 | ||
345 | /** | |
346 | * Create a cross browser selection object that represents a yui node. | |
347 | * @param Node yui node for the selection | |
348 | * @return range (browser dependent) | |
349 | */ | |
350 | get_selection_from_node: function(node) { | |
351 | var range; | |
352 | ||
353 | if (window.getSelection) { | |
354 | range = document.createRange(); | |
355 | ||
356 | range.setStartBefore(node.getDOMNode()); | |
357 | range.setEndAfter(node.getDOMNode()); | |
358 | return [range]; | |
359 | } else if (document.selection) { | |
360 | range = document.body.createTextRange(); | |
361 | range.moveToElementText(node.getDOMNode()); | |
362 | return range; | |
363 | } | |
364 | return false; | |
365 | }, | |
366 | ||
367 | /** | |
368 | * Get the selection object that can be passed back to set_selection. | |
369 | * @return range (browser dependent) | |
370 | */ | |
371 | get_selection : function() { | |
372 | if (window.getSelection) { | |
373 | var sel = window.getSelection(); | |
374 | var ranges = [], i = 0; | |
375 | for (i = 0; i < sel.rangeCount; i++) { | |
376 | ranges.push(sel.getRangeAt(i)); | |
377 | } | |
378 | return ranges; | |
379 | } else if (document.selection) { | |
380 | // IE < 9 | |
381 | if (document.selection.createRange) { | |
382 | return document.selection.createRange(); | |
383 | } | |
384 | } | |
385 | return false; | |
386 | }, | |
387 | ||
388 | /** | |
389 | * Get the dom node representing the common anscestor of the selection nodes. | |
390 | * @return DOMNode | |
391 | */ | |
392 | get_selection_parent_node : function() { | |
393 | var selection = M.editor_atto.get_selection(); | |
394 | if (selection.length > 0) { | |
395 | return selection[0].commonAncestorContainer; | |
396 | } | |
397 | }, | |
398 | ||
399 | /** | |
400 | * Get the list of child nodes of the selection. | |
401 | * @return DOMNode[] | |
402 | */ | |
403 | get_selection_text : function() { | |
404 | var selection = M.editor_atto.get_selection(); | |
405 | if (selection.length > 0 && selection[0].cloneContents) { | |
406 | return selection[0].cloneContents(); | |
407 | } | |
408 | }, | |
409 | ||
410 | /** | |
411 | * Set the current selection. Used to restore a selection. | |
412 | */ | |
413 | set_selection : function(selection) { | |
414 | var sel, i; | |
415 | ||
416 | if (window.getSelection) { | |
417 | sel = window.getSelection(); | |
418 | sel.removeAllRanges(); | |
419 | for (i = 0; i < selection.length; i++) { | |
420 | sel.addRange(selection[i]); | |
421 | } | |
422 | } else if (document.selection) { | |
423 | // IE < 9 | |
424 | if (selection.select) { | |
425 | selection.select(); | |
426 | } | |
427 | } | |
428 | } | |
429 | ||
430 | }; |