Merge branch 'wip-MDL-43021-master' of git://github.com/marinaglancy/moodle
[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 16/**
62467795
AN
17 * @package atto_table
18 * @copyright 2013 Damyon Wiese <damyon@moodle.com>
19 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
20 */
21
22/**
23 * @module moodle-atto_table-button
3ee53a42 24 */
3ee53a42 25
adca7326
DW
26/**
27 * Atto text editor table plugin.
28 *
62467795
AN
29 * @namespace M.atto_table
30 * @class Button
31 * @extends M.editor_atto.EditorPlugin
adca7326 32 */
62467795
AN
33
34var COMPONENT = 'atto_table',
35 TEMPLATE = '' +
36 '<form class="atto_form">' +
37 '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' +
dfd51275 38 '<input class="caption fullwidth" id="{{elementid}}_atto_table_caption" required />' +
62467795
AN
39 '<br/>' +
40 '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' +
41 '<select class="headers" id="{{elementid}}_atto_table_headers">' +
42 '<option value="columns">{{get_string "columns" component}}' + '</option>' +
43 '<option value="rows">{{get_string "rows" component}}' + '</option>' +
44 '<option value="both">{{get_string "both" component}}' + '</option>' +
45 '</select>' +
46 '<br/>' +
47 '<label for="{{elementid}}_atto_table_rows" class="sameline">{{get_string "numberofrows" component}}</label>' +
48 '<input class="rows" type="number" value="3" id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' +
49 '<br/>' +
50 '<label for="{{elementid}}_atto_table_columns" class="sameline">{{get_string "numberofcolumns" component}}</label>' +
51 '<input class="columns" type="number" value="3" id="{{elementid}}_atto_table_columns" size="8" min="1" max="20"/>' +
52 '<br/>' +
53 '<div class="mdl-align">' +
54 '<br/>' +
55 '<button class="submit" type="submit">{{get_string "createtable" component}}</button>' +
56 '</div>' +
23cead68 57 '</form>',
62467795
AN
58 CONTEXTMENUTEMPLATE = '' +
59 '<ul>' +
60 '<li><a href="#" data-change="addcolumnafter">{{get_string "addcolumnafter" component}}</a></li>' +
61 '<li><a href="#" data-change="addrowafter">{{get_string "addrowafter" component}}</a></li>' +
62 '<li><a href="#" data-change="moverowup">{{get_string "moverowup" component}}</a></li>' +
63 '<li><a href="#" data-change="moverowdown">{{get_string "moverowdown" component}}</a></li>' +
64 '<li><a href="#" data-change="movecolumnleft">{{get_string "movecolumnleft" component}}</a></li>' +
65 '<li><a href="#" data-change="movecolumnright">{{get_string "movecolumnright" component}}</a></li>' +
66 '<li><a href="#" data-change="deleterow">{{get_string "deleterow" component}}</a></li>' +
67 '<li><a href="#" data-change="deletecolumn">{{get_string "deletecolumn" component}}</a></li>' +
68 '</ul>';
69
70 CSS = {
71 };
72
73Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
adca7326
DW
74
75 /**
62467795
AN
76 * A reference to the current selection at the time that the dialogue
77 * was opened.
adca7326 78 *
62467795
AN
79 * @property _currentSelection
80 * @type Range
81 * @private
adca7326 82 */
62467795 83 _currentSelection: null,
adca7326
DW
84
85 /**
62467795 86 * The contextual menu that we can open.
adca7326 87 *
62467795
AN
88 * @property _contextMenu
89 * @type M.editor_atto.Menu
90 * @private
91 */
92 _contextMenu: null,
93
94 /**
95 * The last modified target.
96 *
97 * @property _lastTarget
98 * @type Node
99 * @private
adca7326 100 */
62467795
AN
101 _lastTarget: null,
102
103 initializer: function() {
104 this.addButton({
105 icon: 'e/table',
106 callback: this._displayTableEditor,
107 tags: 'table'
108 });
109
110 // Disable mozilla table controls.
111 if (Y.UA.gecko) {
112 document.execCommand("enableInlineTableEditing", false, false);
113 document.execCommand("enableObjectResizing", false, false);
114 }
115 },
adca7326
DW
116
117 /**
62467795 118 * Display the table tool.
adca7326 119 *
62467795
AN
120 * @method _displayDialogue
121 * @private
adca7326 122 */
62467795
AN
123 _displayDialogue: function() {
124 // Store the current cursor position.
125 this._currentSelection = this.get('host').getSelection();
126
127 if (this._currentSelection !== false && (!this._currentSelection.collapsed)) {
128 var dialogue = this.getDialogue({
129 headerContent: M.util.get_string('createtable', COMPONENT),
130 focusAfterHide: true
131 });
132
133 // Set the dialogue content, and then show the dialogue.
134 dialogue.set('bodyContent', this._getDialogueContent())
135 .show();
136 }
137 },
adca7326
DW
138
139 /**
62467795 140 * Display the appropriate table editor.
adca7326 141 *
62467795
AN
142 * If the current selection includes a table, then we show the
143 * contextual menu, otherwise show the table creation dialogue.
144 *
145 * @method _displayTableEditor
146 * @param {EventFacade} e
147 * @private
adca7326 148 */
62467795 149 _displayTableEditor: function(e) {
3a6511a5 150 var cell = this._getSuitableTableCell();
62467795
AN
151 if (cell) {
152 // Add the cell to the EventFacade to save duplication in when showing the menu.
153 e.tableCell = cell;
154 return this._showTableMenu(e);
155 }
62467795
AN
156 return this._displayDialogue(e);
157 },
adca7326 158
3a6511a5
JC
159 /**
160 * Returns whether or not the parameter node exists within the editor.
161 *
162 * @method _stopAtContentEditableFilter
163 * @param {Node} node
164 * @private
165 * @return {boolean} whether or not the parameter node exists within the editor.
166 */
167 _stopAtContentEditableFilter: function(node) {
168 this.editor.contains(node);
169 },
170
adca7326 171 /**
62467795
AN
172 * Return the dialogue content for the tool, attaching any required
173 * events.
adca7326 174 *
62467795
AN
175 * @method _getDialogueContent
176 * @private
177 * @return {Node} The content to place in the dialogue.
adca7326 178 */
62467795
AN
179 _getDialogueContent: function() {
180 var template = Y.Handlebars.compile(TEMPLATE);
181 this._content = Y.Node.create(template({
182 CSS: CSS,
183 elementid: this.get('host').get('elementid'),
184 component: COMPONENT
185 }));
186
187 // Handle table setting.
188 this._content.one('.submit').on('click', this._setTable, this);
189
190 return this._content;
191 },
adca7326 192
3a6511a5
JC
193 /**
194 * Given the current selection, return a table cell suitable for table editing
195 * purposes, i.e. the first table cell selected, or the first cell in the table
196 * that the selection exists in, or null if not within a table.
197 *
198 * @method _getSuitableTableCell
199 * @private
200 * @return {Node} suitable target cell, or null if not within a table
201 */
202 _getSuitableTableCell: function() {
203 var targetcell = null,
204 host = this.get('host');
205
206 host.getSelectedNodes().some(function (node) {
207 if (node.ancestor('td, th, caption', true, this._stopAtContentEditableFilter)) {
208 targetcell = node;
209
210 var caption = node.ancestor('caption', true, this._stopAtContentEditableFilter);
211 if (caption) {
212 var table = caption.get('parentNode');
213 if (table) {
214 targetcell = table.one('td, th');
215 }
216 }
217
218 // Once we've found a cell to target, we shouldn't need to keep looking.
219 return true;
220 }
221 });
222
223 if (targetcell) {
224 var selection = host.getSelectionFromNode(targetcell);
225 host.setSelection(selection);
226 }
227
228 return targetcell;
229 },
230
adca7326 231 /**
62467795 232 * Handle creation of a new table.
adca7326 233 *
62467795
AN
234 * @method _setTable
235 * @param {EventFacade} e
236 * @private
adca7326 237 */
62467795
AN
238 _setTable: function(e) {
239 var caption,
240 rows,
241 cols,
242 headers,
243 tablehtml,
244 i, j;
245
adca7326 246 e.preventDefault();
adca7326 247
62467795
AN
248 // Hide the dialogue.
249 this.getDialogue({
250 focusAfterHide: null
251 }).hide();
252
253 caption = e.currentTarget.ancestor('.atto_form').one('.caption');
254 rows = e.currentTarget.ancestor('.atto_form').one('.rows');
255 cols = e.currentTarget.ancestor('.atto_form').one('.columns');
256 headers = e.currentTarget.ancestor('.atto_form').one('.headers');
257
258 // Set the selection.
259 this.get('host').setSelection(this._currentSelection);
260
261 // Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click.
262 var nl = "\n";
263 tablehtml = '<br/>' + nl + '<table>' + nl;
264 tablehtml += '<caption>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl;
adca7326 265
62467795
AN
266 i = 0;
267 if (headers.get('value') === 'columns' || headers.get('value') === 'both') {
268 i = 1;
269 tablehtml += '<thead>' + nl + '<tr>' + nl;
270 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
271 tablehtml += '<th scope="col"></th>' + nl;
272 }
273 tablehtml += '</tr>' + nl + '</thead>' + nl;
274 }
275 tablehtml += '<tbody>' + nl;
276 for (; i < parseInt(rows.get('value'), 10); i++) {
277 tablehtml += '<tr>' + nl;
278 for (j = 0; j < parseInt(cols.get('value'), 10); j++) {
279 if (j === 0 && (headers.get('value') === 'rows' || headers.get('value') === 'both')) {
280 tablehtml += '<th scope="row"></th>' + nl;
281 } else {
282 tablehtml += '<td></td>' + nl;
283 }
284 }
285 tablehtml += '</tr>' + nl;
adca7326 286 }
62467795
AN
287 tablehtml += '</tbody>' + nl;
288 tablehtml += '</table>' + nl + '<br/>';
adca7326 289
62467795
AN
290 this.get('host').insertContentAtFocusPoint(tablehtml);
291
292 // Mark the content as updated.
293 this.markUpdated();
adca7326
DW
294 },
295
296 /**
62467795 297 * Display the table menu.
adca7326 298 *
62467795
AN
299 * @method _showTableMenu
300 * @param {EventFacade} e
301 * @private
adca7326 302 */
62467795 303 _showTableMenu: function(e) {
adca7326
DW
304 e.preventDefault();
305
62467795
AN
306 var boundingBox;
307
308 if (!this._contextMenu) {
309 var template = Y.Handlebars.compile(CONTEXTMENUTEMPLATE),
310 content = Y.Node.create(template({
311 elementid: this.get('host').get('elementid'),
312 component: COMPONENT
313 }));
314
315 this._contextMenu = new Y.M.editor_atto.Menu({
316 headerText: M.util.get_string('edittable', 'atto_table'),
317 bodyContent: content
adca7326 318 });
62467795
AN
319
320 // Add event handlers for table control menus.
321 boundingBox = this._contextMenu.get('boundingBox');
322 boundingBox.delegate('click', this._handleTableChange, 'a', this);
323 boundingBox.delegate('key', this._handleTableChange, 'down:enter,space', 'a', this);
adca7326 324 }
62467795
AN
325 boundingBox = this._contextMenu.get('boundingBox');
326
adca7326 327 // We store the cell of the last click (the control node is transient).
62467795
AN
328 this._lastTarget = e.tableCell.ancestor('.editor_atto_content td, .editor_atto_content th', true);
329
330 // Show the context menu, and align to the current position.
331 this._contextMenu.show();
f8c3af13 332 this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]);
62467795
AN
333
334 // If there are any anchors in the bounding box, focus on the first.
335 if (boundingBox.one('a')) {
336 boundingBox.one('a').focus();
26f8822d 337 }
62467795
AN
338 },
339
340 /**
341 * Handle a selection from the table control menu.
342 *
343 * @method _handleTableChange
344 * @param {EventFacade} e
345 * @private
346 */
347 _handleTableChange: function(e) {
348 e.preventDefault();
adca7326 349
62467795
AN
350 // Hide the context menu.
351 this._contextMenu.hide();
352
353 // Make our changes.
354 switch (e.target.getData('change')) {
355 case 'addcolumnafter':
356 this._addColumnAfter();
357 break;
358 case 'addrowafter':
359 this._addRowAfter();
360 break;
361 case 'deleterow':
362 this._deleteRow();
363 break;
364 case 'deletecolumn':
365 this._deleteColumn();
366 break;
367 case 'moverowdown':
368 this._moveRowDown();
369 break;
370 case 'moverowup':
371 this._moveRowUp();
372 break;
373 case 'movecolumnleft':
374 this._moveColumnLeft();
375 break;
376 case 'movecolumnright':
377 this._moveColumnRight();
378 break;
adca7326
DW
379 }
380 },
381
382 /**
383 * Determine the index of a row in a table column.
384 *
62467795
AN
385 * @method _getRowIndex
386 * @param {Node} cell
387 * @private
adca7326 388 */
62467795 389 _getRowIndex: function(cell) {
adca7326
DW
390 var tablenode = cell.ancestor('table'),
391 rownode = cell.ancestor('tr');
392
393 if (!tablenode || !rownode) {
394 return;
395 }
396
397 var rows = tablenode.all('tr');
398
399 return rows.indexOf(rownode);
400 },
401
402 /**
403 * Determine the index of a column in a table row.
404 *
62467795
AN
405 * @method _getColumnIndex
406 * @param {Node} cellnode
407 * @private
adca7326 408 */
62467795 409 _getColumnIndex: function(cellnode) {
adca7326
DW
410 var rownode = cellnode.ancestor('tr');
411
412 if (!rownode) {
413 return;
414 }
415
416 var cells = rownode.all('td, th');
417
418 return cells.indexOf(cellnode);
419 },
420
421 /**
62467795 422 * Delete the current row.
adca7326 423 *
62467795
AN
424 * @method _deleteRow
425 * @private
adca7326 426 */
62467795
AN
427 _deleteRow: function() {
428 var row = this._lastTarget.ancestor('tr');
adca7326
DW
429
430 if (row) {
431 // We do not remove rows with no cells (all headers).
432 if (row.one('td')) {
433 row.remove(true);
434 }
435 }
436
437 // Clean the HTML.
62467795 438 this.markUpdated();
adca7326
DW
439 },
440
441 /**
442 * Move row up
443 *
62467795
AN
444 * @method _moveRowUp
445 * @private
adca7326 446 */
62467795
AN
447 _moveRowUp: function() {
448 var row = this._lastTarget.ancestor('tr');
adca7326
DW
449 var prevrow = row.previous('tr');
450 if (!row || !prevrow) {
451 return;
452 }
453
454 row.swap(prevrow);
455 // Clean the HTML.
62467795 456 this.markUpdated();
adca7326
DW
457 },
458
459 /**
460 * Move column left
461 *
62467795
AN
462 * @method _moveColumnLeft
463 * @private
adca7326 464 */
62467795
AN
465 _moveColumnLeft: function() {
466 var columnindex = this._getColumnIndex(this._lastTarget);
467 var rows = this._lastTarget.ancestor('table').all('tr');
adca7326
DW
468 var columncells = new Y.NodeList();
469 var prevcells = new Y.NodeList();
470 var hastd = false;
471
472 rows.each(function(row) {
473 var cells = row.all('td, th');
474 var cell = cells.item(columnindex),
475 cellprev = cells.item(columnindex-1);
476 columncells.push(cell);
477 if (cellprev) {
478 if (cellprev.get('tagName') === 'TD') {
479 hastd = true;
480 }
481 prevcells.push(cellprev);
482 }
483 });
484
485 if (hastd && prevcells.size() > 0) {
486 var i = 0;
487 for (i = 0; i < columncells.size(); i++) {
488 var cell = columncells.item(i);
489 var prevcell = prevcells.item(i);
490
491 cell.swap(prevcell);
492 }
493 }
494 // Cleanup.
62467795 495 this.markUpdated();
adca7326
DW
496 },
497
498 /**
62467795 499 * Move column right.
adca7326 500 *
62467795
AN
501 * @method _moveColumnRight
502 * @private
adca7326 503 */
62467795
AN
504 _moveColumnRight: function() {
505 var columnindex = this._getColumnIndex(this._lastTarget);
506 var rows = this._lastTarget.ancestor('table').all('tr');
adca7326
DW
507 var columncells = new Y.NodeList();
508 var nextcells = new Y.NodeList();
509 var hastd = false;
510
511 rows.each(function(row) {
512 var cells = row.all('td, th');
513 var cell = cells.item(columnindex),
514 cellnext = cells.item(columnindex+1);
515 if (cell.get('tagName') === 'TD') {
516 hastd = true;
517 }
518 columncells.push(cell);
519 if (cellnext) {
520 nextcells.push(cellnext);
521 }
522 });
523
524 if (hastd && nextcells.size() > 0) {
525 var i = 0;
526 for (i = 0; i < columncells.size(); i++) {
527 var cell = columncells.item(i);
528 var nextcell = nextcells.item(i);
529
530 cell.swap(nextcell);
531 }
532 }
533 // Cleanup.
62467795 534 this.markUpdated();
adca7326
DW
535 },
536
537 /**
62467795 538 * Move row down.
adca7326 539 *
62467795
AN
540 * @method _moveRowDown
541 * @private
adca7326 542 */
62467795
AN
543 _moveRowDown: function() {
544 var row = this._lastTarget.ancestor('tr');
adca7326
DW
545 var nextrow = row.next('tr');
546 if (!row || !nextrow) {
547 return;
548 }
549
550 row.swap(nextrow);
551 // Clean the HTML.
62467795 552 this.markUpdated();
adca7326
DW
553 },
554
555 /**
62467795 556 * Delete the current column.
adca7326 557 *
62467795
AN
558 * @method _deleteColumn
559 * @private
adca7326 560 */
62467795
AN
561 _deleteColumn: function() {
562 var columnindex = this._getColumnIndex(this._lastTarget);
563 var rows = this._lastTarget.ancestor('table').all('tr');
adca7326
DW
564 var columncells = new Y.NodeList();
565 var hastd = false;
566
567 rows.each(function(row) {
568 var cells = row.all('td, th');
569 var cell = cells.item(columnindex);
570 if (cell.get('tagName') === 'TD') {
571 hastd = true;
572 }
573 columncells.push(cell);
574 });
575
576 if (hastd) {
577 columncells.remove(true);
578 }
579
580 // Clean the HTML.
62467795 581 this.markUpdated();
adca7326
DW
582 },
583
584 /**
585 * Add a row after the current row.
586 *
62467795
AN
587 * @method _addRowAfter
588 * @private
adca7326 589 */
62467795
AN
590 _addRowAfter: function() {
591 var rowindex = this._getRowIndex(this._lastTarget);
adca7326 592
62467795 593 var tablebody = this._lastTarget.ancestor('table').one('tbody');
adca7326
DW
594 if (!tablebody) {
595 // Not all tables have tbody.
62467795 596 tablebody = this._lastTarget.ancestor('table');
adca7326
DW
597 rowindex += 1;
598 }
599
600 var firstrow = tablebody.one('tr');
601 if (!firstrow) {
62467795 602 firstrow = this._lastTarget.ancestor('table').one('tr');
adca7326
DW
603 }
604 if (!firstrow) {
605 // Table has no rows. Boo.
606 return;
607 }
608 newrow = firstrow.cloneNode(true);
609 newrow.all('th, td').each(function (tablecell) {
610 if (tablecell.get('tagName') === 'TH') {
611 if (tablecell.getAttribute('scope') !== 'row') {
612 var newcell = Y.Node.create('<td></td>');
613 tablecell.replace(newcell);
614 tablecell = newcell;
615 }
616 }
617 tablecell.setHTML('&nbsp;');
618 });
619
620 tablebody.insert(newrow, rowindex);
621
622 // Clean the HTML.
62467795 623 this.markUpdated();
adca7326
DW
624 },
625
626 /**
627 * Add a column after the current column.
628 *
62467795
AN
629 * @method _addColumnAfter
630 * @private
adca7326 631 */
62467795
AN
632 _addColumnAfter: function() {
633 var columnindex = this._getColumnIndex(this._lastTarget);
adca7326 634
62467795 635 var tablecell = this._lastTarget.ancestor('table');
adca7326
DW
636 var rows = tablecell.all('tr');
637 Y.each(rows, function(row) {
638 // Clone the first cell from the row so it has the same type/attributes (e.g. scope).
639 var newcell = row.one('td, th').cloneNode(true);
640 // Clear the content of the cell.
641 newcell.setHTML('&nbsp;');
642
643 row.insert(newcell, columnindex + 1);
644 }, this);
645
646 // Clean the HTML.
62467795 647 this.markUpdated();
adca7326 648 }
62467795 649});