MDL-41238 atto - fixing autoclose of style menu for firefox
[moodle.git] / lib / editor / atto / yui / build / moodle-editor_atto-editor / moodle-editor_atto-editor-debug.js
CommitLineData
c90641fa
DW
1YUI.add('moodle-editor_atto-editor', function (Y, NAME) {
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
18/**
19 * Atto editor main class.
20 * Common functions required by editor plugins.
21 *
22 * @package editor-atto
23 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26M.editor_atto = M.editor_atto || {
27 /**
28 * List of attached button handlers to prevent duplicates.
29 */
30 buttonhandlers : {},
31
32 /**
33 * List of YUI overlays for custom menus.
34 */
35 menus : {},
36
37 /**
38 * List of attached menu handlers to prevent duplicates.
39 */
40 menuhandlers : {},
41
42 /**
43 * List of file picker options for specific editor instances.
44 */
45 filepickeroptions : {},
46
47 /**
48 * List of buttons and menus that have been added to the toolbar.
49 */
50 widgets : {},
51
52 /**
53 * Toggle a menu.
54 * @param event e
55 */
56 showhide_menu_handler : function(e) {
57 e.preventDefault();
58 var disabled = this.getAttribute('disabled');
59 var overlayid = this.getAttribute('data-menu');
60 var overlay = M.editor_atto.menus[overlayid];
21f6c529 61 var menu = overlay.get('bodyContent');
c90641fa
DW
62 if (overlay.get('visible') || disabled) {
63 overlay.hide();
21f6c529 64 menu.detach('clickoutside');
c90641fa 65 } else {
21f6c529 66 menu.on('clickoutside', function(ev) {
c1f10ffb 67 if ((ev.target.ancestor() !== this) && (ev.target !== this)) {
21f6c529
JF
68 if (overlay.get('visible')) {
69 menu.detach('clickoutside');
70 overlay.hide();
71 }
72 }
73 }, this);
c90641fa
DW
74 overlay.show();
75 }
c90641fa
DW
76 },
77
78 /**
79 * Handle clicks on editor buttons.
80 * @param event e
81 */
82 buttonclicked_handler : function(e) {
83 var elementid = this.getAttribute('data-editor');
84 var plugin = this.getAttribute('data-plugin');
85 var handler = this.getAttribute('data-handler');
86 var overlay = M.editor_atto.menus[plugin + '_' + elementid];
87
88 if (overlay) {
89 overlay.hide();
90 }
91
92 if (M.editor_atto.is_enabled(elementid, plugin)) {
93 // Pass it on.
94 handler = M.editor_atto.buttonhandlers[handler];
95 return handler(e, elementid);
96 }
97 },
98
99 /**
100 * Determine if the specified toolbar button/menu is enabled.
101 * @param string elementid, the element id of this editor.
102 * @param string plugin, the plugin that created the button/menu.
103 */
104 is_enabled : function(elementid, plugin) {
105 var element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
106
107 return !element.hasAttribute('disabled');
108 },
109 /**
110 * Disable all buttons and menus in the toolbar.
111 * @param string elementid, the element id of this editor.
112 */
113 disable_all_widgets : function(elementid) {
114 var plugin, element;
115 for (plugin in M.editor_atto.widgets) {
116 element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
117
118 if (element) {
119 element.setAttribute('disabled', 'true');
120 }
121 }
122 },
123
124 /**
125 * Enable a single widget in the toolbar.
126 * @param string elementid, the element id of this editor.
127 * @param string plugin, the name of the plugin that created the widget.
128 */
129 enable_widget : function(elementid, plugin) {
130 var element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
131
132 if (element) {
133 element.removeAttribute('disabled');
134 }
135 },
136
137 /**
138 * Enable all buttons and menus in the toolbar.
139 * @param string elementid, the element id of this editor.
140 */
141 enable_all_widgets : function(elementid) {
142 var plugin, element;
143 for (plugin in M.editor_atto.widgets) {
144 element = Y.one('#' + elementid + '_toolbar .atto_' + plugin + '_button');
145
146 if (element) {
147 element.removeAttribute('disabled');
148 }
149 }
150 },
151
152 /**
153 * Add a button to the toolbar belonging to the editor for element with id "elementid".
154 * @param string elementid - the id of the textarea we created this editor from.
155 * @param string plugin - the plugin defining the button
156 * @param string icon - the html used for the content of the button
157 * @handler function handler- A function to call when the button is clicked.
158 */
159 add_toolbar_menu : function(elementid, plugin, icon, entries) {
160 var toolbar = Y.one('#' + elementid + '_toolbar');
161 var button = Y.Node.create('<button class="atto_' + plugin + '_button atto_hasmenu" ' +
162 'data-editor="' + Y.Escape.html(elementid) + '" ' +
163 'data-menu="' + plugin + '_' + elementid + '" >' +
164 icon +
165 '</button>');
166
167 toolbar.append(button);
168
169 // Save the name of the plugin.
170 M.editor_atto.widgets[plugin] = plugin;
171
172 var menu = Y.Node.create('<div class="atto_' + plugin + '_menu' +
173 ' atto_menu" data-editor="' + Y.Escape.html(elementid) + '"></div>');
c90641fa
DW
174 var i = 0, entry = {};
175
176 for (i = 0; i < entries.length; i++) {
177 entry = entries[i];
178
179 menu.append(Y.Node.create('<div class="atto_menuentry">' +
180 '<a href="#" class="atto_' + plugin + '_action_' + i + '" ' +
181 'data-editor="' + Y.Escape.html(elementid) + '" ' +
182 'data-plugin="' + Y.Escape.html(plugin) + '" ' +
183 'data-handler="' + Y.Escape.html(plugin + '_action_' + i) + '">' +
184 entry.text +
185 '</a>' +
186 '</div>'));
187 if (!M.editor_atto.buttonhandlers[plugin + '_action_' + i]) {
188 Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_action_' + i);
189 M.editor_atto.buttonhandlers[plugin + '_action_' + i] = entry.handler;
190 }
191 }
192
193 if (!M.editor_atto.buttonhandlers[plugin]) {
194 Y.one('body').delegate('click', M.editor_atto.showhide_menu_handler, '.atto_' + plugin + '_button');
195 M.editor_atto.buttonhandlers[plugin] = true;
196 }
197
198 var overlay = new M.core.dialogue({
199 bodyContent : menu,
200 visible : false,
201 width: '14em',
202 zindex: 100,
203 lightbox: false,
204 closeButton: false,
4fd8adab 205 centered : false,
c90641fa
DW
206 align: {node: button, points: [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]}
207 });
208
209 M.editor_atto.menus[plugin + '_' + elementid] = overlay;
210 overlay.render();
211 overlay.hide();
212 overlay.headerNode.hide();
213 },
214
215 /**
216 * Add a button to the toolbar belonging to the editor for element with id "elementid".
217 * @param string elementid - the id of the textarea we created this editor from.
218 * @param string plugin - the plugin defining the button
219 * @param string icon - the html used for the content of the button
220 * @handler function handler- A function to call when the button is clicked.
221 */
222 add_toolbar_button : function(elementid, plugin, icon, handler) {
223 var toolbar = Y.one('#' + elementid + '_toolbar');
224 var button = Y.Node.create('<button class="atto_' + plugin + '_button" ' +
225 'data-editor="' + Y.Escape.html(elementid) + '" ' +
226 'data-plugin="' + Y.Escape.html(plugin) + '" ' +
227 'data-handler="' + Y.Escape.html(plugin) + '">' +
228 icon +
229 '</button>');
230
231 toolbar.append(button);
232
233 // We only need to attach this once.
234 if (!M.editor_atto.buttonhandlers[plugin]) {
235 Y.one('body').delegate('click', M.editor_atto.buttonclicked_handler, '.atto_' + plugin + '_button');
236 M.editor_atto.buttonhandlers[plugin] = handler;
237 }
238
239 // Save the name of the plugin.
240 M.editor_atto.widgets[plugin] = plugin;
241
242 },
243
244 /**
245 * Work out if the cursor is in the editable area for this editor instance.
246 * @param string elementid of this editor
247 * @return bool
248 */
249 is_active : function(elementid) {
250 var selection = M.editor_atto.get_selection();
251
252 if (selection.length) {
253 selection = selection.pop();
254 }
255
256 var node = null;
257 if (selection.parentElement) {
258 node = Y.one(selection.parentElement());
259 } else {
260 node = Y.one(selection.startContainer);
261 }
262
263 return node && node.ancestor('#' + elementid + 'editable') !== null;
264 },
265
266 /**
267 * Focus on the editable area for this editor.
268 * @param string elementid of this editor
269 */
270 focus : function(elementid) {
271 Y.one('#' + elementid + 'editable').focus();
272 },
273
274 /**
275 * Initialise the editor
276 * @param object params for this editor instance.
277 */
278 init : function(params) {
279 var textarea = Y.one('#' +params.elementid);
280 var atto = Y.Node.create('<div id="' + params.elementid + 'editable" ' +
281 'contenteditable="true" ' +
282 'spellcheck="true" ' +
283 'class="editor_atto"/>');
284 var cssfont = '';
285 var toolbar = Y.Node.create('<div class="editor_atto_toolbar" id="' + params.elementid + '_toolbar"/>');
286
287 // Bleh - why are we sent a url and not the css to apply directly?
288 var css = Y.io(params.content_css, { sync: true });
289 var pos = css.responseText.indexOf('font:');
290 if (pos) {
291 cssfont = css.responseText.substring(pos + 'font:'.length, css.responseText.length - 1);
292 atto.setStyle('font', cssfont);
293 }
ceaef9a9 294 atto.setStyle('minHeight', (1.2 * (textarea.getAttribute('rows') - 1)) + 'em');
c90641fa
DW
295
296 // Copy text to editable div.
297 atto.append(textarea.get('value'));
298
299 // Add the toolbar to the page.
300 textarea.get('parentNode').insert(toolbar, textarea);
301 // Add the editable div to the page.
302 textarea.get('parentNode').insert(atto, textarea);
08a95d50
DW
303 atto.setStyle('color', textarea.getStyle('color'));
304 atto.setStyle('lineHeight', textarea.getStyle('lineHeight'));
305 atto.setStyle('fontSize', textarea.getStyle('fontSize'));
c90641fa
DW
306 // Hide the old textarea.
307 textarea.hide();
308
309 // Copy the current value back to the textarea when focus leaves us.
310 atto.on('blur', function() {
311 textarea.set('value', atto.getHTML());
312 });
313
314 // Save the file picker options for later.
315 M.editor_atto.filepickeroptions[params.elementid] = params.filepickeroptions;
316 },
317
318 /**
319 * Show the filepicker.
320 * @param string elementid for this editor instance.
321 * @param string type The media type for the file picker
322 * @param function callback
323 */
324 show_filepicker : function(elementid, type, callback) {
325 Y.use('core_filepicker', function (Y) {
326 var options = M.editor_atto.filepickeroptions[elementid][type];
327
328 options.formcallback = callback;
329 options.editor_target = Y.one(elementid);
330
331 M.core_filepicker.show(Y, options);
332 });
333 },
334
335 /**
336 * Create a cross browser selection object that represents a yui node.
337 * @param Node yui node for the selection
338 * @return range (browser dependent)
339 */
340 get_selection_from_node: function(node) {
341 var range;
342
343 if (window.getSelection) {
344 range = document.createRange();
345
346 range.setStartBefore(node.getDOMNode());
347 range.setEndAfter(node.getDOMNode());
348 return [range];
349 } else if (document.selection) {
350 range = document.body.createTextRange();
351 range.moveToElementText(node.getDOMNode());
352 return range;
353 }
354 return false;
355 },
356
357 /**
358 * Get the selection object that can be passed back to set_selection.
359 * @return range (browser dependent)
360 */
361 get_selection : function() {
362 if (window.getSelection) {
363 var sel = window.getSelection();
364 var ranges = [], i = 0;
365 for (i = 0; i < sel.rangeCount; i++) {
366 ranges.push(sel.getRangeAt(i));
367 }
368 return ranges;
369 } else if (document.selection) {
370 // IE < 9
371 if (document.selection.createRange) {
372 return document.selection.createRange();
373 }
374 }
375 return false;
376 },
377
378 /**
379 * Get the dom node representing the common anscestor of the selection nodes.
380 * @return DOMNode
381 */
382 get_selection_parent_node : function() {
383 var selection = M.editor_atto.get_selection();
384 if (selection.length > 0) {
385 return selection[0].commonAncestorContainer;
386 }
387 },
388
389 /**
390 * Get the list of child nodes of the selection.
391 * @return DOMNode[]
392 */
393 get_selection_text : function() {
394 var selection = M.editor_atto.get_selection();
395 if (selection.length > 0 && selection[0].cloneContents) {
396 return selection[0].cloneContents();
397 }
398 },
399
400 /**
401 * Set the current selection. Used to restore a selection.
402 */
403 set_selection : function(selection) {
404 var sel, i;
405
406 if (window.getSelection) {
407 sel = window.getSelection();
408 sel.removeAllRanges();
409 for (i = 0; i < selection.length; i++) {
410 sel.addRange(selection[i]);
411 }
412 } else if (document.selection) {
413 // IE < 9
414 if (selection.select) {
415 selection.select();
416 }
417 }
418 }
419
420};
421
422
423}, '@VERSION@', {"requires": ["node", "io", "overlay", "escape", "moodle-core-notification"]});