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