UNPKG

fin-hypergrid

Version:

Canvas-based high-performance grid

973 lines (822 loc) 27.3 kB
/* eslint-env browser */ 'use strict'; if (typeof window.CustomEvent !== 'function') { window.CustomEvent = function(event, params) { params = params || { bubbles: false, cancelable: false, detail: undefined }; var evt = document.createEvent('CustomEvent'); evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); return evt; }; window.CustomEvent.prototype = window.Event.prototype; } var rectangular = require('rectangular'); var RESIZE_POLLING_INTERVAL = 200, paintables = [], resizables = [], paintRequest, resizeInterval, charMap = makeCharMap(); // We still support IE 11; we do NOT support older versions of IE (and we do NOT officially support Edge) // https://stackoverflow.com/questions/21825157/internet-explorer-11-detection#answer-21825207 var isIE11 = !!(window.MSInputMethodContext && document.documentMode); function Canvas(div, component, contextAttributes) { var self = this; // create the containing <div>...</div> this.div = div; this.component = component; this.dragEndtime = Date.now(); // create and append the info <div>...</div> (to be displayed when there are no data rows) this.infoDiv = document.createElement('div'); this.infoDiv.className = 'info'; this.div.appendChild(this.infoDiv); // create and append the canvas this.gc = getCachedContext(this.canvas = document.createElement('canvas'), contextAttributes); this.div.appendChild(this.canvas); this.canvas.style.outline = 'none'; this.mouseLocation = new rectangular.Point(-1, -1); this.dragstart = new rectangular.Point(-1, -1); //this.origin = new rectangular.Point(0, 0); this.bounds = new rectangular.Rectangle(0, 0, 0, 0); this.hasMouse = false; document.addEventListener('mousemove', function(e) { if (self.hasMouse || self.isDragging()) { self.finmousemove(e); } }); document.addEventListener('mouseup', function(e) { self.finmouseup(e); }); document.addEventListener('wheel', function(e) { self.finwheelmoved(e); }); document.addEventListener('keydown', function(e) { self.finkeydown(e); }); document.addEventListener('keyup', function(e) { self.finkeyup(e); }); this.canvas.onmouseover = function() { self.hasMouse = true; }; this.addEventListener('focus', function(e) { self.finfocusgained(e); }); this.addEventListener('blur', function(e) { self.finfocuslost(e); }); this.addEventListener('mousedown', function(e) { self.finmousedown(e); }); this.addEventListener('mouseout', function(e) { self.hasMouse = false; self.finmouseout(e); }); this.addEventListener('click', function(e) { self.finclick(e); }); this.addEventListener('dblclick', function(e) { self.findblclick(e); }); this.addEventListener('contextmenu', function(e) { self.fincontextmenu(e); e.preventDefault(); return false; }); this.addEventListener('touchstart', function(e) { self.fintouchstart(e); }); this.addEventListener('touchmove', function(e) { self.fintouchmove(e); }); this.addEventListener('touchend', function(e) { self.fintouchend(e); }); this.canvas.setAttribute('tabindex', 0); this.resetZoom(); this.resize(); this.beginResizing(); this.beginPainting(); } Canvas.prototype = { constructor: Canvas.prototype.constructor, div: null, component: null, canvas: null, focuser: null, buffer: null, ctx: null, mouseLocation: null, dragstart: null, origin: null, bounds: null, dirty: false, size: null, mousedown: false, dragging: false, repeatKeyCount: 0, repeatKey: null, repeatKeyStartTime: 0, currentKeys: [], hasMouse: false, dragEndTime: 0, lastRepaintTime: 0, currentPaintCount: 0, currentFPS: 0, lastFPSComputeTime: 0, addEventListener: function(name, callback) { this.canvas.addEventListener(name, callback); }, removeEventListener: function(name, callback) { this.canvas.removeEventListener(name, callback); }, stopPaintLoop: stopPaintLoop, restartPaintLoop: restartPaintLoop, stopResizeLoop: stopResizeLoop, restartResizeLoop: restartResizeLoop, detached: function() { this.stopPainting(); this.stopResizing(); }, getCurrentFPS:function() { return this.currentFPS; }, tickPaint: function(now) { var isContinuousRepaint = this.component.properties.enableContinuousRepaint, fps = this.component.properties.repaintIntervalRate; if (fps === 0) { return; } var interval = 1000 / fps; var elapsed = now - this.lastRepaintTime; if (elapsed > interval && (isContinuousRepaint || this.dirty)) { this.paintNow(); this.lastRepaintTime = now; /* - (elapsed % interval);*/ if (isContinuousRepaint) { this.currentPaintCount++; if (now - this.lastFPSComputeTime >= 1000) { this.currentFPS = (this.currentPaintCount * 1000) / (now - this.lastFPSComputeTime); this.currentPaintCount = 0; this.lastFPSComputeTime = now; } } } }, beginPainting: function() { var self = this; this.requestRepaint(); this.tickPainter = function(now) { self.tickPaint(now); }; paintables.push(this); }, stopPainting: function() { paintables.splice(paintables.indexOf(this), 1); }, beginResizing: function() { var self = this; this.tickResizer = function() { self.checksize(); }; resizables.push(this); }, stopResizing: function() { resizables.splice(resizables.indexOf(this), 1); }, start: function() { this.beginPainting(); this.beginResizing(); }, stop: function() { this.stopPainting(); this.stopResizing(); }, getBoundingClientRect: function(el) { var rect = el.getBoundingClientRect(); if (isIE11) { var r = 1 / this.bodyZoomFactor; var top = rect.top * r; var right = rect.right * r; var bottom = rect.bottom * r; var left = rect.left * r; rect = { top: top, right: right, bottom: bottom, left: left, width: right - left, height: bottom - top, x: left, y: top }; } return rect; }, getDivBoundingClientRect: function() { // Make sure our canvas has integral dimensions var rect = this.getBoundingClientRect(this.div); var top = Math.floor(rect.top), left = Math.floor(rect.left), width = Math.ceil(rect.width), height = Math.ceil(rect.height); return { top: top, right: left + width, bottom: top + height, left: left, width: width, height: height, x: rect.x, y: rect.y }; }, checksize: function() { var sizeNow = this.getDivBoundingClientRect(); if (sizeNow.width !== this.size.width || sizeNow.height !== this.size.height) { this.resize(sizeNow); } }, resize: function(box) { box = this.size = box || this.getDivBoundingClientRect(); this.width = box.width; this.height = box.height; // http://www.html5rocks.com/en/tutorials/canvas/hidpi/ var isHIDPI = window.devicePixelRatio && this.component.properties.useHiDPI; var ratio = isHIDPI && window.devicePixelRatio || 1; this.devicePixelRatio = ratio *= this.bodyZoomFactor; this.canvas.width = Math.round(this.width * ratio); this.canvas.height = Math.round(this.height * ratio); this.canvas.style.width = this.width + 'px'; this.canvas.style.height = this.height + 'px'; this.gc.scale(ratio, ratio); this.bounds = new rectangular.Rectangle(0, 0, this.width, this.height); this.component.setBounds(this.bounds); this.resizeNotification(); this.paintNow(); }, resizeNotification: function() { this.dispatchNewEvent(undefined, 'fin-canvas-resized', { width: this.width, height: this.height }); }, resetZoom: function() { var factor = 1; // IE11 bug: must use getPropertyValue because zoom is omitted from returned object var zoomProp = getComputedStyle(document.body).getPropertyValue('zoom'); if (zoomProp) { // IE11: always returns percentage + percent sign (others return factor) var m = zoomProp.match(/^(.+?)(%)?$/); if (m) { var zoom = Number(m[1]); if (m[2]) { zoom /= 100; } zoom = Number(zoom || 1); factor *= zoom; } } this.bodyZoomFactor = factor; this.resize(); }, getBounds: function() { return this.bounds; }, paintNow: function() { try { this.gc.cache.save(); this.dirty = false; this.component.paint(this.gc); } catch (e) { console.error(e); } finally { this.gc.cache.restore(); } }, // flushBuffer deprecated in 3.3.0 flushBuffer: function() {}, newEvent: function(primitiveEvent, name, detail) { var event = { detail: detail || {} }; if (primitiveEvent) { event.detail.primitiveEvent = primitiveEvent; } return new CustomEvent(name, event); }, dispatchNewEvent: function(primitiveEvent, name, detail) { return this.canvas.dispatchEvent(this.newEvent(primitiveEvent, name, detail)); }, dispatchNewMouseKeysEvent: function(event, name, detail) { detail = detail || {}; detail.mouse = this.mouseLocation; defKeysProp.call(this, event, 'keys', detail); return this.dispatchNewEvent(event, name, detail); }, dispatchNewTouchEvent: function(event, name, detail) { detail = detail || {}; var touches = [].slice.call(event.changedTouches); detail.touches = touches.map(function(touch) { return this.getLocal(touch); }, this); return this.dispatchNewEvent(event, name, detail); }, finmousemove: function(e) { if (!this.isDragging() && this.mousedown) { this.beDragging(); this.dispatchNewMouseKeysEvent(e, 'fin-canvas-dragstart', { isRightClick: this.isRightClick(e), dragstart: this.dragstart }); this.dragstart = new rectangular.Point(this.mouseLocation.x, this.mouseLocation.y); } this.mouseLocation = this.getLocal(e); if (this.isDragging()) { this.dispatchNewMouseKeysEvent(e, 'fin-canvas-drag', { dragstart: this.dragstart, isRightClick: this.isRightClick(e) }); } if (this.bounds.contains(this.mouseLocation)) { this.dispatchNewMouseKeysEvent(e, 'fin-canvas-mousemove'); } }, finmousedown: function(e) { this.mouseLocation = this.mouseDownLocation = this.getLocal(e); this.mousedown = true; this.dispatchNewMouseKeysEvent(e, 'fin-canvas-mousedown', { isRightClick: this.isRightClick(e) }); this.takeFocus(); }, finmouseup: function(e) { if (!this.mousedown) { // ignore document:mouseup unless preceded by a canvas:mousedown return; } if (this.isDragging()) { this.dispatchNewMouseKeysEvent(e, 'fin-canvas-dragend', { dragstart: this.dragstart, isRightClick: this.isRightClick(e) }); this.beNotDragging(); this.dragEndtime = Date.now(); } this.mousedown = false; this.dispatchNewMouseKeysEvent(e, 'fin-canvas-mouseup', { dragstart: this.dragstart, isRightClick: this.isRightClick(e) }); //this.mouseLocation = new rectangular.Point(-1, -1); }, finmouseout: function(e) { if (!this.mousedown) { this.mouseLocation = new rectangular.Point(-1, -1); } this.repaint(); this.dispatchNewMouseKeysEvent(e, 'fin-canvas-mouseout', { dragstart: this.dragstart }); }, finwheelmoved: function(e) { if (this.isDragging() || !this.hasFocus()) { return; } e.preventDefault(); this.dispatchNewMouseKeysEvent(e, 'fin-canvas-wheelmoved', { isRightClick: this.isRightClick(e) }); }, finclick: function(e) { this.mouseLocation = this.getLocal(e); this.dispatchNewMouseKeysEvent(e, 'fin-canvas-click', { isRightClick: this.isRightClick(e) }); }, findblclick: function(e) { this.mouseLocation = this.getLocal(e); this.dispatchNewMouseKeysEvent(e, 'fin-canvas-dblclick', { isRightClick: this.isRightClick(e) }); }, getCharMap: function() { return charMap; }, getKeyChar: function(e) { var keyCode = e.keyCode || e.detail.key, shift = e.shiftKey || e.detail.shift, key = e.key; e.legacyKey = charMap[keyCode] && charMap[keyCode][shift ? 1 : 0]; if (typeof key === 'string' && key.length === 1) { return key; } return ( e.legacyKey || // legacy unprintable char string key // modern unprintable char string when no such legacy string ); }, finkeydown: function(e) { if (!this.hasFocus()) { return; } var keyChar = updateCurrentKeys.call(this, e, true); if (e.repeat) { if (this.repeatKey === keyChar) { this.repeatKeyCount++; } else { this.repeatKey = keyChar; this.repeatKeyStartTime = Date.now(); } } else { this.repeatKey = null; this.repeatKeyCount = 0; this.repeatKeyStartTime = 0; } this.dispatchNewEvent(e, 'fin-canvas-keydown', defKeysProp.call(this, e, 'currentKeys', { alt: e.altKey, ctrl: e.ctrlKey, char: keyChar, legacyChar: e.legacyKey, code: e.charCode, key: e.keyCode, meta: e.metaKey, repeatCount: this.repeatKeyCount, repeatStartTime: this.repeatKeyStartTime, shift: e.shiftKey, identifier: e.key })); }, finkeyup: function(e) { if (!this.hasFocus()) { return; } var keyChar = updateCurrentKeys.call(this, e, false); this.repeatKeyCount = 0; this.repeatKey = null; this.repeatKeyStartTime = 0; this.dispatchNewEvent(e, 'fin-canvas-keyup', defKeysProp.call(this, e, 'currentKeys', { alt: e.altKey, ctrl: e.ctrlKey, char: keyChar, legacyChar: e.legacyKey, code: e.charCode, key: e.keyCode, meta: e.metaKey, repeat: e.repeat, shift: e.shiftKey, identifier: e.key, currentKeys: this.currentKeys.slice(0) })); }, finfocusgained: function(e) { this.dispatchNewEvent(e, 'fin-canvas-focus-gained'); }, finfocuslost: function(e) { this.dispatchNewEvent(e, 'fin-canvas-focus-lost'); }, fincontextmenu: function(e) { if (e.ctrlKey && this.currentKeys.indexOf('CTRL') === -1) { this.currentKeys.push('CTRL'); } this.dispatchNewMouseKeysEvent(e, 'fin-canvas-context-menu', { isRightClick: this.isRightClick(e) }); }, fintouchstart: function(e) { this.dispatchNewTouchEvent(e, 'fin-canvas-touchstart'); }, fintouchmove: function(e) { this.dispatchNewTouchEvent(e, 'fin-canvas-touchmove'); }, fintouchend: function(e) { this.dispatchNewTouchEvent(e, 'fin-canvas-touchend'); }, paintLoopRunning: function() { return !!paintRequest; }, requestRepaint: function() { this.dirty = true; }, repaint: function() { this.requestRepaint(); if (!paintRequest || this.component.properties.repaintIntervalRate === 0) { this.paintNow(); } }, getMouseLocation: function() { return this.mouseLocation; }, getOrigin: function() { var rect = this.getBoundingClientRect(this.canvas); var p = new rectangular.Point(rect.left, rect.top); return p; }, getLocal: function(e) { var rect = this.getBoundingClientRect(this.canvas); var p = new rectangular.Point( e.clientX / this.bodyZoomFactor - rect.left, e.clientY / this.bodyZoomFactor - rect.top ); return p; }, hasFocus: function() { return document.activeElement === this.canvas; }, takeFocus: function() { var self = this; if (!this.hasFocus()) { setTimeout(function() { self.canvas.focus(); }, 10); } }, beDragging: function() { this.dragging = true; this.disableDocumentElementSelection(); }, beNotDragging: function() { this.dragging = false; this.enableDocumentElementSelection(); }, isDragging: function() { return this.dragging; }, disableDocumentElementSelection: function() { var style = document.body.style; style.cssText = style.cssText + '-webkit-user-select: none'; }, enableDocumentElementSelection: function() { var style = document.body.style; style.cssText = style.cssText.replace('-webkit-user-select: none', ''); }, setFocusable: function(truthy) { this.focuser.style.display = truthy ? '' : 'none'; }, isRightClick: function(e) { var isRightMB; e = e || window.event; if ('which' in e) { // Gecko (Firefox), WebKit (Safari/Chrome) & Opera isRightMB = e.which === 3; } else if ('button' in e) { // IE, Opera isRightMB = e.button === 2; } return isRightMB; }, dispatchEvent: function(e) { return this.canvas.dispatchEvent(e); }, setInfo: function(message, width) { if (message) { if (width !== undefined) { if (width && !isNaN(Number(width))) { width += 'px'; } this.infoDiv.style.width = width; } if (message.indexOf('<')) { this.infoDiv.innerHTML = message; } else { this.infoDiv.innerText = message; } } this.infoDiv.style.display = message ? 'block' : 'none'; } }; function paintLoopFunction(now) { if (paintRequest) { paintables.forEach(function(paintable) { try { paintable.tickPainter(now); } catch (e) { console.error(e); } if (paintable.component.tickNotification) { paintable.component.tickNotification(); } }); paintRequest = requestAnimationFrame(paintLoopFunction); } } function restartPaintLoop() { paintRequest = paintRequest || requestAnimationFrame(paintLoopFunction); } function stopPaintLoop() { if (paintRequest) { cancelAnimationFrame(paintRequest); paintRequest = undefined; } } restartPaintLoop(); function resizablesLoopFunction(now) { if (resizeInterval) { for (var i = 0; i < resizables.length; i++) { try { resizables[i].tickResizer(now); } catch (e) { console.error(e); } } } } function restartResizeLoop() { resizeInterval = resizeInterval || setInterval(resizablesLoopFunction, RESIZE_POLLING_INTERVAL); } function stopResizeLoop() { if (resizeInterval) { clearInterval(resizeInterval); resizeInterval = undefined; } } restartResizeLoop(); function makeCharMap() { var map = []; var empty = ['', '']; for (var i = 0; i < 256; i++) { map[i] = empty; } map[27] = ['ESC', 'ESCSHIFT']; map[192] = ['`', '~']; map[49] = ['1', '!']; map[50] = ['2', '@']; map[51] = ['3', '#']; map[52] = ['4', '$']; map[53] = ['5', '%']; map[54] = ['6', '^']; map[55] = ['7', '&']; map[56] = ['8', '*']; map[57] = ['9', '(']; map[48] = ['0', ')']; map[189] = ['-', '_']; map[187] = ['=', '+']; map[8] = ['BACKSPACE', 'BACKSPACESHIFT']; map[46] = ['DELETE', 'DELETESHIFT']; map[9] = ['TAB', 'TABSHIFT']; map[81] = ['q', 'Q']; map[87] = ['w', 'W']; map[69] = ['e', 'E']; map[82] = ['r', 'R']; map[84] = ['t', 'T']; map[89] = ['y', 'Y']; map[85] = ['u', 'U']; map[73] = ['i', 'I']; map[79] = ['o', 'O']; map[80] = ['p', 'P']; map[219] = ['[', '{']; map[221] = [']', '}']; map[220] = ['\\', '|']; map[220] = ['CAPSLOCK', 'CAPSLOCKSHIFT']; map[65] = ['a', 'A']; map[83] = ['s', 'S']; map[68] = ['d', 'D']; map[70] = ['f', 'F']; map[71] = ['g', 'G']; map[72] = ['h', 'H']; map[74] = ['j', 'J']; map[75] = ['k', 'K']; map[76] = ['l', 'L']; map[186] = [';', ':']; map[222] = ['\'', '|']; map[13] = ['RETURN', 'RETURNSHIFT']; map[16] = ['SHIFT', 'SHIFT']; map[90] = ['z', 'Z']; map[88] = ['x', 'X']; map[67] = ['c', 'C']; map[86] = ['v', 'V']; map[66] = ['b', 'B']; map[78] = ['n', 'N']; map[77] = ['m', 'M']; map[188] = [',', '<']; map[190] = ['.', '>']; map[191] = ['/', '?']; map[16] = ['SHIFT', 'SHIFT']; map[17] = ['CTRL', 'CTRLSHIFT']; map[18] = ['ALT', 'ALTSHIFT']; map[91] = ['COMMANDLEFT', 'COMMANDLEFTSHIFT']; map[32] = ['SPACE', 'SPACESHIFT']; map[93] = ['COMMANDRIGHT', 'COMMANDRIGHTSHIFT']; map[18] = ['ALT', 'ALTSHIFT']; map[38] = ['UP', 'UPSHIFT']; map[37] = ['LEFT', 'LEFTSHIFT']; map[40] = ['DOWN', 'DOWNSHIFT']; map[39] = ['RIGHT', 'RIGHTSHIFT']; map[33] = ['PAGEUP', 'PAGEUPSHIFT']; map[34] = ['PAGEDOWN', 'PAGEDOWNSHIFT']; map[35] = ['PAGERIGHT', 'PAGERIGHTSHIFT']; // END map[36] = ['PAGELEFT', 'PAGELEFTSHIFT']; // HOME map[112] = ['F1', 'F1SHIFT']; map[113] = ['F2', 'F2SHIFT']; map[114] = ['F3', 'F3SHIFT']; map[115] = ['F4', 'F4SHIFT']; map[116] = ['F5', 'F5SHIFT']; map[117] = ['F6', 'F6SHIFT']; map[118] = ['F7', 'F7SHIFT']; map[119] = ['F8', 'F8SHIFT']; map[120] = ['F9', 'F9SHIFT']; map[121] = ['F10', 'F10SHIFT']; map[122] = ['F11', 'F11SHIFT']; map[123] = ['F12', 'F12SHIFT']; return map; } function updateCurrentKeys(e, keydown) { var keyChar = this.getKeyChar(e); // prevent TAB from moving focus off the canvas element switch (keyChar) { case 'TAB': case 'TABSHIFT': case 'Tab': e.preventDefault(); } fixCurrentKeys.call(this, keyChar, keydown); return keyChar; } function fixCurrentKeys(keyChar, keydown) { var index = this.currentKeys.indexOf(keyChar); if (!keydown && index >= 0) { this.currentKeys.splice(index, 1); } if (keyChar === 'SHIFT') { // on keydown, replace unshifted keys with shifted keys // on keyup, vice-versa this.currentKeys.forEach(function(key, index, currentKeys) { var pair = charMap.find(function(pair) { return pair[keydown ? 0 : 1] === key; }); if (pair) { currentKeys[index] = pair[keydown ? 1 : 0]; } }); } if (keydown && index < 0) { this.currentKeys.push(keyChar); } } function defKeysProp(event, propName, object) { var canvas = this; Object.defineProperty(object, propName, { configurable: true, ennumerable: true, get: function() { var shiftKey; if ('shiftKey' in event) { fixCurrentKeys.call(canvas, 'SHIFT', shiftKey = event.shiftKey); } else { shiftKey = canvas.currentKeys.indexOf('SHIFT') >= 0; } var SHIFT = shiftKey ? 'SHIFT' : ''; if ('ctrlKey' in event) { fixCurrentKeys.call(canvas, 'CTRL' + SHIFT, event.ctrlKey); } if ('altKey' in event) { fixCurrentKeys.call(canvas, 'ALT' + SHIFT, event.altKey); } return canvas.currentKeys.slice(); } }); return object; } function getCachedContext(canvasElement, contextAttributes) { var gc = canvasElement.getContext('2d', contextAttributes), props = {}, values = {}; // Stub out all the prototype members of the canvas 2D graphics context: Object.keys(Object.getPrototypeOf(gc)).forEach(makeStub); // Some older browsers (e.g., Chrome 40) did not have all members of canvas // 2D graphics context in the prototype so we make this additional call: Object.keys(gc).forEach(makeStub); function makeStub(key) { if ( !(key in props) && !/^(webkit|moz|ms|o)[A-Z]/.test(key) && typeof gc[key] !== 'function' ) { Object.defineProperty(props, key, { get: function() { return (values[key] = values[key] || gc[key]); }, set: function(value) { if (value !== values[key]) { gc[key] = values[key] = value; } } }); } } gc.cache = props; gc.cache.save = function() { gc.save(); values = Object.create(values); }; gc.cache.restore = function() { gc.restore(); values = Object.getPrototypeOf(values); }; gc.conditionalsStack = []; Object.getOwnPropertyNames(Canvas.graphicsContextAliases).forEach(function(alias) { gc[alias] = gc[Canvas.graphicsContextAliases[alias]]; }); return Object.assign(gc, require('./graphics')); } Canvas.graphicsContextAliases = { simpleText: 'fillText' }; module.exports = Canvas;