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