1/* global initToolbar */ 2 3window.edittable = window.edittable || {}; 4window.edittable_plugins = window.edittable_plugins || {}; 5 6(function (edittable, edittable_plugins) { 7 'use strict'; 8 9 /** 10 * 11 * 12 * @param {Array} movingRowIndexes the indices of the rows to be moved 13 * @param {int} target the row where the rows will be inserted 14 * @param {Array} dmarray the data or meta array 15 * 16 * @return {Array} the new data or meta array 17 */ 18 edittable.moveRow = function moveRow(movingRowIndexes, target, dmarray) { 19 var startIndex = movingRowIndexes[0]; 20 var endIndex = movingRowIndexes[movingRowIndexes.length - 1]; 21 var moveForward = target < startIndex; 22 23 var first = dmarray.slice(0, Math.min(startIndex, target)); 24 var moving = dmarray.slice(startIndex, endIndex + 1); 25 var between; 26 if (moveForward) { 27 between = dmarray.slice(target, startIndex); 28 } else { 29 between = dmarray.slice(endIndex + 1, target); 30 } 31 var last = dmarray.slice(Math.max(endIndex + 1, target)); 32 if (moveForward) { 33 return [].concat(first, moving, between, last); 34 } 35 return [].concat(first, between, moving, last); 36 }; 37 38 edittable.addRowToMeta = function (index, amount, metaArray) { 39 var i; 40 var cols = 1; // minimal number of cells 41 if (metaArray[0]) { 42 cols = metaArray[0].length; 43 } 44 45 // insert into meta array 46 for (i = 0; i < amount; i += 1) { 47 var newrow = Array.apply(null, new Array(cols)).map(function initializeRowMeta() { 48 return { rowspan: 1, colspan: 1 }; 49 }); 50 metaArray.splice(index, 0, newrow); 51 } 52 53 return metaArray; 54 }; 55 56 57 /** 58 * 59 * @param {Array} movingColIndexes the indices of the columns to be moved 60 * @param {int} target the column where the columns will be inserted 61 * @param {Array} dmarray the data or meta array 62 * 63 * @return {Array} the new data or meta array 64 */ 65 edittable.moveCol = function moveCol(movingColIndexes, target, dmarray) { 66 return dmarray.map(function (row) { 67 return edittable.moveRow(movingColIndexes, target, row); 68 }); 69 }; 70 71 /** 72 * 73 * @param {Array} meta the meta array 74 * @returns {Array} an array of the cells with a rowspan or colspan larger than 1 75 */ 76 edittable.getMerges = function (meta) { 77 var merges = []; 78 for (var row = 0; row < meta.length; row += 1) { 79 for (var col = 0; col < meta[0].length; col += 1) { 80 if (meta[row][col].hasOwnProperty('rowspan') && meta[row][col].rowspan > 1 || 81 meta[row][col].hasOwnProperty('colspan') && meta[row][col].colspan > 1) { 82 var merge = {}; 83 merge.row = row; 84 merge.col = col; 85 merge.rowspan = meta[row][col].rowspan; 86 merge.colspan = meta[row][col].colspan; 87 merges.push(merge); 88 } 89 } 90 } 91 return merges; 92 }; 93 94 95 /** 96 * 97 * @param {Array} merges an array of the cells that are part of a merge 98 * @param {int} target the target column or row 99 * @param {string} direction whether we're trying to move a col or row 100 * 101 * @return {bool} wether the target col/row is part of a merge 102 */ 103 edittable.isTargetInMerge = function isTargetInMerge(merges, target, direction) { 104 return merges.some(function (merge) { 105 return (merge[direction] < target && target < merge[direction] + merge[direction + 'span']); 106 }); 107 }; 108 109 edittable.loadEditor = function () { 110 var $container = jQuery('#edittable__editor'); 111 if (!$container.length) { 112 return; 113 } 114 115 var $form = jQuery('#dw__editform'); 116 var $datafield = $form.find('input[name=edittable_data]'); 117 var $metafield = $form.find('input[name=edittable_meta]'); 118 119 var data = JSON.parse($datafield.val()); 120 var meta = JSON.parse($metafield.val()); 121 122 /** 123 * Get the current meta array 124 * 125 * @return {array} the current meta array as array of rows with arrays of columns with objects 126 */ 127 function getMeta() {return meta;} 128 129 /** 130 * Get the current data array 131 * 132 * @return {array} the current data array as array of rows with arrays of columns with strings 133 */ 134 function getData() {return data;} 135 136 var merges = edittable.getMerges(meta); 137 if (merges === []) { 138 merges = true; 139 } 140 var lastselect = { row: 0, col: 0 }; 141 142 var handsontable_config = { 143 data: data, 144 startRows: 5, 145 startCols: 5, 146 colHeaders: true, 147 rowHeaders: true, 148 manualColumnResize: true, 149 outsideClickDeselects: false, 150 contextMenu: edittable.getEditTableContextMenu(getData, getMeta), 151 manualColumnMove: true, 152 manualRowMove: true, 153 mergeCells: merges, 154 155 156 /** 157 * Attach pointers to our raw data structures in the instance 158 * 159 * @return {void} 160 */ 161 afterLoadData: function () { 162 var i; 163 this.raw = { 164 data: data, 165 meta: meta, 166 colinfo: [], 167 rowinfo: [] 168 }; 169 for (i = 0; i < data.length; i += 1) { 170 this.raw.rowinfo[i] = {}; 171 } 172 for (i = 0; i < data[0].length; i += 1) { 173 this.raw.colinfo[i] = {}; 174 } 175 }, 176 177 /** 178 * initialize cell properties 179 * 180 * properties are stored in extra array 181 * 182 * @param {int} row the row of the desired column 183 * @param {int} col the col of the desired column 184 * @returns {Array} the respective cell from the meta array 185 */ 186 cells: function (row, col) { 187 return meta[row][col]; 188 }, 189 190 /** 191 * Custom cell renderer 192 * 193 * It handles all our custom meta attributes like alignments and rowspans 194 * 195 * @param {object} instance the handsontable instance 196 * @param {HTMLTableCellElement} td the dom node of the cell 197 * @param {int} row the row of the cell to be rendered 198 * @param {int} col the column of the cell to be rendered 199 * 200 * @return {void} 201 */ 202 renderer: function (instance, td, row, col) { 203 // for some reason, neither cellProperties nor instance.getCellMeta() give the right data 204 var cellMeta = meta[row][col]; 205 var $td = jQuery(td); 206 207 if (cellMeta.colspan) { 208 $td.attr('colspan', cellMeta.colspan); 209 } else { 210 $td.removeAttr('colspan'); 211 } 212 213 if (cellMeta.rowspan) { 214 $td.attr('rowspan', cellMeta.rowspan); 215 } else { 216 $td.removeAttr('rowspan'); 217 } 218 219 if (cellMeta.hide) { 220 $td.hide(); 221 } else { 222 $td.show(); 223 } 224 225 if (cellMeta.align === 'right') { 226 $td.addClass('right'); 227 $td.removeClass('center'); 228 } else if (cellMeta.align === 'center') { 229 $td.addClass('center'); 230 $td.removeClass('right'); 231 } else { 232 $td.removeClass('center'); 233 $td.removeClass('right'); 234 } 235 236 if (cellMeta.tag === 'th') { 237 $td.addClass('header'); 238 } else { 239 $td.removeClass('header'); 240 } 241 242 /* globals Handsontable */ 243 Handsontable.renderers.TextRenderer.apply(this, arguments); 244 }, 245 246 /** 247 * Initialization after the Editor loaded 248 * 249 * @return {void} 250 */ 251 afterInit: function () { 252 // select first cell 253 this.selectCell(0, 0); 254 255 // we need an ID on the input field 256 jQuery('textarea.handsontableInput').attr('id', 'handsontable__input'); 257 258 // we're ready to intialize the toolbar now 259 initToolbar('tool__bar', 'handsontable__input', window.toolbar, false); 260 261 // we wrap DokuWiki's pasteText() here to get notified when the toolbar inserted something into our editor 262 var original_pasteText = window.pasteText; 263 window.pasteText = function (selection, text, opts) { 264 original_pasteText(selection, text, opts); // do what pasteText does 265 // trigger resize 266 jQuery('#handsontable__input').data('AutoResizer').check(); 267 }; 268 window.pasteText = original_pasteText; 269 270 /* 271 This is a workaround to rerender the table. It serves two functions: 272 1: On wide tables with linebreaks in columns with no pre-defined table widths (via the tablelayout plugin) 273 reset the width of the table columns to what is needed by its no narrower content 274 2: On table with some rows fixed at the top, ensure that the content of these rows stays at the top as well, 275 not only the lefthand rownumbers 276 Attaching this to the event 'afterRenderer' did not have the desired results, as it seemed not to work for 277 usecase 1 at all and for usecase 2 only with a delay. 278 */ 279 var _this = this; 280 this.addHookOnce('afterOnCellMouseOver', function () { 281 _this.updateSettings({}); 282 }); 283 }, 284 285 /** 286 * This recalculates the col and row spans and makes sure all correct cells are hidden 287 * 288 * @return {void} 289 */ 290 beforeRender: function () { 291 var row, r, c, col, i; 292 293 // reset row and column infos - we store spanning info there 294 this.raw.rowinfo = []; 295 this.raw.colinfo = []; 296 for (i = 0; i < data.length; i += 1) { 297 this.raw.rowinfo[i] = {}; 298 } 299 for (i = 0; i < data[0].length; i += 1) { 300 this.raw.colinfo[i] = {}; 301 } 302 303 // unhide all cells 304 for (row = 0; row < data.length; row += 1) { 305 for (col = 0; col < data[0].length; col += 1) { 306 if (meta[row][col].hide) { 307 meta[row][col].hide = false; 308 data[row][col] = ''; 309 } 310 // unset all row/colspans 311 meta[row][col].colspan = 1; 312 meta[row][col].rowspan = 1; 313 314 // make sure no data cell is undefined/null 315 if (!data[row][col]) { 316 data[row][col] = ''; 317 } 318 } 319 } 320 321 for (var merge = 0; merge < this.mergeCells.mergedCellInfoCollection.length; merge += 1) { 322 row = this.mergeCells.mergedCellInfoCollection[merge].row; 323 col = this.mergeCells.mergedCellInfoCollection[merge].col; 324 var colspan = this.mergeCells.mergedCellInfoCollection[merge].colspan; 325 var rowspan = this.mergeCells.mergedCellInfoCollection[merge].rowspan; 326 meta[row][col].colspan = colspan; 327 meta[row][col].rowspan = rowspan; 328 329 // hide the cells hidden by the row/colspan 330 331 for (r = row; r < row + rowspan; r += 1) { 332 for (c = col; c < col + colspan; c += 1) { 333 if (r === row && c === col) { 334 continue; 335 } 336 meta[r][c].hide = true; 337 meta[r][c].rowspan = 1; 338 meta[r][c].colspan = 1; 339 if (data[r][c] && data[r][c] !== ':::') { 340 data[row][col] += ' ' + data[r][c]; 341 } 342 if (r === row) { 343 data[r][c] = ''; 344 } else { 345 data[r][c] = ':::'; 346 } 347 } 348 } 349 } 350 351 // Clone data object 352 // Since we can't use real line breaks (\n) inside table cells, this object is used to store all cell values with DokuWiki's line breaks (\\) instead of actual ones. 353 var dataLBFixed = jQuery.extend(true, {}, data); 354 355 // In dataLBFixed, replace all actual line breaks with DokuWiki line breaks 356 // In data, replace all DokuWiki line breaks with actual ones so the editor displays line breaks properly 357 for (row = 0; row < data.length; row += 1) { 358 for (col = 0; col < data[0].length; col += 1) { 359 dataLBFixed[row][col] = data[row][col].replace(/(\r\n|\n|\r)/g, '\\\\ '); 360 data[row][col] = data[row][col].replace(/\\\\\s/g, '\n'); 361 } 362 } 363 364 // Store dataFixed and meta back in the form 365 $datafield.val(JSON.stringify(dataLBFixed)); 366 $metafield.val(JSON.stringify(meta)); 367 }, 368 369 /** 370 * Disable key handling while the link wizard or any other dialog is visible 371 * 372 * @param {event} e the keydown event object 373 * 374 * @return {void} 375 */ 376 beforeKeyDown: function (e) { 377 if (jQuery('.ui-dialog:visible').length) { 378 e.stopImmediatePropagation(); 379 e.preventDefault(); 380 } 381 }, 382 383 beforeColumnMove: function (movingCols, target) { 384 var disallowMove = edittable.isTargetInMerge(this.mergeCells.mergedCellInfoCollection, target, 'col'); 385 if (disallowMove) { 386 return false; 387 } 388 meta = edittable.moveCol(movingCols, target, meta); 389 data = edittable.moveCol(movingCols, target, data); 390 this.updateSettings({ mergeCells: edittable.getMerges(meta), data: data }); 391 return false; 392 }, 393 394 beforeRowMove: function (movingRows, target) { 395 var disallowMove = edittable.isTargetInMerge(this.mergeCells.mergedCellInfoCollection, target, 'row'); 396 if (disallowMove) { 397 return false; 398 } 399 meta = edittable.moveRow(movingRows, target, meta); 400 data = edittable.moveRow(movingRows, target, data); 401 this.updateSettings({ mergeCells: edittable.getMerges(meta), data: data }); 402 return false; 403 }, 404 405 /** 406 * Update meta data array when rows are added 407 * 408 * @param {int} index the index where the new rows are created 409 * @param {int} amount the number of new rows that are created 410 * 411 * @return {void} 412 */ 413 afterCreateRow: function (index, amount) { 414 meta = edittable.addRowToMeta(index, amount, meta); 415 }, 416 417 /** 418 * Set id for toolbar to current handsontable input textarea 419 * 420 * For some reason (bug?), handsontable creates a new div.handsontableInputHolder with a new textarea and 421 * ignores the old one. For the toolbar to keep working we need make sure the currently used textarea has 422 * also the id `handsontable__input`. 423 * 424 * @return {void} 425 */ 426 afterBeginEditing: function () { 427 if (jQuery('textarea.handsontableInput').length > 1) { 428 jQuery('textarea.handsontableInput:not(:last)').remove(); 429 jQuery('textarea.handsontableInput').attr('id', 'handsontable__input'); 430 } 431 }, 432 433 /** 434 * Update meta data array when rows are removed 435 * 436 * @param {int} index the index where the rows are removed 437 * @param {int} amount the number of rows that are removed 438 * 439 * @return {void} 440 */ 441 afterRemoveRow: function (index, amount) { 442 meta.splice(index, amount); 443 }, 444 445 /** 446 * Update meta data array when columns are added 447 * 448 * @param {int} index the index where the new columns are created 449 * @param {int} amount the number of new columns that are created 450 * 451 * @return {void} 452 */ 453 afterCreateCol: function (index, amount) { 454 for (var row = 0; row < data.length; row += 1) { 455 for (var i = 0; i < amount; i += 1) { 456 meta[row].splice(index, 0, { rowspan: 1, colspan: 1 }); 457 } 458 } 459 }, 460 461 /** 462 * Update meta data array when columns are removed 463 * 464 * @param {int} index the index where the columns are removed 465 * @param {int} amount the number of columns that are removed 466 * 467 * @return {void} 468 */ 469 afterRemoveCol: function (index, amount) { 470 for (var row = 0; row < data.length; row += 1) { 471 meta[row].splice(index, amount); 472 } 473 }, 474 475 /** 476 * Skip hidden cells for selection 477 * 478 * @param {int} r the row of the selected cell 479 * @param {int} c the column of the selected cell 480 * 481 * @return {void} 482 */ 483 afterSelection: function (r, c) { 484 if (meta[r][c].hide) { 485 // user navigated into a hidden cell! we need to find the next selectable cell 486 var x = 0; 487 488 var v = r - lastselect.row; 489 if (v > 0) { 490 v = 1; 491 } 492 if (v < 0) { 493 v = -1; 494 } 495 496 var h = c - lastselect.col; 497 if (h > 0) { 498 h = 1; 499 } 500 if (h < 0) { 501 h = -1; 502 } 503 504 if (v !== 0) { 505 x = r; 506 // user navigated vertically 507 do { 508 x += v; 509 if (!meta[x][c].hide) { 510 // cell is selectable, do it 511 this.selectCell(x, c); 512 return; 513 } 514 515 } while (x > 0 && x < data.length); 516 // found no suitable cell 517 this.deselectCell(); 518 } else if (h !== 0) { 519 x = c; 520 // user navigated horizontally 521 do { 522 x += h; 523 if (!meta[r][x].hide) { 524 // cell is selectable, do it 525 this.selectCell(r, x); 526 return; 527 } 528 529 } while (x > 0 && x < data[0].length); 530 // found no suitable cell 531 this.deselectCell(); 532 } 533 } else { 534 // remember this selection 535 lastselect.row = r; 536 lastselect.col = c; 537 } 538 }, 539 540 /** 541 * 542 * @param {Array} pasteData An array of arrays which contains data to paste. 543 * @param {Array} coords An array of objects with ranges of the visual indexes (startRow, startCol, endRow, endCol) 544 * that correspond to the previously selected area. 545 * @return {true} always allowing the pasting 546 */ 547 beforePaste: function (pasteData, coords) { 548 var startRow = coords[0].startRow; 549 var startCol = coords[0].startCol; 550 var totalRows = this.countRows(); 551 var totalCols = this.countCols(); 552 553 var missingRows = (startRow + pasteData.length) - totalRows; 554 var missingCols = (startCol + pasteData[0].length) - totalCols; 555 if (missingRows > 0) { 556 this.alter('insert_row', undefined, missingRows, 'paste'); 557 } 558 if (missingCols > 0) { 559 this.alter('insert_col', undefined, missingCols, 'paste'); 560 } 561 return true; 562 } 563 }; 564 565 if (window.JSINFO.plugins.edittable['default columnwidth']) { 566 handsontable_config.colWidths = window.JSINFO.plugins.edittable['default columnwidth']; 567 } 568 569 570 for (var plugin in edittable_plugins) { 571 if (edittable_plugins.hasOwnProperty(plugin)) { 572 if (typeof edittable_plugins[plugin].modifyHandsontableConfig === 'function') { 573 edittable_plugins[plugin].modifyHandsontableConfig(handsontable_config, $form); 574 } 575 } 576 } 577 578 579 $container.handsontable(handsontable_config); 580 581 }; 582 583 jQuery(document).ready(edittable.loadEditor); 584 585}(window.edittable, window.edittable_plugins)); 586