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.

1,096 lines (1,094 loc) 136 kB
/*jslint browser: true*/ /*globals Event: false, describe: false, afterEach: false, beforeEach: false, after: false, it: false, canvasDatagrid: false, async: false, requestAnimationFrame: false*/ (function () { 'use strict'; var kcs = { up: 38, down: 40, left: 37, right: 39, enter: 13, tab: 9, space: 32, pgup: 33, pgdown: 34, a: 65, esc: 27 }, blocks = '██████████████████', c = { b: 'rgb(0, 0, 255)', y: 'rgb(255, 255, 0)', r: 'rgb(255, 0, 0)', fu: 'rgb(255, 0, 255)', white: 'rgb(255, 255, 255)', black: 'rgb(0, 0, 0)' }, markerColors = [ '#a50026', '#d73027', '#f46d43', '#fdae61', '#fee090', '#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695' ], smallData = function () { return [ {col1: 'foo', col2: 0, col3: 'a'}, {col1: 'bar', col2: 1, col3: 'b'}, {col1: 'baz', col2: 2, col3: 'c'} ]; }; function getC(v) { return Object.keys(c).filter(function (k) { return c[k] === v; })[0] || v; } function itoa(n) { var ordA = 'a'.charCodeAt(0), ordZ = 'z'.charCodeAt(0), len = ordZ - ordA + 1, s = ''; while (n >= 0) { s = String.fromCharCode(n % len + ordA) + s; n = Math.floor(n / len) - 1; } return s; } function makeData(r, c, dFn) { var y, x, d = []; for (y = 0; y < r; y += 1) { d[y] = {}; for (x = 0; x < c; x += 1) { d[y][itoa(x)] = dFn ? dFn(y, x) : ''; } } return d; } function cleanup(done) { //HACK: this allows for DOM events to cool off? setTimeout(done, 2); var m = document.getElementById('mocha'); m.scrollTop = m.scrollHeight; if (this.currentTest && this.currentTest.grid) { this.currentTest.grid.disposeContextMenu(); } } function marker(grid, x, y) { grid.markerCount = grid.markerCount || 0; grid.markerCount += 1; grid.addEventListener('afterdraw', function () { grid.ctx.fillStyle = markerColors[(grid.markerCount + (markerColors.length / 2)) % markerColors.length]; grid.ctx.fillRect(0, y, grid.canvas.width, 1); grid.ctx.fillRect(x, 0, 1, grid.canvas.height); grid.ctx.fillStyle = markerColors[(grid.markerCount) % markerColors.length]; grid.ctx.fillRect(x - 1, y - 1, 3, 3); }); } function assertPxColorFn(grid, x, y, expected) { var d, match, e; x = x * window.devicePixelRatio; y = y * window.devicePixelRatio; return function (callback) { function f() { d = grid.ctx.getImageData(x, y, 1, 1).data; d = 'rgb(' + [d['0'], d['1'], d['2']].join(', ') + ')'; match = d === expected; if (expected !== undefined) { e = new Error('Expected color ' + getC(expected) + ' but got color ' + getC(d)); if (expected && !match) { console.error(e); } if (callback) { marker(grid, x, y); return callback(expected && !match ? e : undefined); } } requestAnimationFrame(grid.draw); return d; } if (!callback) { return f(); } f(); }; } function assertPxColor(grid, x, y, expected, callback) { return assertPxColorFn(grid, x, y, expected)(callback); } function de(el, event, args) { var e = new Event(event); Object.keys(args).forEach(function (key) { e[key] = args[key]; }); el.dispatchEvent(e); } function keydown(el, keyCode, args) { args = args || {}; args.keyCode = keyCode; de(el, 'keydown', args); } function bb(el) { return el.getBoundingClientRect(); } function mouseup(el, x, y, bbEl) { var p = bb(bbEl || el); de(el, 'mouseup', {clientX: x + p.left, clientY: y + p.top }); } function mousemove(el, x, y, bbEl) { var p = bb(bbEl || el); de(el, 'mousemove', {clientX: x + p.left, clientY: y + p.top }); } function mousedown(el, x, y, bbEl) { var p = bb(bbEl || el); de(el, 'mousedown', {clientX: x + p.left, clientY: y + p.top }); } function contextmenu(el, x, y, bbEl) { var p = bb(bbEl || el); de(el, 'contextmenu', {clientX: x + p.left, clientY: y + p.top }); } function touchstart(el, x, y, bbEl) { var p = bb(bbEl || el); de(el, 'touchstart', {touches: [{clientX: x + p.left, clientY: y + p.top }], changedTouches: [{x: 0, y: 0}]}); } function touchend(el, x, y, bbEl) { var p = bb(bbEl || el); de(el, 'touchend', {touches: [{clientX: x + p.left, clientY: y + p.top }], changedTouches: [{x: 0, y: 0}]}); } function touchcancel(el, x, y, bbEl) { var p = bb(bbEl || el); de(el, 'touchcancel', {touches: [{clientX: x + p.left, clientY: y + p.top }], changedTouches: [{x: 0, y: 0}]}); } function touchmove(el, x, y, bbEl) { var p = bb(bbEl || el); de(el, 'touchmove', {touches: [{clientX: x + p.left, clientY: y + p.top }], changedTouches: [{x: 0, y: 0}]}); } function click(el, x, y, bbEl, ev) { var p = bb(bbEl || el); ev = ev || {}; ev.clientX = x + p.left; ev.clientY = y + p.top; de(el, 'click', ev); } function dblclick(el, x, y, bbEl) { var p = bb(bbEl || el); de(el, 'dblclick', {clientX: x + p.left, clientY: y + p.top }); } function g(args) { var grid, i = document.getElementById('grid'), a = document.createElement('div'), t = document.createElement('div'), d = document.createElement('div'); a.className = 'test-container'; d.className = 'grid-container'; t.className = 'grid-test-title'; t.innerHTML = args.test.title; function poll() { setTimeout(function () { if (args.test.state === 'failed') { t.classList.add('grid-test-failed'); grid.draw(); } else if (args.test.state === 'passed') { t.classList.add('grid-test-passed'); grid.draw(); } else { poll(); } }, 10); } poll(); delete args.testTitle; a.appendChild(t); a.appendChild(d); // i.appendChild(a); i.insertBefore(a, i.firstChild); args = args || {}; args.parentNode = d; if (args.component) { grid = document.createElement('canvas-datagrid'); d.appendChild(grid); Object.keys(args).forEach(function (arg) { if (arg === 'parentNode') { return; } grid[arg] = args[arg]; }); } else { grid = canvasDatagrid(args); } args.test.grid = grid; return grid; } function assertIf(cond, msg) { var x; for (x = 2; x < arguments.length; x += 1) { msg = msg.replace(/%s|%n/, arguments[x]); } if (cond) { return new Error(msg); } } describe('canvas-datagrid', function () { after(function (done) { // git rid of lingering artifacts from the run mouseup(document.body, 1, 1); mouseup(document.body, 1, 1); click(document.body, 1, 1); done(); }); beforeEach(cleanup); afterEach(cleanup); describe('Integration Tests', function () { describe('Instantiation', function () { it('Should be callable without arguments.', function (done) { canvasDatagrid(); done(); }); it('Should create an instance of datagrid', function (done) { var grid = g({test: this.test}); assertIf(!grid, 'Expected a grid instance, instead got something false'); grid.style.gridBackgroundColor = c.y; assertPxColor(grid, 80, 32, c.y, done); }); it('Should create, then completely annihilate the grid.', function (done) { var grid = g({test: this.test}); grid.dispose(); done(assertIf(!grid.parentNode, 'Expected to see the grid gone, it is not.')); }); it('Should create a grid and set data, data should be visible.', function (done) { var grid = g({ test: this.test, data: smallData() }); grid.style.activeCellBackgroundColor = c.white; assertIf(grid.data.length !== 3, 'Expected to see data in the interface.'); assertPxColor(grid, 80, 32, c.white, done); }); }); if (window.customElements) { describe('Web component', function () { it('Should create a web component, set a style', function (done) { var grid = g({ test: this.test, data: smallData(), component: true }); grid.style.activeCellBackgroundColor = c.white; assertIf(grid.data.length !== 3, 'Expected to see data in the interface.'); assertPxColor(grid, 80, 32, c.white, done); }); it('Should create a web component and set a hyphenated style', function (done) { var grid = g({ test: this.test, data: smallData(), component: true }); grid.style['active-cell-background-color'] = c.white; assertIf(grid.data.length !== 3, 'Expected to see data in the interface.'); assertPxColor(grid, 80, 32, c.white, done); }); it('Should create a web component and set a hyphenated style with a custom prefix', function (done) { var grid = g({ test: this.test, data: smallData(), component: true }); grid.style['-cdg-active-cell-background-color'] = c.white; assertIf(grid.data.length !== 3, 'Expected to see data in the interface.'); assertPxColor(grid, 80, 32, c.white, done); }); it('Should create a web component and set a schema', function (done) { var grid = g({ test: this.test, data: [{a: blocks}], component: true }); grid.style.gridBackgroundColor = c.white; grid.schema = [{name: 'a', width: 30}]; assertIf(grid.data.length !== 1, 'Expected to see data in the interface.'); assertPxColor(grid, 80, 32, c.white, done); }); }); } describe('Drawing', function () { it('Should draw row selections.', function (done) { var grid = g({ test: this.test, data: smallData(), selectionMode: 'row', style: { activeCellSelectedBackgroundColor: c.b, cellSelectedBackgroundColor: c.b } }); mousemove(grid.canvas, 45, 37); mousedown(grid.canvas, 45, 37); mouseup(grid.canvas, 45, 37); setTimeout(function () { assertPxColor(grid, 80, 37, c.b, done); }, 1); }); it('Should draw a debug message.', function (done) { var grid = g({ test: this.test, data: smallData(), debug: true }); grid.style.activeCellBackgroundColor = c.b; mousemove(grid.canvas, 100, 113); assertPxColor(grid, 120, 10, 'rgb(90, 90, 90)', done); }); // TODO: after phantomjs has been replaced, re-enable this test. // phantom throws a nonsense error due to the way the data url is constructed in the html function // it('Should draw HTML.', function (done) { // var grid = g({ // test: this.test, // data: [{a: '<span style="background: ' + c.b + ';color: ' + c.b + '">blah</span>' }], // schema: [{name: 'a', type: 'html'}] // }); // setTimeout(function () { // assertPxColor(grid, 50, 32, c.b, done); // }, 100); // }); }); describe('Styles', function () { it('Should set the active cell color to black.', function (done) { var grid = g({ test: this.test, data: smallData() }); grid.style.activeCellBackgroundColor = c.black; assertPxColor(grid, 100, 32, c.black, done); }); }); describe('Data interface', function () { it('Pass array of objects.', function (done) { var grid = g({ test: this.test }); grid.data = [ {'a': 0, 'b': 1, 'c': 2}, {'a': 4, 'b': 5, 'c': 6}, {'a': 7, 'b': 8, 'c': 9} ]; done(assertIf(grid.data[2].c !== 9, 'Expected grid to be able to import and export this format')); }); it('Pass array that contain other arrays of objects.', function (done) { var grid = g({ test: this.test }); grid.data = [ {'a': 0, 'b': 1, 'c': 2}, {'a': 4, 'b': [ {'a': 0, 'b': 1, 'c': 2}, {'a': 4, 'b': 5, 'c': 6}, {'a': 7, 'b': 8, 'c': 9} ], 'c': 6}, {'a': 7, 'b': 8, 'c': 9} ]; //TODO: this test cannot work until cell grids are fixed https://github.com/TonyGermaneri/canvas-datagrid/issues/35 // so this test success is false done(); }); it('Pass array that contains an array of objects with mixed object/primitives as values.', function (done) { var grid = g({ test: this.test }); grid.data = [ {'a': 0, 'b': 1, 'c': 2}, {'a': 4, 'b': {'a': 0, 'b': 1, 'c': 2}, 'c': 6}, {'a': 7, 'b': 8, 'c': 9} ]; //TODO: this test cannot work until cell grids are fixed https://github.com/TonyGermaneri/canvas-datagrid/issues/35 // so this test success is false done(); }); it('Pass jagged data', function (done) { var grid = g({ test: this.test }); grid.data = [['a', 'b', 'c'], ['1', '2'], ['q']]; done(assertIf(grid.data[0][0] !== 'a', 'Expected grid to be able to import and export this format')); }); it('Pass string to data', function (done) { var grid = g({ test: this.test }); grid.data = "blah"; done(assertIf(grid.data[0][0] !== 'blah', 'Expected grid to be able to import and export this format')); }); it('Pass number to data', function (done) { var grid = g({ test: this.test }); grid.data = 4235234234; done(assertIf(grid.data[0][0] !== 4235234234, 'Expected grid to be able to import and export this format')); }); it('Pass boolean to data', function (done) { var grid = g({ test: this.test }); grid.data = false; done(assertIf(grid.data[0][0] !== false, 'Expected grid to be able to import and export this format')); }); it('Pass a function to data', function (done) { var grid = g({ test: this.test }); grid.data = function () { return [ {'a': 0, 'b': 1, 'c': 2}, {'a': 4, 'b': 5, 'c': 6}, {'a': 7, 'b': 8, 'c': 9} ]; }; done(assertIf(grid.data[0].a !== 0, 'Expected grid to be able to import and export this format')); }); it('Pass an async function to data', function (done) { var grid = g({ test: this.test }); grid.data = function (callback) { return callback([ {'a': 0, 'b': 1, 'c': 2}, {'a': 4, 'b': 5, 'c': 6}, {'a': 7, 'b': 8, 'c': 9} ]); }; done(assertIf(grid.data[0].a !== 0, 'Expected grid to be able to import and export this format')); }); it('Pass an async function to data', function (done) { var grid = g({ test: this.test }); grid.data = function (callback) { return callback([ {'a': 0, 'b': 1, 'c': 2}, {'a': 4, 'b': 5, 'c': 6}, {'a': 7, 'b': 8, 'c': 9} ]); }; done(assertIf(grid.data[0].a !== 0, 'Expected grid to be able to import and export this format')); }); }); describe('Public interface', function () { it('Focus on the grid', function (done) { var grid = g({ test: this.test, data: smallData() }); grid.focus(); done(assertIf(!grid.hasFocus, 'Expected the grid to have focus')); }); it('Blur the grid', function (done) { var grid = g({ test: this.test, data: smallData() }); grid.blur(); done(assertIf(grid.hasFocus, 'Expected the grid to not have focus')); }); it('Insert column', function (done) { var grid = g({ test: this.test, data: [{d: '', e: ''}], schema: [{name: 'd'}, {name: 'e'}] }); grid.insertColumn({ name: 'f', defaultValue: 'g' }, 1); done(assertIf(grid.schema[1].name !== 'f' || grid.data[0].f !== 'g', 'Expected to see a specific column here, it is not here.')); }); it('Use a function as a default value', function (done) { var grid = g({ test: this.test, data: [{d: '', e: ''}], schema: [{name: 'd'}, {name: 'e'}] }); grid.insertColumn({ name: 'f', defaultValue: function () { return 'g'; } }, 1); done(assertIf(grid.schema[1].name !== 'f' || grid.data[0].f !== 'g', 'Expected to see a specific column here, it is not here.')); }); it('Autosize a column', function (done) { var grid = g({ test: this.test, data: [{d: '123456', e: '123456'}] }); grid.addEventListener('rendercell', function (e) { if (e.cell.columnIndex === 1) { e.ctx.fillStyle = c.b; } }); grid.autosize('d'); assertPxColor(grid, 200, 32, c.b, done); }); it('Autosize all columns', function (done) { var grid = g({ test: this.test, data: [{d: '123456', e: '123456'}], style: { gridBackgroundColor: c.b } }); grid.autosize(); assertPxColor(grid, 220, 32, c.b, done); }); it('Add a style to the style setter', function (done) { var grid = g({ test: this.test, data: [{d: '123456', e: '123456'}] }); grid.style.gridBackgroundColor = c.b; assertPxColor(grid, 200, 70, c.b, done); }); it('Add an attribute to the attribute setter', function (done) { var grid = g({ test: this.test, data: [{d: '123456', e: '123456'}] }); grid.addEventListener('attributechanged', function (e) { done(assertIf(grid.attributes.name !== 'blah', 'expected name to be blah')); }); grid.attributes.name = 'blah'; }); it('Get visible schema', function (done) { var grid = g({ test: this.test, data: [{d: '123456', e: '123456'}] }); done(assertIf(grid.visibleSchema[0].name !== 'd', 'Expected schema to be returned')); }); it('Get visible rows', function (done) { var grid = g({ test: this.test, data: [{d: '123456', e: '123456'}] }); done(assertIf(grid.visibleRows.length === 1, 'Expected 1 row to be returned')); }); it('Get visible cells', function (done) { var grid = g({ test: this.test, data: [{d: '123456', e: '123456'}] }); done(assertIf(grid.visibleCells.length === 2, 'Expected 2 cells to be returned')); }); it('Get current cell', function (done) { var grid = g({ test: this.test, data: [{d: '123456', e: '123456'}] }); mousemove(grid.canvas, 45, 37); mousedown(grid.canvas, 45, 37); mouseup(grid.canvas, 45, 37); done(assertIf(grid.currentCell.rowIndex !== 0, 'Expected current cell to be rowIndex 0')); }); it('Get offsetLeft of the parent node', function (done) { var grid = g({ test: this.test, data: [{d: '123456', e: '123456'}] }); done(assertIf(grid.offsetLeft === 0, 'Expected offsetLeft to be > 0')); }); it('Get offsetTop of the parent node', function (done) { var grid = g({ test: this.test, data: [{d: '123456', e: '123456'}] }); done(assertIf(grid.offsetTop === 0, 'Expected offsetLeft to be > 0')); }); it('Get the offsetParent node', function (done) { var grid = g({ test: this.test, data: [{d: '123456', e: '123456'}] }); done(assertIf(grid.offsetParent === undefined, 'Expected a DOM node')); }); it('Should throw an error if insertColumn is passed a bad index', function (done) { var e, grid = g({ test: this.test, data: [{d: '', e: ''}], schema: [{name: 'd'}, {name: 'e'}] }); try { grid.insertColumn({ name: 'f', defaultValue: 'g' }, 5000); } catch (er) { e = er; } finally { done(assertIf(e === undefined, 'Expected insertColumn to throw an error.')); } }); it('Delete column', function (done) { var grid = g({ test: this.test, data: [{d: '', e: ''}] }), n = Object.keys(smallData()[0])[0]; grid.deleteColumn(0); done(assertIf(Object.keys(grid.data[0])[0] === n || grid.schema[0].name === n, 'Expected to see column 0 deleted, but it appears to still be there.')); }); it('Add column', function (done) { var l, grid = g({ test: this.test, data: [{d: '', e: ''}], schema: [{name: 'd'}, {name: 'e'}] }); grid.addColumn({ name: 'f', defaultValue: 'g' }); l = grid.schema.length - 1; done(assertIf(grid.schema[l].name !== 'f' || grid.data[0].f !== 'g', 'Expected to see a specific column here, it is not here.')); }); it('Add row', function (done) { var l, grid = g({ test: this.test, data: [{d: '', e: ''}], schema: [{name: 'd'}, {name: 'e', defaultValue: 10}] }); grid.addRow({d: '1'}); l = grid.data.length - 1; done(assertIf(grid.data[l].d !== '1' || grid.data[l].e !== 10, 'Expected to see a specific row here, it is not here.')); }); it('Insert row', function (done) { var grid = g({ test: this.test, data: [{d: '1', e: '2'}, {d: '3', e: '4'}], schema: [{name: 'd'}, {name: 'e', defaultValue: 10}] }); grid.insertRow({d: '6'}, 1); done(assertIf(grid.data[2].d !== '3' || grid.data[1].e !== 10, 'Expected to see a specific row here, it is not here.')); }); it('Should throw an error if insertRow is passed a bad index', function (done) { var e, grid = g({ test: this.test, data: [{d: '', e: ''}], schema: [{name: 'd'}, {name: 'e'}] }); try { grid.insertRow({d: '6'}, 5000); } catch (er) { e = er; } finally { done(assertIf(e === undefined, 'Expected insertRow to throw an error.')); } }); it('Delete row', function (done) { var grid = g({ test: this.test, data: [{d: '1'}, {d: '2'}] }); grid.deleteRow(1); done(assertIf(grid.data.length !== 1 || grid.data[0].d !== '1', 'Expected to see only 1 row, expected row 1 to contain a specific value.')); }); it('Set row height', function (done) { var grid = g({ test: this.test, data: smallData() }); grid.addEventListener('rendercell', function (e) { if (e.cell.rowIndex === 0) { e.ctx.fillStyle = c.y; } }); grid.setRowHeight(0, 60); assertPxColor(grid, 40, 80, c.y, done); }); it('Set column width', function (done) { var grid = g({ test: this.test, data: smallData() }); grid.addEventListener('rendercell', function (e) { if (e.cell.columnIndex === 0) { e.ctx.fillStyle = c.y; } }); grid.setColumnWidth(0, 10); setTimeout(function () { assertPxColor(grid, 35, 78, c.y, done); }, 1); }); it('Reset row height', function (done) { var grid = g({ test: this.test, data: smallData() }); grid.addEventListener('rendercell', function (e) { if (e.cell.rowIndex !== 0) { e.ctx.fillStyle = c.y; } }); grid.setRowHeight(0, 60); grid.resetRowHeights(); assertPxColor(grid, 90, 80, c.y, done); }); it('Reset column width', function (done) { var grid = g({ test: this.test, data: smallData() }); grid.addEventListener('rendercell', function (e) { if (e.cell.columnIndex === 1) { e.ctx.fillStyle = c.y; } }); grid.setColumnWidth(0, 10); grid.resetColumnWidths(); assertPxColor(grid, 340, 80, c.y, done); }); it('Inserting an impossible column index should throw an error', function (done) { var err, grid = g({ test: this.test, data: [{d: '', e: ''}], schema: [{name: 'd'}, {name: 'e'}] }); try { grid.insertColumn({ name: 'f', defaultValue: 'g' }, 999); } catch (e) { err = e; } done(assertIf(!err, 'Expected to see an error.')); }); it('Inserting an impossible row index should throw an error', function (done) { var err, grid = g({ test: this.test, data: [{d: '', e: ''}], schema: [{name: 'd'}, {name: 'e'}] }); try { grid.insertrow({ d: 'f', e: 'g' }, 999); } catch (e) { err = e; } done(assertIf(!err, 'Expected to see an error.')); }); it('Goto a specific row', function (done) { var grid = g({ test: this.test, data: makeData(30, 30) }); grid.gotoRow(20); done(assertIf(grid.scrollTop < 1, 'Expected scrollTop to be a little further along.')); }); it('Fit column size to values', function (done) { var doneCalled, grid = g({ test: this.test, data: makeData(30, 30, function (y, x) { return x + ':' + y; }) }); grid.fitColumnToValues('a'); grid.addEventListener('rendercell', function (e) { if (e.cell.rowIndex === 0 && e.cell.columnIndex === 0) { if (doneCalled) { return; } doneCalled = true; done(assertIf(e.cell.width > 100, 'Expected column to be a little narrower.')); } }); grid.draw(); }); it('isCellVisible should return true when cell is visible, false when it is not', function (done) { var grid = g({ test: this.test, data: makeData(30, 30, function (y, x) { return x + ':' + y; }) }); done(assertIf(!grid.isCellVisible({x: 0, y: 0}) || grid.isCellVisible({x: 0, y: 20}), 'Expected column to be a little narrower.')); }); }); describe('Context menu', function () { it('Should produce a context menu', function (done) { var grid = g({ test: this.test, data: smallData() }); grid.addEventListener('contextmenu', function (e) { setTimeout(function () { done(assertIf(!document.body.contains(e.items[0].title), 'Expected context menu to exist in the body and be visible.')); }, 1); }); contextmenu(grid.canvas, 60, 37); }); it('Clicking Order by asc should order the selected column asc', function (done) { var grid = g({ test: this.test, data: smallData() }); grid.addEventListener('contextmenu', function (e) { setTimeout(function () { e.items[4].contextItemContainer.dispatchEvent(new Event('click')); done(assertIf(grid.data[0].col1 !== 'bar', 'Expected the content to be reordered asc.')); }, 1); }); contextmenu(grid.canvas, 100, 37); }); it('Should produce a context menu very wide requiring the context menu move to be fully visible', function (done) { var d = [], x, grid = g({ test: this.test, data: smallData() }); for (x = 0; x < 100; x += 1) { d.push({ title: 'veryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverywide' }); } grid.addEventListener('contextmenu', function (e) { e.items.push({ title: 'Child menu', items: function () { return d; } }); setTimeout(function () { e.items[6].contextItemContainer.dispatchEvent(new Event('mouseover')); setTimeout(function () { done(assertIf(!e.items[6].contextMenu.container, 'Expected child context menu.')); }, 1); }, 1); }); contextmenu(grid.canvas, 60, 37); }); it('Should create a child context menu using a function that returns items', function (done) { var d = [], x, grid = g({ test: this.test, data: smallData() }); for (x = 0; x < 100; x += 1) { d.push({ title: x }); } grid.addEventListener('contextmenu', function (e) { e.items.push({ title: 'Child menu', items: function () { return d; } }); setTimeout(function () { e.items[6].contextItemContainer.dispatchEvent(new Event('mouseover')); setTimeout(function () { done(assertIf(!e.items[6].contextMenu.container, 'Expected child context menu.')); }, 1); }, 1); }); contextmenu(grid.canvas, 60, 37); }); it('Should create a child context menu using a function that uses a callback argument', function (done) { var d = [], x, grid = g({ test: this.test, data: smallData() }); for (x = 0; x < 100; x += 1) { d.push({ title: x }); } grid.addEventListener('contextmenu', function (e) { e.items.push({ title: 'Child menu', items: function (callback) { return callback(d); } }); setTimeout(function () { e.items[6].contextItemContainer.dispatchEvent(new Event('mouseover')); setTimeout(function () { done(assertIf(!e.items[6].contextMenu.container, 'Expected child context menu.')); }, 1); }, 1); }); contextmenu(grid.canvas, 60, 37); }); it('Create a child context menu and scroll up and down using mouseover events, then exit menu', function (done) { var d = [], x, grid = g({ test: this.test, data: smallData() }); for (x = 0; x < 100; x += 1) { d.push({ title: x }); } grid.addEventListener('contextmenu', function (e) { e.items.push({ title: 'child menu', items: d }); setTimeout(function () { e.items[6].contextItemContainer.dispatchEvent(new Event('mouseover')); e.items[6].contextMenu.downArrow.dispatchEvent(new Event('mouseover')); setTimeout(function () { var err = assertIf(e.items[6].contextMenu.container.scrollTop === 0); if (err) { return done(err); } e.items[6].contextMenu.downArrow.dispatchEvent(new Event('mouseout')); e.items[6].contextMenu.upArrow.dispatchEvent(new Event('mouseover')); setTimeout(function () { e.items[6].contextMenu.upArrow.dispatchEvent(new Event('mouseout')); err = assertIf(e.items[6].contextMenu.container.scrollTop !== 0); if (err) { return done(err); } setTimeout(function () { e.items[6].contextItemContainer.dispatchEvent(new Event('mouseout')); done(assertIf(e.items[6].contextMenu !== undefined, 'expected child context menu to be gone.')); }, 100); }, 1500); }, 1000); }, 1); }); contextmenu(grid.canvas, 60, 37); }).timeout(5000); it('Autocomplete should appear when a value is entered into the filter input', function (done) { var grid = g({ test: this.test, data: smallData() }); grid.addEventListener('contextmenu', function (e) { setTimeout(function () { //HACK: get to filter input element in context menu var i = e.items[0].title.children[1]; i.value = 'f'; i.dispatchEvent(new Event('keyup')); done(assertIf(document.body.lastChild.childNodes.length === 1 && document.body.lastChild.firstChild.innerHTML !== 'foo', 'Expected the autocomplete to be the most recent item added to body and expected it to only contain "foo"')); }, 1); }); contextmenu(grid.canvas, 100, 37); }); it('Autocomplete keys should key down and filter', function (done) { var err, grid = g({ test: this.test, data: smallData() }); grid.addEventListener('contextmenu', function (e) { setTimeout(function () { //HACK: get to filter input element in context menu var i = e.items[0].title.children[1]; i.value = 'b'; i.dispatchEvent(new Event('keyup')); ['down', 'enter'].forEach(function (kk) { var ev = new Event('keydown'); ev.keyCode = kcs[kk]; i.dispatchEvent(ev); if (kk === 'enter') { err = assertIf(grid.data[0].col1 !== 'baz', 'Expected key combination to filter for baz'); } }); done(err); }, 1); }); contextmenu(grid.canvas, 100, 37); }); it('Autocomplete keys should key down, key up and filter', function (done) { var err, grid = g({ test: this.test, data: smallData() }); grid.addEventListener('contextmenu', function (e) { setTimeout(function () { //HACK: get to filter input element in context menu var i = e.items[0].title.children[1]; i.value = 'b'; i.dispatchEvent(new Event('keyup')); ['down', 'up', 'enter'].forEach(function (kk) { var ev = new Event('keydown'); ev.keyCode = kcs[kk]; i.dispatchEvent(ev); if (kk === 'enter') { err = assertIf(grid.data[0].col1 !== 'bar', 'Expected key combination to filter for bar'); } }); done(err); }, 1); }); contextmenu(grid.canvas, 100, 37); }); it('Autocomplete keys should key tab', function (done) { var err, grid = g({ test: this.test, data: smallData() }); grid.addEventListener('contextmenu', function (e) { setTimeout(function () { //HACK: get to filter input element in context menu var i = e.items[0].title.children[1]; i.value = 'f'; i.dispatchEvent(new Event('keyup')); ['tab'].forEach(function (kk) { var ev = new Event('keydown'); ev.keyCode = kcs[kk]; i.dispatchEvent(ev); if (kk === 'tab') { err = assertIf(grid.data[0].col1 !== 'foo', 'Expected key combination to filter for bar'); } }); done(err); }, 1); }); contextmenu(grid.canvas, 100, 37); }); it('Autocomplete keys should key esc', function (done) { var err, grid = g({ test: this.test, data: smallData() }); grid.addEventListener('contextmenu', function (e) { setTimeout(function () { //HACK: get to filter input element in context menu var i = e.items[0].title.children[1]; i.value = 'f'; i.dispatchEvent(new Event('keyup')); ['esc'].forEach(function (kk) { var ev = new Event('keydown'); ev.keyCode = kcs[kk]; i.dispatchEvent(ev); if (kk === 'esc') { err = assertIf(grid.data[0].col1 !== 'foo', 'Expected key combination to filter for bar'); } }); done(err); }, 1); }); contextmenu(grid.canvas, 100, 37); });