export function sleep(delay = 100) { return Promise.resolve({ then(resolve) { setTimeout(resolve, delay); } }); } export function promisfy(fn) { return new Promise((resolve, reject) => fn(resolve, reject)); } /** * Calls a method in current Handsontable instance, returns its output * @param method * @return {Function} */ export function handsontableMethodFactory(method) { return function(...args) { let instance; try { instance = spec().$container.handsontable('getInstance'); } catch (err) { /* eslint-disable */ console.error(err); /* eslint-enable */ } if (instance) { if (method === 'destroy') { spec().$container.removeData(); } } else { if (method === 'destroy') { return; // we can forgive this... maybe it was destroyed in the test } throw new Error('Something wrong with the test spec: Handsontable instance not found'); } return instance[method](...args); }; } export const addHook = handsontableMethodFactory('addHook'); export const alter = handsontableMethodFactory('alter'); export const colToProp = handsontableMethodFactory('colToProp'); export const countCols = handsontableMethodFactory('countCols'); export const countEmptyCols = handsontableMethodFactory('countEmptyCols'); export const countEmptyRows = handsontableMethodFactory('countEmptyRows'); export const countRows = handsontableMethodFactory('countRows'); export const countSourceCols = handsontableMethodFactory('countSourceCols'); export const countSourceRows = handsontableMethodFactory('countSourceRows'); export const deselectCell = handsontableMethodFactory('deselectCell'); export const destroy = handsontableMethodFactory('destroy'); export const destroyEditor = handsontableMethodFactory('destroyEditor'); export const emptySelectedCells = handsontableMethodFactory('emptySelectedCells'); export const getActiveEditor = handsontableMethodFactory('getActiveEditor'); export const getCell = handsontableMethodFactory('getCell'); export const getCellEditor = handsontableMethodFactory('getCellEditor'); export const getCellMeta = handsontableMethodFactory('getCellMeta'); export const getCellMetaAtRow = handsontableMethodFactory('getCellMetaAtRow'); export const getCellRenderer = handsontableMethodFactory('getCellRenderer'); export const getCellsMeta = handsontableMethodFactory('getCellsMeta'); export const getCellValidator = handsontableMethodFactory('getCellValidator'); export const getColHeader = handsontableMethodFactory('getColHeader'); export const getCopyableData = handsontableMethodFactory('getCopyableData'); export const getCopyableText = handsontableMethodFactory('getCopyableText'); export const getData = handsontableMethodFactory('getData'); export const getDataAtCell = handsontableMethodFactory('getDataAtCell'); export const getDataAtCol = handsontableMethodFactory('getDataAtCol'); export const getDataAtRow = handsontableMethodFactory('getDataAtRow'); export const getDataAtRowProp = handsontableMethodFactory('getDataAtRowProp'); export const getDataType = handsontableMethodFactory('getDataType'); export const getInstance = handsontableMethodFactory('getInstance'); export const getPlugin = handsontableMethodFactory('getPlugin'); export const getRowHeader = handsontableMethodFactory('getRowHeader'); export const getSelected = handsontableMethodFactory('getSelected'); export const getSelectedLast = handsontableMethodFactory('getSelectedLast'); export const getSelectedRange = handsontableMethodFactory('getSelectedRange'); export const getSelectedRangeLast = handsontableMethodFactory('getSelectedRangeLast'); export const getSourceData = handsontableMethodFactory('getSourceData'); export const getSourceDataArray = handsontableMethodFactory('getSourceDataArray'); export const getSourceDataAtCell = handsontableMethodFactory('getSourceDataAtCell'); export const getSourceDataAtCol = handsontableMethodFactory('getSourceDataAtCol'); export const getSourceDataAtRow = handsontableMethodFactory('getSourceDataAtRow'); export const getValue = handsontableMethodFactory('getValue'); export const loadData = handsontableMethodFactory('loadData'); export const populateFromArray = handsontableMethodFactory('populateFromArray'); export const propToCol = handsontableMethodFactory('propToCol'); export const removeCellMeta = handsontableMethodFactory('removeCellMeta'); export const render = handsontableMethodFactory('render'); export const selectAll = handsontableMethodFactory('selectAll'); export const selectCell = handsontableMethodFactory('selectCell'); export const selectCells = handsontableMethodFactory('selectCells'); export const selectColumns = handsontableMethodFactory('selectColumns'); export const selectRows = handsontableMethodFactory('selectRows'); export const setCellMeta = handsontableMethodFactory('setCellMeta'); export const setDataAtCell = handsontableMethodFactory('setDataAtCell'); export const setDataAtRowProp = handsontableMethodFactory('setDataAtRowProp'); export const spliceCellsMeta = handsontableMethodFactory('spliceCellsMeta'); export const spliceCol = handsontableMethodFactory('spliceCol'); export const spliceRow = handsontableMethodFactory('spliceRow'); export const updateSettings = handsontableMethodFactory('updateSettings'); export const undo = handsontableMethodFactory('undo'); const specContext = {}; beforeEach(function() { specContext.spec = this; }); afterEach(() => { specContext.spec = null; }); export function spec() { return specContext.spec; } export function hot() { return spec().$container.data('handsontable'); } export function handsontable(options) { const currentSpec = spec(); currentSpec.$container.handsontable(options); currentSpec.$container[0].focus(); // otherwise TextEditor tests do not pass in IE8 return currentSpec.$container.data('handsontable'); } /** * As for v. 0.11 the only scrolling method is native scroll, which creates copies of main htCore table inside of the container. * Therefore, simple $(".htCore") will return more than one object. Most of the time, you're interested in the original * htCore, not the copies made by native scroll. * * This method returns the original htCore object * * @returns {jqObject} reference to the original htCore */ export function getHtCore() { return spec().$container.find('.htCore').first(); } export function getMaster() { return spec().$container.find('.ht_master'); } export function getTopClone() { return spec().$container.find('.ht_clone_top'); } export function getTopLeftClone() { return spec().$container.find('.ht_clone_top_left_corner'); } // for compatybility // const getCornerClone = getTopLeftClone; export function getLeftClone() { return spec().$container.find('.ht_clone_left'); } export function getBottomClone() { return spec().$container.find('.ht_clone_bottom'); } export function getBottomLeftClone() { return spec().$container.find('.ht_clone_bottom_left_corner'); } // Rename me to countTD export function countCells() { return getHtCore().find('tbody td').length; } export function isEditorVisible(editableElement) { if (editableElement && !(editableElement.hasClass('handsontableInput') || editableElement.hasClass('handsontableEditor'))) { throw new Error('Editable element of the editor was not found.'); } const keyProxyHolder = (editableElement || keyProxy()).parent(); if (keyProxyHolder.size() === 0) { return false; } const css = cssProp => keyProxyHolder.css(cssProp); return css('z-index') !== '-1' && css('top') !== '-9999px' && css('left') !== '-9999px'; } export function isFillHandleVisible() { return !!spec().$container.find('.wtBorder.corner:visible').length; } export function getCorrespondingOverlay(cell, container) { const overlay = $(cell).parents('.handsontable'); if (overlay[0] === container[0]) { return $('.ht_master'); } return $(overlay[0]); } /** * Shows context menu */ export function contextMenu(cell) { const hotInstance = spec().$container.data('handsontable'); let clickedCell = cell; let selected = hotInstance.getSelectedLast(); if (!selected) { hotInstance.selectCell(0, 0); selected = hotInstance.getSelectedLast(); } if (!clickedCell) { clickedCell = getCell(selected[0], selected[1]); } const cellOffset = $(clickedCell).offset(); $(clickedCell).simulate('mousedown', { button: 2 }); $(clickedCell).simulate('contextmenu', { clientX: cellOffset.left - Handsontable.dom.getWindowScrollLeft(), clientY: cellOffset.top - Handsontable.dom.getWindowScrollTop(), }); // Chrome doesn't call `mouseup`. // $(cell).simulate('mouseup', { button: 2 }); } export function closeContextMenu() { $(document).simulate('mousedown'); // $(document).trigger('mousedown'); } /** * Shows dropdown menu */ export function dropdownMenu(columnIndex) { const hotInstance = spec().$container.data('handsontable'); const th = hotInstance.view.wt.wtTable.getColumnHeader(columnIndex || 0); const button = th.querySelector('.changeType'); if (button) { $(button).simulate('mousedown'); $(button).simulate('click'); } } export function closeDropdownMenu() { $(document).simulate('mousedown'); } export function dropdownMenuRootElement() { const plugin = hot().getPlugin('dropdownMenu'); let root; if (plugin && plugin.menu) { root = plugin.menu.container; } return root; } /** * Returns a function that triggers a mouse event * @param {String} type Event type * @return {Function} */ export function handsontableMouseTriggerFactory(type, button) { return function(element) { let handsontableElement = element; if (!(handsontableElement instanceof jQuery)) { handsontableElement = $(handsontableElement); } const ev = $.Event(type); ev.which = button || 1; // left click by default handsontableElement.simulate(type, ev); }; } export const mouseDown = handsontableMouseTriggerFactory('mousedown'); export const mouseMove = handsontableMouseTriggerFactory('mousemove'); export const mouseOver = handsontableMouseTriggerFactory('mouseover'); export const mouseUp = handsontableMouseTriggerFactory('mouseup'); export function mouseDoubleClick(element) { mouseDown(element); mouseUp(element); mouseDown(element); mouseUp(element); } export const mouseRightDown = handsontableMouseTriggerFactory('mousedown', 3); export const mouseRightUp = handsontableMouseTriggerFactory('mouseup', 3); /** * Returns a function that triggers a key event * @param {String} type Event type * @return {Function} */ export function handsontableKeyTriggerFactory(type) { return function(key, extend) { const ev = {}; // $.Event(type); let keyToTrigger = key; if (typeof keyToTrigger === 'string') { if (keyToTrigger.indexOf('shift+') > -1) { keyToTrigger = keyToTrigger.substring(6); ev.shiftKey = true; } if (keyToTrigger.indexOf('ctrl+') > -1) { keyToTrigger = keyToTrigger.substring(5); ev.ctrlKey = true; ev.metaKey = true; } switch (keyToTrigger) { case 'tab': ev.keyCode = 9; break; case 'enter': ev.keyCode = 13; break; case 'esc': ev.keyCode = 27; break; case 'f2': ev.keyCode = 113; break; case 'arrow_left': ev.keyCode = 37; break; case 'arrow_up': ev.keyCode = 38; break; case 'arrow_right': ev.keyCode = 39; break; case 'arrow_down': ev.keyCode = 40; break; case 'ctrl': if (window.navigator.platform.includes('Mac')) { ev.keyCode = 91; } else { ev.keyCode = 17; } break; case 'shift': ev.keyCode = 16; break; case 'backspace': ev.keyCode = 8; break; case 'delete': ev.keyCode = 46; break; case 'space': ev.keyCode = 32; break; case 'x': ev.keyCode = 88; break; case 'c': ev.keyCode = 67; break; case 'v': ev.keyCode = 86; break; case 'a': ev.keyCode = 65; break; default: throw new Error(`Unrecognised key name: ${keyToTrigger}`); } } else if (typeof keyToTrigger === 'number') { ev.keyCode = keyToTrigger; } // ev.originalEvent = {}; //needed as long Handsontable searches for event.originalEvent $.extend(ev, extend); $(document.activeElement).simulate(type, ev); }; } export const keyDown = handsontableKeyTriggerFactory('keydown'); export const keyUp = handsontableKeyTriggerFactory('keyup'); /** * Presses keyDown, then keyUp */ export function keyDownUp(key, extend) { if (typeof key === 'string' && key.indexOf('shift+') > -1) { keyDown('shift'); } keyDown(key, extend); keyUp(key, extend); if (typeof key === 'string' && key.indexOf('shift+') > -1) { keyUp('shift'); } } /** * Returns current value of the keyboard proxy textarea * @return {String} */ export function keyProxy() { return spec().$container.find('textarea.handsontableInput'); } export function serveImmediatePropagation(event) { if ((event !== null || event !== void 0) && (event.isImmediatePropagationEnabled === null || event.isImmediatePropagationEnabled === void 0)) { event.stopImmediatePropagation = function() { this.isImmediatePropagationEnabled = false; this.cancelBubble = true; }; event.isImmediatePropagationEnabled = true; event.isImmediatePropagationStopped = function() { return !this.isImmediatePropagationEnabled; }; } return event; } export function autocompleteEditor() { return spec().$container.find('.handsontableInput'); } /** * Sets text cursor inside keyboard proxy */ export function setCaretPosition(pos) { const el = keyProxy()[0]; if (el.setSelectionRange) { el.focus(); el.setSelectionRange(pos, pos); } else if (el.createTextRange) { const range = el.createTextRange(); range.collapse(true); range.moveEnd('character', pos); range.moveStart('character', pos); range.select(); } } /** * Returns autocomplete instance */ export function autocomplete() { return spec().$container.find('.autocompleteEditor'); } /** * Triggers paste string on current selection */ export function triggerPaste(str) { spec().$container.data('handsontable').getPlugin('CopyPaste').paste(str); } /** * Returns column width for HOT container * @param $elem * @param col * @returns {Number} */ export function colWidth($elem, col) { const TR = $elem[0].querySelector('TBODY TR'); let cell; if (TR) { cell = TR.querySelectorAll('TD')[col]; } else { cell = $elem[0].querySelector('THEAD TR').querySelectorAll('TH')[col]; } if (!cell) { throw new Error(`Cannot find table column of index '${col}'`); } return cell.offsetWidth; } /** * Returns row height for HOT container * @param $elem * @param row * @returns {Number} */ export function rowHeight($elem, row) { let TD; if (row >= 0) { TD = $elem[0].querySelector(`tbody tr:nth-child(${row + 1}) td`); } else { TD = $elem[0].querySelector(`thead tr:nth-child(${Math.abs(row)})`); } if (!TD) { throw new Error(`Cannot find table row of index '${row}'`); } return Handsontable.dom.outerHeight(TD); } /** * Returns value that has been rendered in table cell * @param {Number} trIndex * @param {Number} tdIndex * @returns {String} */ export function getRenderedValue(trIndex, tdIndex) { return spec().$container.find('tbody tr').eq(trIndex).find('td').eq(tdIndex).html(); } /** * Returns nodes that have been rendered in table cell * @param {Number} trIndex * @param {Number} tdIndex * @returns {String} */ export function getRenderedContent(trIndex, tdIndex) { return spec().$container.find('tbody tr').eq(trIndex).find('td').eq(tdIndex).children(); } /** * Create numerical data values for the table * @param rowCount * @param colCount * @returns {Array} */ export function createNumericData(rowCount, colCount) { const rowsMax = typeof rowCount === 'number' ? rowCount : 100; const columnsMax = typeof colCount === 'number' ? colCount : 4; const rows = []; let i; let j; for (i = 0; i < rowsMax; i++) { const row = []; for (j = 0; j < columnsMax; j++) { row.push((i + 1)); } rows.push(row); } return rows; } /** * Model factory, which creates object with private properties, accessible by setters and getters. * Created for the purpose of testing HOT with Backbone-like Models * @param opts * @returns {{}} * @constructor */ export function Model(opts) { const obj = {}; const _data = $.extend({ id: undefined, name: undefined, address: undefined }, opts); obj.attr = function(name, value) { if (typeof value === 'undefined') { return this.get(name); } return this.set(name, value); }; obj.get = function(name) { return _data[name]; }; obj.set = function(name, value) { _data[name] = value; return this; }; return obj; } /** * Factory which produces an accessor for objects of type "Model" (see above). * This function should be used to create accessor for a given property name and pass it as `data` option in column * configuration. * * @param name - name of the property for which an accessor function will be created * @returns {Function} */ export function createAccessorForProperty(name) { return function(obj, value) { return obj.attr(name, value); }; } export function resizeColumn(displayedColumnIndex, width) { const $container = spec().$container; const $th = $container.find(`thead tr:eq(0) th:eq(${displayedColumnIndex})`); $th.simulate('mouseover'); const $resizer = $container.find('.manualColumnResizer'); const resizerPosition = $resizer.position(); $resizer.simulate('mousedown', { clientX: resizerPosition.left, }); const delta = width - $th.width() - 2; const newPosition = resizerPosition.left + delta; $resizer.simulate('mousemove', { clientX: newPosition }); $resizer.simulate('mouseup'); } export function resizeRow(displayedRowIndex, height) { const $container = spec().$container; const $th = $container.find(`tbody tr:eq(${displayedRowIndex}) th:eq(0)`); $th.simulate('mouseover'); const $resizer = $container.find('.manualRowResizer'); const resizerPosition = $resizer.position(); $resizer.simulate('mousedown', { clientY: resizerPosition.top }); let delta = height - $th.height() - 2; if (delta < 0) { delta = 0; } $resizer.simulate('mousemove', { clientY: resizerPosition.top + delta }); $resizer.simulate('mouseup'); } export function moveSecondDisplayedRowBeforeFirstRow(container, secondDisplayedRowIndex) { const $mainContainer = container.parents('.handsontable').not('[class*=clone]').not('[class*=master]').first(); const $rowHeaders = container.find('tbody tr th'); const $firstRowHeader = $rowHeaders.eq(secondDisplayedRowIndex - 1); const $secondRowHeader = $rowHeaders.eq(secondDisplayedRowIndex); $secondRowHeader.simulate('mouseover'); const $manualRowMover = $mainContainer.find('.manualRowMover'); if ($manualRowMover.length) { $manualRowMover.simulate('mousedown', { clientY: $manualRowMover[0].getBoundingClientRect().top }); $manualRowMover.simulate('mousemove', { clientY: $manualRowMover[0].getBoundingClientRect().top - 20 }); $firstRowHeader.simulate('mouseover'); $secondRowHeader.simulate('mouseup'); } } export function moveFirstDisplayedRowAfterSecondRow(container, firstDisplayedRowIndex) { const $mainContainer = container.parents('.handsontable').not('[class*=clone]').not('[class*=master]').first(); const $rowHeaders = container.find('tbody tr th'); const $firstRowHeader = $rowHeaders.eq(firstDisplayedRowIndex); const $secondRowHeader = $rowHeaders.eq(firstDisplayedRowIndex + 1); $secondRowHeader.simulate('mouseover'); const $manualRowMover = $mainContainer.find('.manualRowMover'); if ($manualRowMover.length) { $manualRowMover.simulate('mousedown', { clientY: $manualRowMover[0].getBoundingClientRect().top }); $manualRowMover.simulate('mousemove', { clientY: $manualRowMover[0].getBoundingClientRect().top + 20 }); $firstRowHeader.simulate('mouseover'); $secondRowHeader.simulate('mouseup'); } } export function swapDisplayedColumns(container, from, to) { const $mainContainer = container.parents('.handsontable').not('[class*=clone]').not('[class*=master]').first(); const $colHeaders = container.find('thead tr:eq(0) th'); const $to = $colHeaders.eq(to); const $from = $colHeaders.eq(from); // Enter the second column header $from.simulate('mouseover'); const $manualColumnMover = $mainContainer.find('.manualColumnMover'); // Grab the second column $manualColumnMover.simulate('mousedown', { pageX: $manualColumnMover[0].getBoundingClientRect().left, }); // Drag the second column over the first column $manualColumnMover.simulate('mousemove', { pageX: $manualColumnMover[0].getBoundingClientRect().left - 20, }); $to.simulate('mouseover'); // Drop the second column $from.simulate('mouseup'); } export function triggerTouchEvent(type, target, pageX, pageY) { const e = document.createEvent('TouchEvent'); const targetCoords = target.getBoundingClientRect(); const targetPageX = pageX || parseInt(targetCoords.left + 3, 10); const targetPageY = pageY || parseInt(targetCoords.top + 3, 10); let touches; let targetTouches; let changedTouches; const touch = document.createTouch(window, target, 0, targetPageX, targetPageY, targetPageX, targetPageY); if (type === 'touchend') { touches = document.createTouchList(); targetTouches = document.createTouchList(); changedTouches = document.createTouchList(touch); } else { touches = document.createTouchList(touch); targetTouches = document.createTouchList(touch); changedTouches = document.createTouchList(touch); } e.initTouchEvent(type, true, true, window, null, 0, 0, 0, 0, false, false, false, false, touches, targetTouches, changedTouches, 1, 0); target.dispatchEvent(e); } export function createSpreadsheetData(...args) { return Handsontable.helper.createSpreadsheetData(...args); }