MDL-43842: Import atto back into core
[moodle.git] / lib / editor / atto / plugins / table / yui / build / moodle-atto_table-button / moodle-atto_table-button-debug.js
CommitLineData
adca7326
DW
1YUI.add('moodle-atto_table-button', 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 text editor table plugin.
20 *
21 * @package editor-atto
22 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25M.atto_table = M.atto_table || {
26
27 /**
28 * The window used to get the table details.
29 *
30 * @property dialogue
31 * @type M.core.dialogue
32 * @default null
33 */
34 dialogue : null,
35
36 /**
37 * The selection object returned by the browser.
38 *
39 * @property selection
40 * @type Range
41 * @default null
42 */
43 selection : null,
44
45 /**
46 * Yui image for table editing controls.
47 *
48 * @property menunode
49 * @type Y.Node
50 * @default null
51 */
52 menunode : null,
53
54 /**
55 * Popup menu for table controls
56 *
57 * @property controlmenu
58 * @type M.editor_atto.controlmenu
59 * @default null
60 */
61 controlmenu : null,
62
63 /**
64 * Last clicked cell that opened the context menu.
65 *
66 * @property lasttarget
67 * @type Y.Node
68 * @default null
69 */
70 lasttarget : null,
71
72 /**
73 * Display the chooser dialogue.
74 *
75 * @method init
76 * @param Event e
77 * @param string elementid
78 */
79 display_chooser : function(e, elementid) {
80 e.preventDefault();
81 if (!M.editor_atto.is_active(elementid)) {
82 M.editor_atto.focus(elementid);
83 }
84 M.atto_table.selection = M.editor_atto.get_selection();
85 if (M.atto_table.selection !== false && (!M.atto_table.selection.collapsed)) {
86 var dialogue;
87 if (!M.atto_table.dialogue) {
88 dialogue = new M.core.dialogue({
89 visible: false,
90 modal: true,
91 close: true,
92 draggable: true
93 });
94 } else {
95 dialogue = M.atto_table.dialogue;
96 }
97
98 dialogue.render();
99 dialogue.set('bodyContent', M.atto_table.get_form_content(elementid));
100 dialogue.set('headerContent', M.util.get_string('createtable', 'atto_table'));
101
102 dialogue.show();
103 M.atto_table.dialogue = dialogue;
104 }
105
106 },
107
108 /**
109 * Show the context menu
110 *
111 * @method show_menu
112 * @param Event e
113 * @param string elementid
114 */
115 show_menu : function(e, elementid) {
116 var addhandlers = false;
117
118 e.preventDefault();
119
120 if (this.controlmenu === null) {
121 addhandlers = true;
122 // Add event handlers for table control menus.
123 var bodycontent = '<ul>';
124 bodycontent += '<li><a href="#" id="addcolumnafter">' + M.util.get_string('addcolumnafter', 'atto_table') + '</a></li>';
125 bodycontent += '<li><a href="#" id="addrowafter">' + M.util.get_string('addrowafter', 'atto_table') + '</a></li>';
126 bodycontent += '<li><a href="#" id="moverowup">' + M.util.get_string('moverowup', 'atto_table') + '</a></li>';
127 bodycontent += '<li><a href="#" id="moverowdown">' + M.util.get_string('moverowdown', 'atto_table') + '</a></li>';
128 bodycontent += '<li><a href="#" id="movecolumnleft">' + M.util.get_string('movecolumnleft', 'atto_table') + '</a></li>';
129 bodycontent += '<li><a href="#" id="movecolumnright">' + M.util.get_string('movecolumnright', 'atto_table') + '</a></li>';
130 bodycontent += '<li><a href="#" id="deleterow">' + M.util.get_string('deleterow', 'atto_table') + '</a></li>';
131 bodycontent += '<li><a href="#" id="deletecolumn">' + M.util.get_string('deletecolumn', 'atto_table') + '</a></li>';
132 bodycontent += '</ul>';
133
134 this.controlmenu = new M.editor_atto.controlmenu({
135 headerText : M.util.get_string('edittable', 'atto_table'),
136 bodyContent : bodycontent
137 });
138 }
139 // We store the cell of the last click (the control node is transient).
140 this.lasttarget = e.target.ancestor('td, th');
141 this.controlmenu.show();
142 this.controlmenu.align(e.target, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
143
144 if (addhandlers) {
145 var bodynode = this.controlmenu.get('boundingBox');
146 bodynode.delegate('click', this.handle_table_control, 'a', this, elementid);
147 bodynode.delegate('key', this.handle_table_control, 'down:enter,space', 'a', this, elementid);
148 }
149 },
150
151 /**
152 * Determine the index of a row in a table column.
153 *
154 * @method get_row_index
155 * @param Y.Node node
156 */
157 get_row_index : function(cell) {
158 var tablenode = cell.ancestor('table'),
159 rownode = cell.ancestor('tr');
160
161 if (!tablenode || !rownode) {
162 return;
163 }
164
165 var rows = tablenode.all('tr');
166
167 return rows.indexOf(rownode);
168 },
169
170 /**
171 * Determine the index of a column in a table row.
172 *
173 * @method get_column_index
174 * @param Y.Node node
175 */
176 get_column_index : function(cellnode) {
177 var rownode = cellnode.ancestor('tr');
178
179 if (!rownode) {
180 return;
181 }
182
183 var cells = rownode.all('td, th');
184
185 return cells.indexOf(cellnode);
186 },
187
188 /**
189 * Delete the current row
190 *
191 * @method delete_row
192 * @param string elementid
193 */
194 delete_row : function(elementid) {
195 var row = this.lasttarget.ancestor('tr');
196
197 if (row) {
198 // We do not remove rows with no cells (all headers).
199 if (row.one('td')) {
200 row.remove(true);
201 }
202 }
203
204 // Clean the HTML.
205 M.editor_atto.text_updated(elementid);
206 },
207
208 /**
209 * Move row up
210 *
211 * @method move_row_up
212 * @param string elementid
213 */
214 move_row_up : function(elementid) {
215 var row = this.lasttarget.ancestor('tr');
216 var prevrow = row.previous('tr');
217 if (!row || !prevrow) {
218 return;
219 }
220
221 row.swap(prevrow);
222 // Clean the HTML.
223 M.editor_atto.text_updated(elementid);
224 },
225
226 /**
227 * Move column left
228 *
229 * @method move_column_left
230 * @param string elementid
231 */
232 move_column_left : function(elementid) {
233 var columnindex = this.get_column_index(this.lasttarget);
234 var rows = this.lasttarget.ancestor('table').all('tr');
235 var columncells = new Y.NodeList();
236 var prevcells = new Y.NodeList();
237 var hastd = false;
238
239 rows.each(function(row) {
240 var cells = row.all('td, th');
241 var cell = cells.item(columnindex),
242 cellprev = cells.item(columnindex-1);
243 columncells.push(cell);
244 if (cellprev) {
245 if (cellprev.get('tagName') === 'TD') {
246 hastd = true;
247 }
248 prevcells.push(cellprev);
249 }
250 });
251
252 if (hastd && prevcells.size() > 0) {
253 var i = 0;
254 for (i = 0; i < columncells.size(); i++) {
255 var cell = columncells.item(i);
256 var prevcell = prevcells.item(i);
257
258 cell.swap(prevcell);
259 }
260 }
261 // Cleanup.
262 M.editor_atto.text_updated(elementid);
263 },
264
265 /**
266 * Move column right
267 *
268 * @method move_column_right
269 * @param string elementid
270 */
271 move_column_right : function(elementid) {
272 var columnindex = this.get_column_index(this.lasttarget);
273 var rows = this.lasttarget.ancestor('table').all('tr');
274 var columncells = new Y.NodeList();
275 var nextcells = new Y.NodeList();
276 var hastd = false;
277
278 rows.each(function(row) {
279 var cells = row.all('td, th');
280 var cell = cells.item(columnindex),
281 cellnext = cells.item(columnindex+1);
282 if (cell.get('tagName') === 'TD') {
283 hastd = true;
284 }
285 columncells.push(cell);
286 if (cellnext) {
287 nextcells.push(cellnext);
288 }
289 });
290
291 if (hastd && nextcells.size() > 0) {
292 var i = 0;
293 for (i = 0; i < columncells.size(); i++) {
294 var cell = columncells.item(i);
295 var nextcell = nextcells.item(i);
296
297 cell.swap(nextcell);
298 }
299 }
300 // Cleanup.
301 M.editor_atto.text_updated(elementid);
302 },
303
304 /**
305 * Move row down
306 *
307 * @method move_row_down
308 * @param string elementid
309 */
310 move_row_down : function(elementid) {
311 var row = this.lasttarget.ancestor('tr');
312 var nextrow = row.next('tr');
313 if (!row || !nextrow) {
314 return;
315 }
316
317 row.swap(nextrow);
318 // Clean the HTML.
319 M.editor_atto.text_updated(elementid);
320 },
321
322 /**
323 * Delete the current column
324 *
325 * @method delete_column
326 * @param string elementid
327 */
328 delete_column : function(elementid) {
329 var columnindex = this.get_column_index(this.lasttarget);
330 var rows = this.lasttarget.ancestor('table').all('tr');
331 var columncells = new Y.NodeList();
332 var hastd = false;
333
334 rows.each(function(row) {
335 var cells = row.all('td, th');
336 var cell = cells.item(columnindex);
337 if (cell.get('tagName') === 'TD') {
338 hastd = true;
339 }
340 columncells.push(cell);
341 });
342
343 if (hastd) {
344 columncells.remove(true);
345 }
346
347 // Clean the HTML.
348 M.editor_atto.text_updated(elementid);
349 },
350
351 /**
352 * Add a row after the current row.
353 *
354 * @method add_row_after
355 * @param string elementid
356 */
357 add_row_after : function(elementid) {
358 var rowindex = this.get_row_index(this.lasttarget);
359
360 var tablebody = this.lasttarget.ancestor('table').one('tbody');
361 if (!tablebody) {
362 // Not all tables have tbody.
363 tablebody = this.lasttarget.ancestor('table');
364 rowindex += 1;
365 }
366
367 var firstrow = tablebody.one('tr');
368 if (!firstrow) {
369 firstrow = this.lasttarget.ancestor('table').one('tr');
370 }
371 if (!firstrow) {
372 // Table has no rows. Boo.
373 return;
374 }
375 newrow = firstrow.cloneNode(true);
376 newrow.all('th, td').each(function (tablecell) {
377 if (tablecell.get('tagName') === 'TH') {
378 if (tablecell.getAttribute('scope') !== 'row') {
379 var newcell = Y.Node.create('<td></td>');
380 tablecell.replace(newcell);
381 tablecell = newcell;
382 }
383 }
384 tablecell.setHTML('&nbsp;');
385 });
386
387 tablebody.insert(newrow, rowindex);
388
389 // Clean the HTML.
390 M.editor_atto.text_updated(elementid);
391 },
392
393 /**
394 * Add a column after the current column.
395 *
396 * @method add_column_after
397 * @param string elementid
398 */
399 add_column_after : function(elementid) {
400 var columnindex = this.get_column_index(this.lasttarget);
401
402 var tablecell = this.lasttarget.ancestor('table');
403 var rows = tablecell.all('tr');
404 Y.each(rows, function(row) {
405 // Clone the first cell from the row so it has the same type/attributes (e.g. scope).
406 var newcell = row.one('td, th').cloneNode(true);
407 // Clear the content of the cell.
408 newcell.setHTML('&nbsp;');
409
410 row.insert(newcell, columnindex + 1);
411 }, this);
412
413 // Clean the HTML.
414 M.editor_atto.text_updated(elementid);
415 },
416
417 /**
418 * Handle a selection from the table control menu.
419 *
420 * @method handle_table_control
421 * @param Y.Event event
422 * @param string elementid
423 */
424 handle_table_control : function(event, elementid) {
425 event.preventDefault();
426
427 this.controlmenu.hide();
428
429 switch (event.target.get('id')) {
430 case 'addcolumnafter':
431 this.add_column_after(elementid);
432 break;
433 case 'addrowafter':
434 this.add_row_after(elementid);
435 break;
436 case 'deleterow':
437 this.delete_row(elementid);
438 break;
439 case 'deletecolumn':
440 this.delete_column(elementid);
441 break;
442 case 'moverowdown':
443 this.move_row_down(elementid);
444 break;
445 case 'moverowup':
446 this.move_row_up(elementid);
447 break;
448 case 'movecolumnleft':
449 this.move_column_left(elementid);
450 break;
451 case 'movecolumnright':
452 this.move_column_right(elementid);
453 break;
454 }
455 },
456
457 /**
458 * Add this button to the form.
459 *
460 * @method init
461 * @param {Object} params
462 */
463 init : function(params) {
464
465 if (!M.atto_table.menunode) {
466 // Used for inline table editing controls.
467 var img = Y.Node.create('<img/>');
468 img.setAttrs({
469 alt : M.util.get_string('edittable', 'atto_table'),
470 src : M.util.image_url('t/contextmenu', 'core'),
471 width : '12',
472 height : '12'
473 });
474 var anchor = Y.Node.create('<a href="#" contenteditable="false"/>');
475 anchor.appendChild(img);
476 anchor.addClass('atto_control');
477 M.atto_table.menunode = anchor;
478 }
479
480 M.editor_atto.add_toolbar_button(params.elementid, 'table', params.icon, params.group, this.display_chooser, this);
481 //Y.one('#' + params.elementid + 'editable').on('valuechange', this.insert_table_controls, this, params.elementid);
482
483 var contenteditable = Y.one('#' + params.elementid + 'editable');
484 contenteditable.delegate('click', this.show_menu, 'td > .atto_control, th > .atto_control', this, params.elementid);
485 contenteditable.delegate('key', this.show_menu, 'down:enter,space', 'td > .atto_control, th > .atto_control', this, params.elementid);
486 // Disable mozilla table controls.
487 if (Y.UA.gecko) {
488 document.execCommand("enableInlineTableEditing", false, "false");
489 document.execCommand("enableObjectResizing", false, false);
490 }
491
492 this.insert_table_controls(params.elementid);
493
494 // Re-add the table controls whenever the content is updated.
495 M.editor_atto.add_text_updated_handler(params.elementid, this.insert_table_controls);
496 },
497
498 /**
499 * Add the table editing controls to the content area.
500 *
501 * @method insert_table_controls
502 * @param String elementid - The id of the text area backed by the content editable field.
503 */
504 insert_table_controls : function(elementid) {
505 var contenteditable = Y.one('#' + elementid + 'editable'),
506 allcells = contenteditable.all('td .atto_control,th .atto_control'),
507 cells = contenteditable.all('td:last-child,th:last-child,tbody tr:last-child > td, tbody tr:last-child > th');
508
509 allcells.each(function(node) {
510 if (cells.indexOf(node) === -1) {
511 node.remove(true);
512 }
513 });
514
515 cells.each(function(node) {
516 if (!node.one('.atto_control')) {
517 node.append(M.atto_table.menunode.cloneNode(true));
518 }
519 }, this);
520 },
521
522 /**
523 * The OK button has been pressed - make the changes to the source.
524 *
525 * @method set_table
526 * @param Event e
527 */
528 set_table : function(e, elementid) {
529 var caption,
530 rows,
531 cols,
532 headers,
533 tablehtml,
534 i, j;
535
536 e.preventDefault();
537 M.atto_table.dialogue.hide();
538
539 caption = e.currentTarget.ancestor('.atto_form').one('#atto_table_caption');
540 rows = e.currentTarget.ancestor('.atto_form').one('#atto_table_rows');
541 cols = e.currentTarget.ancestor('.atto_form').one('#atto_table_columns');
542 headers = e.currentTarget.ancestor('.atto_form').one('#atto_table_headers');
543
544 M.editor_atto.set_selection(M.atto_table.selection);
545
546 // Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click.
547 tablehtml = '<br/><table>';
548 tablehtml += '<caption>' + Y.Escape.html(caption.get('value')) + '</caption>';
549
550 i = 0;
551 if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
552 i = 1;
553 tablehtml += '<thead><tr>';
554 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
555 tablehtml += '<th scope="col"></th>';
556 }
557 tablehtml += '</tr></thead>';
558 }
559 tablehtml += '<tbody>';
560 for (; i < parseInt(rows.get('value'), 10); i++) {
561 tablehtml += '<tr>';
562 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
563 if (j === 0 && (headers.get('value') === 'rows' || headers.get('value') === 'both')) {
564 tablehtml += '<th scope="row"></th>';
565 } else {
566 tablehtml += '<td></td>';
567 }
568 }
569 tablehtml += '</tr>';
570 }
571 tablehtml += '</tbody>';
572 tablehtml += '</table><br/>';
573
574 document.execCommand('insertHTML', false, tablehtml);
575
576 // Clean the YUI ids from the HTML.
577 M.editor_atto.text_updated(elementid);
578 },
579
580 /**
581 * Return the HTML of the form to show in the dialogue.
582 *
583 * @method get_form_content
584 * @param string elementid
585 * @return string
586 */
587 get_form_content : function(elementid) {
588 var content = Y.Node.create('<form class="atto_form">' +
589 '<label for="atto_table_caption">' + M.util.get_string('caption', 'atto_table') +
590 '</label>' +
591 '<textarea id="atto_table_caption" rows="4" class="fullwidth" required></textarea>' +
592 '<br/>' +
593 '<label for="atto_table_headers" class="sameline">' + M.util.get_string('headers', 'atto_table') +
594 '</label>' +
595 '<select id="atto_table_headers">' +
596 '<option value="columns">' + M.util.get_string('columns', 'atto_table') + '</option>' +
597 '<option value="rows">' + M.util.get_string('rows', 'atto_table') + '</option>' +
598 '<option value="both">' + M.util.get_string('both', 'atto_table') + '</option>' +
599 '</select>' +
600 '<br/>' +
601 '<label for="atto_table_rows" class="sameline">' + M.util.get_string('numberofrows', 'atto_table') +
602 '</label>' +
603 '<input type="number" value="3" id="atto_table_rows" size="8" min="1" max="50"/>' +
604 '<br/>' +
605 '<label for="atto_table_columns" class="sameline">' + M.util.get_string('numberofcolumns', 'atto_table') +
606 '</label>' +
607 '<input type="number" value="3" id="atto_table_columns" size="8" min="1" max="20"/>' +
608 '<br/>' +
609 '<div class="mdl-align">' +
610 '<br/>' +
611 '<button id="atto_table_submit">' +
612 M.util.get_string('createtable', 'atto_table') +
613 '</button>' +
614 '</div>' +
615 '</form>' +
616 '<hr/>' + M.util.get_string('accessibilityhint', 'atto_table'));
617
618 content.one('#atto_table_submit').on('click', M.atto_table.set_table, this, elementid);
619 return content;
620 }
621};
622
623
624}, '@VERSION@', {"requires": ["node", "escape", "event", "event-valuechange"]});