MDL-41098 Atto Text editor: Add new text editor to core
[moodle.git] / lib / editor / atto / yui / src / editor / js / editor.js
CommitLineData
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 */
24M.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};