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