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