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 | |
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>'); | |
164 | ||
165 | toolbar.append(button); | |
166 | ||
167 | // Save the name of the plugin. | |
168 | M.editor_atto.widgets[plugin] = plugin; | |
169 | ||
170 | var menu = Y.Node.create('<div class="atto_' + plugin + '_menu' + | |
171 | ' atto_menu" data-editor="' + Y.Escape.html(elementid) + '"></div>'); | |
c90641fa DW |
172 | var i = 0, entry = {}; |
173 | ||
174 | for (i = 0; i < entries.length; i++) { | |
175 | entry = entries[i]; | |
176 | ||
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 | } | |
190 | ||
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 | } | |
195 | ||
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, | |
4fd8adab | 203 | centered : false, |
c90641fa DW |
204 | align: {node: button, points: [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]} |
205 | }); | |
206 | ||
207 | M.editor_atto.menus[plugin + '_' + elementid] = overlay; | |
208 | overlay.render(); | |
209 | overlay.hide(); | |
210 | overlay.headerNode.hide(); | |
211 | }, | |
212 | ||
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>'); | |
228 | ||
229 | toolbar.append(button); | |
230 | ||
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 | } | |
236 | ||
237 | // Save the name of the plugin. | |
238 | M.editor_atto.widgets[plugin] = plugin; | |
239 | ||
240 | }, | |
241 | ||
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(); | |
249 | ||
250 | if (selection.length) { | |
251 | selection = selection.pop(); | |
252 | } | |
253 | ||
254 | var node = null; | |
255 | if (selection.parentElement) { | |
256 | node = Y.one(selection.parentElement()); | |
257 | } else { | |
258 | node = Y.one(selection.startContainer); | |
259 | } | |
260 | ||
261 | return node && node.ancestor('#' + elementid + 'editable') !== null; | |
262 | }, | |
263 | ||
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 | }, | |
271 | ||
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"/>'); | |
284 | ||
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 | } | |
ceaef9a9 | 292 | atto.setStyle('minHeight', (1.2 * (textarea.getAttribute('rows') - 1)) + 'em'); |
c90641fa DW |
293 | |
294 | // Copy text to editable div. | |
295 | atto.append(textarea.get('value')); | |
296 | ||
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); | |
08a95d50 DW |
301 | atto.setStyle('color', textarea.getStyle('color')); |
302 | atto.setStyle('lineHeight', textarea.getStyle('lineHeight')); | |
303 | atto.setStyle('fontSize', textarea.getStyle('fontSize')); | |
c90641fa DW |
304 | // Hide the old textarea. |
305 | textarea.hide(); | |
306 | ||
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 | }); | |
311 | ||
312 | // Save the file picker options for later. | |
313 | M.editor_atto.filepickeroptions[params.elementid] = params.filepickeroptions; | |
314 | }, | |
315 | ||
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]; | |
325 | ||
326 | options.formcallback = callback; | |
327 | options.editor_target = Y.one(elementid); | |
328 | ||
329 | M.core_filepicker.show(Y, options); | |
330 | }); | |
331 | }, | |
332 | ||
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; | |
340 | ||
341 | if (window.getSelection) { | |
342 | range = document.createRange(); | |
343 | ||
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 | }, | |
354 | ||
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 | }, | |
375 | ||
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 | }, | |
386 | ||
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 | }, | |
397 | ||
398 | /** | |
399 | * Set the current selection. Used to restore a selection. | |
400 | */ | |
401 | set_selection : function(selection) { | |
402 | var sel, i; | |
403 | ||
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 | } | |
417 | ||
418 | }; |