MDL-43863 Add Undo/Redo plugins to Atto
[moodle.git] / lib / editor / atto / plugins / table / yui / build / moodle-atto_table-button / moodle-atto_table-button.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).
05843fd3 140 this.lasttarget = e.target.ancestor('.editor_atto_content td, .editor_atto_content th', true);
adca7326
DW
141 this.controlmenu.show();
142 this.controlmenu.align(e.target, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
26f8822d
DW
143 var bodynode = this.controlmenu.get('boundingBox');
144 if (bodynode.one('a')) {
145 bodynode.one('a').focus();
146 }
adca7326
DW
147
148 if (addhandlers) {
adca7326
DW
149 bodynode.delegate('click', this.handle_table_control, 'a', this, elementid);
150 bodynode.delegate('key', this.handle_table_control, 'down:enter,space', 'a', this, elementid);
151 }
152 },
153
154 /**
155 * Determine the index of a row in a table column.
156 *
157 * @method get_row_index
158 * @param Y.Node node
159 */
160 get_row_index : function(cell) {
161 var tablenode = cell.ancestor('table'),
162 rownode = cell.ancestor('tr');
163
164 if (!tablenode || !rownode) {
165 return;
166 }
167
168 var rows = tablenode.all('tr');
169
170 return rows.indexOf(rownode);
171 },
172
173 /**
174 * Determine the index of a column in a table row.
175 *
176 * @method get_column_index
177 * @param Y.Node node
178 */
179 get_column_index : function(cellnode) {
180 var rownode = cellnode.ancestor('tr');
181
182 if (!rownode) {
183 return;
184 }
185
186 var cells = rownode.all('td, th');
187
188 return cells.indexOf(cellnode);
189 },
190
191 /**
192 * Delete the current row
193 *
194 * @method delete_row
195 * @param string elementid
196 */
197 delete_row : function(elementid) {
198 var row = this.lasttarget.ancestor('tr');
199
200 if (row) {
201 // We do not remove rows with no cells (all headers).
202 if (row.one('td')) {
203 row.remove(true);
204 }
205 }
206
207 // Clean the HTML.
208 M.editor_atto.text_updated(elementid);
209 },
210
211 /**
212 * Move row up
213 *
214 * @method move_row_up
215 * @param string elementid
216 */
217 move_row_up : function(elementid) {
218 var row = this.lasttarget.ancestor('tr');
219 var prevrow = row.previous('tr');
220 if (!row || !prevrow) {
221 return;
222 }
223
224 row.swap(prevrow);
225 // Clean the HTML.
226 M.editor_atto.text_updated(elementid);
227 },
228
229 /**
230 * Move column left
231 *
232 * @method move_column_left
233 * @param string elementid
234 */
235 move_column_left : function(elementid) {
236 var columnindex = this.get_column_index(this.lasttarget);
237 var rows = this.lasttarget.ancestor('table').all('tr');
238 var columncells = new Y.NodeList();
239 var prevcells = new Y.NodeList();
240 var hastd = false;
241
242 rows.each(function(row) {
243 var cells = row.all('td, th');
244 var cell = cells.item(columnindex),
245 cellprev = cells.item(columnindex-1);
246 columncells.push(cell);
247 if (cellprev) {
248 if (cellprev.get('tagName') === 'TD') {
249 hastd = true;
250 }
251 prevcells.push(cellprev);
252 }
253 });
254
255 if (hastd && prevcells.size() > 0) {
256 var i = 0;
257 for (i = 0; i < columncells.size(); i++) {
258 var cell = columncells.item(i);
259 var prevcell = prevcells.item(i);
260
261 cell.swap(prevcell);
262 }
263 }
264 // Cleanup.
265 M.editor_atto.text_updated(elementid);
266 },
267
268 /**
269 * Move column right
270 *
271 * @method move_column_right
272 * @param string elementid
273 */
274 move_column_right : function(elementid) {
275 var columnindex = this.get_column_index(this.lasttarget);
276 var rows = this.lasttarget.ancestor('table').all('tr');
277 var columncells = new Y.NodeList();
278 var nextcells = new Y.NodeList();
279 var hastd = false;
280
281 rows.each(function(row) {
282 var cells = row.all('td, th');
283 var cell = cells.item(columnindex),
284 cellnext = cells.item(columnindex+1);
285 if (cell.get('tagName') === 'TD') {
286 hastd = true;
287 }
288 columncells.push(cell);
289 if (cellnext) {
290 nextcells.push(cellnext);
291 }
292 });
293
294 if (hastd && nextcells.size() > 0) {
295 var i = 0;
296 for (i = 0; i < columncells.size(); i++) {
297 var cell = columncells.item(i);
298 var nextcell = nextcells.item(i);
299
300 cell.swap(nextcell);
301 }
302 }
303 // Cleanup.
304 M.editor_atto.text_updated(elementid);
305 },
306
307 /**
308 * Move row down
309 *
310 * @method move_row_down
311 * @param string elementid
312 */
313 move_row_down : function(elementid) {
314 var row = this.lasttarget.ancestor('tr');
315 var nextrow = row.next('tr');
316 if (!row || !nextrow) {
317 return;
318 }
319
320 row.swap(nextrow);
321 // Clean the HTML.
322 M.editor_atto.text_updated(elementid);
323 },
324
325 /**
326 * Delete the current column
327 *
328 * @method delete_column
329 * @param string elementid
330 */
331 delete_column : function(elementid) {
332 var columnindex = this.get_column_index(this.lasttarget);
333 var rows = this.lasttarget.ancestor('table').all('tr');
334 var columncells = new Y.NodeList();
335 var hastd = false;
336
337 rows.each(function(row) {
338 var cells = row.all('td, th');
339 var cell = cells.item(columnindex);
340 if (cell.get('tagName') === 'TD') {
341 hastd = true;
342 }
343 columncells.push(cell);
344 });
345
346 if (hastd) {
347 columncells.remove(true);
348 }
349
350 // Clean the HTML.
351 M.editor_atto.text_updated(elementid);
352 },
353
354 /**
355 * Add a row after the current row.
356 *
357 * @method add_row_after
358 * @param string elementid
359 */
360 add_row_after : function(elementid) {
361 var rowindex = this.get_row_index(this.lasttarget);
362
363 var tablebody = this.lasttarget.ancestor('table').one('tbody');
364 if (!tablebody) {
365 // Not all tables have tbody.
366 tablebody = this.lasttarget.ancestor('table');
367 rowindex += 1;
368 }
369
370 var firstrow = tablebody.one('tr');
371 if (!firstrow) {
372 firstrow = this.lasttarget.ancestor('table').one('tr');
373 }
374 if (!firstrow) {
375 // Table has no rows. Boo.
376 return;
377 }
378 newrow = firstrow.cloneNode(true);
379 newrow.all('th, td').each(function (tablecell) {
380 if (tablecell.get('tagName') === 'TH') {
381 if (tablecell.getAttribute('scope') !== 'row') {
382 var newcell = Y.Node.create('<td></td>');
383 tablecell.replace(newcell);
384 tablecell = newcell;
385 }
386 }
387 tablecell.setHTML('&nbsp;');
388 });
389
390 tablebody.insert(newrow, rowindex);
391
392 // Clean the HTML.
393 M.editor_atto.text_updated(elementid);
394 },
395
396 /**
397 * Add a column after the current column.
398 *
399 * @method add_column_after
400 * @param string elementid
401 */
402 add_column_after : function(elementid) {
403 var columnindex = this.get_column_index(this.lasttarget);
404
405 var tablecell = this.lasttarget.ancestor('table');
406 var rows = tablecell.all('tr');
407 Y.each(rows, function(row) {
408 // Clone the first cell from the row so it has the same type/attributes (e.g. scope).
409 var newcell = row.one('td, th').cloneNode(true);
410 // Clear the content of the cell.
411 newcell.setHTML('&nbsp;');
412
413 row.insert(newcell, columnindex + 1);
414 }, this);
415
416 // Clean the HTML.
417 M.editor_atto.text_updated(elementid);
418 },
419
05843fd3
DW
420 /**
421 * Handle a button click - this will either open the new table dialogue,
422 * or the edit table context menu.
423 *
424 * @method handle_button
425 * @param Y.Event event
426 * @param string elementid
427 */
428 handle_button : function(event, elementid) {
429 var selection = M.editor_atto.get_selection_parent_node();
430 var editable = M.editor_atto.get_editable_node(elementid);
431 var cell;
432
433 if (!selection) {
434 return M.atto_table.display_chooser(event, elementid);
435 }
436 Y.one(selection).ancestors('th, td', true).each(function(node) {
437 if (editable.contains(node)) {
438 cell = node;
439 }
440 });
441
442 if (cell) {
443 event.target = cell;
444 return M.atto_table.show_menu(event, elementid);
445 }
446 return M.atto_table.display_chooser(event, elementid);
447 },
448
449
adca7326
DW
450 /**
451 * Handle a selection from the table control menu.
452 *
453 * @method handle_table_control
454 * @param Y.Event event
455 * @param string elementid
456 */
457 handle_table_control : function(event, elementid) {
458 event.preventDefault();
459
460 this.controlmenu.hide();
461
462 switch (event.target.get('id')) {
463 case 'addcolumnafter':
464 this.add_column_after(elementid);
465 break;
466 case 'addrowafter':
467 this.add_row_after(elementid);
468 break;
469 case 'deleterow':
470 this.delete_row(elementid);
471 break;
472 case 'deletecolumn':
473 this.delete_column(elementid);
474 break;
475 case 'moverowdown':
476 this.move_row_down(elementid);
477 break;
478 case 'moverowup':
479 this.move_row_up(elementid);
480 break;
481 case 'movecolumnleft':
482 this.move_column_left(elementid);
483 break;
484 case 'movecolumnright':
485 this.move_column_right(elementid);
486 break;
487 }
488 },
489
490 /**
491 * Add this button to the form.
492 *
493 * @method init
494 * @param {Object} params
495 */
496 init : function(params) {
497
498 if (!M.atto_table.menunode) {
499 // Used for inline table editing controls.
500 var img = Y.Node.create('<img/>');
501 img.setAttrs({
502 alt : M.util.get_string('edittable', 'atto_table'),
503 src : M.util.image_url('t/contextmenu', 'core'),
504 width : '12',
505 height : '12'
506 });
507 var anchor = Y.Node.create('<a href="#" contenteditable="false"/>');
508 anchor.appendChild(img);
509 anchor.addClass('atto_control');
510 M.atto_table.menunode = anchor;
511 }
512
55c0403c 513 var iconurl = M.util.image_url('e/table', 'core');
05843fd3 514 M.editor_atto.add_toolbar_button(params.elementid, 'table', iconurl, params.group, this.handle_button);
adca7326 515
adca7326
DW
516 // Disable mozilla table controls.
517 if (Y.UA.gecko) {
05843fd3 518 document.execCommand("enableInlineTableEditing", false, false);
adca7326
DW
519 document.execCommand("enableObjectResizing", false, false);
520 }
521
adca7326
DW
522 },
523
524 /**
525 * The OK button has been pressed - make the changes to the source.
526 *
527 * @method set_table
528 * @param Event e
529 */
530 set_table : function(e, elementid) {
531 var caption,
532 rows,
533 cols,
534 headers,
535 tablehtml,
536 i, j;
537
538 e.preventDefault();
539 M.atto_table.dialogue.hide();
540
541 caption = e.currentTarget.ancestor('.atto_form').one('#atto_table_caption');
542 rows = e.currentTarget.ancestor('.atto_form').one('#atto_table_rows');
543 cols = e.currentTarget.ancestor('.atto_form').one('#atto_table_columns');
544 headers = e.currentTarget.ancestor('.atto_form').one('#atto_table_headers');
545
546 M.editor_atto.set_selection(M.atto_table.selection);
547
548 // Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click.
8095505e
DW
549 var nl = "\n";
550 tablehtml = '<br/>' + nl + '<table>' + nl;
551 tablehtml += '<caption>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;
adca7326
DW
552
553 i = 0;
554 if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
555 i = 1;
8095505e 556 tablehtml += '<thead>' + nl + '<tr>' + nl;
adca7326 557 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
8095505e 558 tablehtml += '<th scope="col"></th>' + nl;
adca7326 559 }
8095505e 560 tablehtml += '</tr>' + nl + '</thead>' + nl;
adca7326 561 }
8095505e 562 tablehtml += '<tbody>' + nl;
adca7326 563 for (; i < parseInt(rows.get('value'), 10); i++) {
8095505e 564 tablehtml += '<tr>' + nl;
adca7326
DW
565 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
566 if (j === 0 && (headers.get('value') === 'rows' || headers.get('value') === 'both')) {
8095505e 567 tablehtml += '<th scope="row"></th>' + nl;
adca7326 568 } else {
8095505e 569 tablehtml += '<td></td>' + nl;
adca7326
DW
570 }
571 }
8095505e 572 tablehtml += '</tr>' + nl;
adca7326 573 }
8095505e
DW
574 tablehtml += '</tbody>' + nl;
575 tablehtml += '</table>' + nl + '<br/>';
adca7326
DW
576
577 document.execCommand('insertHTML', false, tablehtml);
578
579 // Clean the YUI ids from the HTML.
580 M.editor_atto.text_updated(elementid);
581 },
582
583 /**
584 * Return the HTML of the form to show in the dialogue.
585 *
586 * @method get_form_content
587 * @param string elementid
588 * @return string
589 */
590 get_form_content : function(elementid) {
591 var content = Y.Node.create('<form class="atto_form">' +
592 '<label for="atto_table_caption">' + M.util.get_string('caption', 'atto_table') +
593 '</label>' +
594 '<textarea id="atto_table_caption" rows="4" class="fullwidth" required></textarea>' +
595 '<br/>' +
596 '<label for="atto_table_headers" class="sameline">' + M.util.get_string('headers', 'atto_table') +
597 '</label>' +
598 '<select id="atto_table_headers">' +
599 '<option value="columns">' + M.util.get_string('columns', 'atto_table') + '</option>' +
600 '<option value="rows">' + M.util.get_string('rows', 'atto_table') + '</option>' +
601 '<option value="both">' + M.util.get_string('both', 'atto_table') + '</option>' +
602 '</select>' +
603 '<br/>' +
604 '<label for="atto_table_rows" class="sameline">' + M.util.get_string('numberofrows', 'atto_table') +
605 '</label>' +
606 '<input type="number" value="3" id="atto_table_rows" size="8" min="1" max="50"/>' +
607 '<br/>' +
608 '<label for="atto_table_columns" class="sameline">' + M.util.get_string('numberofcolumns', 'atto_table') +
609 '</label>' +
610 '<input type="number" value="3" id="atto_table_columns" size="8" min="1" max="20"/>' +
611 '<br/>' +
612 '<div class="mdl-align">' +
613 '<br/>' +
d19cf39e 614 '<button id="atto_table_submit" type="submit">' +
adca7326
DW
615 M.util.get_string('createtable', 'atto_table') +
616 '</button>' +
617 '</div>' +
618 '</form>' +
619 '<hr/>' + M.util.get_string('accessibilityhint', 'atto_table'));
620
621 content.one('#atto_table_submit').on('click', M.atto_table.set_table, this, elementid);
622 return content;
623 }
624};
625
626
627}, '@VERSION@', {"requires": ["node", "escape", "event", "event-valuechange"]});