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