[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 YUI.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 18 /** 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 26 */ 27 28 /** 29 * Atto text editor table plugin. 30 * 31 * @namespace M.atto_table 32 * @class Button 33 * @extends M.editor_atto.EditorPlugin 34 */ 35 36 var COMPONENT = 'atto_table', 37 DEFAULT = { 38 BORDERSTYLE: 'none', 39 BORDERWIDTH: '1' 40 }, 41 DIALOGUE = { 42 WIDTH: '480px' 43 }, 44 TEMPLATE = '' + 45 '<form class="{{CSS.FORM}}">' + 46 '<label for="{{elementid}}_atto_table_caption">{{get_string "caption" component}}</label>' + 47 '<input class="{{CSS.CAPTION}} fullwidth" id="{{elementid}}_atto_table_caption" required />' + 48 '<br/>' + 49 '<br/>' + 50 '<label for="{{elementid}}_atto_table_captionposition" class="sameline">' + 51 '{{get_string "captionposition" component}}</label>' + 52 '<select class="{{CSS.CAPTIONPOSITION}}" id="{{elementid}}_atto_table_captionposition">' + 53 '<option value=""></option>' + 54 '<option value="top">{{get_string "top" "editor"}}</option>' + 55 '<option value="bottom">{{get_string "bottom" "editor"}}</option>' + 56 '</select>' + 57 '<br/>' + 58 '<label for="{{elementid}}_atto_table_headers" class="sameline">{{get_string "headers" component}}</label>' + 59 '<select class="{{CSS.HEADERS}}" id="{{elementid}}_atto_table_headers">' + 60 '<option value="columns">{{get_string "columns" component}}' + '</option>' + 61 '<option value="rows">{{get_string "rows" component}}' + '</option>' + 62 '<option value="both">{{get_string "both" component}}' + '</option>' + 63 '</select>' + 64 '<br/>' + 65 '{{#if nonedit}}' + 66 '<label for="{{elementid}}_atto_table_rows" class="sameline">{{get_string "numberofrows" component}}</label>' + 67 '<input class="{{CSS.ROWS}}" type="number" value="3" ' + 68 'id="{{elementid}}_atto_table_rows" size="8" min="1" max="50"/>' + 69 '<br/>' + 70 '<label for="{{elementid}}_atto_table_columns" ' + 71 'class="sameline">{{get_string "numberofcolumns" component}}</label>' + 72 '<input class="{{CSS.COLUMNS}}" type="number" value="3" id="{{elementid}}_atto_table_columns"' + 73 'size="8" min="1" max="20"/>' + 74 '<br/>' + 75 '{{/if}}' + 76 '{{#if allowStyling}}' + 77 '<fieldset>' + 78 '<legend class="mdl-align">{{get_string "appearance" component}}</legend>' + 79 '{{#if allowBorders}}' + 80 '<label for="{{elementid}}_atto_table_borders" class="sameline">{{get_string "borders" component}}</label>' + 81 '<select name="borders" class="{{CSS.BORDERS}}" id="{{elementid}}_atto_table_borders">' + 82 '<option value="default">{{get_string "themedefault" component}}' + '</option>' + 83 '<option value="outer">{{get_string "outer" component}}' + '</option>' + 84 '<option value="all">{{get_string "all" component}}' + '</option>' + 85 '</select>' + 86 '<br>' + 87 '<label for="{{elementid}}_atto_table_borderstyle" class="sameline">' + 88 '{{get_string "borderstyles" component}}</label>' + 89 '<select name="borderstyles" class="{{CSS.BORDERSTYLE}}" id="{{elementid}}_atto_table_borderstyle">' + 90 '{{#each borderStyles}}' + 91 '<option value="' + '{{this}}' + '">' + '{{get_string this ../component}}' + '</option>' + 92 '{{/each}}' + 93 '</select>' + 94 '<br>' + 95 '<label for="{{elementid}}_atto_table_bordersize" class="sameline">' + 96 '{{get_string "bordersize" component}}</label>' + 97 '<input name="bordersize" id="{{elementid}}_atto_table_bordersize" class="{{CSS.BORDERSIZE}}"' + 98 'type="number" value="1" size="8" min="1" max="50"/>' + 99 '<label style="display: inline-block;">{{CSS.BORDERSIZEUNIT}}</label>' + 100 '<br>' + 101 '<label for="{{elementid}}_atto_table_bordercolour" class="sameline">' + 102 '{{get_string "bordercolour" component}}</label>' + 103 '<div id="{{elementid}}_atto_table_bordercolour"' + 104 'class="{{CSS.BORDERCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' + 105 '<label class="hideborder" for="{{../elementid}}_atto_table_bordercolour_-1"' + 106 'style="background-color:transparent;color:transparent">' + 107 108 '<input id="{{../elementid}}_atto_table_bordercolour_-1"' + 109 'type="radio" name="borderColour" value="none" checked="checked"' + 110 'title="{{get_string "themedefault" component}}"></input>' + 111 112 '{{get_string "themedefault" component}}' + 113 '</label>' + 114 '{{#each availableColours}}' + 115 '<label for="{{../elementid}}_atto_table_bordercolour_{{@index}}"' + 116 'style="background-color:{{this}};color:{{this}}">' + 117 118 '<input id="{{../elementid}}_atto_table_bordercolour_{{@index}}"' + 119 'type="radio" name="borderColour" value="' + '{{this}}' + '" title="{{this}}">' + 120 121 '{{this}}' + 122 '</label>' + 123 '{{/each}}' + 124 '</div>' + 125 '<br>' + 126 '{{/if}}' + 127 '{{#if allowBackgroundColour}}' + 128 '<label for="{{elementid}}_atto_table_backgroundcolour" class="sameline">' + 129 '{{get_string "backgroundcolour" component}}</label>' + 130 '<div id="{{elementid}}_atto_table_backgroundcolour"' + 131 'class="{{CSS.BACKGROUNDCOLOUR}} {{CSS.AVAILABLECOLORS}}" size="1">' + 132 '<label class="hideborder" for="{{../elementid}}_atto_table_backgroundcolour_-1"' + 133 'style="background-color:transparent;color:transparent">' + 134 135 '<input id="{{../elementid}}_atto_table_backgroundcolour_-1"' + 136 'type="radio" name="backgroundColour" value="none" checked="checked"' + 137 'title="{{get_string "themedefault" component}}"></input>' + 138 139 '{{get_string "themedefault" component}}' + 140 '</label>' + 141 142 '{{#each availableColours}}' + 143 '<label for="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' + 144 'style="background-color:{{this}};color:{{this}}">' + 145 146 '<input id="{{../elementid}}_atto_table_backgroundcolour_{{@index}}"' + 147 'type="radio" name="backgroundColour" value="' + '{{this}}' + '" title="{{this}}">' + 148 149 '{{this}}' + 150 '</label>' + 151 '{{/each}}' + 152 '</div>' + 153 '<br>' + 154 '{{/if}}' + 155 '{{#if allowWidth}}' + 156 '<label for="{{elementid}}_atto_table_width" class="sameline">' + 157 '{{get_string "width" component}}</label>' + 158 '<input name="width" id="{{elementid}}_atto_table_width" class="{{CSS.WIDTH}}" size="8" ' + 159 'type="number" min="0" max="100"/>' + 160 '<label style="display: inline-block;">{{CSS.WIDTHUNIT}}</label>' + 161 '<br>' + 162 '{{/if}}' + 163 '</fieldset>' + 164 '{{/if}}' + 165 '<div class="mdl-align">' + 166 '<br/>' + 167 '{{#if edit}}' + 168 '<button class="submit" type="submit">{{get_string "updatetable" component}}</button>' + 169 '{{/if}}' + 170 '{{#if nonedit}}' + 171 '<button class="submit" type="submit">{{get_string "createtable" component}}</button>' + 172 '{{/if}}' + 173 '</div>' + 174 '</form>', 175 CSS = { 176 CAPTION: 'caption', 177 CAPTIONPOSITION: 'captionposition', 178 HEADERS: 'headers', 179 ROWS: 'rows', 180 COLUMNS: 'columns', 181 SUBMIT: 'submit', 182 FORM: 'atto_form', 183 BORDERS: 'borders', 184 BORDERSIZE: 'bordersize', 185 BORDERSIZEUNIT: 'px', 186 BORDERCOLOUR: 'bordercolour', 187 BORDERSTYLE: 'borderstyle', 188 BACKGROUNDCOLOUR: 'backgroundcolour', 189 WIDTH: 'customwidth', 190 WIDTHUNIT: '%', 191 AVAILABLECOLORS: 'availablecolors', 192 COLOURROW: 'colourrow' 193 }, 194 SELECTORS = { 195 CAPTION: '.' + CSS.CAPTION, 196 CAPTIONPOSITION: '.' + CSS.CAPTIONPOSITION, 197 HEADERS: '.' + CSS.HEADERS, 198 ROWS: '.' + CSS.ROWS, 199 COLUMNS: '.' + CSS.COLUMNS, 200 SUBMIT: '.' + CSS.SUBMIT, 201 BORDERS: '.' + CSS.BORDERS, 202 BORDERSIZE: '.' + CSS.BORDERSIZE, 203 BORDERCOLOURS: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]', 204 SELECTEDBORDERCOLOUR: '.' + CSS.BORDERCOLOUR + ' input[name="borderColour"]:checked', 205 BORDERSTYLE: '.' + CSS.BORDERSTYLE, 206 BACKGROUNDCOLOURS: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]', 207 SELECTEDBACKGROUNDCOLOUR: '.' + CSS.BACKGROUNDCOLOUR + ' input[name="backgroundColour"]:checked', 208 FORM: '.atto_form', 209 WIDTH: '.' + CSS.WIDTH, 210 AVAILABLECOLORS: '.' + CSS.AVAILABLECOLORS 211 }; 212 213 Y.namespace('M.atto_table').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], { 214 215 /** 216 * A reference to the current selection at the time that the dialogue 217 * was opened. 218 * 219 * @property _currentSelection 220 * @type Range 221 * @private 222 */ 223 _currentSelection: null, 224 225 /** 226 * The contextual menu that we can open. 227 * 228 * @property _contextMenu 229 * @type M.editor_atto.Menu 230 * @private 231 */ 232 _contextMenu: null, 233 234 /** 235 * The last modified target. 236 * 237 * @property _lastTarget 238 * @type Node 239 * @private 240 */ 241 _lastTarget: null, 242 243 /** 244 * The list of menu items. 245 * 246 * @property _menuOptions 247 * @type Object 248 * @private 249 */ 250 _menuOptions: null, 251 252 initializer: function() { 253 this.addButton({ 254 icon: 'e/table', 255 callback: this._displayTableEditor, 256 tags: 'table' 257 }); 258 // Disable mozilla table controls. 259 if (Y.UA.gecko) { 260 document.execCommand("enableInlineTableEditing", false, false); 261 document.execCommand("enableObjectResizing", false, false); 262 } 263 }, 264 265 /** 266 * Display the table tool. 267 * 268 * @method _displayDialogue 269 * @private 270 */ 271 _displayDialogue: function() { 272 // Store the current cursor position. 273 this._currentSelection = this.get('host').getSelection(); 274 275 if (this._currentSelection !== false && (!this._currentSelection.collapsed)) { 276 var dialogue = this.getDialogue({ 277 headerContent: M.util.get_string('createtable', COMPONENT), 278 focusAfterHide: true, 279 focusOnShowSelector: SELECTORS.CAPTION, 280 width: DIALOGUE.WIDTH 281 }); 282 283 // Set the dialogue content, and then show the dialogue. 284 dialogue.set('bodyContent', this._getDialogueContent(false)) 285 .show(); 286 287 this._updateAvailableSettings(); 288 } 289 }, 290 291 /** 292 * Display the appropriate table editor. 293 * 294 * If the current selection includes a table, then we show the 295 * contextual menu, otherwise show the table creation dialogue. 296 * 297 * @method _displayTableEditor 298 * @param {EventFacade} e 299 * @private 300 */ 301 _displayTableEditor: function(e) { 302 var cell = this._getSuitableTableCell(); 303 if (cell) { 304 // Add the cell to the EventFacade to save duplication in when showing the menu. 305 e.tableCell = cell; 306 return this._showTableMenu(e); 307 } 308 return this._displayDialogue(e); 309 }, 310 311 /** 312 * Returns whether or not the parameter node exists within the editor. 313 * 314 * @method _stopAtContentEditableFilter 315 * @param {Node} node 316 * @private 317 * @return {boolean} whether or not the parameter node exists within the editor. 318 */ 319 _stopAtContentEditableFilter: function(node) { 320 this.editor.contains(node); 321 }, 322 323 /** 324 * Return the dialogue content for the tool, attaching any required 325 * events. 326 * 327 * @method _getDialogueContent 328 * @private 329 * @return {Node} The content to place in the dialogue. 330 */ 331 _getDialogueContent: function(edit) { 332 var template = Y.Handlebars.compile(TEMPLATE); 333 var allowBorders = this.get('allowBorders'); 334 335 this._content = Y.Node.create(template({ 336 CSS: CSS, 337 elementid: this.get('host').get('elementid'), 338 component: COMPONENT, 339 edit: edit, 340 nonedit: !edit, 341 allowStyling: this.get('allowStyling'), 342 allowBorders: allowBorders, 343 borderStyles: this.get('borderStyles'), 344 allowBackgroundColour: this.get('allowBackgroundColour'), 345 availableColours: this.get('availableColors'), 346 allowWidth: this.get('allowWidth') 347 })); 348 349 // Handle table setting. 350 if (edit) { 351 this._content.one('.submit').on('click', this._updateTable, this); 352 } else { 353 this._content.one('.submit').on('click', this._setTable, this); 354 } 355 356 if (allowBorders) { 357 this._content.one('[name="borders"]').on('change', this._updateAvailableSettings, this); 358 } 359 360 return this._content; 361 }, 362 363 /** 364 * Disables options within the dialogue if they shouldn't be available. 365 * E.g. 366 * If borders are set to "Theme default" then the border size, style and 367 * colour options are disabled. 368 * 369 * @method _updateAvailableSettings 370 * @private 371 */ 372 _updateAvailableSettings: function() { 373 var tableForm = this._content, 374 enableBorders = tableForm.one('[name="borders"]'), 375 borderStyle = tableForm.one('[name="borderstyles"]'), 376 borderSize = tableForm.one('[name="bordersize"]'), 377 borderColour = tableForm.all('[name="borderColour"]'), 378 disabledValue = 'removeAttribute'; 379 380 if (!enableBorders) { 381 return; 382 } 383 384 if (enableBorders.get('value') === 'default') { 385 disabledValue = 'setAttribute'; 386 } 387 388 if (borderStyle) { 389 borderStyle[disabledValue]('disabled'); 390 } 391 392 if (borderSize) { 393 borderSize[disabledValue]('disabled'); 394 } 395 396 if (borderColour) { 397 borderColour[disabledValue]('disabled'); 398 } 399 400 }, 401 402 /** 403 * Given the current selection, return a table cell suitable for table editing 404 * purposes, i.e. the first table cell selected, or the first cell in the table 405 * that the selection exists in, or null if not within a table. 406 * 407 * @method _getSuitableTableCell 408 * @private 409 * @return {Node} suitable target cell, or null if not within a table 410 */ 411 _getSuitableTableCell: function() { 412 var targetcell = null, 413 host = this.get('host'); 414 415 host.getSelectedNodes().some(function(node) { 416 if (node.ancestor('td, th, caption', true, this._stopAtContentEditableFilter)) { 417 targetcell = node; 418 419 var caption = node.ancestor('caption', true, this._stopAtContentEditableFilter); 420 if (caption) { 421 var table = caption.get('parentNode'); 422 if (table) { 423 targetcell = table.one('td, th'); 424 } 425 } 426 427 // Once we've found a cell to target, we shouldn't need to keep looking. 428 return true; 429 } 430 }); 431 432 if (targetcell) { 433 var selection = host.getSelectionFromNode(targetcell); 434 host.setSelection(selection); 435 } 436 437 return targetcell; 438 }, 439 440 /** 441 * Change a node from one type to another, copying all attributes and children. 442 * 443 * @method _changeNodeType 444 * @param {Y.Node} node 445 * @param {String} new node type 446 * @private 447 * @chainable 448 */ 449 _changeNodeType: function(node, newType) { 450 var newNode = Y.Node.create('<' + newType + '></' + newType + '>'); 451 newNode.setAttrs(node.getAttrs()); 452 node.get('childNodes').each(function(child) { 453 newNode.append(child.remove()); 454 }); 455 node.replace(newNode); 456 return newNode; 457 }, 458 459 /** 460 * Handle updating an existing table. 461 * 462 * @method _updateTable 463 * @param {EventFacade} e 464 * @private 465 */ 466 _updateTable: function(e) { 467 var caption, 468 captionposition, 469 headers, 470 borders, 471 bordersize, 472 borderstyle, 473 bordercolour, 474 backgroundcolour, 475 table, 476 width, 477 captionnode; 478 479 e.preventDefault(); 480 // Hide the dialogue. 481 this.getDialogue({ 482 focusAfterHide: null 483 }).hide(); 484 485 // Add/update the caption. 486 caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION); 487 captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION); 488 headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS); 489 borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS); 490 bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE); 491 bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR); 492 borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE); 493 backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR); 494 width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH); 495 496 table = this._lastTarget.ancestor('table'); 497 this._setAppearance(table, { 498 width: width, 499 borders: borders, 500 borderColour: bordercolour, 501 borderSize: bordersize, 502 borderStyle: borderstyle, 503 backgroundColour: backgroundcolour 504 }); 505 506 captionnode = table.one('caption'); 507 if (!captionnode) { 508 captionnode = Y.Node.create('<caption></caption>'); 509 table.insert(captionnode, 0); 510 } 511 captionnode.setHTML(caption.get('value')); 512 captionnode.setStyle('caption-side', captionposition.get('value')); 513 if (!captionnode.getAttribute('style')) { 514 captionnode.removeAttribute('style'); 515 } 516 517 // Add the row headers. 518 if (headers.get('value') === 'rows' || headers.get('value') === 'both') { 519 table.all('tr').each(function(row) { 520 var cells = row.all('th, td'), 521 firstCell = cells.shift(), 522 newCell; 523 524 if (firstCell.get('tagName') === 'TD') { 525 // Cell is a td but should be a th - change it. 526 newCell = this._changeNodeType(firstCell, 'th'); 527 newCell.setAttribute('scope', 'row'); 528 } else { 529 firstCell.setAttribute('scope', 'row'); 530 } 531 532 // Now make sure all other cells in the row are td. 533 cells.each(function(cell) { 534 if (cell.get('tagName') === 'TH') { 535 newCell = this._changeNodeType(cell, 'td'); 536 newCell.removeAttribute('scope'); 537 } 538 }, this); 539 540 }, this); 541 } 542 // Add the col headers. These may overrule the row headers in the first cell. 543 if (headers.get('value') === 'columns' || headers.get('value') === 'both') { 544 var rows = table.all('tr'), 545 firstRow = rows.shift(), 546 newCell; 547 548 firstRow.all('td, th').each(function(cell) { 549 if (cell.get('tagName') === 'TD') { 550 // Cell is a td but should be a th - change it. 551 newCell = this._changeNodeType(cell, 'th'); 552 newCell.setAttribute('scope', 'col'); 553 } else { 554 cell.setAttribute('scope', 'col'); 555 } 556 }, this); 557 // Change all the cells in the rest of the table to tds (unless they are row headers). 558 rows.each(function(row) { 559 var cells = row.all('th, td'); 560 561 if (headers.get('value') === 'both') { 562 // Ignore the first cell because it's a row header. 563 cells.shift(); 564 } 565 cells.each(function(cell) { 566 if (cell.get('tagName') === 'TH') { 567 newCell = this._changeNodeType(cell, 'td'); 568 newCell.removeAttribute('scope'); 569 } 570 }, this); 571 572 }, this); 573 } 574 // Clean the HTML. 575 this.markUpdated(); 576 }, 577 578 /** 579 * Handle creation of a new table. 580 * 581 * @method _setTable 582 * @param {EventFacade} e 583 * @private 584 */ 585 _setTable: function(e) { 586 var caption, 587 captionposition, 588 borders, 589 bordersize, 590 borderstyle, 591 bordercolour, 592 rows, 593 cols, 594 headers, 595 tablehtml, 596 backgroundcolour, 597 width, 598 i, j; 599 600 e.preventDefault(); 601 602 // Hide the dialogue. 603 this.getDialogue({ 604 focusAfterHide: null 605 }).hide(); 606 607 caption = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTION); 608 captionposition = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.CAPTIONPOSITION); 609 borders = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERS); 610 bordersize = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSIZE); 611 bordercolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBORDERCOLOUR); 612 borderstyle = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.BORDERSTYLE); 613 backgroundcolour = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.SELECTEDBACKGROUNDCOLOUR); 614 rows = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.ROWS); 615 cols = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.COLUMNS); 616 headers = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.HEADERS); 617 width = e.currentTarget.ancestor(SELECTORS.FORM).one(SELECTORS.WIDTH); 618 619 // Set the selection. 620 this.get('host').setSelection(this._currentSelection); 621 622 // Note there are some spaces inserted in the cells and before and after, so that users have somewhere to click. 623 var nl = "\n"; 624 var tableId = Y.guid(); 625 tablehtml = '<br/>' + nl + '<table id="' + tableId + '">' + nl; 626 627 var captionstyle = ''; 628 if (captionposition.get('value')) { 629 captionstyle = ' style="caption-side: ' + captionposition.get('value') + '"'; 630 } 631 tablehtml += '<caption' + captionstyle + '>' + Y.Escape.html(caption.get('value')) + '</caption>' + nl; 632 i = 0; 633 if (headers.get('value') === 'columns' || headers.get('value') === 'both') { 634 i = 1; 635 tablehtml += '<thead>' + nl + '<tr>' + nl; 636 for (j = 0; j < parseInt(cols.get('value'), 10); j++) { 637 tablehtml += '<th scope="col"></th>' + nl; 638 } 639 tablehtml += '</tr>' + nl + '</thead>' + nl; 640 } 641 tablehtml += '<tbody>' + nl; 642 for (; i < parseInt(rows.get('value'), 10); i++) { 643 tablehtml += '<tr>' + nl; 644 for (j = 0; j < parseInt(cols.get('value'), 10); j++) { 645 if (j === 0 && (headers.get('value') === 'rows' || headers.get('value') === 'both')) { 646 tablehtml += '<th scope="row"></th>' + nl; 647 } else { 648 tablehtml += '<td ></td>' + nl; 649 } 650 } 651 tablehtml += '</tr>' + nl; 652 } 653 tablehtml += '</tbody>' + nl; 654 tablehtml += '</table>' + nl + '<br/>'; 655 656 this.get('host').insertContentAtFocusPoint(tablehtml); 657 658 var tableNode = Y.one('#' + tableId); 659 this._setAppearance(tableNode, { 660 width: width, 661 borders: borders, 662 borderColour: bordercolour, 663 borderSize: bordersize, 664 borderStyle: borderstyle, 665 backgroundColour: backgroundcolour 666 }); 667 tableNode.removeAttribute('id'); 668 669 // Mark the content as updated. 670 this.markUpdated(); 671 }, 672 673 /** 674 * Search for all the cells in the current, next and previous columns. 675 * 676 * @method _findColumnCells 677 * @private 678 * @return {Object} containing current, prev and next {Y.NodeList}s 679 */ 680 _findColumnCells: function() { 681 var columnindex = this._getColumnIndex(this._lastTarget), 682 rows = this._lastTarget.ancestor('table').all('tr'), 683 currentcells = new Y.NodeList(), 684 prevcells = new Y.NodeList(), 685 nextcells = new Y.NodeList(); 686 687 rows.each(function(row) { 688 var cells = row.all('td, th'), 689 cell = cells.item(columnindex), 690 cellprev = cells.item(columnindex - 1), 691 cellnext = cells.item(columnindex + 1); 692 currentcells.push(cell); 693 if (cellprev) { 694 prevcells.push(cellprev); 695 } 696 if (cellnext) { 697 nextcells.push(cellnext); 698 } 699 }); 700 701 return { 702 current: currentcells, 703 prev: prevcells, 704 next: nextcells 705 }; 706 }, 707 708 /** 709 * Hide the entries in the context menu that don't make sense with the 710 * current selection. 711 * 712 * @method _hideInvalidEntries 713 * @param {Y.Node} node - The node containing the menu. 714 * @private 715 */ 716 _hideInvalidEntries: function(node) { 717 // Moving rows. 718 var table = this._lastTarget.ancestor('table'), 719 row = this._lastTarget.ancestor('tr'), 720 rows = table.all('tr'), 721 rowindex = rows.indexOf(row), 722 prevrow = rows.item(rowindex - 1), 723 prevrowhascells = prevrow ? prevrow.one('td') : null; 724 725 if (!row || !prevrowhascells) { 726 node.one('[data-change="moverowup"]').hide(); 727 } else { 728 node.one('[data-change="moverowup"]').show(); 729 } 730 731 var nextrow = rows.item(rowindex + 1), 732 rowhascell = row ? row.one('td') : false; 733 734 if (!row || !nextrow || !rowhascell) { 735 node.one('[data-change="moverowdown"]').hide(); 736 } else { 737 node.one('[data-change="moverowdown"]').show(); 738 } 739 740 // Moving columns. 741 var cells = this._findColumnCells(); 742 if (cells.prev.filter('td').size() > 0) { 743 node.one('[data-change="movecolumnleft"]').show(); 744 } else { 745 node.one('[data-change="movecolumnleft"]').hide(); 746 } 747 748 var colhascell = cells.current.filter('td').size() > 0; 749 if ((cells.next.size() > 0) && colhascell) { 750 node.one('[data-change="movecolumnright"]').show(); 751 } else { 752 node.one('[data-change="movecolumnright"]').hide(); 753 } 754 755 // Delete col 756 if (cells.current.filter('td').size() > 0) { 757 node.one('[data-change="deletecolumn"]').show(); 758 } else { 759 node.one('[data-change="deletecolumn"]').hide(); 760 } 761 // Delete row 762 if (!row || !row.one('td')) { 763 node.one('[data-change="deleterow"]').hide(); 764 } else { 765 node.one('[data-change="deleterow"]').show(); 766 } 767 }, 768 769 /** 770 * Display the table menu. 771 * 772 * @method _showTableMenu 773 * @param {EventFacade} e 774 * @private 775 */ 776 _showTableMenu: function(e) { 777 e.preventDefault(); 778 779 var boundingBox; 780 781 if (!this._contextMenu) { 782 this._menuOptions = [ 783 { 784 text: M.util.get_string("addcolumnafter", COMPONENT), 785 data: { 786 change: "addcolumnafter" 787 } 788 }, { 789 text: M.util.get_string("addrowafter", COMPONENT), 790 data: { 791 change: "addrowafter" 792 } 793 }, { 794 text: M.util.get_string("moverowup", COMPONENT), 795 data: { 796 change: "moverowup" 797 } 798 }, { 799 text: M.util.get_string("moverowdown", COMPONENT), 800 data: { 801 change: "moverowdown" 802 } 803 }, { 804 text: M.util.get_string("movecolumnleft", COMPONENT), 805 data: { 806 change: "movecolumnleft" 807 } 808 }, { 809 text: M.util.get_string("movecolumnright", COMPONENT), 810 data: { 811 change: "movecolumnright" 812 } 813 }, { 814 text: M.util.get_string("deleterow", COMPONENT), 815 data: { 816 change: "deleterow" 817 } 818 }, { 819 text: M.util.get_string("deletecolumn", COMPONENT), 820 data: { 821 change: "deletecolumn" 822 } 823 }, { 824 text: M.util.get_string("edittable", COMPONENT), 825 data: { 826 change: "edittable" 827 } 828 } 829 ]; 830 831 this._contextMenu = new Y.M.editor_atto.Menu({ 832 items: this._menuOptions 833 }); 834 835 // Add event handlers for table control menus. 836 boundingBox = this._contextMenu.get('boundingBox'); 837 boundingBox.delegate('click', this._handleTableChange, 'a', this); 838 } 839 840 boundingBox = this._contextMenu.get('boundingBox'); 841 842 // We store the cell of the last click (the control node is transient). 843 this._lastTarget = e.tableCell.ancestor('.editor_atto_content td, .editor_atto_content th', true); 844 845 this._hideInvalidEntries(boundingBox); 846 847 // Clear the focusAfterHide for any other menus which may be open. 848 Y.Array.each(this.get('host').openMenus, function(menu) { 849 menu.set('focusAfterHide', null); 850 }); 851 852 // Ensure that we focus on the button in the toolbar when we tab back to the menu. 853 var creatorButton = this.buttons[this.name]; 854 this.get('host')._setTabFocus(creatorButton); 855 856 // Show the context menu, and align to the current position. 857 this._contextMenu.show(); 858 this._contextMenu.align(this.buttons.table, [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.BL]); 859 this._contextMenu.set('focusAfterHide', creatorButton); 860 861 // If there are any anchors in the bounding box, focus on the first. 862 if (boundingBox.one('a')) { 863 boundingBox.one('a').focus(); 864 } 865 866 // Add this menu to the list of open menus. 867 this.get('host').openMenus = [this._contextMenu]; 868 }, 869 870 /** 871 * Handle a selection from the table control menu. 872 * 873 * @method _handleTableChange 874 * @param {EventFacade} e 875 * @private 876 */ 877 _handleTableChange: function(e) { 878 e.preventDefault(); 879 880 this._contextMenu.set('focusAfterHide', this.get('host').editor); 881 // Hide the context menu. 882 this._contextMenu.hide(e); 883 884 // Make our changes. 885 switch (e.target.getData('change')) { 886 case 'addcolumnafter': 887 this._addColumnAfter(); 888 break; 889 case 'addrowafter': 890 this._addRowAfter(); 891 break; 892 case 'deleterow': 893 this._deleteRow(); 894 break; 895 case 'deletecolumn': 896 this._deleteColumn(); 897 break; 898 case 'edittable': 899 this._editTable(); 900 break; 901 case 'moverowdown': 902 this._moveRowDown(); 903 break; 904 case 'moverowup': 905 this._moveRowUp(); 906 break; 907 case 'movecolumnleft': 908 this._moveColumnLeft(); 909 break; 910 case 'movecolumnright': 911 this._moveColumnRight(); 912 break; 913 } 914 }, 915 916 /** 917 * Determine the index of a row in a table column. 918 * 919 * @method _getRowIndex 920 * @param {Node} cell 921 * @private 922 */ 923 _getRowIndex: function(cell) { 924 var tablenode = cell.ancestor('table'), 925 rownode = cell.ancestor('tr'); 926 927 if (!tablenode || !rownode) { 928 return; 929 } 930 931 var rows = tablenode.all('tr'); 932 933 return rows.indexOf(rownode); 934 }, 935 936 /** 937 * Determine the index of a column in a table row. 938 * 939 * @method _getColumnIndex 940 * @param {Node} cellnode 941 * @private 942 */ 943 _getColumnIndex: function(cellnode) { 944 var rownode = cellnode.ancestor('tr'); 945 946 if (!rownode) { 947 return; 948 } 949 950 var cells = rownode.all('td, th'); 951 952 return cells.indexOf(cellnode); 953 }, 954 955 /** 956 * Delete the current row. 957 * 958 * @method _deleteRow 959 * @private 960 */ 961 _deleteRow: function() { 962 var row = this._lastTarget.ancestor('tr'); 963 964 if (row && row.one('td')) { 965 // Only delete rows with at least one non-header cell. 966 row.remove(true); 967 } 968 969 // Clean the HTML. 970 this.markUpdated(); 971 }, 972 973 /** 974 * Move row up 975 * 976 * @method _moveRowUp 977 * @private 978 */ 979 _moveRowUp: function() { 980 var row = this._lastTarget.ancestor('tr'), 981 prevrow = row.previous('tr'); 982 if (!row || !prevrow) { 983 return; 984 } 985 986 row.swap(prevrow); 987 // Clean the HTML. 988 this.markUpdated(); 989 }, 990 991 /** 992 * Move column left 993 * 994 * @method _moveColumnLeft 995 * @private 996 */ 997 _moveColumnLeft: function() { 998 var cells = this._findColumnCells(); 999 1000 if (cells.current.size() > 0 && cells.prev.size() > 0 && cells.current.size() === cells.prev.size()) { 1001 var i = 0; 1002 for (i = 0; i < cells.current.size(); i++) { 1003 var cell = cells.current.item(i), 1004 prevcell = cells.prev.item(i); 1005 1006 cell.swap(prevcell); 1007 } 1008 } 1009 // Cleanup. 1010 this.markUpdated(); 1011 }, 1012 1013 /** 1014 * Add a caption to the table if it doesn't have one. 1015 * 1016 * @method _addCaption 1017 * @private 1018 */ 1019 _addCaption: function() { 1020 var table = this._lastTarget.ancestor('table'), 1021 caption = table.one('caption'); 1022 1023 if (!caption) { 1024 table.insert(Y.Node.create('<caption> </caption>'), 1); 1025 } 1026 }, 1027 1028 /** 1029 * Remove a caption from the table if has one. 1030 * 1031 * @method _removeCaption 1032 * @private 1033 */ 1034 _removeCaption: function() { 1035 var table = this._lastTarget.ancestor('table'), 1036 caption = table.one('caption'); 1037 1038 if (caption) { 1039 caption.remove(true); 1040 } 1041 }, 1042 1043 /** 1044 * Move column right. 1045 * 1046 * @method _moveColumnRight 1047 * @private 1048 */ 1049 _moveColumnRight: function() { 1050 var cells = this._findColumnCells(); 1051 1052 // Check we have some tds in this column, and one exists to the right. 1053 if ((cells.next.size() > 0) && 1054 (cells.current.size() === cells.next.size()) && 1055 (cells.current.filter('td').size() > 0)) { 1056 var i = 0; 1057 for (i = 0; i < cells.current.size(); i++) { 1058 var cell = cells.current.item(i), 1059 nextcell = cells.next.item(i); 1060 1061 cell.swap(nextcell); 1062 } 1063 } 1064 // Cleanup. 1065 this.markUpdated(); 1066 }, 1067 1068 /** 1069 * Move row down. 1070 * 1071 * @method _moveRowDown 1072 * @private 1073 */ 1074 _moveRowDown: function() { 1075 var row = this._lastTarget.ancestor('tr'), 1076 nextrow = row.next('tr'); 1077 if (!row || !nextrow || !row.one('td')) { 1078 return; 1079 } 1080 1081 row.swap(nextrow); 1082 // Clean the HTML. 1083 this.markUpdated(); 1084 }, 1085 1086 /** 1087 * Obtain values for the table borders 1088 * 1089 * @method _getBorderConfiguration 1090 * @param {Node} node 1091 * @private 1092 * @return {Array} or {Boolean} Returns the settings, if presents, or else returns false 1093 */ 1094 _getBorderConfiguration: function(node) { 1095 // We need to make a clone of the node in order to avoid grabbing any 1096 // of the computed styles from the DOM. We only want inline styles set by us. 1097 var shadowNode = node.cloneNode(true); 1098 var borderStyle = shadowNode.getStyle('borderStyle'), 1099 borderColor = shadowNode.getStyle('borderColor'), 1100 borderWidth = shadowNode.getStyle('borderWidth'); 1101 1102 if (borderStyle || borderColor || borderWidth) { 1103 var hexColour = Y.Color.toHex(borderColor); 1104 var width = parseInt(borderWidth, 10); 1105 return { 1106 borderStyle: borderStyle, 1107 borderColor: hexColour === "#" ? null : hexColour, 1108 borderWidth: isNaN(width) ? null : width 1109 }; 1110 } 1111 1112 return false; 1113 }, 1114 1115 /** 1116 * Set the appropriate styles on the given table node according to 1117 * the provided configuration. 1118 * 1119 * @method _setAppearance 1120 * @param {Node} The table node to be modified. 1121 * @param {Object} Configuration object (associative array) containing the form nodes for 1122 * border styling. 1123 * @private 1124 */ 1125 _setAppearance: function(tableNode, configuration) { 1126 var borderhex, 1127 borderSizeValue, 1128 borderStyleValue, 1129 backgroundcolourvalue; 1130 1131 if (configuration.borderColour) { 1132 borderhex = configuration.borderColour.get('value'); 1133 } 1134 1135 if (configuration.borderSize) { 1136 borderSizeValue = configuration.borderSize.get('value'); 1137 } 1138 1139 if (configuration.borderStyle) { 1140 borderStyleValue = configuration.borderStyle.get('value'); 1141 } 1142 1143 if (configuration.backgroundColour) { 1144 backgroundcolourvalue = configuration.backgroundColour.get('value'); 1145 } 1146 1147 // Clear the inline border styling 1148 tableNode.removeAttribute('style'); 1149 tableNode.all('td, th').each(function(cell) { 1150 cell.removeAttribute('style'); 1151 }, this); 1152 1153 if (configuration.borders) { 1154 if (configuration.borders.get('value') === 'outer') { 1155 tableNode.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT); 1156 tableNode.setStyle('borderStyle', borderStyleValue); 1157 1158 if (borderhex !== 'none') { 1159 tableNode.setStyle('borderColor', borderhex); 1160 } 1161 } else if (configuration.borders.get('value') === 'all') { 1162 tableNode.all('td, th').each(function(cell) { 1163 cell.setStyle('borderWidth', borderSizeValue + CSS.BORDERSIZEUNIT); 1164 cell.setStyle('borderStyle', borderStyleValue); 1165 1166 if (borderhex !== 'none') { 1167 cell.setStyle('borderColor', borderhex); 1168 } 1169 }, this); 1170 } 1171 } 1172 1173 if (backgroundcolourvalue !== 'none') { 1174 tableNode.setStyle('backgroundColor', backgroundcolourvalue); 1175 } 1176 1177 if (configuration.width && configuration.width.get('value')) { 1178 tableNode.setStyle('width', configuration.width.get('value') + CSS.WIDTHUNIT); 1179 } 1180 }, 1181 1182 /** 1183 * Edit table (show the dialogue). 1184 * 1185 * @method _editTable 1186 * @private 1187 */ 1188 _editTable: function() { 1189 var dialogue = this.getDialogue({ 1190 headerContent: M.util.get_string('edittable', COMPONENT), 1191 focusAfterHide: false, 1192 focusOnShowSelector: SELECTORS.CAPTION, 1193 width: DIALOGUE.WIDTH 1194 }); 1195 1196 // Set the dialogue content, and then show the dialogue. 1197 var node = this._getDialogueContent(true), 1198 captioninput = node.one(SELECTORS.CAPTION), 1199 captionpositioninput = node.one(SELECTORS.CAPTIONPOSITION), 1200 headersinput = node.one(SELECTORS.HEADERS), 1201 borderinput = node.one(SELECTORS.BORDERS), 1202 borderstyle = node.one(SELECTORS.BORDERSTYLE), 1203 bordercolours = node.all(SELECTORS.BORDERCOLOURS), 1204 bordersize = node.one(SELECTORS.BORDERSIZE), 1205 backgroundcolours = node.all(SELECTORS.BACKGROUNDCOLOURS), 1206 width = node.one(SELECTORS.WIDTH), 1207 table = this._lastTarget.ancestor('table'), 1208 captionnode = table.one('caption'), 1209 hexColour, 1210 matchedInput; 1211 1212 if (captionnode) { 1213 captioninput.set('value', captionnode.getHTML()); 1214 } else { 1215 captioninput.set('value', ''); 1216 } 1217 1218 if (width && table.getStyle('width').indexOf('px') === -1) { 1219 width.set('value', parseInt(table.getStyle('width'), 10)); 1220 } 1221 1222 if (captionpositioninput && captionnode && captionnode.getAttribute('style')) { 1223 captionpositioninput.set('value', captionnode.getStyle('caption-side')); 1224 } else { 1225 // Default to none. 1226 captionpositioninput.set('value', ''); 1227 } 1228 1229 if (table.getStyle('backgroundColor') && this.get('allowBackgroundColour')) { 1230 hexColour = Y.Color.toHex(table.getStyle('backgroundColor')); 1231 matchedInput = backgroundcolours.filter('[value="' + hexColour + '"]'); 1232 1233 if (matchedInput) { 1234 matchedInput.set("checked", true); 1235 } 1236 } 1237 1238 if (this.get('allowBorders')) { 1239 var borderValue = 'default', 1240 borderConfiguration = this._getBorderConfiguration(table); 1241 1242 if (borderConfiguration) { 1243 borderValue = 'outer'; 1244 } else { 1245 borderConfiguration = this._getBorderConfiguration(table.one('td')); 1246 if (borderConfiguration) { 1247 borderValue = 'all'; 1248 } 1249 } 1250 1251 if (borderConfiguration) { 1252 var borderStyle = borderConfiguration.borderStyle || DEFAULT.BORDERSTYLE; 1253 var borderSize = borderConfiguration.borderWidth || DEFAULT.BORDERWIDTH; 1254 borderstyle.set('value', borderStyle); 1255 bordersize.set('value', borderSize); 1256 borderinput.set('value', borderValue); 1257 1258 hexColour = borderConfiguration.borderColor; 1259 matchedInput = bordercolours.filter('[value="' + hexColour + '"]'); 1260 1261 if (matchedInput) { 1262 matchedInput.set("checked", true); 1263 } 1264 } 1265 } 1266 1267 var headersvalue = 'columns'; 1268 if (table.one('th[scope="row"]')) { 1269 headersvalue = 'rows'; 1270 if (table.one('th[scope="col"]')) { 1271 headersvalue = 'both'; 1272 } 1273 } 1274 headersinput.set('value', headersvalue); 1275 dialogue.set('bodyContent', node).show(); 1276 this._updateAvailableSettings(); 1277 }, 1278 1279 1280 /** 1281 * Delete the current column. 1282 * 1283 * @method _deleteColumn 1284 * @private 1285 */ 1286 _deleteColumn: function() { 1287 var columnindex = this._getColumnIndex(this._lastTarget), 1288 table = this._lastTarget.ancestor('table'), 1289 rows = table.all('tr'), 1290 columncells = new Y.NodeList(), 1291 hastd = false; 1292 1293 rows.each(function(row) { 1294 var cells = row.all('td, th'); 1295 var cell = cells.item(columnindex); 1296 if (cell.get('tagName') === 'TD') { 1297 hastd = true; 1298 } 1299 columncells.push(cell); 1300 }); 1301 1302 // Do not delete all the headers. 1303 if (hastd) { 1304 columncells.remove(true); 1305 } 1306 1307 // Clean the HTML. 1308 this.markUpdated(); 1309 }, 1310 1311 /** 1312 * Add a row after the current row. 1313 * 1314 * @method _addRowAfter 1315 * @private 1316 */ 1317 _addRowAfter: function() { 1318 var target = this._lastTarget.ancestor('tr'), 1319 tablebody = this._lastTarget.ancestor('table').one('tbody'); 1320 if (!tablebody) { 1321 // Not all tables have tbody. 1322 tablebody = this._lastTarget.ancestor('table'); 1323 } 1324 1325 var firstrow = tablebody.one('tr'); 1326 if (!firstrow) { 1327 firstrow = this._lastTarget.ancestor('table').one('tr'); 1328 } 1329 if (!firstrow) { 1330 // Table has no rows. Boo. 1331 return; 1332 } 1333 var newrow = firstrow.cloneNode(true); 1334 newrow.all('th, td').each(function(tablecell) { 1335 if (tablecell.get('tagName') === 'TH') { 1336 if (tablecell.getAttribute('scope') !== 'row') { 1337 var newcell = Y.Node.create('<td></td>'); 1338 tablecell.replace(newcell); 1339 tablecell = newcell; 1340 } 1341 } 1342 tablecell.setHTML(' '); 1343 }); 1344 1345 if (target.ancestor('thead')) { 1346 target = firstrow; 1347 tablebody.insert(newrow, target); 1348 } else { 1349 target.insert(newrow, 'after'); 1350 } 1351 1352 // Clean the HTML. 1353 this.markUpdated(); 1354 }, 1355 1356 /** 1357 * Add a column after the current column. 1358 * 1359 * @method _addColumnAfter 1360 * @private 1361 */ 1362 _addColumnAfter: function() { 1363 var cells = this._findColumnCells(), 1364 before = true, 1365 clonecells = cells.next; 1366 if (cells.next.size() <= 0) { 1367 before = false; 1368 clonecells = cells.current; 1369 } 1370 1371 Y.each(clonecells, function(cell) { 1372 var newcell = cell.cloneNode(); 1373 // Clear the content of the cell. 1374 newcell.setHTML(' '); 1375 1376 if (before) { 1377 cell.get('parentNode').insert(newcell, cell); 1378 } else { 1379 cell.get('parentNode').insert(newcell, cell); 1380 cell.swap(newcell); 1381 } 1382 }, this); 1383 1384 // Clean the HTML. 1385 this.markUpdated(); 1386 } 1387 1388 }, { 1389 ATTRS: { 1390 /** 1391 * Whether or not to allow borders 1392 * 1393 * @attribute allowBorder 1394 * @type Boolean 1395 */ 1396 allowBorders: { 1397 value: true 1398 }, 1399 1400 /** 1401 * What border styles to allow 1402 * 1403 * @attribute borderStyles 1404 * @type Array 1405 */ 1406 borderStyles: { 1407 value: [ 1408 'none', 1409 'solid', 1410 'dashed', 1411 'dotted' 1412 ] 1413 }, 1414 1415 /** 1416 * Whether or not to allow colourizing the background 1417 * 1418 * @attribute allowBackgroundColour 1419 * @type Boolean 1420 */ 1421 allowBackgroundColour: { 1422 value: true 1423 }, 1424 1425 /** 1426 * Whether or not to allow setting the table width 1427 * 1428 * @attribute allowWidth 1429 * @type Boolean 1430 */ 1431 allowWidth: { 1432 value: true 1433 }, 1434 1435 /** 1436 * Whether we allow styling 1437 * @attribute allowStyling 1438 * @type Boolean 1439 */ 1440 allowStyling: { 1441 readOnly: true, 1442 getter: function() { 1443 return this.get('allowBorders') || this.get('allowBackgroundColour') || this.get('allowWidth'); 1444 } 1445 }, 1446 1447 /** 1448 * Available colors 1449 * @attribute availableColors 1450 * @type Array 1451 */ 1452 availableColors: { 1453 value: [ 1454 '#FFFFFF', 1455 '#EF4540', 1456 '#FFCF35', 1457 '#98CA3E', 1458 '#7D9FD3', 1459 '#333333' 1460 ], 1461 readOnly: true 1462 } 1463 } 1464 }); 1465 1466 1467 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin", "moodle-editor_atto-menu", "event", "event-valuechange"]});
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |