UNPKG

canvas-datagrid

Version:

Canvas based data grid web component. Capable of displaying millions of contiguous hierarchical rows and columns without paging or loading, on a single canvas element.

568 lines (567 loc) 28.8 kB
/*jslint browser: true, unparam: true, todo: true*/ /*globals define: true, MutationObserver: false, requestAnimationFrame: false, performance: false, btoa: false, Event: false*/ define([], function () { 'use strict'; return function (self) { var zIndexTop, hoverScrollTimeout, autoCompleteContext; function applyContextItemStyle(contextItemContainer) { self.createInlineStyle(contextItemContainer, 'canvas-datagrid-context-menu-item' + (self.mobile ? '-mobile' : '')); contextItemContainer.addEventListener('mouseover', function () { self.createInlineStyle(contextItemContainer, 'canvas-datagrid-context-menu-item:hover'); }); contextItemContainer.addEventListener('mouseout', function () { self.createInlineStyle(contextItemContainer, 'canvas-datagrid-context-menu-item'); }); } function createContextMenu(ev, pos, items, parentContextMenu) { var container = document.createElement('div'), upArrow = document.createElement('div'), downArrow = document.createElement('div'), children = [], selectedIndex = -1, intf = {}, rect; if (!Array.isArray(items)) { throw new Error('createContextMenu expects an array.'); } function createItems() { items.forEach(function (item) { var contextItemContainer = document.createElement('div'), childMenuArrow; function removeChildContext(e) { if (e.relatedTarget === container || item.contextMenu.container === e.relatedTarget || childMenuArrow === e.relatedTarget || (contextItemContainer === e.relatedTarget) || item.contextMenu.container.contains(e.relatedTarget) ) { return; } item.contextMenu.dispose(); children.splice(children.indexOf(item.contextMenu), 1); item.contextMenu = undefined; contextItemContainer.removeEventListener('mouseout', removeChildContext); container.removeEventListener('mouseout', removeChildContext); contextItemContainer.setAttribute('contextOpen', '0'); contextItemContainer.setAttribute('opening', '0'); } function contextAddCallback(items) { // check yet again if the user hasn't moved off if (contextItemContainer.getAttribute('opening') !== '1' || contextItemContainer.getAttribute('contextOpen') === '1') { return; } var cPos = contextItemContainer.getBoundingClientRect(); cPos = { left: cPos.left + self.style.childContextMenuMarginLeft + container.offsetWidth, top: cPos.top + self.style.childContextMenuMarginTop, bottom: cPos.bottom, right: cPos.right }; item.contextMenu = createContextMenu(ev, cPos, items, intf); contextItemContainer.setAttribute('contextOpen', '1'); contextItemContainer.addEventListener('mouseout', removeChildContext); container.addEventListener('mouseout', removeChildContext); children.push(item.contextMenu); } function createChildContext() { var i; if (contextItemContainer.getAttribute('contextOpen') === '1') { return; } contextItemContainer.setAttribute('opening', '1'); if (typeof item.items === 'function') { i = item.items.apply(intf, [function (items) { contextAddCallback(items); }]); if (i !== undefined && Array.isArray(i)) { contextAddCallback(i); } return; } contextAddCallback(item.items); } function addItem(item) { function addContent(content) { if (content === null) { return; } if (typeof content === 'function') { return addContent(content(ev)); } if (typeof content === 'object') { contextItemContainer.appendChild(content); return; } applyContextItemStyle(contextItemContainer); contextItemContainer.innerHTML = content; return; } addContent(item.title); item.contextItemContainer = contextItemContainer; if ((item.items && item.items.length > 0) || typeof item.items === 'function') { childMenuArrow = document.createElement('div'); self.createInlineStyle(childMenuArrow, 'canvas-datagrid-context-child-arrow'); childMenuArrow.innerHTML = self.style.childContextMenuArrowHTML; contextItemContainer.appendChild(childMenuArrow); contextItemContainer.addEventListener('mouseover', createChildContext); contextItemContainer.addEventListener('mouseout', function () { contextItemContainer.setAttribute('opening', '0'); }); } if (item.click) { contextItemContainer.addEventListener('click', function (ev) { item.click.apply(self, [ev]); }); } } addItem(item); container.appendChild(contextItemContainer); }); } function clickIndex(idx) { items[idx].contextItemContainer.dispatchEvent(new Event('click')); } function checkArrowVisibility() { if (container.scrollTop > 0) { self.parentDOMNode.appendChild(upArrow); } else if (upArrow.parentNode) { upArrow.parentNode.removeChild(upArrow); } if (container.scrollTop >= container.scrollHeight - container.offsetHeight && downArrow.parentNode) { downArrow.parentNode.removeChild(downArrow); } else if (container.scrollHeight - container.offsetHeight > 0 && !(container.scrollTop >= container.scrollHeight - container.offsetHeight)) { self.parentDOMNode.appendChild(downArrow); } } function startHoverScroll(type) { return function t() { var a = self.attributes.contextHoverScrollAmount; if (type === 'up' && container.scrollTop === 0) { return; } if (type === 'down' && container.scrollTop === container.scrollHeight) { return; } container.scrollTop += (type === 'up' ? -a : a); hoverScrollTimeout = setTimeout(t, self.attributes.contextHoverScrollRateMs, type); }; } function endHoverScroll(type) { return function () { clearTimeout(hoverScrollTimeout); }; } function init() { var loc = {}, s = self.scrollOffset(self.canvas); if (zIndexTop === undefined) { zIndexTop = self.style.contextMenuZIndex; } createItems(); self.createInlineStyle(container, 'canvas-datagrid-context-menu' + (self.mobile ? '-mobile' : '')); loc.x = pos.left - s.left; loc.y = pos.top - s.top; loc.height = 0; zIndexTop += 1; container.style.position = 'absolute'; upArrow.style.color = self.style.contextMenuArrowColor; downArrow.style.color = self.style.contextMenuArrowColor; [upArrow, downArrow].forEach(function (el) { el.style.textAlign = 'center'; el.style.position = 'absolute'; el.style.zIndex = zIndexTop + 1; }); container.style.zIndex = zIndexTop; if (parentContextMenu && parentContextMenu.inputDropdown) { container.style.maxHeight = window.innerHeight - loc.y - self.style.autocompleteBottomMargin + 'px'; container.style.minWidth = pos.width + 'px'; loc.y += pos.height; } if (self.mobile) { container.style.width = pos.width + 'px'; } container.style.left = loc.x + 'px'; container.style.top = loc.y + 'px'; container.addEventListener('scroll', checkArrowVisibility); container.addEventListener('wheel', function (e) { if (self.hasFocus) { container.scrollTop += e.deltaY; container.scrollLeft += e.deltaX; } checkArrowVisibility(); }); upArrow.innerHTML = self.style.contextMenuArrowUpHTML; downArrow.innerHTML = self.style.contextMenuArrowDownHTML; container.appendChild(upArrow); document.body.appendChild(downArrow); document.body.appendChild(container); rect = container.getBoundingClientRect(); // TODO: fix !(parentContextMenu && parentContextMenu.inputDropdown) state (autocomplete) if (rect.bottom > window.innerHeight) { if (!(parentContextMenu && parentContextMenu.inputDropdown)) { loc.y -= (rect.bottom + self.style.contextMenuWindowMargin) - window.innerHeight; } if (loc.y < 0) { loc.y = 0; } if (container.offsetHeight > window.innerHeight - self.style.contextMenuWindowMargin) { container.style.height = window.innerHeight - self.style.contextMenuWindowMargin + 'px'; loc.y -= self.style.contextMenuWindowMargin; } } if (rect.right > window.innerWidth) { loc.x -= rect.right - window.innerWidth + self.style.contextMenuWindowMargin; } if (loc.x < 0) { loc.x = 0; } if (loc.y < 0) { loc.y = 0; } container.style.left = loc.x + 'px'; container.style.top = loc.y + 'px'; rect = container.getBoundingClientRect(); upArrow.style.top = rect.top + 'px'; downArrow.style.top = rect.top + rect.height - downArrow.offsetHeight + 'px'; upArrow.style.left = rect.left + 'px'; downArrow.style.left = rect.left + 'px'; downArrow.style.width = container.offsetWidth + 'px'; upArrow.style.width = container.offsetWidth + 'px'; downArrow.addEventListener('mouseover', startHoverScroll('down')); downArrow.addEventListener('mouseout', endHoverScroll('down')); upArrow.addEventListener('mouseover', startHoverScroll('up')); upArrow.addEventListener('mouseout', endHoverScroll('up')); checkArrowVisibility(); } intf.parentGrid = self.intf; intf.parentContextMenu = parentContextMenu; intf.container = container; init(); intf.clickIndex = clickIndex; intf.rect = rect; intf.items = items; intf.upArrow = upArrow; intf.downArrow = downArrow; intf.dispose = function () { clearTimeout(hoverScrollTimeout); children.forEach(function (c) { c.dispose(); }); [downArrow, upArrow, container].forEach(function (el) { if (el.parentNode) { el.parentNode.removeChild(el); } }); }; Object.defineProperty(intf, 'selectedIndex', { get: function () { return selectedIndex; }, set: function (value) { if (typeof value !== 'number' || isNaN(value || !isFinite(value))) { throw new Error('Context menu selected index must be a sane number.'); } selectedIndex = value; if (selectedIndex > items.length - 1) { selectedIndex = items.length - 1; } if (selectedIndex < 0) { selectedIndex = 0; } items.forEach(function (item, index) { if (index === selectedIndex) { return self.createInlineStyle(item.contextItemContainer, 'canvas-datagrid-context-menu-item:hover'); } self.createInlineStyle(item.contextItemContainer, 'canvas-datagrid-context-menu-item'); }); } }); return intf; } function createFilterContextMenuItems(e) { var filterContainer = document.createElement('div'), filterLabel = document.createElement('div'), filterAutoCompleteButton = document.createElement('button'), filterInput = document.createElement('input'), n = e.cell && e.cell.header ? e.cell.header.title || e.cell.header.name : '', autoCompleteItems, iRect; function checkRegExpErrorState() { filterInput.style.background = self.style.contextFilterInputBackground; filterInput.style.color = self.style.contextFilterInputColor; if (self.invalidFilterRegEx) { filterInput.style.background = self.style.contextFilterInvalidRegExpBackground; filterInput.style.color = self.style.contextFilterInvalidRegExpColor; } } function fillAutoComplete() { var count = 0; autoCompleteItems = {}; self.data.forEach(function (row) { var value = row[e.cell.header.name]; if (autoCompleteItems[value] || count > self.attributes.maxAutoCompleteItems) { return; } count += 1; autoCompleteItems[value] = { title: self.formatters[e.cell.header.type || 'string']({ cell: { value: value }}), click: function (e) { filterInput.value = value; e.stopPropagation(); filterInput.dispatchEvent(new Event('keyup')); self.disposeAutocomplete(); return; } }; }); autoCompleteItems = Object.keys(autoCompleteItems).map(function (key) { return autoCompleteItems[key]; }); } function createAutoCompleteContext(ev) { if (ev && [40, 38, 13, 9].indexOf(ev.keyCode) !== -1) { return; } fillAutoComplete(); iRect = filterInput.getBoundingClientRect(); if (autoCompleteContext) { autoCompleteContext.dispose(); autoCompleteContext = undefined; } autoCompleteContext = createContextMenu(e, { left: iRect.left, top: iRect.top, right: iRect.right, bottom: iRect.bottom, height: iRect.height, width: iRect.width }, autoCompleteItems, {inputDropdown: true}); autoCompleteContext.selectedIndex = 0; } self.createInlineStyle(filterLabel, 'canvas-datagrid-context-menu-label'); self.createInlineStyle(filterAutoCompleteButton, 'canvas-datagrid-context-menu-filter-button'); self.createInlineStyle(filterInput, 'canvas-datagrid-context-menu-filter-input'); checkRegExpErrorState(); filterInput.onclick = self.disposeAutocomplete; filterInput.addEventListener('keydown', function (e) { //down if (e.keyCode === 40) { autoCompleteContext.selectedIndex += 1; } //up if (e.keyCode === 38) { autoCompleteContext.selectedIndex -= 1; } //enter if (e.keyCode === 13) { autoCompleteContext.clickIndex(autoCompleteContext.selectedIndex); self.disposeContextMenu(); } //tab if (e.keyCode === 9) { autoCompleteContext.clickIndex(autoCompleteContext.selectedIndex); e.preventDefault(); } //esc if (e.keyCode === 27) { self.disposeContextMenu(); } }); filterInput.addEventListener('keyup', function () { self.setFilter(e.cell.header.name, filterInput.value); }); filterInput.addEventListener('keyup', createAutoCompleteContext); ['focus', 'blur', 'keydown', 'keyup', 'change'].forEach(function (en) { filterInput.addEventListener(en, checkRegExpErrorState); }); filterInput.value = e.cell.header ? self.columnFilters[e.cell.header.name] || '' : ''; filterLabel.innerHTML = self.attributes.filterOptionText.replace(/%s/g, n); filterAutoCompleteButton.onclick = function () { if (autoCompleteContext) { return self.disposeAutocomplete(); } createAutoCompleteContext(); }; filterAutoCompleteButton.innerHTML = self.style.contextFilterButtonHTML; filterContainer.addEventListener('click', function (e) { return e.stopPropagation(); }); filterContainer.appendChild(filterLabel); filterContainer.appendChild(filterInput); filterContainer.appendChild(filterAutoCompleteButton); e.items.push({ title: filterContainer }); if (Object.keys(self.columnFilters).length) { Object.keys(self.columnFilters).forEach(function (cf) { var h = self.getHeaderByName(cf); e.items.push({ title: self.attributes.removeFilterOptionText.replace(/%s/g, h.title || h.name), click: function removeFilterClick(e) { e.preventDefault(); self.setFilter(cf, ''); self.controlInput.focus(); } }); }); } } function addDefaultContextMenuItem(e) { var isNormalCell = !(e.cell.isBackground || e.cell.isHeaderCellCap || e.cell.isScrollBar || e.cell.isCorner || e.cell.isRowHeader) && e.cell.header; if (self.attributes.showFilter && isNormalCell) { createFilterContextMenuItems(e); } if (self.attributes.showCopy && self.selections.reduce(function (p, r) { return p + r.length; }, 0) > 0) { e.items.push({ title: self.attributes.copyText, click: function () { document.execCommand('copy'); self.disposeContextMenu(); self.controlInput.focus(); } }); } if (self.attributes.showPaste && self.clipBoardData) { e.items.push({ title: self.attributes.pasteText, click: function () { self.paste(self.clipBoardData, e.cell.columnIndex, e.cell.rowIndex); self.draw(); } }); } if (self.attributes.showColumnSelector) { e.items.push({ title: self.attributes.columnSelectorText, items: function () { var d = []; self.getSchema().forEach(function (column) { if (column.name === self.uniqueId) { return; } function toggleColumnVisibility(e) { column.hidden = !column.hidden; e.preventDefault(); self.stopPropagation(e); self.disposeContextMenu(); self.resize(true); self.setStorageData(); } var el = document.createElement('div'); applyContextItemStyle(el); el.addEventListener('touchstart', toggleColumnVisibility); el.addEventListener('click', toggleColumnVisibility); el.innerHTML = (column.hidden ? self.attributes.columnSelectorHiddenText : self.attributes.columnSelectorVisibleText) + (column.title || column.name); d.push({ title: el }); }); return d; } }); if (e.cell && e.cell.header) { e.items.push({ title: self.attributes.hideColumnText .replace(/%s/ig, e.cell.header.title || e.cell.header.name), click: function (ev) { e.cell.header.hidden = true; ev.preventDefault(); self.stopPropagation(ev); self.disposeContextMenu(); self.setStorageData(); setTimeout(function () { self.resize(true); }, 10); } }); } } if (self.attributes.saveAppearance && self.attributes.showClearSettingsOption && (Object.keys(self.sizes.rows).length > 0 || Object.keys(self.sizes.columns).length > 0)) { e.items.push({ title: self.attributes.clearSettingsOptionText, click: function (e) { e.preventDefault(); self.sizes.rows = {}; self.sizes.columns = {}; self.createRowOrders(); self.createColumnOrders(); self.storedSettings = undefined; self.dispatchEvent('resizecolumn', {columnWidth: self.style.cellWidth}); self.dispatchEvent('resizerow', {cellHeight: self.style.cellHeight}); self.setStorageData(); self.resize(true); self.disposeContextMenu(); self.controlInput.focus(); } }); } if (self.attributes.allowSorting && self.attributes.showOrderByOption && isNormalCell) { e.items.push({ title: self.attributes.showOrderByOptionTextAsc.replace('%s', e.cell.header.title || e.cell.header.name), click: function (ev) { ev.preventDefault(); self.order(e.cell.header.name, 'asc'); self.controlInput.focus(); } }); e.items.push({ title: self.attributes.showOrderByOptionTextDesc.replace('%s', e.cell.header.title || e.cell.header.name), click: function (ev) { ev.preventDefault(); self.order(e.cell.header.name, 'desc'); self.disposeContextMenu(); self.controlInput.focus(); } }); } } self.disposeAutocomplete = function () { if (autoCompleteContext) { autoCompleteContext.dispose(); autoCompleteContext = undefined; } }; self.disposeContextMenu = function () { document.removeEventListener('click', self.disposeContextMenu); zIndexTop = self.style.contextMenuZIndex; self.disposeAutocomplete(); if (self.contextMenu) { self.contextMenu.dispose(); } self.contextMenu = undefined; }; self.contextmenuEvent = function (e, overridePos) { if (!self.hasFocus && e.target !== self.canvas) { return; } function createDiposeEvent() { requestAnimationFrame(function () { document.addEventListener('click', self.disposeContextMenu); document.removeEventListener('mouseup', createDiposeEvent); }); } var contextPosition, items = [], pos = overridePos || self.getLayerPos(e), ev = { NativeEvent: e, cell: self.getCellAt(pos.x, pos.y), items: items }; if (!ev.cell.isGrid) { addDefaultContextMenuItem(ev); } if (self.dispatchEvent('contextmenu', ev)) { return; } if (!ev.cell.isGrid) { if (self.contextMenu) { self.disposeContextMenu(); } contextPosition = { left: pos.x + pos.rect.left + self.style.contextMenuMarginLeft + self.canvasOffsetLeft, top: pos.y + pos.rect.top + self.style.contextMenuMarginTop + self.canvasOffsetTop, right: ev.cell.width + ev.cell.x + pos.rect.left, bottom: ev.cell.height + ev.cell.y + pos.rect.top, height: ev.cell.height, width: ev.cell.width }; if (self.mobile) { contextPosition.left = self.style.mobileContextMenuMargin + 'px'; contextPosition.width = self.width - (self.style.mobileContextMenuMargin * 2) + 'px'; } self.contextMenu = createContextMenu(ev, contextPosition, items); document.addEventListener('mouseup', createDiposeEvent); e.preventDefault(); } }; return; }; });