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