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