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