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