knockout.editableCell.js |             |
|---|---|
                                
 |                               
 |             
                                Documentation |                               ko.bindingHandlers.editableCell = {
 |             
|                                  Binding initialization makes sure the common selection is initialized, before initializing the cell in question and registering it with the selection. Every instance of the   |                                   init: function (element, valueAccessor, allBindingsAccessor) {
        var table = $(element).parents('table')[0],
            selection = table._cellSelection;
        if (selection === undefined) {
            table._cellSelection = selection = new ko.bindingHandlers.editableCell.Selection(table);
        }
        selection.registerCell(element);
        element._cellValue = valueAccessor;
        element._cellText = function () { return allBindingsAccessor().cellText || element._cellValue(); };
        element._cellReadOnly = function () { return ko.utils.unwrapObservable(allBindingsAccessor().cellReadOnly); };
        element._cellValueUpdater = function (newValue) {
            ko.bindingHandlers.editableCell.updateBindingValue('editableCell', element._cellValue, allBindingsAccessor, newValue);
            if (!ko.isObservable(element._cellValue())) {
                element.textContent = ko.utils.unwrapObservable(element._cellText());
            }
        };
    },
 |             
|                                  Binding update simply updates the text content of the table cell.  |                                   update: function (element, valueAccessor, allBindingsAccessor) {
        element.textContent = ko.utils.unwrapObservable(element._cellText());
    },
 |             
|                                  
  |                                   updateBindingValue: function (bindingName, valueAccessor, allBindingsAccessor, newValue) {
        if (ko.isWriteableObservable(valueAccessor())) {
            valueAccessor()(newValue);
            return;
        }
        var propertyWriters = allBindingsAccessor()._ko_property_writers;
        if (propertyWriters && propertyWriters[bindingName]) {
            propertyWriters[bindingName](newValue);
        }
        if (!ko.isObservable(valueAccessor())) {
            allBindingsAccessor()[bindingName] = newValue;
        }
    },
 |             
                                 
 |                                   Selection: function (table) {
        var self = this;
        self.view = new ko.bindingHandlers.editableCell.SelectionView(table, self);
        self.range = new ko.bindingHandlers.editableCell.SelectionRange(cellIsSelectable);
        self.range.selection.subscribe(function (newSelection) {
            if (newSelection.length === 0) {
                self.view.hide();
                return;
            }
            self.view.update(newSelection[0], newSelection[newSelection.length - 1]);
        });
        self.focus = self.view.focus;
        self.registerCell = function (cell) {
            ko.utils.registerEventHandler(cell, "mousedown", function (event) {
                if (self.isEditingCell(cell)) {
                    return;
                }
                self.onCellMouseDown(cell, event.shiftKey);
                event.preventDefault();
            });
            ko.utils.registerEventHandler(cell, "mouseup", function (event) {
                if (self.isEditingCell(cell)) {
                    event.stopPropagation();
                    return;
                }
            });
            ko.utils.registerEventHandler(cell, "mouseover", self.onCellMouseOver);
            ko.utils.registerEventHandler(cell, "blur", self.onCellBlur);
            ko.utils.registerEventHandler(cell, "keydown", self.onCellKeyDown);
        };
        self.updateCellValue = function (cell, newValue) {
            var value;
            if (!self.cellIsEditable(cell)) {
                return;
            }
            if (newValue === undefined) {
                value = cell.textContent;
                self.restoreCellText(cell);
            }
            else {
                value = newValue;
            }
            cell._cellValueUpdater(value);
            return value;
        };
        self.restoreCellText = function (cell) {
            cell.textContent = cell._oldTextContent;
        };
        self.startEditing = function () {
            self.startEditingCell(self.range.start);
        };
        self.startEditingCell = function (cell) {
            if (!self.cellIsEditable(cell)) {
                return;
            }
            if (self.range.start !== cell) {
                self.range.setStart(cell);
            }
            cell._oldTextContent = cell.textContent;
            cell.textContent = ko.utils.unwrapObservable(cell._cellValue());
            cell.contentEditable = true;
            cell.focus();
            document.execCommand('selectAll', false, null);
            self.view.element.style.pointerEvents = 'none';
        };
        self.isEditingCell = function (cell) {
            return cell.contentEditable === 'true';
        };
        self.cancelEditingCell = function (cell) {
            cell.contentEditable = false;
            self.restoreCellText(cell);
            self.view.element.style.pointerEvents = 'inherit';
        };
        self.endEditingCell = function (cell) {
            cell.contentEditable = false;
            self.view.element.style.pointerEvents = 'inherit';
            return self.updateCellValue(cell);
        };
        function cellIsSelectable (cell) {
            return cell._cellValue !== undefined;
        };
        self.cellIsEditable = function (cell) {
            return cell._cellReadOnly() !== true;
        }
        self.onCellMouseDown = function (cell, shiftKey) {
            if (shiftKey) {
                self.range.setEnd(cell);
            }
            else {
                self.range.setStart(cell);
            }
            self.view.beginDrag();
            event.preventDefault();
        };
        self.onCellMouseOver = function (event) {
            if (self.view.isDragging && event.target !== self.range.end) {
                self.range.setEnd(event.target);
            }
        };
        self.onCellKeyDown = function (event) {
            if (event.keyCode === 13) { // Return
                var value = self.endEditingCell(event.target);
                if (event.ctrlKey) {
                    ko.utils.arrayForEach(self.range.getCells(), function (cell) {
                        self.updateCellValue(cell, value);
                    });
                }
                self.onReturn(event, event.ctrlKey);
                self.focus();
                event.preventDefault();
            }
            else if ([37, 38, 39, 40].indexOf(event.keyCode) !== -1) { // Arrows
                self.focus();
                self.onArrows(event);
                event.preventDefault();
            }
            else if (event.keyCode === 27) { // Escape
                self.cancelEditingCell(event.target);
                self.focus();
            }
        };
        self.onCellBlur = function (event) {
            if (event.target.contentEditable !== 'true') {
                return;
            }
            self.endEditingCell(event.target);
        };
        self.onReturn = function (event, preventMove) {
            if (preventMove !== true) {
                self.range.moveInDirection('Down');
            }
            event.preventDefault();
        };
        self.onArrows = function (event) {
            var preventDefault;
            if (event.shiftKey && !event.ctrlKey) {
                preventDefault = self.range.extendInDirection(self.keyCodeIdentifier[event.keyCode]);
            }
            else if (!event.ctrlKey) {
                preventDefault = self.range.moveInDirection(self.keyCodeIdentifier[event.keyCode]);
            }
            if (preventDefault) {
                event.preventDefault();
            }
        };
        self.onCopy = function () {
            var cells = self.range.getCells(),
                cols = cells[cells.length - 1].cellIndex - cells[0].cellIndex + 1,
                rows = cells.length / cols,
                lines = [],
                i = 0;
            ko.utils.arrayForEach(cells, function (cell) {
                var lineIndex = i % rows,
                    rowIndex = Math.floor(i / rows);
                lines[lineIndex] = lines[lineIndex] || [];
                lines[lineIndex][rowIndex] = ko.utils.unwrapObservable(cell._cellValue());
                i++;
            });
            return ko.utils.arrayMap(lines, function (line) {
                return line.join('\t');
            }).join('\r\n');
        };
        self.onPaste = function (text) {
            var selStart = self.range.getCells()[0],
                cells,
                values = ko.utils.arrayMap(text.trim().split(/\r?\n/), function (line) { return line.split('\t'); }),
                row = values.length,
                col = values[0].length,
                rows = 1,
                cols = 1,
                i = 0;
            self.range.setStart(selStart);
            while (row-- > 1 && self.range.extendInDirection('Down')) { rows++ };
            while (col-- > 1 && self.range.extendInDirection('Right')) { cols++ };
            cells = self.range.getCells();
            for (col = 0; col < cols; col++) {
                for (row = 0; row < rows; row++) {
                    self.updateCellValue(cells[i], values[row][col]);
                    i++;
                }
            }
        };
        self.keyCodeIdentifier = {
            37: 'Left',
            38: 'Up',
            39: 'Right',
            40: 'Down'
        };
    },
 |             
                                 
 |                                   SelectionView: function (table, selection) {
        var self = this;
        self.element = document.createElement('div');
        self.element.style.position = 'absolute';
        self.element.style.display = 'none';
        self.element.tabIndex = -1;
        self.copyPasteElement = document.createElement('textarea');
        self.copyPasteElement.style.position = 'absolute';
        self.copyPasteElement.style.opacity = '0.0';
        self.copyPasteElement.style.display = 'none';
        table.appendChild(self.element);
        table.appendChild(self.copyPasteElement);
        self.show = function () {
            self.element.style.display = 'block';
            self.element.focus();
        };
        self.hide = function () {
            self.element.style.display = 'none';
        };
        self.focus = function () {
            self.element.focus();
        };
        self.update = function (start, end) {
            var top = Math.min(start.offsetTop, end.offsetTop),
                left = Math.min(start.offsetLeft, end.offsetLeft),
                bottom = Math.max(start.offsetTop + start.offsetHeight,
                                end.offsetTop + end.offsetHeight),
                right = Math.max(start.offsetLeft + start.offsetWidth,
                                end.offsetLeft + end.offsetWidth);
            self.element.style.top = top + 1 + 'px';
            self.element.style.left = left + 1 + 'px';
            self.element.style.height = bottom - top - 1 + 'px';
            self.element.style.width = right - left - 1 + 'px';
            self.element.style.backgroundColor = 'rgba(245, 142, 00, 0.15)';
            self.show();
        };
        self.beginDrag = function () {
            self.canDrag = true;
            ko.utils.registerEventHandler(self.element, 'mousemove', self.doBeginDrag);
        };
        self.doBeginDrag = function () {
            self.element.removeEventListener('mousemove', self.doBeginDrag);
            if (!self.canDrag) {
                return;
            }
            self.isDragging = true;
            self.element.style.pointerEvents = 'none';
        };
        self.endDrag = function () {
            self.element.removeEventListener('mousemove', self.doBeginDrag);
            self.isDragging = false;
            self.canDrag = false;
            self.element.style.pointerEvents = 'inherit';
        };
        ko.utils.registerEventHandler(document.getElementsByTagName('html')[0], "mouseup", function (event) {
            self.endDrag();
        });
        ko.utils.registerEventHandler(self.element, "mousedown", function (event) {
            if (event.button !== 0) {
                return;
            }
            self.hide();
            var cell = event.view.document.elementFromPoint(event.clientX, event.clientY);
            selection.onCellMouseDown(cell, event.shiftKey);
            event.preventDefault();
        });
        ko.utils.registerEventHandler(self.element, "dblclick", function (event) {
            selection.startEditing();
        });
        ko.utils.registerEventHandler(self.element, "keypress", function (event) {
            selection.startEditing();
        });
        ko.utils.registerEventHandler(self.element, "keydown", function (event) {
            if (event.keyCode === 13) { selection.onReturn(event); }
            else if ([37, 38, 39, 40].indexOf(event.keyCode) !== -1) { selection.onArrows(event); }
            else if (event.keyCode === 86 && event.ctrlKey) {
                self.copyPasteElement.value = '';
                self.copyPasteElement.style.display = 'block';
                self.copyPasteElement.focus();
                setTimeout(function () {
                    selection.onPaste(self.copyPasteElement.value);
                    self.copyPasteElement.style.display = 'none';
                    self.focus();
                }, 0);
            }
            else if (event.keyCode === 67 && event.ctrlKey) {
                self.copyPasteElement.value = selection.onCopy();
                self.copyPasteElement.style.display = 'block';
                self.copyPasteElement.focus();
                document.execCommand('selectAll', false, null);
                setTimeout(function () {
                    self.copyPasteElement.style.display = 'none';
                    self.focus();
                }, 0);
            }
        });
        ko.utils.registerEventHandler(self.element, "blur", function (event) {
            setTimeout(function () {
                if (selection.range.start && !selection.isEditingCell(selection.range.start)) {
                    selection.range.clear();
                }
            }, 0);
        });
    },
 |             
                                 
 |                                   SelectionRange: function (cellIsSelectable) {
        var self = this;
        self.start = undefined;
        self.end = undefined;
        self.selection = ko.observableArray();
 |             
|                                  
  |                                       self.moveInDirection = function (direction) {
            var newStart = self.getSelectableCellInDirection(self.start, direction),
                startChanged = newStart !== self.start;
            if (newStart !== self.start || self.start !== self.end) {
                self.setStart(newStart);
            }
            return startChanged;
        };
 |             
|                                  
  |                                       self.extendInDirection = function (direction) {
            var newEnd = self.getCellInDirection(self.end, direction),
                endChanged = newEnd !== self.end;
            self.setEnd(newEnd);
            return endChanged;
        };
 |             
|                                  
  |                                       self.getCells = function () {
            return self.getCellsInArea(self.start, self.end);
        };
 |             
|                                  
  |                                       self.clear = function () {
            self.start = undefined;
            self.end = undefined;
            self.selection([]);
        };
        self.setStart = function (element) {
            self.start = element;
            self.end = element;
            self.selection(self.getCells());
        };
        self.setEnd = function (element) {
            if (element === self.end) {
                return;
            }
            self.start = self.start || element;
            var cellsInArea = self.getCellsInArea(self.start, element),
                allEditable = true;
            ko.utils.arrayForEach(cellsInArea, function (cell) {
                allEditable = allEditable && cellIsSelectable(cell);
            });
            if (!allEditable) {
                return;
            }
            self.end = element;
            self.selection(self.getCells());
        };
        self.getCellInDirection = function (originCell, direction, rowIndex, cellIndex) {
            var originRow = originCell.parentNode,
                cell;
            rowIndex = typeof rowIndex !== 'undefined' ? rowIndex : originRow.rowIndex - self.getRowsOffset(originCell),
            cellIndex = typeof cellIndex !== 'undefined' ? cellIndex : originCell.cellIndex;
            if (direction === 'Left' && cellIndex > 0) {
                return originRow.children[cellIndex - 1];
            }
            if (direction === 'Up' && rowIndex > 0) {
                cell = originRow.parentNode.children[rowIndex - 1].children[cellIndex];
                return cell || self.getCellInDirection(originCell, direction, rowIndex - 1, cellIndex);
            }
            if (direction === 'Right' && cellIndex < originCell.parentNode.children.length - 1) {
                return originRow.children[cellIndex + 1];
            }
            if (direction === 'Down' && rowIndex < originCell.parentNode.parentNode.children.length - 1) {
                cell = originRow.parentNode.children[rowIndex + 1].children[cellIndex];
                return cell || self.getCellInDirection(originCell, direction, rowIndex + 1, cellIndex);
            }
            return originCell;
        };
        self.getSelectableCellInDirection = function (originCell, direction) {
            var lastCell,
                cell = originCell;
            while (cell !== lastCell) {
                lastCell = cell;
                cell = self.getCellInDirection(cell, direction);
                if (cellIsSelectable(cell)) {
                    return cell;
                }
            }
            return originCell;
        };
        self.getCellsInArea = function (startCell, endCell) {
            var startX = Math.min(startCell.cellIndex, endCell.cellIndex),
                startY = Math.min(startCell.parentNode.rowIndex, endCell.parentNode.rowIndex),
                endX = Math.max(startCell.cellIndex, endCell.cellIndex),
                endY = Math.max(startCell.parentNode.rowIndex, endCell.parentNode.rowIndex),
                x, y,
                rowsOffset = self.getRowsOffset(startCell),
                cell,
                cells = [];
            for (x = startX; x <= endX; ++x) {
                for (y = startY; y <= endY; ++y) {
                    cell = startCell.parentNode.parentNode.children[y - rowsOffset].children[x];
                    cells.push(cell || {});
                }
            }
            return cells;
        };
        self.getRowsOffset = function (cell) {
            var rows = cell.parentNode.parentNode.children;
            return rows[rows.length - 1].rowIndex + 1 - rows.length;
        };
    }
};
 |             
                                 
 |                               
ko.bindingHandlers.editableCellSelection = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        var table = element,
            selection = table._cellSelection;
 
        if (element.tagName !== 'TABLE') {
            throw new Error('editableCellSelection binding can only be applied to tables');
        }
        if (selection === undefined) {
            table._cellSelection = selection = new ko.bindingHandlers.editableCell.Selection(table);
        }
        selection.range.selection.subscribe(function (newSelection) {
            var selection = ko.utils.arrayMap(newSelection, function (cell) {
                return {
                    cell: cell,
                    value: cell._cellValue(),
                    text: cell._cellText()
                };
            });
            ko.bindingHandlers.editableCell.updateBindingValue('editableCellSelection', valueAccessor, allBindingsAccessor, selection);
        });
    }
};
 |