MDL-41713 atto - implement new icons with matching UI
[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];
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};