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