├── .bowerrc ├── .config ├── base.js ├── bin │ └── trigger-on-stdout-change.js ├── development.js ├── languages-development.js ├── languages-production.js ├── loader │ ├── empty-loader.js │ └── exports-to-window-loader.js ├── plugin │ ├── jasmine-html.js │ └── template.ejs ├── production.js ├── test-e2e.js ├── test-mobile.js ├── test-production.js ├── test-walkontable.js ├── walkontable.js └── watch.js ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CNAME ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── bower.json ├── dist ├── README.md ├── handsontable.css ├── handsontable.css.map ├── handsontable.full.css ├── handsontable.full.js ├── handsontable.full.min.css ├── handsontable.full.min.js ├── handsontable.js ├── handsontable.js.map ├── handsontable.min.css ├── handsontable.min.js ├── languages │ ├── all.js │ ├── all.min.js │ ├── de-CH.js │ ├── de-CH.min.js │ ├── de-DE.js │ ├── de-DE.min.js │ ├── en-US.js │ ├── en-US.min.js │ ├── es-MX.js │ ├── es-MX.min.js │ ├── fr-FR.js │ ├── fr-FR.min.js │ ├── it-IT.js │ ├── it-IT.min.js │ ├── ja-JP.js │ ├── ja-JP.min.js │ ├── ko-KR.js │ ├── ko-KR.min.js │ ├── lv-LV.js │ ├── lv-LV.min.js │ ├── nb-NO.js │ ├── nb-NO.min.js │ ├── nl-NL.js │ ├── nl-NL.min.js │ ├── pl-PL.js │ ├── pl-PL.min.js │ ├── pt-BR.js │ ├── pt-BR.min.js │ ├── ru-RU.js │ ├── ru-RU.min.js │ ├── zh-CN.js │ ├── zh-CN.min.js │ ├── zh-TW.js │ └── zh-TW.min.js ├── moment │ ├── LICENSE │ ├── locale │ │ ├── af.js │ │ ├── ar-dz.js │ │ ├── ar-kw.js │ │ ├── ar-ly.js │ │ ├── ar-ma.js │ │ ├── ar-sa.js │ │ ├── ar-tn.js │ │ ├── ar.js │ │ ├── az.js │ │ ├── be.js │ │ ├── bg.js │ │ ├── bm.js │ │ ├── bn.js │ │ ├── bo.js │ │ ├── br.js │ │ ├── bs.js │ │ ├── ca.js │ │ ├── cs.js │ │ ├── cv.js │ │ ├── cy.js │ │ ├── da.js │ │ ├── de-at.js │ │ ├── de-ch.js │ │ ├── de.js │ │ ├── dv.js │ │ ├── el.js │ │ ├── en-au.js │ │ ├── en-ca.js │ │ ├── en-gb.js │ │ ├── en-ie.js │ │ ├── en-nz.js │ │ ├── eo.js │ │ ├── es-do.js │ │ ├── es-us.js │ │ ├── es.js │ │ ├── et.js │ │ ├── eu.js │ │ ├── fa.js │ │ ├── fi.js │ │ ├── fo.js │ │ ├── fr-ca.js │ │ ├── fr-ch.js │ │ ├── fr.js │ │ ├── fy.js │ │ ├── gd.js │ │ ├── gl.js │ │ ├── gom-latn.js │ │ ├── gu.js │ │ ├── he.js │ │ ├── hi.js │ │ ├── hr.js │ │ ├── hu.js │ │ ├── hy-am.js │ │ ├── id.js │ │ ├── is.js │ │ ├── it.js │ │ ├── ja.js │ │ ├── jv.js │ │ ├── ka.js │ │ ├── kk.js │ │ ├── km.js │ │ ├── kn.js │ │ ├── ko.js │ │ ├── ky.js │ │ ├── lb.js │ │ ├── lo.js │ │ ├── lt.js │ │ ├── lv.js │ │ ├── me.js │ │ ├── mi.js │ │ ├── mk.js │ │ ├── ml.js │ │ ├── mr.js │ │ ├── ms-my.js │ │ ├── ms.js │ │ ├── mt.js │ │ ├── my.js │ │ ├── nb.js │ │ ├── ne.js │ │ ├── nl-be.js │ │ ├── nl.js │ │ ├── nn.js │ │ ├── pa-in.js │ │ ├── pl.js │ │ ├── pt-br.js │ │ ├── pt.js │ │ ├── ro.js │ │ ├── ru.js │ │ ├── sd.js │ │ ├── se.js │ │ ├── si.js │ │ ├── sk.js │ │ ├── sl.js │ │ ├── sq.js │ │ ├── sr-cyrl.js │ │ ├── sr.js │ │ ├── ss.js │ │ ├── sv.js │ │ ├── sw.js │ │ ├── ta.js │ │ ├── te.js │ │ ├── tet.js │ │ ├── th.js │ │ ├── tl-ph.js │ │ ├── tlh.js │ │ ├── tr.js │ │ ├── tzl.js │ │ ├── tzm-latn.js │ │ ├── tzm.js │ │ ├── uk.js │ │ ├── ur.js │ │ ├── uz-latn.js │ │ ├── uz.js │ │ ├── vi.js │ │ ├── x-pseudo.js │ │ ├── yo.js │ │ ├── zh-cn.js │ │ ├── zh-hk.js │ │ └── zh-tw.js │ └── moment.js ├── numbro │ ├── LICENSE │ ├── LICENSE-Numeraljs │ ├── languages.min.js │ ├── languages │ │ ├── bg.min.js │ │ ├── cs-CZ.min.js │ │ ├── da-DK.min.js │ │ ├── de-AT.min.js │ │ ├── de-CH.min.js │ │ ├── de-DE.min.js │ │ ├── de-LI.min.js │ │ ├── el.min.js │ │ ├── en-AU.min.js │ │ ├── en-GB.min.js │ │ ├── en-IE.min.js │ │ ├── en-NZ.min.js │ │ ├── en-ZA.min.js │ │ ├── es-AR.min.js │ │ ├── es-CL.min.js │ │ ├── es-CO.min.js │ │ ├── es-CR.min.js │ │ ├── es-ES.min.js │ │ ├── es-MX.min.js │ │ ├── es-NI.min.js │ │ ├── es-PE.min.js │ │ ├── es-PR.min.js │ │ ├── es-SV.min.js │ │ ├── et-EE.min.js │ │ ├── fa-IR.min.js │ │ ├── fi-FI.min.js │ │ ├── fil-PH.min.js │ │ ├── fr-CA.min.js │ │ ├── fr-CH.min.js │ │ ├── fr-FR.min.js │ │ ├── he-IL.min.js │ │ ├── hu-HU.min.js │ │ ├── id.min.js │ │ ├── it-CH.min.js │ │ ├── it-IT.min.js │ │ ├── ja-JP.min.js │ │ ├── ko-KR.min.js │ │ ├── lv-LV.min.js │ │ ├── nb-NO.min.js │ │ ├── nb.min.js │ │ ├── nl-BE.min.js │ │ ├── nl-NL.min.js │ │ ├── nn.min.js │ │ ├── pl-PL.min.js │ │ ├── pt-BR.min.js │ │ ├── pt-PT.min.js │ │ ├── ro-RO.min.js │ │ ├── ro.min.js │ │ ├── ru-RU.min.js │ │ ├── ru-UA.min.js │ │ ├── sk-SK.min.js │ │ ├── sl.min.js │ │ ├── sr-Cyrl-RS.min.js │ │ ├── sv-SE.min.js │ │ ├── th-TH.min.js │ │ ├── tr-TR.min.js │ │ ├── uk-UA.min.js │ │ ├── zh-CN.min.js │ │ ├── zh-MO.min.js │ │ ├── zh-SG.min.js │ │ └── zh-TW.min.js │ └── numbro.js └── pikaday │ ├── LICENSE │ ├── pikaday.css │ └── pikaday.js ├── handsontable.d.ts ├── handsontable.jquery.json ├── hot.config.js ├── languages ├── all.js ├── de-CH.js ├── de-DE.js ├── en-US.js ├── es-MX.js ├── fr-FR.js ├── index.js ├── it-IT.js ├── ja-JP.js ├── ko-KR.js ├── lv-LV.js ├── nb-NO.js ├── nl-NL.js ├── pl-PL.js ├── pt-BR.js ├── ru-RU.js ├── zh-CN.js └── zh-TW.js ├── lib ├── SheetClip │ └── SheetClip.js ├── autoResize │ └── autoResize.js └── jsonpatch │ └── json-patch-duplex.js ├── package.json ├── src ├── 3rdparty │ └── walkontable │ │ ├── css │ │ ├── bootstrap.css │ │ └── walkontable.css │ │ ├── package.json │ │ ├── src │ │ ├── border.js │ │ ├── calculator │ │ │ ├── viewportColumns.js │ │ │ └── viewportRows.js │ │ ├── cell │ │ │ ├── coords.js │ │ │ └── range.js │ │ ├── core.js │ │ ├── event.js │ │ ├── filter │ │ │ ├── column.js │ │ │ └── row.js │ │ ├── index.js │ │ ├── overlay │ │ │ ├── _base.js │ │ │ ├── bottom.js │ │ │ ├── bottomLeftCorner.js │ │ │ ├── debug.js │ │ │ ├── left.js │ │ │ ├── top.js │ │ │ └── topLeftCorner.js │ │ ├── overlays.js │ │ ├── scroll.js │ │ ├── selection.js │ │ ├── settings.js │ │ ├── table.js │ │ ├── tableRenderer.js │ │ └── viewport.js │ │ └── test │ │ ├── SpecRunner.html │ │ ├── helpers │ │ ├── common.js │ │ ├── index.js │ │ └── jasmine-bridge-reporter.js │ │ ├── lib │ │ ├── jquery.min.js │ │ └── jquery.simulate.js │ │ └── spec │ │ ├── border.spec.js │ │ ├── calculator │ │ ├── viewportColumns.spec.js │ │ └── viewportRows.spec.js │ │ ├── cell │ │ ├── coords.spec.js │ │ └── range.spec.js │ │ ├── core.spec.js │ │ ├── event.spec.js │ │ ├── filter │ │ ├── column.spec.js │ │ └── row.spec.js │ │ ├── index.js │ │ ├── scroll.spec.js │ │ ├── scrollbar.spec.js │ │ ├── scrollbarNative.spec.js │ │ ├── selection.spec.js │ │ ├── settings │ │ ├── columnHeaders.spec.js │ │ ├── preventOverflow.spec.js │ │ ├── rowHeaders.spec.js │ │ └── stretchH.spec.js │ │ └── table.spec.js ├── cellTypes │ ├── autocompleteType.js │ ├── checkboxType.js │ ├── dateType.js │ ├── dropdownType.js │ ├── handsontableType.js │ ├── index.js │ ├── numericType.js │ ├── passwordType.js │ ├── textType.js │ └── timeType.js ├── core.js ├── css │ ├── bootstrap.css │ ├── handsontable.css │ └── mobile.handsontable.css ├── dataMap.js ├── dataSource.js ├── defaultSettings.js ├── editorManager.js ├── editors │ ├── _baseEditor.js │ ├── autocompleteEditor.js │ ├── checkboxEditor.js │ ├── dateEditor.js │ ├── dropdownEditor.js │ ├── handsontableEditor.js │ ├── index.js │ ├── numericEditor.js │ ├── passwordEditor.js │ ├── selectEditor.js │ └── textEditor.js ├── eventManager.js ├── helpers │ ├── array.js │ ├── browser.js │ ├── console.js │ ├── data.js │ ├── date.js │ ├── dom │ │ ├── element.js │ │ └── event.js │ ├── feature.js │ ├── function.js │ ├── mixed.js │ ├── number.js │ ├── object.js │ ├── setting.js │ ├── string.js │ ├── templateLiteralTag.js │ ├── unicode.js │ └── wrappers │ │ └── jquery.js ├── i18n │ ├── constants.js │ ├── dictionariesManager.js │ ├── index.js │ ├── languages │ │ ├── de-CH.js │ │ ├── de-DE.js │ │ ├── en-US.js │ │ ├── es-MX.js │ │ ├── fr-FR.js │ │ ├── index.js │ │ ├── it-IT.js │ │ ├── ja-JP.js │ │ ├── ko-KR.js │ │ ├── lv-LV.js │ │ ├── nb-NO.js │ │ ├── nl-NL.js │ │ ├── pl-PL.js │ │ ├── pt-BR.js │ │ ├── ru-RU.js │ │ ├── zh-CN.js │ │ └── zh-TW.js │ ├── phraseFormatters │ │ ├── index.js │ │ ├── pluralize.js │ │ └── substituteVariables.js │ └── utils.js ├── index.js ├── mixins │ ├── arrayMapper.js │ ├── localHooks.js │ └── stateSaver.js ├── multiMap.js ├── pluginHooks.js ├── plugins.js ├── plugins │ ├── _base.js │ ├── autoColumnSize │ │ ├── autoColumnSize.js │ │ └── test │ │ │ └── autoColumnSize.e2e.js │ ├── autoRowSize │ │ ├── autoRowSize.js │ │ └── test │ │ │ └── autoRowSize.e2e.js │ ├── autofill │ │ ├── autofill.js │ │ ├── test │ │ │ └── autofill.e2e.js │ │ └── utils.js │ ├── columnSorting │ │ ├── columnSorting.js │ │ ├── columnStatesManager.js │ │ ├── domHelpers.js │ │ ├── rootComparator.js │ │ ├── rowsMapper.js │ │ ├── sortFunction │ │ │ ├── date.js │ │ │ ├── default.js │ │ │ └── numeric.js │ │ ├── sortService │ │ │ ├── engine.js │ │ │ ├── index.js │ │ │ └── registry.js │ │ ├── test │ │ │ ├── columnSorting.e2e.js │ │ │ ├── columnSorting.types.ts │ │ │ ├── columnStatesManager.unit.js │ │ │ ├── domHelpers.unit.js │ │ │ ├── sortFunction │ │ │ │ ├── date.unit.js │ │ │ │ ├── default.unit.js │ │ │ │ └── numeric.unit.js │ │ │ └── utils.unit.js │ │ └── utils.js │ ├── comments │ │ ├── commentEditor.js │ │ ├── comments.css │ │ ├── comments.js │ │ ├── displaySwitch.js │ │ └── test │ │ │ ├── comments.e2e.js │ │ │ └── displaySwitch.unit.js │ ├── contextMenu │ │ ├── commandExecutor.js │ │ ├── contextMenu.css │ │ ├── contextMenu.js │ │ ├── cursor.js │ │ ├── itemsFactory.js │ │ ├── menu.js │ │ ├── predefinedItems.js │ │ ├── predefinedItems │ │ │ ├── alignment.js │ │ │ ├── clearColumn.js │ │ │ ├── columnLeft.js │ │ │ ├── columnRight.js │ │ │ ├── readOnly.js │ │ │ ├── redo.js │ │ │ ├── removeColumn.js │ │ │ ├── removeRow.js │ │ │ ├── rowAbove.js │ │ │ ├── rowBelow.js │ │ │ ├── separator.js │ │ │ └── undo.js │ │ ├── test │ │ │ ├── contextMenu.e2e.js │ │ │ ├── cursor.unit.js │ │ │ └── predefinedItems │ │ │ │ ├── alignment.e2e.js │ │ │ │ ├── readOnly.e2e.js │ │ │ │ ├── removeColumn.e2e.js │ │ │ │ └── removeRow.e2e.js │ │ └── utils.js │ ├── copyPaste │ │ ├── clipboardData.js │ │ ├── contextMenuItem │ │ │ ├── copy.js │ │ │ └── cut.js │ │ ├── copyPaste.css │ │ ├── copyPaste.js │ │ ├── focusableElement.js │ │ ├── pasteEvent.js │ │ ├── test │ │ │ ├── copyPaste.e2e.js │ │ │ ├── copyPaste.types.ts │ │ │ └── focusableElement.unit.js │ │ └── utils.js │ ├── customBorders │ │ ├── contextMenuItem │ │ │ ├── bottom.js │ │ │ ├── index.js │ │ │ ├── left.js │ │ │ ├── noBorders.js │ │ │ ├── right.js │ │ │ └── top.js │ │ ├── customBorders.js │ │ ├── test │ │ │ └── customBorders.e2e.js │ │ └── utils.js │ ├── dragToScroll │ │ ├── dragToScroll.js │ │ └── test │ │ │ └── dragToScroll.e2e.js │ ├── index.js │ ├── manualColumnFreeze │ │ ├── contextMenuItem │ │ │ ├── freezeColumn.js │ │ │ └── unfreezeColumn.js │ │ ├── manualColumnFreeze.css │ │ ├── manualColumnFreeze.js │ │ └── test │ │ │ └── manualColumnFreeze.e2e.js │ ├── manualColumnMove │ │ ├── columnsMapper.js │ │ ├── manualColumnMove.css │ │ ├── manualColumnMove.js │ │ ├── test │ │ │ ├── columnsMapper.unit.js │ │ │ ├── manualColumnMove.e2e.js │ │ │ └── manualColumnMoveUI.e2e.js │ │ └── ui │ │ │ ├── _base.js │ │ │ ├── backlight.js │ │ │ └── guideline.js │ ├── manualColumnResize │ │ ├── manualColumnResize.js │ │ └── test │ │ │ └── manualColumnResize.e2e.js │ ├── manualRowMove │ │ ├── manualRowMove.css │ │ ├── manualRowMove.js │ │ ├── rowsMapper.js │ │ ├── test │ │ │ ├── manualRowMove.e2e.js │ │ │ ├── manualRowMoveUI.e2e.js │ │ │ └── rowsMapper.unit.js │ │ └── ui │ │ │ ├── _base.js │ │ │ ├── backlight.js │ │ │ └── guideline.js │ ├── manualRowResize │ │ ├── manualRowResize.js │ │ └── test │ │ │ └── manualRowResize.e2e.js │ ├── mergeCells │ │ ├── calculations │ │ │ ├── autofill.js │ │ │ └── selection.js │ │ ├── cellCoords.js │ │ ├── cellsCollection.js │ │ ├── contextMenuItem │ │ │ └── toggleMerge.js │ │ ├── mergeCells.css │ │ ├── mergeCells.js │ │ ├── test │ │ │ ├── autofillCalculations.unit.js │ │ │ ├── cellCoords.unit.js │ │ │ ├── cellsCollection.unit.js │ │ │ ├── mergeCells.e2e.js │ │ │ ├── selection.e2e.js │ │ │ ├── selection.unit.js │ │ │ └── utils.unit.js │ │ └── utils.js │ ├── multipleSelectionHandles │ │ └── multipleSelectionHandles.js │ ├── observeChanges │ │ ├── dataObserver.js │ │ ├── observeChanges.js │ │ ├── test │ │ │ └── observeChanges.e2e.js │ │ └── utils.js │ ├── persistentState │ │ ├── persistentState.js │ │ ├── storage.js │ │ └── test │ │ │ ├── persistentState.e2e.js │ │ │ └── storage.unit.js │ ├── search │ │ ├── search.js │ │ └── test │ │ │ └── search.e2e.js │ ├── touchScroll │ │ └── touchScroll.js │ └── undoRedo │ │ ├── test │ │ └── UndoRedo.e2e.js │ │ └── undoRedo.js ├── renderers │ ├── _cellDecorator.js │ ├── autocompleteRenderer.js │ ├── checkboxRenderer.js │ ├── htmlRenderer.js │ ├── index.js │ ├── numericRenderer.js │ ├── passwordRenderer.js │ └── textRenderer.js ├── selection │ ├── highlight │ │ ├── highlight.js │ │ └── types │ │ │ ├── activeHeader.js │ │ │ ├── area.js │ │ │ ├── cell.js │ │ │ ├── customSelection.js │ │ │ ├── fill.js │ │ │ ├── header.js │ │ │ └── index.js │ ├── index.js │ ├── mouseEventHandler.js │ ├── range.js │ ├── selection.js │ ├── transformation.js │ └── utils.js ├── tableView.js ├── utils │ ├── dataStructures │ │ ├── linkedList.js │ │ ├── queue.js │ │ └── stack.js │ ├── ghostTable.js │ ├── interval.js │ ├── keyStateObserver.js │ ├── recordTranslator.js │ ├── rootInstance.js │ ├── samplesGenerator.js │ ├── sortingAlgorithms │ │ └── mergeSort.js │ └── staticRegister.js └── validators │ ├── autocompleteValidator.js │ ├── dateValidator.js │ ├── index.js │ ├── numericValidator.js │ └── timeValidator.js ├── test ├── __mocks__ │ └── styleMock.js ├── bootstrap.js ├── e2e │ ├── ColHeader.spec.js │ ├── Core_alter.spec.js │ ├── Core_beforeKeyDown.spec.js │ ├── Core_beforechange.spec.js │ ├── Core_count.spec.js │ ├── Core_countEmptyCols.spec.js │ ├── Core_countEmptyRows.spec.js │ ├── Core_dataSchema.spec.js │ ├── Core_datachange.spec.js │ ├── Core_destroy.spec.js │ ├── Core_destroyEditor.spec.js │ ├── Core_getCellMeta.spec.js │ ├── Core_getColHeader.spec.js │ ├── Core_getDataAt.spec.js │ ├── Core_getDataType.spec.js │ ├── Core_getRowHeader.spec.js │ ├── Core_init.spec.js │ ├── Core_isEmpty.spec.js │ ├── Core_keepEmptyRows.spec.js │ ├── Core_listen.spec.js │ ├── Core_loadData.spec.js │ ├── Core_navigation.spec.js │ ├── Core_onKeyDown.spec.js │ ├── Core_populateFromArray.spec.js │ ├── Core_reCreate.spec.js │ ├── Core_removeCellMeta.spec.js │ ├── Core_render.spec.js │ ├── Core_selection.spec.js │ ├── Core_setDataAtCell.spec.js │ ├── Core_splice.spec.js │ ├── Core_update.spec.js │ ├── Core_validate.spec.js │ ├── Core_view.spec.js │ ├── Dom.spec.js │ ├── MemoryLeakTest.js │ ├── Performance.spec.js │ ├── PluginHooks.spec.js │ ├── RowHeader.spec.js │ ├── cellTypes │ │ └── index.spec.js │ ├── core │ │ ├── colToProp.spec.js │ │ ├── countSourceCols.spec.js │ │ ├── emptySelectedCells.spec.js │ │ ├── getCellMetaAtRow.spec.js │ │ ├── getCellsMeta.spec.js │ │ ├── getCopyableData.spec.js │ │ ├── getCopyableText.spec.js │ │ ├── getSelected.spec.js │ │ ├── getSelectedLast.spec.js │ │ ├── getSelectedRange.spec.js │ │ ├── getSelectedRangeLast.spec.js │ │ ├── getSourceDataArray.spec.js │ │ ├── getSourceDataAtCell.spec.js │ │ ├── getSourceDataAtCol.spec.js │ │ ├── propToCol.spec.js │ │ ├── selectAll.spec.js │ │ ├── selectCell.spec.js │ │ ├── selectCells.spec.js │ │ ├── selectColumns.spec.js │ │ ├── selectRows.spec.js │ │ ├── setCellMeta.spec.js │ │ ├── spliceCellsMeta.spec.js │ │ ├── spliceCol.spec.js │ │ ├── spliceRow.spec.js │ │ ├── toPhysicalColumn.spec.js │ │ ├── toPhysicalRow.spec.js │ │ ├── toVisualColumn.spec.js │ │ └── toVisualRow.spec.js │ ├── editors │ │ ├── autocompleteEditor.spec.js │ │ ├── baseEditor.spec.js │ │ ├── dateEditor.spec.js │ │ ├── dropdownEditor.spec.js │ │ ├── handsontableEditor.spec.js │ │ ├── index.spec.js │ │ ├── noEditor.spec.js │ │ ├── numericEditor.spec.js │ │ ├── passwordEditor.spec.js │ │ ├── selectEditor.spec.js │ │ └── textEditor.spec.js │ ├── i18n │ │ └── index.spec.js │ ├── index.js │ ├── mobile │ │ ├── events.spec.js │ │ ├── index.js │ │ ├── scroll.spec.js │ │ └── selection.spec.js │ ├── publicAPI.spec.js │ ├── renderers │ │ ├── autocompleteRenderer.spec.js │ │ ├── cellDecorator.spec.js │ │ ├── checkboxRenderer.spec.js │ │ ├── htmlRenderer.spec.js │ │ ├── index.spec.js │ │ ├── numericRenderer.spec.js │ │ ├── passwordRenderer.spec.js │ │ └── textRenderer.spec.js │ ├── settings │ │ ├── autoWrapCol.spec.js │ │ ├── autoWrapRow.spec.js │ │ ├── colWidths.spec.js │ │ ├── columns.spec.js │ │ ├── copyable.spec.js │ │ ├── currentHeaderClassName.spec.js │ │ ├── currentRowClassName.spec.js │ │ ├── editor.spec.js │ │ ├── fixedColumnsLeft.spec.js │ │ ├── fixedRowsBottom.spec.js │ │ ├── fixedRowsTop.spec.js │ │ ├── fragmentSelection.spec.js │ │ ├── maxCols.spec.js │ │ ├── maxRows.spec.js │ │ ├── outsideClickDeselects.spec.js │ │ ├── renderer.spec.js │ │ └── tableClassName.spec.js │ ├── utils │ │ └── ghostTable.spec.js │ └── validators │ │ ├── autocompleteValidator.spec.js │ │ ├── dateValidator.spec.js │ │ ├── index.spec.js │ │ ├── numericValidator.spec.js │ │ └── timeValidator.spec.js ├── helpers │ ├── asciiTable.js │ ├── common.css │ ├── common.js │ ├── custom-matchers.js │ ├── index.js │ └── jasmine-bridge-reporter.js ├── lib │ ├── backbone.js │ ├── jquery.min.js │ ├── jquery.simulate.js │ ├── lodash.underscore.js │ └── normalize.css ├── scripts │ ├── run-puppeteer.js │ ├── trigger-hot-builder-tests.sh │ └── trigger-pro-tests.sh ├── types │ ├── editors │ │ ├── password.types.ts │ │ └── text.types.ts │ ├── helpers │ │ ├── dom.types.ts │ │ └── helpers.types.ts │ ├── index.types.ts │ ├── methods.types.ts │ ├── renderers.types.ts │ ├── settings.types.ts │ └── tsconfig.json └── unit │ ├── EventManager.spec.js │ ├── PluginHooks.spec.js │ ├── helpers │ ├── Array.spec.js │ ├── Browser.spec.js │ ├── Console.spec.js │ ├── Data.spec.js │ ├── Date.spec.js │ ├── Feature.spec.js │ ├── Function.spec.js │ ├── Mixed.spec.js │ ├── Number.spec.js │ ├── Object.spec.js │ ├── String.spec.js │ ├── TemplateLiteralTag.spec.js │ ├── Unicode.spec.js │ └── dom │ │ ├── Element.spec.js │ │ └── Event.spec.js │ ├── i18n │ ├── dictionariesManager.spec.js │ ├── index.spec.js │ ├── phraseFormatters │ │ ├── index.spec.js │ │ ├── pluralize.spec.js │ │ └── substituteVariables.spec.js │ └── utils.spec.js │ ├── mixins │ ├── arrayMapper.spec.js │ └── localHooks.spec.js │ ├── multiMap.spec.js │ ├── selection │ ├── range.spec.js │ └── utils.spec.js │ └── utils │ ├── Interval.spec.js │ ├── dataStructures │ └── LinkedList.spec.js │ ├── keyStateObserver.spec.js │ ├── recordTranslator.spec.js │ ├── rootInstance.spec.js │ ├── samplesGenerator.spec.js │ ├── sortingAlgorithms │ └── mergeSort.spec.js │ └── staticRegister.spec.js ├── update.json ├── webpack.config.js └── yarn.lock /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "demo/bower_components" 3 | } -------------------------------------------------------------------------------- /.config/languages-production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Config responsible for building minified Handsontable `dist/languages/` files. 5 | */ 6 | const path = require('path'); 7 | const webpack = require('webpack'); 8 | const configFactory = require('./languages-development'); 9 | const OUTPUT_LANGUAGES_DIRECTORY = 'dist/languages'; 10 | 11 | module.exports.create = function create() { 12 | const configs = configFactory.create(); 13 | 14 | // Add uglifyJs plugin for each configuration 15 | configs.forEach(function(config) { 16 | config.output.path = path.resolve(__dirname, '../', OUTPUT_LANGUAGES_DIRECTORY); 17 | config.output.filename = '[name].min.js'; 18 | 19 | config.plugins = [ 20 | new webpack.optimize.UglifyJsPlugin({ 21 | compressor: { 22 | pure_getters: true, 23 | unsafe: true, 24 | unsafe_comps: true, 25 | warnings: false, 26 | screw_ie8: true, 27 | }, 28 | mangle: { 29 | screw_ie8: true, 30 | }, 31 | output: { 32 | comments: /^!|@preserve|@license|@cc_on/i, 33 | screw_ie8: true, 34 | }, 35 | }) 36 | ]; 37 | }); 38 | 39 | return [].concat(configs); 40 | }; 41 | -------------------------------------------------------------------------------- /.config/loader/empty-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | this.cacheable(); 3 | 4 | return ''; 5 | }; 6 | 7 | module.exports.pitch = function() { 8 | this.cacheable(); 9 | 10 | return ''; 11 | }; 12 | -------------------------------------------------------------------------------- /.config/loader/exports-to-window-loader.js: -------------------------------------------------------------------------------- 1 | var loaderUtils = require('loader-utils'); 2 | var FOOTER = '/*** EXPORTS FROM exports-to-window-loader ***/\n'; 3 | var alreadyExported = {}; 4 | 5 | module.exports = function(content, sourceMap) { 6 | if (this.cacheable) { 7 | this.cacheable(); 8 | } 9 | var query = loaderUtils.getOptions(this) || {}; 10 | var exports = []; 11 | var keys = Object.keys(query); 12 | 13 | keys.forEach(function(key) { 14 | if (!alreadyExported[key]) { 15 | alreadyExported[key] = true; 16 | exports.push("window['" + key + "'] = require('" + query[key] + "');"); 17 | } 18 | }); 19 | 20 | if (exports.length) { 21 | content = content + '\n\n' + FOOTER + exports.join('\n'); 22 | } 23 | 24 | return content; 25 | } 26 | -------------------------------------------------------------------------------- /.config/plugin/jasmine-html.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | var jasmineCore = require('jasmine-core'); 6 | 7 | var jasmineFiles = jasmineCore.files; 8 | var jasminePath = toRelativePath(jasmineFiles.path); 9 | var jasmineBootDir = toRelativePath(jasmineFiles.bootDir); 10 | 11 | function JasmineWebpackPlugin(options) { 12 | options = options || {}; 13 | 14 | return new HtmlWebpackPlugin({ 15 | inject: true, 16 | filename: options.filename || 'SpecRunner.html', 17 | template: path.join(__dirname, 'template.ejs'), 18 | baseJasminePath: options.baseJasminePath || '', 19 | jasmineJsFiles: toRelativeFiles(jasminePath, jasmineFiles.jsFiles).concat(toRelativeFiles(jasmineBootDir, jasmineFiles.bootFiles)), 20 | jasmineCssFiles: toRelativeFiles(jasminePath, jasmineFiles.cssFiles), 21 | externalJsFiles: options.externalJsFiles || [], 22 | externalCssFiles: options.externalCssFiles || [], 23 | minify: false, 24 | }); 25 | } 26 | 27 | function toRelativePath(dirname) { 28 | return dirname.replace(process.cwd(), '').replace(/^\//, ''); 29 | } 30 | 31 | function toRelativeFiles(dirname, files) { 32 | return files.map(function(file) { 33 | return path.join(dirname, file); 34 | }); 35 | } 36 | 37 | module.exports = JasmineWebpackPlugin; 38 | -------------------------------------------------------------------------------- /.config/plugin/template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jasmine Spec Runner 6 | <% for (var i = 0; i < htmlWebpackPlugin.options.jasmineCssFiles.length; i++) { %> 7 | 8 | <% } %> 9 | <% for (var i = 0; i < htmlWebpackPlugin.options.externalCssFiles.length; i++) { %> 10 | 11 | <% } %> 12 | <% for (var i = 0; i < htmlWebpackPlugin.options.jasmineJsFiles.length; i++) { %> 13 | 14 | <% } %> 15 | <% for (var i = 0; i < htmlWebpackPlugin.options.externalJsFiles.length; i++) { %> 16 | 17 | <% } %> 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.config/walkontable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Config responsible for building Walkontable (bundled into `src/3rdparty/walkontable/dist/`): 5 | * - walkontable.js 6 | */ 7 | const path = require('path'); 8 | const webpack = require('webpack'); 9 | const configFactory = require('./base'); 10 | const JasmineHtml = require('./plugin/jasmine-html'); 11 | 12 | const wotPath = path.resolve(__dirname, '../src/3rdparty/walkontable'); 13 | 14 | module.exports.create = function create(envArgs) { 15 | const config = { 16 | devtool: 'cheap-module-source-map', 17 | output: { 18 | library: 'Walkontable', 19 | libraryTarget: 'var', 20 | filename: 'walkontable.js', 21 | path: path.resolve(wotPath, 'dist'), 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js$/, 27 | loader: 'babel-loader', 28 | exclude: [ 29 | /node_modules/, 30 | ], 31 | options: { 32 | cacheDirectory: true, 33 | }, 34 | } 35 | ] 36 | }, 37 | plugins: [ 38 | // This helps ensure the builds are consistent if source code hasn't changed 39 | new webpack.optimize.OccurrenceOrderPlugin(), 40 | ], 41 | }; 42 | 43 | return [].concat(config); 44 | } 45 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = false 17 | insert_final_newline = true 18 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/3rdparty/walkontable/test/lib/* 3 | Gruntfile.js 4 | dist/* 5 | lib/* 6 | src/3rdparty/walkontable/test/dist/* 7 | src/3rdparty/walkontable/dist/* 8 | test/lib/* 9 | test/dist/* 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | * text=crlf 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | ### Steps to reproduce 5 | 6 | 1. 7 | 2. 8 | 3. 9 | 10 | ### Demo 11 | 12 | https://jsfiddle.net/handsoncode/8ffpsqt6/ 13 | 14 | ### Your environment 15 | * Handsontable version: 16 | * Browser Name and version: 17 | * Operating System: 18 | 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Context 2 | 3 | 4 | ### How has this been tested? 5 | 6 | 7 | ### Types of changes 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature or improvement (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] Additional language file or change to the existing one (translations) 13 | 14 | ### Related issue(s): 15 | 1. 16 | 2. 17 | 3. 18 | 19 | ### Checklist: 20 | 21 | 22 | - [ ] My code follows the code style of this project, 23 | - [ ] My change requires a change to the documentation. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .grunt 3 | .idea 4 | .vscode 5 | _SpecRunner.html 6 | test/E2ERunner.html 7 | test/UnitRunner.html 8 | test/MobileRunner.html 9 | test/dist/ 10 | src/3rdparty/walkontable/test/dist/ 11 | src/3rdparty/walkontable/dist/ 12 | cars.sqlite 13 | dev*.html 14 | npm-debug.log 15 | !dist/README.md 16 | node_modules/ 17 | commonjs/ 18 | es/ 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .grunt 3 | .config 4 | .github 5 | .idea 6 | _SpecRunner.html 7 | cars.sqlite 8 | dev.html 9 | webpack.config.js 10 | 11 | !dist/README.md 12 | !test/ 13 | node_modules/ 14 | 15 | # this directories are necessary for build PRO package 16 | #demo/ 17 | #src/ 18 | #lib/ 19 | #test/ 20 | #tasks/ 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | node_js: 6 | - '8' 7 | 8 | notifications: 9 | email: false 10 | slack: 11 | secure: LDlPC3IbXLbve4yq6kNznoIR5WgMhS2x+/KomgbVRDkUpySzyt27dUtRJqgkAqtkHhDhd0FbpROZNPriQVJI8nLLux0xdl9dngy6Lazl2qVzShrtdN01I3lVxysJlDM3phKHQCPmGhd/1v1qjfTAu45tM9pnYACSnDfdfh46b84= 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | All releases are described at https://github.com/handsontable/handsontable/releases 2 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | handsontable.com 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012-2014 Marcin Warpechowski 4 | Copyright (c) 2015 Handsoncode sp. z o.o. 5 | Copyright (c) 2019 Rathbone Labs, LLC 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | 'Software'), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "handsontable", 3 | "description": "Spreadsheet-like data grid editor that provides copy/paste functionality compatible with Excel/Google Docs", 4 | "main": ["./dist/handsontable.js", "./dist/handsontable.css"], 5 | "homepage": "http://handsontable.com/", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/handsontable/handsontable.git" 9 | }, 10 | "authors": [ 11 | "Handsoncode", "Handsoncode " 12 | ], 13 | "keywords": [ 14 | "data", 15 | "grid", 16 | "table", 17 | "editor", 18 | "grid-editor", 19 | "data-grid", 20 | "data-table", 21 | "spreadsheet", 22 | "excel", 23 | "tabular-data", 24 | "edit-cell", 25 | "editable-table", 26 | "data-spreadsheet" 27 | ], 28 | "ignore": [ 29 | "**/.*", 30 | "components", 31 | "demo", 32 | "node_modules", 33 | "src", 34 | "test" 35 | ], 36 | "dependencies": { 37 | "moment": "^2.13.0", 38 | "numbro": "^2.0.6", 39 | "pikaday": "^1.4.0" 40 | }, 41 | "devDependencies": { 42 | "chroma-js": "~0.5.6" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /dist/handsontable.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"handsontable.css","sourceRoot":""} -------------------------------------------------------------------------------- /dist/moment/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) JS Foundation and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /dist/numbro/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Benjamin Van Ryseghem 2 | Copyright (c) 2015-2017 Företagsplatsen 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /dist/numbro/LICENSE-Numeraljs: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Adam Draper 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /dist/numbro/languages/bg.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var n;((n="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).numbro||(n.numbro={})).bg=e()}}(function(){return function e(n,o,r){function i(f,u){if(!o[f]){if(!n[f]){var d="function"==typeof require&&require;if(!u&&d)return d(f,!0);if(t)return t(f,!0);var l=new Error("Cannot find module '"+f+"'");throw l.code="MODULE_NOT_FOUND",l}var a=o[f]={exports:{}};n[f][0].call(a.exports,function(e){var o=n[f][1][e];return i(o||e)},a,a.exports,e,n,o,r)}return o[f].exports}for(var t="function"==typeof require&&require,f=0;f=20?"ste":"de"},currency:{symbol:"€",position:"postfix",code:"EUR"},currencyFormat:{thousandSeparated:!0,totalLength:4,spaceSeparated:!0,average:!0},formats:{fourDigits:{totalLength:4,spaceSeparated:!0,average:!0},fullWithTwoDecimals:{output:"currency",mantissa:2,spaceSeparated:!0,thousandSeparated:!0},fullWithTwoDecimalsNoCurrency:{mantissa:2,thousandSeparated:!0},fullWithNoDecimals:{output:"currency",spaceSeparated:!0,thousandSeparated:!0,mantissa:0}}}},{}]},{},[1])(1)}); 2 | //# sourceMappingURL=nl-BE.min.js.map 3 | -------------------------------------------------------------------------------- /dist/numbro/languages/nl-NL.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;((t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).numbro||(t.numbro={})).nlNL=e()}}(function(){return function e(t,n,r){function o(i,u){if(!n[i]){if(!t[i]){var f="function"==typeof require&&require;if(!u&&f)return f(i,!0);if(a)return a(i,!0);var s=new Error("Cannot find module '"+i+"'");throw s.code="MODULE_NOT_FOUND",s}var l=n[i]={exports:{}};t[i][0].call(l.exports,function(e){var n=t[i][1][e];return o(n||e)},l,l.exports,e,t,n,r)}return n[i].exports}for(var a="function"==typeof require&&require,i=0;i=20?"ste":"de"},currency:{symbol:"€",position:"prefix",code:"EUR"},currencyFormat:{thousandSeparated:!0,totalLength:4,spaceSeparated:!0,average:!0},formats:{fourDigits:{totalLength:4,spaceSeparated:!0,average:!0},fullWithTwoDecimals:{output:"currency",mantissa:2,spaceSeparated:!0,thousandSeparated:!0},fullWithTwoDecimalsNoCurrency:{mantissa:2,thousandSeparated:!0},fullWithNoDecimals:{output:"currency",spaceSeparated:!0,thousandSeparated:!0,mantissa:0}}}},{}]},{},[1])(1)}); 2 | //# sourceMappingURL=nl-NL.min.js.map 3 | -------------------------------------------------------------------------------- /dist/numbro/languages/nn.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var n;((n="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).numbro||(n.numbro={})).nn=e()}}(function(){return function e(n,r,o){function i(f,u){if(!r[f]){if(!n[f]){var l="function"==typeof require&&require;if(!u&&l)return l(f,!0);if(t)return t(f,!0);var d=new Error("Cannot find module '"+f+"'");throw d.code="MODULE_NOT_FOUND",d}var a=r[f]={exports:{}};n[f][0].call(a.exports,function(e){var r=n[f][1][e];return i(r||e)},a,a.exports,e,n,r,o)}return r[f].exports}for(var t="function"==typeof require&&require,f=0;f=1.7", 34 | "bootstrap-typeahead.js": "2.1.1", 35 | "contextMenu": ">=1.5.25" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /hot.config.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const packageBody = require('./package.json'); 3 | 4 | module.exports = { 5 | HOT_FILENAME: 'handsontable', 6 | HOT_VERSION: packageBody.version, 7 | HOT_BASE_VERSION: '', 8 | HOT_PACKAGE_TYPE: 'ce', 9 | HOT_PACKAGE_NAME: packageBody.name, 10 | HOT_BUILD_DATE: moment().format('DD/MM/YYYY HH:mm:ss'), 11 | HOT_RELEASE_DATE: '19/12/2018', 12 | }; 13 | -------------------------------------------------------------------------------- /src/3rdparty/walkontable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "walkontable", 3 | "description": "Table renderer for Handsontable", 4 | "homepage": "http://handsontable.com/", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/handsontable/handsontable.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/handsontable/handsontable/issues" 11 | }, 12 | "author": "Handsoncode ", 13 | "version": "0.0.1", 14 | "license": "MIT" 15 | } 16 | -------------------------------------------------------------------------------- /src/3rdparty/walkontable/src/overlay/debug.js: -------------------------------------------------------------------------------- 1 | import { addClass } from './../../../../helpers/dom/element'; 2 | import Overlay from './_base'; 3 | 4 | /** 5 | * A overlay that renders ALL available rows & columns positioned on top of the original Walkontable instance and all other overlays. 6 | * Used for debugging purposes to see if the other overlays (that render only part of the rows & columns) are positioned correctly 7 | * 8 | * @class DebugOverlay 9 | */ 10 | class DebugOverlay extends Overlay { 11 | /** 12 | * @param {Walkontable} wotInstance 13 | */ 14 | constructor(wotInstance) { 15 | super(wotInstance); 16 | 17 | this.clone = this.makeClone(Overlay.CLONE_DEBUG); 18 | this.clone.wtTable.holder.style.opacity = 0.4; 19 | this.clone.wtTable.holder.style.textShadow = '0 0 2px #ff0000'; 20 | 21 | addClass(this.clone.wtTable.holder.parentNode, 'wtDebugVisible'); 22 | } 23 | } 24 | 25 | Overlay.registerOverlay(Overlay.CLONE_DEBUG, DebugOverlay); 26 | 27 | export default DebugOverlay; 28 | -------------------------------------------------------------------------------- /src/3rdparty/walkontable/test/SpecRunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jasmine Spec Runner 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/3rdparty/walkontable/test/helpers/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import window from 'window'; 3 | import * as common from './common'; 4 | 5 | // Export all helpers to the window. 6 | Object.keys(common).forEach((key) => { 7 | window[key] = common[key]; 8 | }); 9 | -------------------------------------------------------------------------------- /src/3rdparty/walkontable/test/spec/filter/column.spec.js: -------------------------------------------------------------------------------- 1 | describe('Walkontable.ColumnFilter', () => { 2 | describe('offsettedTH', () => { 3 | it('should do nothing if row header is not visible', () => { 4 | const filter = new Walkontable.ColumnFilter(); 5 | filter.countTH = 0; 6 | expect(filter.offsettedTH(1)).toEqual(1); 7 | }); 8 | 9 | it('should decrease n by 1 if row header is visible', () => { 10 | const filter = new Walkontable.ColumnFilter(); 11 | filter.countTH = 1; 12 | expect(filter.offsettedTH(1)).toEqual(0); 13 | }); 14 | }); 15 | 16 | describe('unOffsettedTH', () => { 17 | it('should do nothing if row header is not visible', () => { 18 | const filter = new Walkontable.ColumnFilter(); 19 | filter.countTH = 0; 20 | expect(filter.unOffsettedTH(1)).toEqual(1); 21 | }); 22 | 23 | it('should increase n by 1 if row header is visible', () => { 24 | const filter = new Walkontable.ColumnFilter(); 25 | filter.countTH = 1; 26 | expect(filter.unOffsettedTH(0)).toEqual(1); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/3rdparty/walkontable/test/spec/index.js: -------------------------------------------------------------------------------- 1 | [ 2 | require.context('.', true, /\.spec\.js$/), 3 | ].forEach((req) => { 4 | req.keys().forEach((key) => { 5 | req(key); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/cellTypes/autocompleteType.js: -------------------------------------------------------------------------------- 1 | import { getEditor } from './../editors'; 2 | import { getRenderer } from './../renderers'; 3 | import { getValidator } from './../validators'; 4 | 5 | const CELL_TYPE = 'autocomplete'; 6 | 7 | export default { 8 | editor: getEditor(CELL_TYPE), 9 | renderer: getRenderer(CELL_TYPE), 10 | validator: getValidator(CELL_TYPE), 11 | }; 12 | -------------------------------------------------------------------------------- /src/cellTypes/checkboxType.js: -------------------------------------------------------------------------------- 1 | import { getEditor } from './../editors'; 2 | import { getRenderer } from './../renderers'; 3 | 4 | const CELL_TYPE = 'checkbox'; 5 | 6 | export default { 7 | editor: getEditor(CELL_TYPE), 8 | renderer: getRenderer(CELL_TYPE), 9 | }; 10 | -------------------------------------------------------------------------------- /src/cellTypes/dateType.js: -------------------------------------------------------------------------------- 1 | import { getEditor } from './../editors'; 2 | import { getRenderer } from './../renderers'; 3 | import { getValidator } from './../validators'; 4 | 5 | const CELL_TYPE = 'date'; 6 | 7 | export default { 8 | editor: getEditor(CELL_TYPE), 9 | // displays small gray arrow on right side of the cell 10 | renderer: getRenderer('autocomplete'), 11 | validator: getValidator(CELL_TYPE), 12 | }; 13 | -------------------------------------------------------------------------------- /src/cellTypes/dropdownType.js: -------------------------------------------------------------------------------- 1 | import { getEditor } from './../editors'; 2 | import { getRenderer } from './../renderers'; 3 | import { getValidator } from './../validators'; 4 | 5 | const CELL_TYPE = 'dropdown'; 6 | 7 | export default { 8 | editor: getEditor(CELL_TYPE), 9 | // displays small gray arrow on right side of the cell 10 | renderer: getRenderer('autocomplete'), 11 | validator: getValidator('autocomplete'), 12 | }; 13 | -------------------------------------------------------------------------------- /src/cellTypes/handsontableType.js: -------------------------------------------------------------------------------- 1 | import { getEditor } from './../editors'; 2 | import { getRenderer } from './../renderers'; 3 | 4 | const CELL_TYPE = 'handsontable'; 5 | 6 | export default { 7 | editor: getEditor(CELL_TYPE), 8 | // displays small gray arrow on right side of the cell 9 | renderer: getRenderer('autocomplete'), 10 | }; 11 | -------------------------------------------------------------------------------- /src/cellTypes/numericType.js: -------------------------------------------------------------------------------- 1 | import { getEditor } from './../editors'; 2 | import { getRenderer } from './../renderers'; 3 | import { getValidator } from './../validators'; 4 | 5 | const CELL_TYPE = 'numeric'; 6 | 7 | export default { 8 | editor: getEditor(CELL_TYPE), 9 | renderer: getRenderer(CELL_TYPE), 10 | validator: getValidator(CELL_TYPE), 11 | dataType: 'number', 12 | }; 13 | -------------------------------------------------------------------------------- /src/cellTypes/passwordType.js: -------------------------------------------------------------------------------- 1 | import { getEditor } from './../editors'; 2 | import { getRenderer } from './../renderers'; 3 | 4 | const CELL_TYPE = 'password'; 5 | 6 | export default { 7 | editor: getEditor(CELL_TYPE), 8 | renderer: getRenderer(CELL_TYPE), 9 | copyable: false, 10 | }; 11 | -------------------------------------------------------------------------------- /src/cellTypes/textType.js: -------------------------------------------------------------------------------- 1 | import { getEditor } from './../editors'; 2 | import { getRenderer } from './../renderers'; 3 | 4 | const CELL_TYPE = 'text'; 5 | 6 | export default { 7 | editor: getEditor(CELL_TYPE), 8 | renderer: getRenderer(CELL_TYPE), 9 | }; 10 | -------------------------------------------------------------------------------- /src/cellTypes/timeType.js: -------------------------------------------------------------------------------- 1 | import { getEditor } from './../editors'; 2 | import { getRenderer } from './../renderers'; 3 | import { getValidator } from './../validators'; 4 | 5 | const CELL_TYPE = 'time'; 6 | 7 | export default { 8 | editor: getEditor('text'), 9 | // displays small gray arrow on right side of the cell 10 | renderer: getRenderer('text'), 11 | validator: getValidator(CELL_TYPE), 12 | }; 13 | -------------------------------------------------------------------------------- /src/editors/checkboxEditor.js: -------------------------------------------------------------------------------- 1 | import BaseEditor from './_baseEditor'; 2 | import { hasClass } from './../helpers/dom/element'; 3 | 4 | /** 5 | * @private 6 | * @editor CheckboxEditor 7 | * @class CheckboxEditor 8 | */ 9 | class CheckboxEditor extends BaseEditor { 10 | beginEditing(initialValue, event) { 11 | // Just some events connected with checkbox editor are delegated here. Some `keydown` events like `enter` and `space` key press 12 | // are handled inside `checkboxRenderer`. Some events come here from `editorManager`. Below `if` statement was created by author 13 | // for purpose of handling only `doubleclick` event which may be done on a cell with checkbox. 14 | 15 | if (event && event.type === 'mouseup') { 16 | const checkbox = this.TD.querySelector('input[type="checkbox"]'); 17 | 18 | if (!hasClass(checkbox, 'htBadValue')) { 19 | checkbox.click(); 20 | } 21 | } 22 | } 23 | 24 | finishEditing() {} 25 | init() {} 26 | open() {} 27 | close() {} 28 | getValue() {} 29 | setValue() {} 30 | focus() {} 31 | } 32 | 33 | export default CheckboxEditor; 34 | -------------------------------------------------------------------------------- /src/editors/dropdownEditor.js: -------------------------------------------------------------------------------- 1 | import AutocompleteEditor from './autocompleteEditor'; 2 | import Hooks from './../pluginHooks'; 3 | 4 | /** 5 | * @private 6 | * @editor DropdownEditor 7 | * @class DropdownEditor 8 | * @dependencies AutocompleteEditor 9 | */ 10 | class DropdownEditor extends AutocompleteEditor { 11 | prepare(row, col, prop, td, originalValue, cellProperties) { 12 | super.prepare(row, col, prop, td, originalValue, cellProperties); 13 | this.cellProperties.filter = false; 14 | this.cellProperties.strict = true; 15 | } 16 | } 17 | 18 | Hooks.getSingleton().add('beforeValidate', function(value, row, col) { 19 | const cellMeta = this.getCellMeta(row, this.propToCol(col)); 20 | 21 | if (cellMeta.editor === DropdownEditor) { 22 | if (cellMeta.strict === void 0) { 23 | cellMeta.filter = false; 24 | cellMeta.strict = true; 25 | } 26 | } 27 | }); 28 | 29 | export default DropdownEditor; 30 | -------------------------------------------------------------------------------- /src/editors/numericEditor.js: -------------------------------------------------------------------------------- 1 | import TextEditor from './textEditor'; 2 | 3 | /** 4 | * @private 5 | * @editor NumericEditor 6 | * @class NumericEditor 7 | */ 8 | class NumericEditor extends TextEditor {} 9 | 10 | export default NumericEditor; 11 | -------------------------------------------------------------------------------- /src/editors/passwordEditor.js: -------------------------------------------------------------------------------- 1 | import { empty } from './../helpers/dom/element'; 2 | import TextEditor from './textEditor'; 3 | 4 | /** 5 | * @private 6 | * @editor PasswordEditor 7 | * @class PasswordEditor 8 | * @dependencies TextEditor 9 | */ 10 | class PasswordEditor extends TextEditor { 11 | createElements() { 12 | super.createElements(); 13 | 14 | this.TEXTAREA = document.createElement('input'); 15 | this.TEXTAREA.setAttribute('type', 'password'); 16 | this.TEXTAREA.className = 'handsontableInput'; 17 | this.textareaStyle = this.TEXTAREA.style; 18 | this.textareaStyle.width = 0; 19 | this.textareaStyle.height = 0; 20 | 21 | empty(this.TEXTAREA_PARENT); 22 | this.TEXTAREA_PARENT.appendChild(this.TEXTAREA); 23 | } 24 | } 25 | 26 | export default PasswordEditor; 27 | -------------------------------------------------------------------------------- /src/helpers/console.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | /* eslint-disable no-restricted-globals */ 3 | 4 | /** 5 | * "In Internet Explorer 9 (and 8), the console object is only exposed when the developer tools are opened 6 | * for a particular tab." 7 | * 8 | * Source: https://stackoverflow.com/a/5473193 9 | */ 10 | 11 | import { isDefined } from './mixed'; 12 | 13 | /** 14 | * Logs message to the console if the `console` object is exposed. 15 | * 16 | * @param {...*} args Values which will be logged. 17 | */ 18 | export function log(...args) { 19 | if (isDefined(console)) { 20 | console.log(...args); 21 | } 22 | } 23 | 24 | /** 25 | * Logs warn to the console if the `console` object is exposed. 26 | * 27 | * @param {...*} args Values which will be logged. 28 | */ 29 | export function warn(...args) { 30 | if (isDefined(console)) { 31 | console.warn(...args); 32 | } 33 | } 34 | 35 | /** 36 | * Logs info to the console if the `console` object is exposed. 37 | * 38 | * @param {...*} args Values which will be logged. 39 | */ 40 | export function info(...args) { 41 | if (isDefined(console)) { 42 | console.info(...args); 43 | } 44 | } 45 | 46 | /** 47 | * Logs error to the console if the `console` object is exposed. 48 | * 49 | * @param {...*} args Values which will be logged. 50 | */ 51 | export function error(...args) { 52 | if (isDefined(console)) { 53 | console.error(...args); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/helpers/date.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | /** 4 | * Get normalized Date object for the ISO formatted date strings. 5 | * Natively, the date object parsed from a ISO 8601 string will be offsetted by the timezone difference, which may result in returning a wrong date. 6 | * See: Github issue #3338. 7 | * 8 | * @param {String} dateString String representing the date. 9 | * @returns {Date} The proper Date object. 10 | */ 11 | export function getNormalizedDate(dateString) { 12 | const nativeDate = new Date(dateString); 13 | 14 | // NaN if dateString is not in ISO format 15 | if (!isNaN(new Date(`${dateString}T00:00`).getDate())) { 16 | 17 | // Compensate timezone offset 18 | return new Date(nativeDate.getTime() + (nativeDate.getTimezoneOffset() * 60000)); 19 | } 20 | 21 | return nativeDate; 22 | } 23 | -------------------------------------------------------------------------------- /src/helpers/setting.js: -------------------------------------------------------------------------------- 1 | import { inherit } from './object'; 2 | /* eslint-disable import/prefer-default-export */ 3 | /** 4 | * Factory for columns constructors. 5 | * 6 | * @param {Object} GridSettings 7 | * @param {Array} conflictList 8 | * @return {Object} ColumnSettings 9 | */ 10 | export function columnFactory(GridSettings, conflictList) { 11 | function ColumnSettings() {} 12 | 13 | inherit(ColumnSettings, GridSettings); 14 | 15 | // Clear conflict settings 16 | for (let i = 0, len = conflictList.length; i < len; i++) { 17 | ColumnSettings.prototype[conflictList[i]] = void 0; 18 | } 19 | 20 | return ColumnSettings; 21 | } 22 | -------------------------------------------------------------------------------- /src/helpers/templateLiteralTag.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import { arrayReduce } from '../helpers/array'; 3 | 4 | /** 5 | * Tags a multiline string and return new one without line break characters and following spaces. 6 | * 7 | * @param {Array} strings Parts of the entire string without expressions. 8 | * @param {...String} expressions Expressions converted to strings, which are added to the entire string. 9 | * @returns {String} 10 | */ 11 | export function toSingleLine(strings, ...expressions) { 12 | const result = arrayReduce(strings, (previousValue, currentValue, index) => { 13 | 14 | const valueWithoutWhiteSpaces = currentValue.replace(/(?:\r?\n\s+)/g, ''); 15 | const expressionForIndex = expressions[index] ? expressions[index] : ''; 16 | 17 | return previousValue + valueWithoutWhiteSpaces + expressionForIndex; 18 | }, ''); 19 | 20 | return result.trim(); 21 | } 22 | -------------------------------------------------------------------------------- /src/helpers/wrappers/jquery.js: -------------------------------------------------------------------------------- 1 | export default function jQueryWrapper(Handsontable) { 2 | const jQuery = typeof window === 'undefined' ? false : window.jQuery; 3 | 4 | if (!jQuery) { 5 | return; 6 | } 7 | 8 | jQuery.fn.handsontable = function(action, ...args) { 9 | const $this = this.first(); // Use only first element from list 10 | let instance = $this.data('handsontable'); 11 | 12 | // Init case 13 | if (typeof action !== 'string') { 14 | const userSettings = action || {}; 15 | 16 | if (instance) { 17 | instance.updateSettings(userSettings); 18 | 19 | } else { 20 | instance = new Handsontable.Core($this[0], userSettings); 21 | $this.data('handsontable', instance); 22 | instance.init(); 23 | } 24 | 25 | return $this; 26 | } 27 | 28 | let output; 29 | 30 | // Action case 31 | if (instance) { 32 | if (typeof instance[action] !== 'undefined') { 33 | output = instance[action].call(instance, ...args); 34 | 35 | if (action === 'destroy') { 36 | $this.removeData(); 37 | } 38 | 39 | } else { 40 | throw new Error(`Handsontable do not provide action: ${action}`); 41 | } 42 | } 43 | 44 | return output; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/i18n/languages/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | import deCH from './de-CH'; 4 | import deDE from './de-DE'; 5 | import enUS from './en-US'; 6 | import esMX from './es-MX'; 7 | import frFR from './fr-FR'; 8 | import itIT from './it-IT'; 9 | import jaJP from './ja-JP'; 10 | import koKR from './ko-KR'; 11 | import lvLV from './lv-LV'; 12 | import nbNO from './nb-NO'; 13 | import nlNL from './nl-NL'; 14 | import plPL from './pl-PL'; 15 | import ptBR from './pt-BR'; 16 | import ruRU from './ru-RU'; 17 | import zhCN from './zh-CN'; 18 | import zhTW from './zh-TW'; 19 | 20 | export { 21 | deCH, 22 | deDE, 23 | enUS, 24 | esMX, 25 | frFR, 26 | itIT, 27 | jaJP, 28 | koKR, 29 | lvLV, 30 | nbNO, 31 | nlNL, 32 | plPL, 33 | ptBR, 34 | ruRU, 35 | zhCN, 36 | zhTW 37 | }; 38 | -------------------------------------------------------------------------------- /src/i18n/phraseFormatters/index.js: -------------------------------------------------------------------------------- 1 | import staticRegister from './../../utils/staticRegister'; 2 | import pluralizeFn from './pluralize'; 3 | 4 | const { 5 | register: registerGloballyPhraseFormatter, 6 | getValues: getGlobalPhraseFormatters, 7 | } = staticRegister('phraseFormatters'); 8 | 9 | /** 10 | * Register phrase formatter. 11 | * 12 | * @param {String} name Name of formatter. 13 | * @param {Function} formatterFn Function which will be applied on phrase propositions. It will transform them if it's possible. 14 | */ 15 | export function register(name, formatterFn) { 16 | registerGloballyPhraseFormatter(name, formatterFn); 17 | } 18 | 19 | /** 20 | * Get all registered previously formatters. 21 | * 22 | * @returns {Array} 23 | */ 24 | export function getAll() { 25 | return getGlobalPhraseFormatters(); 26 | } 27 | 28 | export { 29 | register as registerPhraseFormatter, 30 | getAll as getPhraseFormatters 31 | }; 32 | 33 | register('pluralize', pluralizeFn); 34 | -------------------------------------------------------------------------------- /src/i18n/phraseFormatters/pluralize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Try to choose plural form from available phrase propositions. 3 | * 4 | * @param {Array} phrasePropositions List of phrases propositions. 5 | * @param {number} pluralForm Number determining which phrase form should be used. 6 | * 7 | * @returns {String|Array} One particular phrase if it's possible, list of unchanged phrase propositions otherwise. 8 | */ 9 | export default function pluralize(phrasePropositions, pluralForm) { 10 | const isPluralizable = Array.isArray(phrasePropositions) && Number.isInteger(pluralForm); 11 | 12 | if (isPluralizable) { 13 | return phrasePropositions[pluralForm]; 14 | } 15 | 16 | return phrasePropositions; 17 | } 18 | -------------------------------------------------------------------------------- /src/i18n/phraseFormatters/substituteVariables.js: -------------------------------------------------------------------------------- 1 | import { substitute } from './../../helpers/string'; 2 | 3 | /** 4 | * Try to substitute variable inside phrase propositions. 5 | * 6 | * @param {Array} phrasePropositions List of phrases propositions. 7 | * @param {Object} zippedVariablesAndValues Object containing variables and corresponding values. 8 | * 9 | * @returns {String} Phrases with substituted variables if it's possible, list of unchanged phrase propositions otherwise. 10 | */ 11 | export default function substituteVariables(phrasePropositions, zippedVariablesAndValues) { 12 | if (Array.isArray(phrasePropositions)) { 13 | return phrasePropositions.map(phraseProposition => substituteVariables(phraseProposition, zippedVariablesAndValues)); 14 | } 15 | 16 | return substitute(phrasePropositions, zippedVariablesAndValues); 17 | } 18 | -------------------------------------------------------------------------------- /src/mixins/localHooks.js: -------------------------------------------------------------------------------- 1 | import { arrayEach } from './../helpers/array'; 2 | import { defineGetter } from './../helpers/object'; 3 | 4 | const MIXIN_NAME = 'localHooks'; 5 | 6 | /** 7 | * Mixin object to extend objects functionality for local hooks. 8 | * 9 | * @type {Object} 10 | */ 11 | const localHooks = { 12 | /** 13 | * Internal hooks storage. 14 | */ 15 | _localHooks: Object.create(null), 16 | 17 | /** 18 | * Add hook to the collection. 19 | * 20 | * @param {String} key Hook name. 21 | * @param {Function} callback Hook callback 22 | * @returns {Object} 23 | */ 24 | addLocalHook(key, callback) { 25 | if (!this._localHooks[key]) { 26 | this._localHooks[key] = []; 27 | } 28 | this._localHooks[key].push(callback); 29 | 30 | return this; 31 | }, 32 | 33 | /** 34 | * Run hooks. 35 | * 36 | * @param {String} key Hook name. 37 | * @param {*} params 38 | */ 39 | runLocalHooks(key, ...params) { 40 | if (this._localHooks[key]) { 41 | arrayEach(this._localHooks[key], callback => callback.apply(this, params)); 42 | } 43 | }, 44 | 45 | /** 46 | * Clear all added hooks. 47 | * 48 | * @returns {Object} 49 | */ 50 | clearLocalHooks() { 51 | this._localHooks = {}; 52 | 53 | return this; 54 | }, 55 | }; 56 | 57 | defineGetter(localHooks, 'MIXIN_NAME', MIXIN_NAME, { 58 | writable: false, 59 | enumerable: false, 60 | }); 61 | 62 | export default localHooks; 63 | -------------------------------------------------------------------------------- /src/multiMap.js: -------------------------------------------------------------------------------- 1 | function MultiMap() { 2 | const map = { 3 | arrayMap: [], 4 | weakMap: new WeakMap(), 5 | }; 6 | 7 | return { 8 | get(key) { 9 | if (canBeAnArrayMapKey(key)) { 10 | return map.arrayMap[key]; 11 | } else if (canBeAWeakMapKey(key)) { 12 | return map.weakMap.get(key); 13 | } 14 | }, 15 | 16 | set(key, value) { 17 | if (canBeAnArrayMapKey(key)) { 18 | map.arrayMap[key] = value; 19 | } else if (canBeAWeakMapKey(key)) { 20 | map.weakMap.set(key, value); 21 | } else { 22 | throw new Error('Invalid key type'); 23 | } 24 | }, 25 | 26 | delete(key) { 27 | if (canBeAnArrayMapKey(key)) { 28 | delete map.arrayMap[key]; 29 | } else if (canBeAWeakMapKey(key)) { 30 | map.weakMap.delete(key); 31 | } 32 | }, 33 | }; 34 | 35 | function canBeAnArrayMapKey(obj) { 36 | return obj !== null && !isNaNSymbol(obj) && (typeof obj === 'string' || typeof obj === 'number'); 37 | } 38 | 39 | function canBeAWeakMapKey(obj) { 40 | return obj !== null && (typeof obj === 'object' || typeof obj === 'function'); 41 | } 42 | 43 | function isNaNSymbol(obj) { 44 | /* eslint-disable no-self-compare */ 45 | return obj !== obj; // NaN === NaN is always false 46 | } 47 | } 48 | 49 | export default MultiMap; 50 | -------------------------------------------------------------------------------- /src/plugins/columnSorting/rowsMapper.js: -------------------------------------------------------------------------------- 1 | import arrayMapper from '../../mixins/arrayMapper'; 2 | import { mixin } from '../../helpers/object'; 3 | import { rangeEach } from '../../helpers/number'; 4 | 5 | /** 6 | * @class RowsMapper 7 | */ 8 | class RowsMapper { 9 | /** 10 | * Reset current map array and create new one. 11 | * 12 | * @param {Number} [length] Custom generated map length. 13 | */ 14 | createMap(length) { 15 | const originLength = length === void 0 ? this._arrayMap.length : length; 16 | 17 | this._arrayMap.length = 0; 18 | 19 | rangeEach(originLength - 1, (itemIndex) => { 20 | this._arrayMap[itemIndex] = itemIndex; 21 | }); 22 | } 23 | 24 | /** 25 | * Destroy class. 26 | */ 27 | destroy() { 28 | this._arrayMap = null; 29 | } 30 | } 31 | 32 | mixin(RowsMapper, arrayMapper); 33 | 34 | export default RowsMapper; 35 | -------------------------------------------------------------------------------- /src/plugins/columnSorting/sortService/engine.js: -------------------------------------------------------------------------------- 1 | import mergeSort from '../../../utils/sortingAlgorithms/mergeSort'; 2 | import { getRootComparator } from './registry'; 3 | 4 | export const DO_NOT_SWAP = 0; 5 | export const FIRST_BEFORE_SECOND = -1; 6 | export const FIRST_AFTER_SECOND = 1; 7 | 8 | export function sort(indexesWithData, rootComparatorId, ...argsForRootComparator) { 9 | const rootComparator = getRootComparator(rootComparatorId); 10 | 11 | mergeSort(indexesWithData, rootComparator(...argsForRootComparator)); 12 | } 13 | -------------------------------------------------------------------------------- /src/plugins/columnSorting/sortService/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | registerRootComparator, 3 | getRootComparator, 4 | getCompareFunctionFactory 5 | } from './registry'; 6 | 7 | import { 8 | FIRST_AFTER_SECOND, 9 | FIRST_BEFORE_SECOND, 10 | DO_NOT_SWAP, 11 | sort 12 | } from './engine'; 13 | 14 | export { 15 | registerRootComparator, 16 | getRootComparator, 17 | getCompareFunctionFactory, 18 | FIRST_AFTER_SECOND, 19 | FIRST_BEFORE_SECOND, 20 | DO_NOT_SWAP, 21 | sort 22 | }; 23 | -------------------------------------------------------------------------------- /src/plugins/columnSorting/test/columnSorting.types.ts: -------------------------------------------------------------------------------- 1 | import * as Handsontable from 'handsontable'; 2 | 3 | const columnSorting = Handsontable.plugins.ColumnSorting; 4 | 5 | columnSorting.clearSort(); 6 | 7 | columnSorting.getSortConfig(); 8 | columnSorting.getSortConfig(0); 9 | 10 | const sortConfig0 = columnSorting.getSortConfig(0); 11 | 12 | if (typeof sortConfig0 !== 'undefined') { 13 | sortConfig0.column; 14 | sortConfig0.sortOrder; 15 | } 16 | 17 | const sortConfigs = columnSorting.getSortConfig(); 18 | 19 | sortConfigs[0].column; 20 | sortConfigs[0].sortOrder; 21 | 22 | columnSorting.setSortConfig(); 23 | columnSorting.setSortConfig({ column: 0, sortOrder: 'asc' }); 24 | columnSorting.setSortConfig([{ column: 0, sortOrder: 'asc' }]); 25 | columnSorting.setSortConfig([]); 26 | 27 | columnSorting.isSorted(); 28 | 29 | columnSorting.sort(); 30 | columnSorting.sort({ column: 0, sortOrder: 'asc' }); 31 | -------------------------------------------------------------------------------- /src/plugins/columnSorting/test/sortFunction/date.unit.js: -------------------------------------------------------------------------------- 1 | import { compareFunctionFactory as dateSort } from 'handsontable/plugins/columnSorting/sortFunction/date'; 2 | 3 | it('dateSort comparing function shouldn\'t change order when comparing empty string, null and undefined', () => { 4 | expect(dateSort('asc', {}, {})(null, null)).toEqual(0); 5 | expect(dateSort('desc', {}, {})(null, null)).toEqual(0); 6 | 7 | expect(dateSort('asc', {}, {})('', '')).toEqual(0); 8 | expect(dateSort('desc', {}, {})('', '')).toEqual(0); 9 | 10 | expect(dateSort('asc', {}, {})(undefined, undefined)).toEqual(0); 11 | expect(dateSort('desc', {}, {})(undefined, undefined)).toEqual(0); 12 | 13 | expect(dateSort('asc', {}, {})('', null)).toEqual(0); 14 | expect(dateSort('desc', {}, {})('', null)).toEqual(0); 15 | expect(dateSort('asc', {}, {})(null, '')).toEqual(0); 16 | expect(dateSort('desc', {}, {})(null, '')).toEqual(0); 17 | 18 | expect(dateSort('asc', {}, {})('', undefined)).toEqual(0); 19 | expect(dateSort('desc', {}, {})('', undefined)).toEqual(0); 20 | expect(dateSort('asc', {}, {})(undefined, '')).toEqual(0); 21 | expect(dateSort('desc', {}, {})(undefined, '')).toEqual(0); 22 | 23 | expect(dateSort('asc', {}, {})(null, undefined)).toEqual(0); 24 | expect(dateSort('desc', {}, {})(null, undefined)).toEqual(0); 25 | expect(dateSort('asc', {}, {})(undefined, null)).toEqual(0); 26 | expect(dateSort('desc', {}, {})(undefined, null)).toEqual(0); 27 | }); 28 | -------------------------------------------------------------------------------- /src/plugins/columnSorting/test/sortFunction/default.unit.js: -------------------------------------------------------------------------------- 1 | import { compareFunctionFactory as defaultSort } from 'handsontable/plugins/columnSorting/sortFunction/default'; 2 | 3 | it('defaultSort comparing function shouldn\'t change order when comparing empty string, null and undefined', () => { 4 | expect(defaultSort('asc', {}, {})(null, null)).toEqual(0); 5 | expect(defaultSort('desc', {}, {})(null, null)).toEqual(0); 6 | 7 | expect(defaultSort('asc', {}, {})('', '')).toEqual(0); 8 | expect(defaultSort('desc', {}, {})('', '')).toEqual(0); 9 | 10 | expect(defaultSort('asc', {}, {})(undefined, undefined)).toEqual(0); 11 | expect(defaultSort('desc', {}, {})(undefined, undefined)).toEqual(0); 12 | 13 | expect(defaultSort('asc', {}, {})('', null)).toEqual(0); 14 | expect(defaultSort('desc', {}, {})('', null)).toEqual(0); 15 | expect(defaultSort('asc', {}, {})(null, '')).toEqual(0); 16 | expect(defaultSort('desc', {}, {})(null, '')).toEqual(0); 17 | 18 | expect(defaultSort('asc', {}, {})('', undefined)).toEqual(0); 19 | expect(defaultSort('desc', {}, {})('', undefined)).toEqual(0); 20 | expect(defaultSort('asc', {}, {})(undefined, '')).toEqual(0); 21 | expect(defaultSort('desc', {}, {})(undefined, '')).toEqual(0); 22 | 23 | expect(defaultSort('asc', {}, {})(null, undefined)).toEqual(0); 24 | expect(defaultSort('desc', {}, {})(null, undefined)).toEqual(0); 25 | expect(defaultSort('asc', {}, {})(undefined, null)).toEqual(0); 26 | expect(defaultSort('desc', {}, {})(undefined, null)).toEqual(0); 27 | }); 28 | -------------------------------------------------------------------------------- /src/plugins/columnSorting/test/sortFunction/numeric.unit.js: -------------------------------------------------------------------------------- 1 | import { compareFunctionFactory as numericSort } from 'handsontable/plugins/columnSorting/sortFunction/numeric'; 2 | 3 | it('numericSort comparing function shouldn\'t change order when comparing empty string, null and undefined', () => { 4 | expect(numericSort('asc', {}, {})(null, null)).toEqual(0); 5 | expect(numericSort('desc', {}, {})(null, null)).toEqual(0); 6 | 7 | expect(numericSort('asc', {}, {})('', '')).toEqual(0); 8 | expect(numericSort('desc', {}, {})('', '')).toEqual(0); 9 | 10 | expect(numericSort('asc', {}, {})(undefined, undefined)).toEqual(0); 11 | expect(numericSort('desc', {}, {})(undefined, undefined)).toEqual(0); 12 | 13 | expect(numericSort('asc', {}, {})('', null)).toEqual(0); 14 | expect(numericSort('desc', {}, {})('', null)).toEqual(0); 15 | expect(numericSort('asc', {}, {})(null, '')).toEqual(0); 16 | expect(numericSort('desc', {}, {})(null, '')).toEqual(0); 17 | 18 | expect(numericSort('asc', {}, {})('', undefined)).toEqual(0); 19 | expect(numericSort('desc', {}, {})('', undefined)).toEqual(0); 20 | expect(numericSort('asc', {}, {})(undefined, '')).toEqual(0); 21 | expect(numericSort('desc', {}, {})(undefined, '')).toEqual(0); 22 | 23 | expect(numericSort('asc', {}, {})(null, undefined)).toEqual(0); 24 | expect(numericSort('desc', {}, {})(null, undefined)).toEqual(0); 25 | expect(numericSort('asc', {}, {})(undefined, null)).toEqual(0); 26 | expect(numericSort('desc', {}, {})(undefined, null)).toEqual(0); 27 | }); 28 | -------------------------------------------------------------------------------- /src/plugins/columnSorting/test/utils.unit.js: -------------------------------------------------------------------------------- 1 | import { areValidSortStates, ASC_SORT_STATE, DESC_SORT_STATE } from 'handsontable/plugins/columnSorting/utils'; 2 | 3 | describe('ColumnSorting', () => { 4 | it('areValidSortStates', () => { 5 | expect(areValidSortStates([{}])).toBeFalsy(); 6 | expect(areValidSortStates([{ column: 1 }])).toBeFalsy(); 7 | expect(areValidSortStates([{ sortOrder: ASC_SORT_STATE }])).toBeFalsy(); 8 | expect(areValidSortStates([{ sortOrder: DESC_SORT_STATE }])).toBeFalsy(); 9 | expect(areValidSortStates([{ column: 1, sortOrder: DESC_SORT_STATE }, { 10 | column: 1, 11 | sortOrder: DESC_SORT_STATE 12 | }])).toBeFalsy(); 13 | expect(areValidSortStates([{ column: 1, sortOrder: DESC_SORT_STATE }])).toBeTruthy(); 14 | expect(areValidSortStates([{ column: 1, sortOrder: ASC_SORT_STATE }])).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/plugins/comments/comments.css: -------------------------------------------------------------------------------- 1 | .htCommentCell { 2 | position: relative; 3 | } 4 | 5 | .htCommentCell:after { 6 | content: ''; 7 | position: absolute; 8 | top: 0; 9 | right: 0; 10 | border-left: 6px solid transparent; 11 | border-top: 6px solid black; 12 | } 13 | 14 | .htComments { 15 | display: none; 16 | z-index: 1059; 17 | position: absolute; 18 | } 19 | 20 | .htCommentTextArea { 21 | box-shadow: rgba(0, 0, 0, 0.117647) 0 1px 3px, rgba(0, 0, 0, 0.239216) 0 1px 2px; 22 | -webkit-box-sizing: border-box; 23 | -moz-box-sizing: border-box; 24 | box-sizing: border-box; 25 | border: none; 26 | border-left: 3px solid #ccc; 27 | background-color: #fff; 28 | width: 215px; 29 | height: 90px; 30 | font-size: 12px; 31 | padding: 5px; 32 | outline: 0px !important; 33 | -webkit-appearance: none; 34 | } 35 | 36 | .htCommentTextArea:focus { 37 | box-shadow: rgba(0, 0, 0, 0.117647) 0 1px 3px, rgba(0, 0, 0, 0.239216) 0 1px 2px, inset 0 0 0 1px #5292f7; 38 | border-left: 3px solid #5292f7; 39 | } 40 | -------------------------------------------------------------------------------- /src/plugins/contextMenu/predefinedItems/clearColumn.js: -------------------------------------------------------------------------------- 1 | import { getValidSelection } from './../utils'; 2 | import * as C from './../../../i18n/constants'; 3 | 4 | export const KEY = 'clear_column'; 5 | 6 | export default function clearColumnItem() { 7 | return { 8 | key: KEY, 9 | name() { 10 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_CLEAR_COLUMN); 11 | }, 12 | callback(key, selection) { 13 | const column = selection[0].start.col; 14 | 15 | if (this.countRows()) { 16 | this.populateFromArray(0, column, [[null]], Math.max(selection[0].start.row, selection[0].end.row), column, 'ContextMenu.clearColumn'); 17 | } 18 | }, 19 | disabled() { 20 | const selected = getValidSelection(this); 21 | 22 | if (!selected) { 23 | return true; 24 | } 25 | const [startRow, startColumn, endRow] = selected[0]; 26 | const entireRowSelection = [startRow, 0, endRow, this.countCols() - 1]; 27 | const rowSelected = entireRowSelection.join(',') === selected.join(','); 28 | 29 | return startColumn < 0 || this.countCols() >= this.getSettings().maxCols || rowSelected; 30 | } 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/plugins/contextMenu/predefinedItems/columnLeft.js: -------------------------------------------------------------------------------- 1 | import { getValidSelection } from './../utils'; 2 | import * as C from './../../../i18n/constants'; 3 | 4 | export const KEY = 'col_left'; 5 | 6 | export default function columnLeftItem() { 7 | return { 8 | key: KEY, 9 | name() { 10 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_INSERT_LEFT); 11 | }, 12 | callback(key, normalizedSelection) { 13 | const latestSelection = normalizedSelection[Math.max(normalizedSelection.length - 1, 0)]; 14 | 15 | this.alter('insert_col', latestSelection.start.col, 1, 'ContextMenu.columnLeft'); 16 | }, 17 | disabled() { 18 | const selected = getValidSelection(this); 19 | 20 | if (!selected) { 21 | return true; 22 | } 23 | if (!this.isColumnModificationAllowed()) { 24 | return true; 25 | } 26 | const [startRow, startColumn, endRow] = selected[0]; 27 | const entireRowSelection = [startRow, 0, endRow, this.countCols() - 1]; 28 | const rowSelected = entireRowSelection.join(',') === selected.join(','); 29 | const onlyOneColumn = this.countCols() === 1; 30 | 31 | return startColumn < 0 || this.countCols() >= this.getSettings().maxCols || (!onlyOneColumn && rowSelected); 32 | }, 33 | hidden() { 34 | return !this.getSettings().allowInsertColumn; 35 | } 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/plugins/contextMenu/predefinedItems/columnRight.js: -------------------------------------------------------------------------------- 1 | import { getValidSelection } from './../utils'; 2 | import * as C from './../../../i18n/constants'; 3 | 4 | export const KEY = 'col_right'; 5 | 6 | export default function columnRightItem() { 7 | return { 8 | key: KEY, 9 | name() { 10 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_INSERT_RIGHT); 11 | }, 12 | callback(key, normalizedSelection) { 13 | const latestSelection = normalizedSelection[Math.max(normalizedSelection.length - 1, 0)]; 14 | 15 | this.alter('insert_col', latestSelection.end.col + 1, 1, 'ContextMenu.columnRight'); 16 | }, 17 | disabled() { 18 | const selected = getValidSelection(this); 19 | 20 | if (!selected) { 21 | return true; 22 | } 23 | if (!this.isColumnModificationAllowed()) { 24 | return true; 25 | } 26 | const [startRow, startColumn, endRow] = selected[0]; 27 | const entireRowSelection = [startRow, 0, endRow, this.countCols() - 1]; 28 | const rowSelected = entireRowSelection.join(',') === selected.join(','); 29 | const onlyOneColumn = this.countCols() === 1; 30 | 31 | return startColumn < 0 || this.countCols() >= this.getSettings().maxCols || (!onlyOneColumn && rowSelected); 32 | }, 33 | hidden() { 34 | return !this.getSettings().allowInsertColumn; 35 | } 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/plugins/contextMenu/predefinedItems/readOnly.js: -------------------------------------------------------------------------------- 1 | import { checkSelectionConsistency, markLabelAsSelected } from './../utils'; 2 | import { arrayEach } from './../../../helpers/array'; 3 | import * as C from './../../../i18n/constants'; 4 | 5 | export const KEY = 'make_read_only'; 6 | 7 | export default function readOnlyItem() { 8 | return { 9 | key: KEY, 10 | name() { 11 | let label = this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_READ_ONLY); 12 | const atLeastOneReadOnly = checkSelectionConsistency(this.getSelectedRange(), (row, col) => this.getCellMeta(row, col).readOnly); 13 | 14 | if (atLeastOneReadOnly) { 15 | label = markLabelAsSelected(label); 16 | } 17 | 18 | return label; 19 | }, 20 | callback() { 21 | const ranges = this.getSelectedRange(); 22 | const atLeastOneReadOnly = checkSelectionConsistency(ranges, (row, col) => this.getCellMeta(row, col).readOnly); 23 | 24 | arrayEach(ranges, (range) => { 25 | range.forAll((row, col) => { 26 | this.setCellMeta(row, col, 'readOnly', !atLeastOneReadOnly); 27 | }); 28 | }); 29 | 30 | this.render(); 31 | }, 32 | disabled() { 33 | return !(this.getSelectedRange() && !this.selection.isSelectedByCorner()); 34 | } 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/plugins/contextMenu/predefinedItems/redo.js: -------------------------------------------------------------------------------- 1 | import * as C from './../../../i18n/constants'; 2 | 3 | export const KEY = 'redo'; 4 | 5 | export default function redoItem() { 6 | return { 7 | key: KEY, 8 | name() { 9 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_REDO); 10 | }, 11 | callback() { 12 | this.redo(); 13 | }, 14 | disabled() { 15 | return this.undoRedo && !this.undoRedo.isRedoAvailable(); 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/plugins/contextMenu/predefinedItems/removeColumn.js: -------------------------------------------------------------------------------- 1 | import { getValidSelection } from './../utils'; 2 | import { transformSelectionToColumnDistance } from './../../../selection/utils'; 3 | import * as C from './../../../i18n/constants'; 4 | 5 | export const KEY = 'remove_col'; 6 | 7 | export default function removeColumnItem() { 8 | return { 9 | key: KEY, 10 | name() { 11 | const selection = this.getSelected(); 12 | let pluralForm = 0; 13 | 14 | if (selection) { 15 | if (selection.length > 1) { 16 | pluralForm = 1; 17 | } else { 18 | const [, fromColumn, , toColumn] = selection[0]; 19 | 20 | if (fromColumn - toColumn !== 0) { 21 | pluralForm = 1; 22 | } 23 | } 24 | } 25 | 26 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_REMOVE_COLUMN, pluralForm); 27 | }, 28 | callback() { 29 | this.alter('remove_col', transformSelectionToColumnDistance(this.getSelected()), null, 'ContextMenu.removeColumn'); 30 | }, 31 | disabled() { 32 | const selected = getValidSelection(this); 33 | const totalColumns = this.countCols(); 34 | 35 | if (!selected) { 36 | return true; 37 | } 38 | 39 | return this.selection.isSelectedByRowHeader() || this.selection.isSelectedByCorner() || 40 | !this.isColumnModificationAllowed() || !totalColumns; 41 | }, 42 | hidden() { 43 | return !this.getSettings().allowRemoveColumn; 44 | } 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/plugins/contextMenu/predefinedItems/rowAbove.js: -------------------------------------------------------------------------------- 1 | import { getValidSelection } from './../utils'; 2 | import * as C from './../../../i18n/constants'; 3 | 4 | export const KEY = 'row_above'; 5 | 6 | export default function rowAboveItem() { 7 | return { 8 | key: KEY, 9 | name() { 10 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_ROW_ABOVE); 11 | }, 12 | callback(key, normalizedSelection) { 13 | const latestSelection = normalizedSelection[Math.max(normalizedSelection.length - 1, 0)]; 14 | 15 | this.alter('insert_row', latestSelection.start.row, 1, 'ContextMenu.rowAbove'); 16 | }, 17 | disabled() { 18 | const selected = getValidSelection(this); 19 | 20 | if (!selected) { 21 | return true; 22 | } 23 | 24 | return this.selection.isSelectedByColumnHeader() || this.countRows() >= this.getSettings().maxRows; 25 | }, 26 | hidden() { 27 | return !this.getSettings().allowInsertRow; 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/plugins/contextMenu/predefinedItems/rowBelow.js: -------------------------------------------------------------------------------- 1 | import { getValidSelection } from './../utils'; 2 | import * as C from './../../../i18n/constants'; 3 | 4 | export const KEY = 'row_below'; 5 | 6 | export default function rowBelowItem() { 7 | return { 8 | key: KEY, 9 | name() { 10 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_ROW_BELOW); 11 | }, 12 | callback(key, normalizedSelection) { 13 | const latestSelection = normalizedSelection[Math.max(normalizedSelection.length - 1, 0)]; 14 | 15 | this.alter('insert_row', latestSelection.end.row + 1, 1, 'ContextMenu.rowBelow'); 16 | }, 17 | disabled() { 18 | const selected = getValidSelection(this); 19 | 20 | if (!selected) { 21 | return true; 22 | } 23 | 24 | return this.selection.isSelectedByColumnHeader() || this.countRows() >= this.getSettings().maxRows; 25 | }, 26 | hidden() { 27 | return !this.getSettings().allowInsertRow; 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/plugins/contextMenu/predefinedItems/separator.js: -------------------------------------------------------------------------------- 1 | export const KEY = '---------'; 2 | 3 | export default function separatorItem() { 4 | return { 5 | name: KEY 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /src/plugins/contextMenu/predefinedItems/undo.js: -------------------------------------------------------------------------------- 1 | import * as C from './../../../i18n/constants'; 2 | 3 | export const KEY = 'undo'; 4 | 5 | export default function undoItem() { 6 | return { 7 | key: KEY, 8 | name() { 9 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_UNDO); 10 | }, 11 | callback() { 12 | this.undo(); 13 | }, 14 | disabled() { 15 | return this.undoRedo && !this.undoRedo.isUndoAvailable(); 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/plugins/contextMenu/test/predefinedItems/readOnly.e2e.js: -------------------------------------------------------------------------------- 1 | describe('ContextMenuReadOnly', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should trigger `afterSetCellMeta` callback after changing cell to read only by context menu', () => { 16 | const afterSetCellMetaCallback = jasmine.createSpy('afterSetCellMetaCallback'); 17 | const rows = 5; 18 | const columns = 5; 19 | 20 | handsontable({ 21 | data: Handsontable.helper.createSpreadsheetData(rows, columns), 22 | rowHeaders: true, 23 | colHeaders: true, 24 | contextMenu: true, 25 | afterSetCellMeta: afterSetCellMetaCallback 26 | }); 27 | 28 | selectCell(2, 3); 29 | contextMenu(); 30 | 31 | const changeToReadOnluButton = $('.htItemWrapper').filter(function() { 32 | return $(this).text() === 'Read only'; 33 | })[0]; 34 | 35 | $(changeToReadOnluButton).simulate('mousedown'); 36 | expect(afterSetCellMetaCallback).toHaveBeenCalledWith(2, 3, 'readOnly', true, undefined, undefined); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/plugins/copyPaste/clipboardData.js: -------------------------------------------------------------------------------- 1 | export default class ClipboardData { 2 | constructor() { 3 | this.data = {}; 4 | } 5 | setData(type, value) { 6 | this.data[type] = value; 7 | } 8 | getData(type) { 9 | return this.data[type] || void 0; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/copyPaste/contextMenuItem/copy.js: -------------------------------------------------------------------------------- 1 | import * as C from './../../../i18n/constants'; 2 | 3 | export default function copyItem(copyPastePlugin) { 4 | return { 5 | key: 'copy', 6 | name() { 7 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_COPY); 8 | }, 9 | callback() { 10 | copyPastePlugin.copy(); 11 | }, 12 | disabled() { 13 | const selected = this.getSelected(); 14 | 15 | if (!selected || selected.length > 1) { 16 | return true; 17 | } 18 | 19 | return false; 20 | }, 21 | hidden: false 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/plugins/copyPaste/contextMenuItem/cut.js: -------------------------------------------------------------------------------- 1 | import * as C from './../../../i18n/constants'; 2 | 3 | export default function cutItem(copyPastePlugin) { 4 | return { 5 | key: 'cut', 6 | name() { 7 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_CUT); 8 | }, 9 | callback() { 10 | copyPastePlugin.cut(); 11 | }, 12 | disabled() { 13 | const selected = this.getSelected(); 14 | 15 | if (!selected || selected.length > 1) { 16 | return true; 17 | } 18 | 19 | return false; 20 | }, 21 | hidden: false 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/plugins/copyPaste/copyPaste.css: -------------------------------------------------------------------------------- 1 | textarea#HandsontableCopyPaste { 2 | position: fixed !important; 3 | top: 0 !important; 4 | right: 100% !important; 5 | overflow: hidden; 6 | opacity: 0; 7 | outline: 0 none !important; 8 | } 9 | -------------------------------------------------------------------------------- /src/plugins/copyPaste/pasteEvent.js: -------------------------------------------------------------------------------- 1 | import ClipboardData from './clipboardData'; 2 | 3 | export default class PasteEvent { 4 | constructor() { 5 | this.clipboardData = new ClipboardData(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/plugins/copyPaste/test/copyPaste.types.ts: -------------------------------------------------------------------------------- 1 | import * as Handsontable from 'handsontable'; 2 | 3 | const copyPaste = Handsontable.plugins.CopyPaste; 4 | 5 | copyPaste.columnsLimit = 10; 6 | copyPaste.rowsLimit = 10; 7 | copyPaste.pasteMode = 'overwrite'; 8 | copyPaste.pasteMode = 'shift_down'; 9 | copyPaste.pasteMode = 'shift_right'; 10 | 11 | copyPaste.copy(); 12 | copyPaste.copy(true); 13 | 14 | copyPaste.cut(); 15 | copyPaste.cut(true); 16 | 17 | copyPaste.paste(); 18 | copyPaste.paste(true); 19 | 20 | copyPaste.getRangedData([{ startRow: 1, startCol: 1, endRow: 2, endCol: 2 }]); 21 | copyPaste.getRangedCopyableData([{ startRow: 1, startCol: 1, endRow: 2, endCol: 2 }]); 22 | copyPaste.setCopyableText(); 23 | -------------------------------------------------------------------------------- /src/plugins/customBorders/contextMenuItem/bottom.js: -------------------------------------------------------------------------------- 1 | import * as C from './../../../i18n/constants'; 2 | import { checkSelectionBorders, markSelected } from './../utils'; 3 | 4 | export default function bottom(customBordersPlugin) { 5 | return { 6 | key: 'borders:bottom', 7 | name() { 8 | let label = this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_BORDERS_BOTTOM); 9 | const hasBorder = checkSelectionBorders(this, 'bottom'); 10 | if (hasBorder) { 11 | label = markSelected(label); 12 | } 13 | return label; 14 | }, 15 | callback(key, selected) { 16 | const hasBorder = checkSelectionBorders(this, 'bottom'); 17 | customBordersPlugin.prepareBorder(selected, 'bottom', hasBorder); 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/plugins/customBorders/contextMenuItem/index.js: -------------------------------------------------------------------------------- 1 | import bottom from './bottom'; 2 | import left from './left'; 3 | import noBorders from './noBorders'; 4 | import right from './right'; 5 | import top from './top'; 6 | 7 | export { 8 | bottom, 9 | left, 10 | noBorders, 11 | right, 12 | top 13 | }; 14 | -------------------------------------------------------------------------------- /src/plugins/customBorders/contextMenuItem/left.js: -------------------------------------------------------------------------------- 1 | import * as C from './../../../i18n/constants'; 2 | import { checkSelectionBorders, markSelected } from './../utils'; 3 | 4 | export default function left(customBordersPlugin) { 5 | return { 6 | key: 'borders:left', 7 | name() { 8 | let label = this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_BORDERS_LEFT); 9 | const hasBorder = checkSelectionBorders(this, 'left'); 10 | if (hasBorder) { 11 | label = markSelected(label); 12 | } 13 | 14 | return label; 15 | }, 16 | callback(key, selected) { 17 | const hasBorder = checkSelectionBorders(this, 'left'); 18 | customBordersPlugin.prepareBorder(selected, 'left', hasBorder); 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/plugins/customBorders/contextMenuItem/noBorders.js: -------------------------------------------------------------------------------- 1 | import * as C from './../../../i18n/constants'; 2 | import { checkSelectionBorders } from './../utils'; 3 | 4 | export default function noBorders(customBordersPlugin) { 5 | return { 6 | key: 'borders:no_borders', 7 | name() { 8 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_REMOVE_BORDERS); 9 | }, 10 | callback(key, selected) { 11 | customBordersPlugin.prepareBorder(selected, 'noBorders'); 12 | }, 13 | disabled() { 14 | return !checkSelectionBorders(this); 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/plugins/customBorders/contextMenuItem/right.js: -------------------------------------------------------------------------------- 1 | import * as C from './../../../i18n/constants'; 2 | import { checkSelectionBorders, markSelected } from './../utils'; 3 | 4 | export default function right(customBordersPlugin) { 5 | return { 6 | key: 'borders:right', 7 | name() { 8 | let label = this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_BORDERS_RIGHT); 9 | const hasBorder = checkSelectionBorders(this, 'right'); 10 | if (hasBorder) { 11 | label = markSelected(label); 12 | } 13 | return label; 14 | }, 15 | callback(key, selected) { 16 | const hasBorder = checkSelectionBorders(this, 'right'); 17 | customBordersPlugin.prepareBorder(selected, 'right', hasBorder); 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/plugins/customBorders/contextMenuItem/top.js: -------------------------------------------------------------------------------- 1 | import * as C from './../../../i18n/constants'; 2 | import { checkSelectionBorders, markSelected } from './../utils'; 3 | 4 | export default function top(customBordersPlugin) { 5 | return { 6 | key: 'borders:top', 7 | name() { 8 | let label = this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_BORDERS_TOP); 9 | const hasBorder = checkSelectionBorders(this, 'top'); 10 | if (hasBorder) { 11 | label = markSelected(label); 12 | } 13 | 14 | return label; 15 | }, 16 | callback(key, selected) { 17 | const hasBorder = checkSelectionBorders(this, 'top'); 18 | customBordersPlugin.prepareBorder(selected, 'top', hasBorder); 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/plugins/manualColumnFreeze/contextMenuItem/freezeColumn.js: -------------------------------------------------------------------------------- 1 | import * as C from './../../../i18n/constants'; 2 | 3 | export default function freezeColumnItem(manualColumnFreezePlugin) { 4 | return { 5 | key: 'freeze_column', 6 | name() { 7 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_FREEZE_COLUMN); 8 | }, 9 | callback(key, selected) { 10 | const [{ start: { col: selectedColumn } }] = selected; 11 | 12 | manualColumnFreezePlugin.freezeColumn(selectedColumn); 13 | 14 | this.render(); 15 | this.view.wt.wtOverlays.adjustElementsSize(true); 16 | }, 17 | hidden() { 18 | const selection = this.getSelectedRange(); 19 | let hide = false; 20 | 21 | if (selection === void 0) { 22 | hide = true; 23 | 24 | } else if (selection.length > 1) { 25 | hide = true; 26 | 27 | } else if ((selection[0].from.col !== selection[0].to.col) || (selection[0].from.col <= this.getSettings().fixedColumnsLeft - 1)) { 28 | hide = true; 29 | } 30 | 31 | return hide; 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/plugins/manualColumnFreeze/contextMenuItem/unfreezeColumn.js: -------------------------------------------------------------------------------- 1 | import * as C from './../../../i18n/constants'; 2 | 3 | export default function unfreezeColumnItem(manualColumnFreezePlugin) { 4 | return { 5 | key: 'unfreeze_column', 6 | name() { 7 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_UNFREEZE_COLUMN); 8 | }, 9 | callback(key, selected) { 10 | const [{ start: { col: selectedColumn } }] = selected; 11 | 12 | manualColumnFreezePlugin.unfreezeColumn(selectedColumn); 13 | 14 | this.render(); 15 | this.view.wt.wtOverlays.adjustElementsSize(true); 16 | }, 17 | hidden() { 18 | const selection = this.getSelectedRange(); 19 | let hide = false; 20 | 21 | if (selection === void 0) { 22 | hide = true; 23 | 24 | } else if (selection.length > 1) { 25 | hide = true; 26 | 27 | } else if ((selection[0].from.col !== selection[0].to.col) || selection[0].from.col >= this.getSettings().fixedColumnsLeft) { 28 | hide = true; 29 | } 30 | 31 | return hide; 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/plugins/manualColumnFreeze/manualColumnFreeze.css: -------------------------------------------------------------------------------- 1 | .htRowHeaders .ht_master.innerBorderLeft ~ .ht_clone_top_left_corner th:nth-child(2), 2 | .htRowHeaders .ht_master.innerBorderLeft ~ .ht_clone_left td:first-of-type { 3 | border-left: 0 none; 4 | } 5 | -------------------------------------------------------------------------------- /src/plugins/manualColumnMove/manualColumnMove.css: -------------------------------------------------------------------------------- 1 | .handsontable .wtHider { 2 | position: relative; 3 | } 4 | .handsontable.ht__manualColumnMove.after-selection--columns thead th.ht__highlight { 5 | cursor: move; 6 | cursor: -moz-grab; 7 | cursor: -webkit-grab; 8 | cursor: grab; 9 | } 10 | .handsontable.ht__manualColumnMove.on-moving--columns, 11 | .handsontable.ht__manualColumnMove.on-moving--columns thead th.ht__highlight { 12 | cursor: move; 13 | cursor: -moz-grabbing; 14 | cursor: -webkit-grabbing; 15 | cursor: grabbing; 16 | } 17 | .handsontable.ht__manualColumnMove.on-moving--columns .manualColumnResizer { 18 | display: none; 19 | } 20 | .handsontable .ht__manualColumnMove--guideline, 21 | .handsontable .ht__manualColumnMove--backlight { 22 | position: absolute; 23 | height: 100%; 24 | display: none; 25 | } 26 | .handsontable .ht__manualColumnMove--guideline { 27 | background: #757575; 28 | width: 2px; 29 | top: 0; 30 | margin-left: -1px; 31 | z-index: 105; 32 | } 33 | .handsontable .ht__manualColumnMove--backlight { 34 | background: #343434; 35 | background: rgba(52, 52, 52, 0.25); 36 | display: none; 37 | z-index: 105; 38 | pointer-events: none; 39 | } 40 | .handsontable.on-moving--columns.show-ui .ht__manualColumnMove--guideline, 41 | .handsontable.on-moving--columns .ht__manualColumnMove--backlight { 42 | display: block; 43 | } 44 | -------------------------------------------------------------------------------- /src/plugins/manualColumnMove/ui/backlight.js: -------------------------------------------------------------------------------- 1 | import BaseUI from './_base'; 2 | import { addClass } from './../../../helpers/dom/element'; 3 | 4 | const CSS_CLASSNAME = 'ht__manualColumnMove--backlight'; 5 | 6 | /** 7 | * @class BacklightUI 8 | * @util 9 | */ 10 | class BacklightUI extends BaseUI { 11 | /** 12 | * Custom className on build process. 13 | */ 14 | build() { 15 | super.build(); 16 | 17 | addClass(this._element, CSS_CLASSNAME); 18 | } 19 | } 20 | 21 | export default BacklightUI; 22 | -------------------------------------------------------------------------------- /src/plugins/manualColumnMove/ui/guideline.js: -------------------------------------------------------------------------------- 1 | import BaseUI from './_base'; 2 | import { addClass } from './../../../helpers/dom/element'; 3 | 4 | const CSS_CLASSNAME = 'ht__manualColumnMove--guideline'; 5 | 6 | /** 7 | * @class GuidelineUI 8 | * @util 9 | */ 10 | class GuidelineUI extends BaseUI { 11 | /** 12 | * Custom className on build process. 13 | */ 14 | build() { 15 | super.build(); 16 | 17 | addClass(this._element, CSS_CLASSNAME); 18 | } 19 | } 20 | 21 | export default GuidelineUI; 22 | -------------------------------------------------------------------------------- /src/plugins/manualRowMove/manualRowMove.css: -------------------------------------------------------------------------------- 1 | .handsontable .wtHider { 2 | position: relative; 3 | } 4 | .handsontable.ht__manualRowMove.after-selection--rows tbody th.ht__highlight { 5 | cursor: move; 6 | cursor: -moz-grab; 7 | cursor: -webkit-grab; 8 | cursor: grab; 9 | } 10 | .handsontable.ht__manualRowMove.on-moving--rows, 11 | .handsontable.ht__manualRowMove.on-moving--rows tbody th.ht__highlight { 12 | cursor: move; 13 | cursor: -moz-grabbing; 14 | cursor: -webkit-grabbing; 15 | cursor: grabbing; 16 | } 17 | .handsontable.ht__manualRowMove.on-moving--rows .manualRowResizer { 18 | display: none; 19 | } 20 | .handsontable .ht__manualRowMove--guideline, 21 | .handsontable .ht__manualRowMove--backlight { 22 | position: absolute; 23 | width: 100%; 24 | display: none; 25 | } 26 | .handsontable .ht__manualRowMove--guideline { 27 | background: #757575; 28 | height: 2px; 29 | left: 0; 30 | margin-top: -1px; 31 | z-index: 105; 32 | } 33 | .handsontable .ht__manualRowMove--backlight { 34 | background: #343434; 35 | background: rgba(52, 52, 52, 0.25); 36 | display: none; 37 | z-index: 105; 38 | pointer-events: none; 39 | } 40 | .handsontable.on-moving--rows.show-ui .ht__manualRowMove--guideline, 41 | .handsontable.on-moving--rows .ht__manualRowMove--backlight { 42 | display: block; 43 | } 44 | -------------------------------------------------------------------------------- /src/plugins/manualRowMove/ui/backlight.js: -------------------------------------------------------------------------------- 1 | import BaseUI from './_base'; 2 | import { addClass } from './../../../helpers/dom/element'; 3 | 4 | const CSS_CLASSNAME = 'ht__manualRowMove--backlight'; 5 | 6 | /** 7 | * @class BacklightUI 8 | * @util 9 | */ 10 | class BacklightUI extends BaseUI { 11 | /** 12 | * Custom className on build process. 13 | */ 14 | build() { 15 | super.build(); 16 | 17 | addClass(this._element, CSS_CLASSNAME); 18 | } 19 | } 20 | 21 | export default BacklightUI; 22 | -------------------------------------------------------------------------------- /src/plugins/manualRowMove/ui/guideline.js: -------------------------------------------------------------------------------- 1 | import BaseUI from './_base'; 2 | import { addClass } from './../../../helpers/dom/element'; 3 | 4 | const CSS_CLASSNAME = 'ht__manualRowMove--guideline'; 5 | 6 | /** 7 | * @class GuidelineUI 8 | * @util 9 | */ 10 | class GuidelineUI extends BaseUI { 11 | /** 12 | * Custom className on build process. 13 | */ 14 | build() { 15 | super.build(); 16 | 17 | addClass(this._element, CSS_CLASSNAME); 18 | } 19 | } 20 | 21 | export default GuidelineUI; 22 | -------------------------------------------------------------------------------- /src/plugins/mergeCells/contextMenuItem/toggleMerge.js: -------------------------------------------------------------------------------- 1 | import * as C from '../../../i18n/constants'; 2 | import MergedCellCoords from '../cellCoords'; 3 | 4 | export default function toggleMergeItem(plugin) { 5 | return { 6 | key: 'mergeCells', 7 | name() { 8 | const sel = this.getSelectedLast(); 9 | 10 | if (sel) { 11 | const info = plugin.mergedCellsCollection.get(sel[0], sel[1]); 12 | 13 | if (info.row === sel[0] && info.col === sel[1] && info.row + info.rowspan - 1 === sel[2] && info.col + info.colspan - 1 === sel[3]) { 14 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_UNMERGE_CELLS); 15 | } 16 | } 17 | 18 | return this.getTranslatedPhrase(C.CONTEXTMENU_ITEMS_MERGE_CELLS); 19 | }, 20 | callback() { 21 | plugin.toggleMergeOnSelection(); 22 | }, 23 | disabled() { 24 | const sel = this.getSelectedLast(); 25 | 26 | if (!sel) { 27 | return true; 28 | } 29 | 30 | const isSingleCell = MergedCellCoords.isSingleCell({ 31 | row: sel[0], 32 | col: sel[1], 33 | rowspan: sel[2] - sel[0] + 1, 34 | colspan: sel[3] - sel[1] + 1 35 | }); 36 | 37 | return isSingleCell || this.selection.isSelectedByCorner(); 38 | }, 39 | hidden: false 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/plugins/mergeCells/mergeCells.css: -------------------------------------------------------------------------------- 1 | .handsontable tbody td[rowspan][class*="area"][class*="highlight"]:not([class*="fullySelectedMergedCell"]):before { 2 | opacity: 0; 3 | } 4 | 5 | .handsontable tbody td[rowspan][class*="area"][class*="highlight"][class*="fullySelectedMergedCell-multiple"]:before { 6 | opacity: 0.1; 7 | } 8 | 9 | .handsontable tbody td[rowspan][class*="area"][class*="highlight"][class*="fullySelectedMergedCell-0"]:before { 10 | opacity: 0.1; 11 | } 12 | 13 | .handsontable tbody td[rowspan][class*="area"][class*="highlight"][class*="fullySelectedMergedCell-1"]:before { 14 | opacity: 0.2; 15 | } 16 | 17 | .handsontable tbody td[rowspan][class*="area"][class*="highlight"][class*="fullySelectedMergedCell-2"]:before { 18 | opacity: 0.27; 19 | } 20 | 21 | .handsontable tbody td[rowspan][class*="area"][class*="highlight"][class*="fullySelectedMergedCell-3"]:before { 22 | opacity: 0.35; 23 | } 24 | 25 | .handsontable tbody td[rowspan][class*="area"][class*="highlight"][class*="fullySelectedMergedCell-4"]:before { 26 | opacity: 0.41; 27 | } 28 | 29 | .handsontable tbody td[rowspan][class*="area"][class*="highlight"][class*="fullySelectedMergedCell-5"]:before { 30 | opacity: 0.47; 31 | } 32 | 33 | .handsontable tbody td[rowspan][class*="area"][class*="highlight"][class*="fullySelectedMergedCell-6"]:before { 34 | opacity: 0.54; 35 | } 36 | 37 | .handsontable tbody td[rowspan][class*="area"][class*="highlight"][class*="fullySelectedMergedCell-7"]:before { 38 | opacity: 0.58; 39 | } 40 | -------------------------------------------------------------------------------- /src/plugins/mergeCells/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Apply the `colspan`/`rowspan` properties. 3 | * 4 | * @param {HTMLElement} TD The soon-to-be-modified cell. 5 | * @param {MergedCellCoords} merged cellInfo The merged cell in question. 6 | * @param {Number} row Row index. 7 | * @param {Number} col Column index. 8 | */ 9 | // eslint-disable-next-line import/prefer-default-export 10 | export function applySpanProperties(TD, mergedCellInfo, row, col) { 11 | if (mergedCellInfo) { 12 | if (mergedCellInfo.row === row && mergedCellInfo.col === col) { 13 | TD.setAttribute('rowspan', mergedCellInfo.rowspan.toString()); 14 | TD.setAttribute('colspan', mergedCellInfo.colspan.toString()); 15 | 16 | } else { 17 | TD.removeAttribute('rowspan'); 18 | TD.removeAttribute('colspan'); 19 | 20 | TD.style.display = 'none'; 21 | } 22 | 23 | } else { 24 | TD.removeAttribute('rowspan'); 25 | TD.removeAttribute('colspan'); 26 | 27 | TD.style.display = ''; 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/renderers/_cellDecorator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds appropriate CSS class to table cell, based on cellProperties 3 | */ 4 | import { addClass, removeClass } from './../helpers/dom/element'; 5 | 6 | function cellDecorator(instance, TD, row, col, prop, value, cellProperties) { 7 | const classesToAdd = []; 8 | const classesToRemove = []; 9 | 10 | if (cellProperties.className) { 11 | if (TD.className) { 12 | TD.className = `${TD.className} ${cellProperties.className}`; 13 | } else { 14 | TD.className = cellProperties.className; 15 | } 16 | } 17 | 18 | if (cellProperties.readOnly) { 19 | classesToAdd.push(cellProperties.readOnlyCellClassName); 20 | } 21 | 22 | if (cellProperties.valid === false && cellProperties.invalidCellClassName) { 23 | classesToAdd.push(cellProperties.invalidCellClassName); 24 | 25 | } else { 26 | classesToRemove.push(cellProperties.invalidCellClassName); 27 | } 28 | 29 | if (cellProperties.wordWrap === false && cellProperties.noWordWrapClassName) { 30 | classesToAdd.push(cellProperties.noWordWrapClassName); 31 | } 32 | 33 | if (!value && cellProperties.placeholder) { 34 | classesToAdd.push(cellProperties.placeholderCellClassName); 35 | } 36 | 37 | removeClass(TD, classesToRemove); 38 | addClass(TD, classesToAdd); 39 | } 40 | 41 | export default cellDecorator; 42 | -------------------------------------------------------------------------------- /src/renderers/htmlRenderer.js: -------------------------------------------------------------------------------- 1 | import { fastInnerHTML } from './../helpers/dom/element'; 2 | import { getRenderer } from './index'; 3 | 4 | /** 5 | * @private 6 | * @renderer HtmlRenderer 7 | * @param instance 8 | * @param TD 9 | * @param row 10 | * @param col 11 | * @param prop 12 | * @param value 13 | * @param cellProperties 14 | */ 15 | function htmlRenderer(instance, TD, row, col, prop, value, ...args) { 16 | getRenderer('base').apply(this, [instance, TD, row, col, prop, value, ...args]); 17 | 18 | fastInnerHTML(TD, value === null || value === void 0 ? '' : value); 19 | } 20 | 21 | export default htmlRenderer; 22 | -------------------------------------------------------------------------------- /src/renderers/index.js: -------------------------------------------------------------------------------- 1 | import staticRegister from './../utils/staticRegister'; 2 | 3 | import baseRenderer from './_cellDecorator'; 4 | import autocompleteRenderer from './autocompleteRenderer'; 5 | import checkboxRenderer from './checkboxRenderer'; 6 | import htmlRenderer from './htmlRenderer'; 7 | import numericRenderer from './numericRenderer'; 8 | import passwordRenderer from './passwordRenderer'; 9 | import textRenderer from './textRenderer'; 10 | 11 | const { 12 | register, 13 | getItem, 14 | hasItem, 15 | getNames, 16 | getValues, 17 | } = staticRegister('renderers'); 18 | 19 | register('base', baseRenderer); 20 | register('autocomplete', autocompleteRenderer); 21 | register('checkbox', checkboxRenderer); 22 | register('html', htmlRenderer); 23 | register('numeric', numericRenderer); 24 | register('password', passwordRenderer); 25 | register('text', textRenderer); 26 | 27 | /** 28 | * Retrieve renderer function. 29 | * 30 | * @param {String} name Renderer identification. 31 | * @returns {Function} Returns renderer function. 32 | */ 33 | function _getItem(name) { 34 | if (typeof name === 'function') { 35 | return name; 36 | } 37 | if (!hasItem(name)) { 38 | throw Error(`No registered renderer found under "${name}" name`); 39 | } 40 | 41 | return getItem(name); 42 | } 43 | 44 | export { 45 | register as registerRenderer, 46 | _getItem as getRenderer, 47 | hasItem as hasRenderer, 48 | getNames as getRegisteredRendererNames, 49 | getValues as getRegisteredRenderers, 50 | }; 51 | -------------------------------------------------------------------------------- /src/renderers/passwordRenderer.js: -------------------------------------------------------------------------------- 1 | import { fastInnerHTML } from './../helpers/dom/element'; 2 | import { getRenderer } from './index'; 3 | import { rangeEach } from './../helpers/number'; 4 | 5 | /** 6 | * @private 7 | * @renderer PasswordRenderer 8 | * @param instance 9 | * @param TD 10 | * @param row 11 | * @param col 12 | * @param prop 13 | * @param value 14 | * @param cellProperties 15 | */ 16 | function passwordRenderer(instance, TD, row, col, prop, value, cellProperties, ...args) { 17 | getRenderer('text').apply(this, [instance, TD, row, col, prop, value, cellProperties, ...args]); 18 | 19 | const hashLength = cellProperties.hashLength || TD.innerHTML.length; 20 | const hashSymbol = cellProperties.hashSymbol || '*'; 21 | 22 | let hash = ''; 23 | 24 | rangeEach(hashLength - 1, () => { 25 | hash += hashSymbol; 26 | }); 27 | fastInnerHTML(TD, hash); 28 | } 29 | 30 | export default passwordRenderer; 31 | -------------------------------------------------------------------------------- /src/selection/highlight/types/activeHeader.js: -------------------------------------------------------------------------------- 1 | import { Selection } from './../../../3rdparty/walkontable/src'; 2 | 3 | /** 4 | * @return {Selection} 5 | */ 6 | function createHighlight({ activeHeaderClassName }) { 7 | const s = new Selection({ 8 | highlightHeaderClassName: activeHeaderClassName, 9 | }); 10 | 11 | return s; 12 | } 13 | 14 | export default createHighlight; 15 | -------------------------------------------------------------------------------- /src/selection/highlight/types/area.js: -------------------------------------------------------------------------------- 1 | import { Selection } from './../../../3rdparty/walkontable/src'; 2 | 3 | /** 4 | * Creates the new instance of Selection responsible for highlighting area of the selected multiple cells. 5 | * 6 | * @return {Selection} 7 | */ 8 | function createHighlight({ layerLevel, areaCornerVisible }) { 9 | const s = new Selection({ 10 | className: 'area', 11 | markIntersections: true, 12 | layerLevel: Math.min(layerLevel, 7), 13 | border: { 14 | width: 1, 15 | color: '#4b89ff', 16 | cornerVisible: areaCornerVisible, 17 | }, 18 | }); 19 | 20 | return s; 21 | } 22 | 23 | export default createHighlight; 24 | -------------------------------------------------------------------------------- /src/selection/highlight/types/cell.js: -------------------------------------------------------------------------------- 1 | import { Selection } from './../../../3rdparty/walkontable/src'; 2 | 3 | /** 4 | * Creates the new instance of Selection responsible for highlighting currently selected cell. This type of selection 5 | * can present on the table only one at the time. 6 | * 7 | * @return {Selection} 8 | */ 9 | function createHighlight({ cellCornerVisible }) { 10 | const s = new Selection({ 11 | className: 'current', 12 | border: { 13 | width: 2, 14 | color: '#4b89ff', 15 | cornerVisible: cellCornerVisible, 16 | }, 17 | }); 18 | 19 | return s; 20 | } 21 | 22 | export default createHighlight; 23 | -------------------------------------------------------------------------------- /src/selection/highlight/types/customSelection.js: -------------------------------------------------------------------------------- 1 | import { Selection } from './../../../3rdparty/walkontable/src'; 2 | 3 | /** 4 | * Creates the new instance of Selection responsible for highlighting currently selected cell. This type of selection 5 | * can present on the table only one at the time. 6 | * 7 | * @return {Selection} 8 | */ 9 | function createHighlight({ border, cellRange }) { 10 | const s = new Selection(border, cellRange); 11 | 12 | return s; 13 | } 14 | 15 | export default createHighlight; 16 | -------------------------------------------------------------------------------- /src/selection/highlight/types/fill.js: -------------------------------------------------------------------------------- 1 | import { Selection } from './../../../3rdparty/walkontable/src'; 2 | 3 | /** 4 | * Creates the new instance of Selection, responsible for highlighting cells which are covered by fill handle 5 | * functionality. This type of selection can present on the table only one at the time. 6 | * 7 | * @return {Selection} 8 | */ 9 | function createHighlight() { 10 | const s = new Selection({ 11 | className: 'fill', 12 | border: { 13 | width: 1, 14 | color: '#ff0000', 15 | }, 16 | }); 17 | 18 | return s; 19 | } 20 | 21 | export default createHighlight; 22 | -------------------------------------------------------------------------------- /src/selection/highlight/types/header.js: -------------------------------------------------------------------------------- 1 | import { Selection } from './../../../3rdparty/walkontable/src'; 2 | 3 | /** 4 | * Creates the new instance of Selection, responsible for highlighting row and column headers. This type of selection 5 | * can occur multiple times. 6 | * 7 | * @return {Selection} 8 | */ 9 | function createHighlight({ headerClassName, rowClassName, columnClassName }) { 10 | const s = new Selection({ 11 | className: 'highlight', 12 | highlightHeaderClassName: headerClassName, 13 | highlightRowClassName: rowClassName, 14 | highlightColumnClassName: columnClassName, 15 | }); 16 | 17 | return s; 18 | } 19 | 20 | export default createHighlight; 21 | -------------------------------------------------------------------------------- /src/selection/highlight/types/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | import staticRegister from './../../../utils/staticRegister'; 3 | 4 | import activeHeaderHighlight from './activeHeader'; 5 | import areaHighlight from './area'; 6 | import cellHighlight from './cell'; 7 | import customSelection from './customSelection'; 8 | import fillHighlight from './fill'; 9 | import headerHighlight from './header'; 10 | 11 | const { 12 | register, 13 | getItem, 14 | } = staticRegister('highlight/types'); 15 | 16 | register('active-header', activeHeaderHighlight); 17 | register('area', areaHighlight); 18 | register('cell', cellHighlight); 19 | register('custom-selection', customSelection); 20 | register('fill', fillHighlight); 21 | register('header', headerHighlight); 22 | 23 | function createHighlight(highlightType, options) { 24 | return getItem(highlightType)(options); 25 | } 26 | 27 | export { 28 | createHighlight, 29 | }; 30 | -------------------------------------------------------------------------------- /src/selection/index.js: -------------------------------------------------------------------------------- 1 | import Highlight from './highlight/highlight'; 2 | import Selection from './selection'; 3 | import { handleMouseEvent } from './mouseEventHandler'; 4 | import { 5 | detectSelectionType, 6 | normalizeSelectionFactory, 7 | } from './utils'; 8 | 9 | export { 10 | handleMouseEvent, 11 | Highlight, 12 | Selection, 13 | detectSelectionType, 14 | normalizeSelectionFactory 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/dataStructures/queue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class Queue 3 | * @util 4 | */ 5 | class Queue { 6 | constructor(initial = []) { 7 | /** 8 | * Items collection. 9 | * 10 | * @type {Array} 11 | */ 12 | this.items = initial; 13 | } 14 | 15 | /** 16 | * Add new item or items at the back of the queue. 17 | * 18 | * @param {*} items An item to add. 19 | */ 20 | enqueue(...items) { 21 | this.items.push(...items); 22 | } 23 | 24 | /** 25 | * Remove the first element from the queue and returns it. 26 | * 27 | * @returns {*} 28 | */ 29 | dequeue() { 30 | return this.items.shift(); 31 | } 32 | 33 | /** 34 | * Return the first element from the queue (without modification queue stack). 35 | * 36 | * @returns {*} 37 | */ 38 | peek() { 39 | return this.isEmpty() ? void 0 : this.items[0]; 40 | } 41 | 42 | /** 43 | * Check if the queue is empty. 44 | * 45 | * @returns {Boolean} 46 | */ 47 | isEmpty() { 48 | return !this.size(); 49 | } 50 | 51 | /** 52 | * Return number of elements in the queue. 53 | * 54 | * @returns {Number} 55 | */ 56 | size() { 57 | return this.items.length; 58 | } 59 | } 60 | 61 | export default Queue; 62 | -------------------------------------------------------------------------------- /src/utils/dataStructures/stack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class Stack 3 | * @util 4 | */ 5 | class Stack { 6 | constructor(initial = []) { 7 | /** 8 | * Items collection. 9 | * 10 | * @type {Array} 11 | */ 12 | this.items = initial; 13 | } 14 | 15 | /** 16 | * Add new item or items at the back of the stack. 17 | * 18 | * @param {*} items An item to add. 19 | */ 20 | push(...items) { 21 | this.items.push(...items); 22 | } 23 | 24 | /** 25 | * Remove the last element from the stack and returns it. 26 | * 27 | * @returns {*} 28 | */ 29 | pop() { 30 | return this.items.pop(); 31 | } 32 | 33 | /** 34 | * Return the last element from the stack (without modification stack). 35 | * 36 | * @returns {*} 37 | */ 38 | peek() { 39 | return this.isEmpty() ? void 0 : this.items[this.items.length - 1]; 40 | } 41 | 42 | /** 43 | * Check if the stack is empty. 44 | * 45 | * @returns {Boolean} 46 | */ 47 | isEmpty() { 48 | return !this.size(); 49 | } 50 | 51 | /** 52 | * Return number of elements in the stack. 53 | * 54 | * @returns {Number} 55 | */ 56 | size() { 57 | return this.items.length; 58 | } 59 | } 60 | 61 | export default Stack; 62 | -------------------------------------------------------------------------------- /src/utils/rootInstance.js: -------------------------------------------------------------------------------- 1 | export const holder = new WeakMap(); 2 | 3 | export const rootInstanceSymbol = Symbol('rootInstance'); 4 | 5 | /** 6 | * Register an object as a root instance. 7 | * 8 | * @param {Object} object An object to associate with root instance flag. 9 | */ 10 | export function registerAsRootInstance(object) { 11 | holder.set(object, true); 12 | } 13 | 14 | /** 15 | * Check if the source of the root indication call is valid. 16 | * 17 | * @param {Symbol} rootSymbol A symbol as a source of truth. 18 | * @return {Boolean} 19 | */ 20 | export function hasValidParameter(rootSymbol) { 21 | return rootSymbol === rootInstanceSymbol; 22 | } 23 | 24 | /** 25 | * Check if passed an object was flagged as a root instance. 26 | * 27 | * @param {Object} object An object to check. 28 | * @return {Boolean} 29 | */ 30 | export function isRootInstance(object) { 31 | return holder.has(object); 32 | } 33 | -------------------------------------------------------------------------------- /src/validators/autocompleteValidator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Autocomplete cell validator. 3 | * 4 | * @private 5 | * @validator AutocompleteValidator 6 | * @param {*} value - Value of edited cell 7 | * @param {Function} callback - Callback called with validation result 8 | */ 9 | export default function autocompleteValidator(value, callback) { 10 | let valueToValidate = value; 11 | 12 | if (valueToValidate === null || valueToValidate === void 0) { 13 | valueToValidate = ''; 14 | } 15 | 16 | if (this.allowEmpty && valueToValidate === '') { 17 | callback(true); 18 | 19 | return; 20 | } 21 | 22 | if (this.strict && this.source) { 23 | if (typeof this.source === 'function') { 24 | this.source(valueToValidate, process(valueToValidate, callback)); 25 | } else { 26 | process(valueToValidate, callback)(this.source); 27 | } 28 | } else { 29 | callback(true); 30 | } 31 | } 32 | 33 | /** 34 | * Function responsible for validation of autocomplete value. 35 | * 36 | * @param {*} value - Value of edited cell 37 | * @param {Function} callback - Callback called with validation result 38 | */ 39 | function process(value, callback) { 40 | const originalVal = value; 41 | 42 | return function(source) { 43 | let found = false; 44 | 45 | for (let s = 0, slen = source.length; s < slen; s++) { 46 | if (originalVal === source[s]) { 47 | found = true; // perfect match 48 | break; 49 | } 50 | } 51 | 52 | callback(found); 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/validators/index.js: -------------------------------------------------------------------------------- 1 | import staticRegister from './../utils/staticRegister'; 2 | 3 | import autocompleteValidator from './autocompleteValidator'; 4 | import dateValidator from './dateValidator'; 5 | import numericValidator from './numericValidator'; 6 | import timeValidator from './timeValidator'; 7 | 8 | const { 9 | register, 10 | getItem, 11 | hasItem, 12 | getNames, 13 | getValues, 14 | } = staticRegister('validators'); 15 | 16 | register('autocomplete', autocompleteValidator); 17 | register('date', dateValidator); 18 | register('numeric', numericValidator); 19 | register('time', timeValidator); 20 | 21 | /** 22 | * Retrieve validator function. 23 | * 24 | * @param {String} name Validator identification. 25 | * @returns {Function} Returns validator function. 26 | */ 27 | function _getItem(name) { 28 | if (typeof name === 'function') { 29 | return name; 30 | } 31 | if (!hasItem(name)) { 32 | throw Error(`No registered validator found under "${name}" name`); 33 | } 34 | 35 | return getItem(name); 36 | } 37 | 38 | export { 39 | register as registerValidator, 40 | _getItem as getValidator, 41 | hasItem as hasValidator, 42 | getNames as getRegisteredValidatorNames, 43 | getValues as getRegisteredValidators, 44 | }; 45 | -------------------------------------------------------------------------------- /src/validators/numericValidator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Numeric cell validator 3 | * 4 | * @private 5 | * @validator NumericValidator 6 | * @param {*} value - Value of edited cell 7 | * @param {*} callback - Callback called with validation result 8 | */ 9 | 10 | import { isNumeric } from './../helpers/number'; 11 | 12 | export default function numericValidator(value, callback) { 13 | let valueToValidate = value; 14 | 15 | if (valueToValidate === null || valueToValidate === void 0) { 16 | valueToValidate = ''; 17 | } 18 | if (this.allowEmpty && valueToValidate === '') { 19 | callback(true); 20 | 21 | } else if (valueToValidate === '') { 22 | callback(false); 23 | 24 | } else { 25 | callback(isNumeric(value)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | import './helpers/custom-matchers'; 2 | 3 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; 4 | 5 | beforeEach(() => { 6 | if (document.activeElement && document.activeElement !== document.body) { 7 | document.activeElement.blur(); 8 | 9 | } else if (!document.activeElement) { // IE 10 | document.body.focus(); 11 | } 12 | }); 13 | 14 | afterEach(() => { 15 | /* eslint-disable no-unused-expressions */ 16 | (window.scrollTo || window.scrollTo(0, 0)); 17 | }); 18 | -------------------------------------------------------------------------------- /test/e2e/Core_reCreate.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core_reCreate', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should correctly re-render corner header when there is multiline content', () => { 16 | const settings = { 17 | rowHeaders: true, 18 | colHeaders(col) { 19 | return `Column
${col}`; 20 | } 21 | }; 22 | handsontable(settings); 23 | destroy(); 24 | handsontable(settings); 25 | 26 | expect(getTopLeftClone().width()).toBe(54); 27 | expect(getTopLeftClone().height()).toBe(45); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/e2e/MemoryLeakTest.js: -------------------------------------------------------------------------------- 1 | // this file is called MemoryLeakTest.js (not MemoryLeak.spec.js) to make sure it is manually executed as the last suite 2 | describe('MemoryLeakTest', () => { 3 | it('after all Handsontable instances are destroy()\'d, there should be no more active listeners', () => { 4 | expect(Handsontable._getListenersCounter()).toBe(0); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /test/e2e/core/colToProp.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.colToProp', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return the property name for the provided column number', () => { 16 | handsontable({ 17 | data: [{ 18 | id: 1, 19 | firstName: 'Tobias', 20 | lastName: 'Forge' 21 | }] 22 | }); 23 | 24 | expect(colToProp(0)).toBe('id'); 25 | expect(colToProp(1)).toBe('firstName'); 26 | expect(colToProp(2)).toBe('lastName'); 27 | }); 28 | 29 | it('it should return the provided property name, when the user passes a property name as a column number', () => { 30 | handsontable({ 31 | data: [{ 32 | id: 1, 33 | sort: true, 34 | length: 2 35 | }] 36 | }); 37 | 38 | expect(colToProp('id')).toBe('id'); 39 | expect(colToProp('sort')).toBe('sort'); 40 | expect(colToProp('length')).toBe('length'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/e2e/core/countSourceCols.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.countSourceCols', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return properly index from ', () => { 16 | const hot = handsontable({ 17 | data: [['', '', '', '', '', '', '', '', '', '', '', '', '', '', '']], 18 | columns(column) { 19 | return [1, 5, 9].indexOf(column) > -1 ? {} : null; 20 | } 21 | }); 22 | 23 | expect(hot.countSourceCols()).toBe(15); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/e2e/core/getCellMetaAtRow.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.getCellMetaAtRow', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return a row of cell meta in a form of an array', () => { 16 | handsontable(); 17 | 18 | const rowOfMeta = getCellMetaAtRow(0); 19 | expect(rowOfMeta.length).toBe(5); 20 | expect(rowOfMeta[0].row).toBe(0); 21 | expect(rowOfMeta[1].row).toBe(0); 22 | expect(rowOfMeta[2].row).toBe(0); 23 | expect(rowOfMeta[3].row).toBe(0); 24 | expect(rowOfMeta[4].row).toBe(0); 25 | expect(rowOfMeta[0].col).toBe(0); 26 | expect(rowOfMeta[1].col).toBe(1); 27 | expect(rowOfMeta[2].col).toBe(2); 28 | expect(rowOfMeta[3].col).toBe(3); 29 | expect(rowOfMeta[4].col).toBe(4); 30 | expect(rowOfMeta[0].prop).toBe(0); 31 | expect(rowOfMeta[1].prop).toBe(1); 32 | expect(rowOfMeta[2].prop).toBe(2); 33 | expect(rowOfMeta[3].prop).toBe(3); 34 | expect(rowOfMeta[4].prop).toBe(4); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/e2e/core/getCellsMeta.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.getCellsMeta', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return all initialized cells meta as flatten array', () => { 16 | handsontable(); 17 | 18 | const metas = getCellsMeta(); 19 | 20 | expect(metas.length).toBe(25); // default data size 21 | expect(metas[0].row).toBe(0); 22 | expect(metas[0].col).toBe(0); 23 | expect(metas[0].prop).toBe(0); 24 | expect(metas[19].row).toBe(3); 25 | expect(metas[19].col).toBe(4); 26 | expect(metas[19].prop).toBe(4); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/e2e/core/getCopyableData.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.getCopyableData', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return copyable data when `copyable` option is enabled', () => { 16 | handsontable({ 17 | data: Handsontable.helper.createSpreadsheetData(10, 10), 18 | copyable: true 19 | }); 20 | 21 | expect(getCopyableData(0, 0)).toBe('A1'); 22 | expect(getCopyableData(1, 1)).toBe('B2'); 23 | expect(getCopyableData(5, 1)).toBe('B6'); 24 | expect(getCopyableData(8, 9)).toBe('J9'); 25 | }); 26 | 27 | it('should return empty string as copyable data when `copyable` option is disabled', () => { 28 | handsontable({ 29 | data: Handsontable.helper.createSpreadsheetData(10, 10), 30 | copyable: false 31 | }); 32 | 33 | expect(getCopyableData(0, 0)).toBe(''); 34 | expect(getCopyableData(1, 1)).toBe(''); 35 | expect(getCopyableData(5, 1)).toBe(''); 36 | expect(getCopyableData(8, 9)).toBe(''); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/e2e/core/getCopyableText.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.getCopyableText', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return copyable string when `copyable` option is enabled', () => { 16 | handsontable({ 17 | data: Handsontable.helper.createSpreadsheetData(5, 5), 18 | copyable: true 19 | }); 20 | 21 | expect(getCopyableText(0, 0)).toBe('A1'); 22 | expect(getCopyableText(0, 0, 1, 2)).toBe('A1\tB1\tC1\nA2\tB2\tC2'); 23 | }); 24 | 25 | it('should return empty string as copyable data when `copyable` option is disabled', () => { 26 | handsontable({ 27 | data: Handsontable.helper.createSpreadsheetData(5, 5), 28 | copyable: false 29 | }); 30 | 31 | expect(getCopyableText(0, 0)).toBe(''); 32 | expect(getCopyableText(0, 0, 1, 2)).toBe('\t\t\n\t\t'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/e2e/core/getSourceDataArray.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.getSourceDataArray', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return data as an array when provided data was an array of arrays', () => { 16 | handsontable({ 17 | data: [[1, 2, 3], ['a', 'b', 'c']], 18 | copyable: true 19 | }); 20 | 21 | expect(getSourceDataArray()).toEqual([[1, 2, 3], ['a', 'b', 'c']]); 22 | expect(getSourceDataArray(0, 1, 1, 2)).toEqual([[2, 3], ['b', 'c']]); 23 | }); 24 | 25 | it('should return data as an array when provided data was an array of objects', () => { 26 | handsontable({ 27 | data: [{ a: 1, b: 2, c: 3 }, { a: 'a', b: 'b', c: 'c' }], 28 | copyable: true 29 | }); 30 | 31 | expect(getSourceDataArray()).toEqual([[1, 2, 3], ['a', 'b', 'c']]); 32 | expect(getSourceDataArray(0, 1, 1, 2)).toEqual([[2, 3], ['b', 'c']]); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/e2e/core/propToCol.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.propToCol', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return valid index for newly added column when manualColumnMove is enabled', () => { 16 | const hot = handsontable({ 17 | data: Handsontable.helper.createSpreadsheetData(10, 10), 18 | manualColumnMove: true, 19 | }); 20 | 21 | hot.alter('insert_col', 5); 22 | 23 | expect(propToCol(0)).toBe(0); 24 | expect(propToCol(10)).toBe(10); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/e2e/core/spliceCellsMeta.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.spliceCellsMeta', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should splice the cell meta array analogously to the native `splice` method', () => { 16 | handsontable(); 17 | 18 | let allMeta = getCellsMeta(); 19 | expect(allMeta.length).toBe(25); 20 | spliceCellsMeta(3, 1); 21 | allMeta = getCellsMeta(); 22 | expect(allMeta.length).toBe(20); 23 | 24 | let metaAtRow = getCellMetaAtRow(2); 25 | expect(metaAtRow[0].row).toEqual(2); 26 | metaAtRow = getCellMetaAtRow(3); 27 | expect(metaAtRow[0].row).toEqual(4); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/e2e/core/toPhysicalColumn.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.toPhysicalColumn', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return valid physical row index', () => { 16 | const hot = handsontable({ 17 | data: Handsontable.helper.createSpreadsheetData(10, 10), 18 | modifyCol(column) { 19 | return column + 3; 20 | } 21 | }); 22 | 23 | expect(hot.toPhysicalColumn(0)).toBe(3); 24 | expect(hot.toPhysicalColumn(1)).toBe(4); 25 | expect(hot.toPhysicalColumn(2)).toBe(5); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/e2e/core/toPhysicalRow.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.toPhysicalRow', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return valid physical row index', () => { 16 | const hot = handsontable({ 17 | data: Handsontable.helper.createSpreadsheetData(10, 10), 18 | modifyRow(row) { 19 | return row + 3; 20 | } 21 | }); 22 | 23 | expect(hot.toPhysicalRow(0)).toBe(3); 24 | expect(hot.toPhysicalRow(1)).toBe(4); 25 | expect(hot.toPhysicalRow(2)).toBe(5); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/e2e/core/toVisualColumn.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.toVisualColumn', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return valid visual row index', () => { 16 | const hot = handsontable({ 17 | data: Handsontable.helper.createSpreadsheetData(10, 10), 18 | unmodifyCol(column) { 19 | return column + 3; 20 | } 21 | }); 22 | 23 | expect(hot.toVisualColumn(0)).toBe(3); 24 | expect(hot.toVisualColumn(1)).toBe(4); 25 | expect(hot.toVisualColumn(2)).toBe(5); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/e2e/core/toVisualRow.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core.toVisualRow', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should return valid visual row index', () => { 16 | const hot = handsontable({ 17 | data: Handsontable.helper.createSpreadsheetData(10, 10), 18 | unmodifyRow(row) { 19 | return row + 3; 20 | } 21 | }); 22 | 23 | expect(hot.toVisualRow(0)).toBe(3); 24 | expect(hot.toVisualRow(1)).toBe(4); 25 | expect(hot.toVisualRow(2)).toBe(5); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/e2e/index.js: -------------------------------------------------------------------------------- 1 | require('@babel/polyfill/lib/noConflict'); 2 | require('jasmine-co').install(); 3 | 4 | let testPathRegExp = null; 5 | 6 | if (typeof __ENV_ARGS__ === 'object' && __ENV_ARGS__.testPathPattern) { 7 | // Remove string between % signs. On Windows' machines an empty env variable was visible as '%{variable_name}%' so it must be stripped. 8 | // See https://github.com/handsontable/handsontable/issues/4378). 9 | const pattern = __ENV_ARGS__.testPathPattern.replace(/^%(.*)%$/, ''); 10 | 11 | if (pattern) { 12 | testPathRegExp = new RegExp(pattern, 'i'); 13 | } 14 | } 15 | 16 | const ignoredE2ETestsPath = './mobile'; 17 | 18 | [ 19 | require.context('.', true, /\.spec\.js$/), 20 | require.context('./../../src/plugins', true, /\.e2e\.js$/), 21 | ].forEach((req) => { 22 | req.keys().forEach((filePath) => { 23 | if (filePath.includes(ignoredE2ETestsPath) === false) { 24 | if (testPathRegExp === null || (testPathRegExp instanceof RegExp && testPathRegExp.test(filePath))) { 25 | req(filePath); 26 | } 27 | } 28 | }); 29 | }); 30 | 31 | require('./MemoryLeakTest'); 32 | -------------------------------------------------------------------------------- /test/e2e/mobile/index.js: -------------------------------------------------------------------------------- 1 | require('@babel/polyfill'); 2 | require('jasmine-co').install(); 3 | 4 | let testPathRegExp = null; 5 | 6 | if (typeof __ENV_ARGS__ === 'object' && __ENV_ARGS__.testPathPattern) { 7 | // Remove string between % signs. On Windows' machines an empty env variable was visible as '%{variable_name}%' so it must be stripped. 8 | // See https://github.com/handsontable/handsontable/issues/4378). 9 | const pattern = __ENV_ARGS__.testPathPattern.replace(/^%(.*)%$/, ''); 10 | 11 | if (pattern) { 12 | testPathRegExp = new RegExp(pattern, 'i'); 13 | } 14 | } 15 | 16 | [ 17 | require.context('.', true, /\.spec\.js$/) 18 | ].forEach((req) => { 19 | req.keys().forEach((filePath) => { 20 | if (testPathRegExp === null || (testPathRegExp instanceof RegExp && testPathRegExp.test(filePath))) { 21 | req(filePath); 22 | } 23 | }); 24 | }); 25 | 26 | require('../MemoryLeakTest'); 27 | -------------------------------------------------------------------------------- /test/e2e/renderers/htmlRenderer.spec.js: -------------------------------------------------------------------------------- 1 | describe('HTMLRenderer', () => { 2 | const id = 'testContainer'; 3 | 4 | beforeEach(function() { 5 | this.$container = $(`
`).appendTo('body'); 6 | }); 7 | 8 | afterEach(function() { 9 | if (this.$container) { 10 | destroy(); 11 | this.$container.remove(); 12 | } 13 | }); 14 | 15 | it('should not fill empty rows with null values', () => { 16 | handsontable({ 17 | data: [['a', 'b', 'c', 'd', 'e', 'f']], 18 | colHeaders: true, 19 | rowHeaders: true, 20 | minSpareRows: 5, 21 | renderer: 'html' 22 | }); 23 | 24 | expect($('.handsontable table tr:last-child td:eq(0)').html()).toEqual(''); 25 | expect($('.handsontable table tr:last-child td:eq(1)').html()).toEqual(''); 26 | expect($('.handsontable table tr:last-child td:eq(2)').html()).toEqual(''); 27 | expect($('.handsontable table tr:last-child td:eq(3)').html()).toEqual(''); 28 | expect($('.handsontable table tr:last-child td:eq(4)').html()).toEqual(''); 29 | expect($('.handsontable table tr:last-child td:eq(5)').html()).toEqual(''); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/helpers/common.css: -------------------------------------------------------------------------------- 1 | .red-background { 2 | background-color: #ff0000 !important; 3 | } 4 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import window from 'window'; 3 | import './../bootstrap'; 4 | import * as common from './common'; 5 | 6 | const exportToWindow = (helpersHolder) => { 7 | Object.keys(helpersHolder).forEach((key) => { 8 | if (key === '__esModule') { 9 | return; 10 | } 11 | 12 | if (window[key] !== void 0) { 13 | throw Error(`Cannot export "${key}" helper because this name is already assigned.`); 14 | } 15 | 16 | window[key] = helpersHolder[key]; 17 | }); 18 | }; 19 | 20 | // Export all helpers to the window. 21 | exportToWindow(common); 22 | -------------------------------------------------------------------------------- /test/helpers/jasmine-bridge-reporter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (typeof jasmineStarted === 'undefined') { 3 | return; 4 | } 5 | 6 | function JasmineBridgeReporter() { 7 | this.started = false; 8 | this.finished = false; 9 | this.suites_ = []; 10 | this.results_ = {}; 11 | this.buffer = ''; 12 | } 13 | 14 | JasmineBridgeReporter.prototype.jasmineStarted = function(metadata) { 15 | this.started = true; 16 | jasmineStarted(metadata); 17 | }; 18 | 19 | JasmineBridgeReporter.prototype.specStarted = function(specMetadata) { 20 | specMetadata.startTime = Date.now(); 21 | jasmineSpecStarted(specMetadata); 22 | }; 23 | 24 | JasmineBridgeReporter.prototype.suiteStarted = function(suiteMetadata) { 25 | suiteMetadata.startTime = Date.now(); 26 | jasmineSuiteStarted(suiteMetadata); 27 | }; 28 | 29 | JasmineBridgeReporter.prototype.jasmineDone = function() { 30 | this.finished = true; 31 | jasmineDone(); 32 | }; 33 | 34 | JasmineBridgeReporter.prototype.suiteDone = function(suiteMetadata) { 35 | suiteMetadata.duration = Date.now() - suiteMetadata.startTime; 36 | jasmineSuiteDone(suiteMetadata); 37 | }; 38 | 39 | JasmineBridgeReporter.prototype.specDone = function(specMetadata) { 40 | specMetadata.duration = Date.now() - specMetadata.startTime; 41 | this.results_[specMetadata.id] = specMetadata; 42 | 43 | jasmineSpecDone(specMetadata); 44 | }; 45 | 46 | jasmine.getEnv().addReporter(new JasmineBridgeReporter()); 47 | }()); 48 | -------------------------------------------------------------------------------- /test/scripts/trigger-hot-builder-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const request = require('request'); 3 | 4 | const ENDPOINT = 'https://api.travis-ci.org'; 5 | const REPO_OWNER = 'handsontable'; 6 | const REPO_PROJECT = 'hot-builder'; 7 | 8 | const options = { 9 | method: 'POST', 10 | url: `${ENDPOINT}/repo/${REPO_OWNER}%2F${REPO_PROJECT}/requests`, 11 | headers: { 12 | Authorization: `token ${process.env.TCI_TOKEN}`, 13 | Accept: 'application/json', 14 | ContentType: 'application/json', 15 | 'Travis-API-Version': '3', 16 | }, 17 | json: { 18 | request: { 19 | message: `Checking triggered from handsontable/handsontable repository (the ${process.env.TRAVIS_BRANCH} branch)`, 20 | // Always check only master branch (release branch) of the hot-builder repository. 21 | branch: 'master', 22 | config: { 23 | env: { 24 | global: [`HOT_BRANCH=${process.env.TRAVIS_BRANCH}`], 25 | }, 26 | }, 27 | } 28 | }, 29 | }; 30 | 31 | request(options, (err, res) => { 32 | if (err) { 33 | process.exit(1); 34 | } 35 | 36 | if (res.statusCode >= 400) { 37 | process.exit(1); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /test/scripts/trigger-pro-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const request = require('request'); 3 | 4 | const ENDPOINT = 'https://api.travis-ci.org'; 5 | const REPO_OWNER = 'handsontable'; 6 | const REPO_PROJECT = 'handsontable-pro'; 7 | 8 | const options = { 9 | method: 'POST', 10 | url: `${ENDPOINT}/repo/${REPO_OWNER}%2F${REPO_PROJECT}/requests`, 11 | headers: { 12 | Authorization: `token ${process.env.TCI_TOKEN}`, 13 | Accept: 'application/json', 14 | ContentType: 'application/json', 15 | 'Travis-API-Version': '3', 16 | }, 17 | json: { 18 | request: { 19 | message: `Checking triggered from handsontable/handsontable repository (the ${process.env.TRAVIS_BRANCH} branch)`, 20 | branch: 'develop', 21 | config: { 22 | env: { 23 | global: [ 24 | `HOT_BRANCH=${process.env.TRAVIS_BRANCH}`, 25 | `HOT_FOREIGN_TRIGGER=true`, 26 | ], 27 | }, 28 | }, 29 | } 30 | }, 31 | }; 32 | 33 | request(options, (err, res) => { 34 | if (err) { 35 | process.exit(1); 36 | } 37 | 38 | if (res.statusCode >= 400) { 39 | process.exit(1); 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /test/types/editors/password.types.ts: -------------------------------------------------------------------------------- 1 | import * as Handsontable from 'handsontable'; 2 | 3 | class PasswordEditor extends Handsontable.editors.TextEditor { 4 | createElements() { 5 | // Call the original createElements method 6 | super.createElements.apply(this, arguments); 7 | 8 | // Create password input and update relevant properties 9 | this.TEXTAREA = document.createElement('input'); 10 | this.TEXTAREA.setAttribute('type', 'password'); 11 | this.TEXTAREA.className = 'handsontableInput'; 12 | this.textareaStyle = this.TEXTAREA.style; 13 | this.textareaStyle.width = '0'; 14 | this.textareaStyle.height = '0'; 15 | 16 | //replace textarea with password input 17 | Handsontable.dom.empty(this.TEXTAREA_PARENT); 18 | this.TEXTAREA_PARENT.appendChild(this.TEXTAREA); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/types/editors/text.types.ts: -------------------------------------------------------------------------------- 1 | import * as Handsontable from 'handsontable'; 2 | 3 | class TextEditor extends Handsontable.editors.TextEditor { 4 | init() { 5 | super.init(); 6 | } 7 | 8 | prepare(row: number, col: number, prop: string | number, td: HTMLElement, originalValue: any, cellProperties: Handsontable.GridSettings) { 9 | super.prepare(row, col, prop, td, originalValue, cellProperties); 10 | } 11 | 12 | hideEditableElement() { 13 | super.hideEditableElement(); 14 | } 15 | 16 | showEditableElement() { 17 | super.showEditableElement(); 18 | } 19 | 20 | getValue() { 21 | super.getValue(); 22 | } 23 | 24 | setValue(value: any) { 25 | super.setValue(value); 26 | } 27 | 28 | beginEditing(newInitialValue?: any) { 29 | super.beginEditing(newInitialValue); 30 | } 31 | 32 | open() { 33 | super.open(); 34 | } 35 | 36 | close() { 37 | super.close(); 38 | } 39 | 40 | focus() { 41 | super.focus(); 42 | } 43 | 44 | createElements() { 45 | super.createElements(); 46 | } 47 | 48 | getEditedCell() { 49 | const editedCell = super.getEditedCell(); 50 | 51 | return editedCell; 52 | } 53 | 54 | refreshValue() { 55 | super.refreshValue(); 56 | } 57 | 58 | refreshDimensions(force: boolean = false) { 59 | super.refreshDimensions(force); 60 | } 61 | 62 | bindEvents() { 63 | super.bindEvents(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/types/index.types.ts: -------------------------------------------------------------------------------- 1 | import * as Handsontable from '../../handsontable'; 2 | 3 | const baseVersion = Handsontable.baseVersion; 4 | const buildDate = Handsontable.buildDate; 5 | const packageName = Handsontable.packageName; 6 | const version = Handsontable.version; 7 | -------------------------------------------------------------------------------- /test/types/renderers.types.ts: -------------------------------------------------------------------------------- 1 | import * as Handsontable from 'handsontable'; 2 | 3 | const elem = document.createElement('div'); 4 | const hot = new Handsontable(elem, {}); 5 | 6 | const gridSettings: Handsontable.GridSettings = { 7 | valid: true, 8 | className: 'foo' 9 | }; 10 | 11 | Handsontable.renderers.AutocompleteRenderer(hot, new HTMLTableDataCellElement(), 0, 0, 'prop', 1.235, gridSettings); 12 | Handsontable.renderers.BaseRenderer(hot, new HTMLTableDataCellElement(), 0, 0, 'prop', 1.235, gridSettings); 13 | Handsontable.renderers.CheckboxRenderer(hot, new HTMLTableDataCellElement(), 0, 0, 'prop', 1.235, gridSettings); 14 | Handsontable.renderers.HtmlRenderer(hot, new HTMLTableDataCellElement(), 0, 0, 'prop', 1.235, gridSettings); 15 | Handsontable.renderers.NumericRenderer(hot, new HTMLTableDataCellElement(), 0, 0, 'prop', 1.235, gridSettings); 16 | Handsontable.renderers.PasswordRenderer(hot, new HTMLTableDataCellElement(), 0, 0, 'prop', 1.235, gridSettings); 17 | Handsontable.renderers.TextRenderer(hot, new HTMLTableDataCellElement(), 0, 0, 'prop', 1.235, gridSettings); 18 | -------------------------------------------------------------------------------- /test/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es6", 5 | "dom" 6 | ], 7 | "module": "commonjs", 8 | "noEmit": true, 9 | "noImplicitAny": true, 10 | "noImplicitThis": true, 11 | "strictNullChecks": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "baseUrl": "", 14 | "paths": { 15 | "handsontable": [ "../../" ] 16 | } 17 | }, 18 | "include": [ 19 | "../../handsontable.d.ts", 20 | "**/*.types.ts", 21 | "../../src/plugins/**/test/*.types.ts" 22 | ] 23 | } -------------------------------------------------------------------------------- /test/unit/helpers/Data.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | spreadsheetColumnLabel, 3 | spreadsheetColumnIndex, 4 | } from 'handsontable/helpers/data'; 5 | 6 | describe('Data helper', () => { 7 | // 8 | // Handsontable.helper.spreadsheetColumnLabel 9 | // 10 | describe('spreadsheetColumnLabel', () => { 11 | it('should return valid column names based on provided column index', () => { 12 | expect(spreadsheetColumnLabel()).toBe(''); 13 | expect(spreadsheetColumnLabel(0)).toBe('A'); 14 | expect(spreadsheetColumnLabel(11)).toBe('L'); 15 | expect(spreadsheetColumnLabel(113)).toBe('DJ'); 16 | expect(spreadsheetColumnLabel(33439273)).toBe('BUDNIX'); 17 | }); 18 | }); 19 | 20 | // 21 | // Handsontable.helper.spreadsheetColumnIndex 22 | // 23 | describe('spreadsheetColumnIndex', () => { 24 | it('should return valid column indexes based on provided column name', () => { 25 | expect(spreadsheetColumnIndex('')).toBe(-1); 26 | expect(spreadsheetColumnIndex('A')).toBe(0); 27 | expect(spreadsheetColumnIndex('L')).toBe(11); 28 | expect(spreadsheetColumnIndex('DJ')).toBe(113); 29 | expect(spreadsheetColumnIndex('BUDNIX')).toBe(33439273); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/helpers/Date.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | getNormalizedDate, 3 | } from 'handsontable/helpers/date'; 4 | 5 | describe('Date helper', () => { 6 | describe('getNormalizedDate', () => { 7 | it('should return a proper date object, with time set to 00:00, when providing it with a date-only string', () => { 8 | const date1 = getNormalizedDate('2016-02-02'); 9 | const date2 = getNormalizedDate('2016/02/02'); 10 | const date3 = getNormalizedDate('02/02/2016'); 11 | 12 | expect(date1.getDate()).toEqual(2); 13 | expect(date2.getDate()).toEqual(2); 14 | expect(date3.getDate()).toEqual(2); 15 | 16 | expect(date1.getMonth()).toEqual(1); 17 | expect(date2.getMonth()).toEqual(1); 18 | expect(date3.getMonth()).toEqual(1); 19 | 20 | expect(date1.getFullYear()).toEqual(2016); 21 | expect(date2.getFullYear()).toEqual(2016); 22 | expect(date3.getFullYear()).toEqual(2016); 23 | 24 | expect(date1.getFullYear()).toEqual(2016); 25 | expect(date2.getFullYear()).toEqual(2016); 26 | expect(date3.getFullYear()).toEqual(2016); 27 | 28 | expect(date1.getHours()).toEqual(0); 29 | expect(date2.getHours()).toEqual(0); 30 | expect(date3.getHours()).toEqual(0); 31 | 32 | expect(date1.getMinutes()).toEqual(0); 33 | expect(date2.getMinutes()).toEqual(0); 34 | expect(date3.getMinutes()).toEqual(0); 35 | }); 36 | 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/unit/helpers/Feature.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | getComparisonFunction, 3 | } from 'handsontable/helpers/feature'; 4 | 5 | describe('Feature helper', () => { 6 | // 7 | // Handsontable.helper.getComparisonFunction 8 | // 9 | describe('getComparisonFunction', () => { 10 | it('should correct equals strings', () => { 11 | const comparisonFunction = getComparisonFunction(); 12 | 13 | expect(comparisonFunction('a', 'b')).toBe(-1); 14 | expect(comparisonFunction('b', 'a')).toBe(1); 15 | expect(comparisonFunction('b', 'b')).toBe(0); 16 | // pl 17 | expect(comparisonFunction('a', 'ł')).toBe(-1); 18 | expect(comparisonFunction('ł', 'a')).toBe(1); 19 | expect(comparisonFunction('Ą', 'A')).toBe(1); 20 | expect(comparisonFunction('Ź', 'Ż')).toBe(-1); 21 | expect(comparisonFunction('Ż', 'Ź')).toBe(1); 22 | expect(comparisonFunction('ą', 'ą')).toBe(0); 23 | 24 | expect(comparisonFunction('1', '10')).toBe(-1); 25 | expect(comparisonFunction('10', '1')).toBe(1); 26 | expect(comparisonFunction('10', '10')).toBe(0); 27 | expect(comparisonFunction(1, 10)).toBe(-1); 28 | expect(comparisonFunction(10, 1)).toBe(1); 29 | expect(comparisonFunction(10, 10)).toBe(0); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/helpers/TemplateLiteralTag.spec.js: -------------------------------------------------------------------------------- 1 | import { toSingleLine } from 'handsontable/helpers/templateLiteralTag'; 2 | 3 | describe('Helpers for template literals', () => { 4 | describe('toSingleLine', () => { 5 | it('should strip two line string (string with whitespace at end of first line and indention at second one)', () => { 6 | const text = toSingleLine`Hello world 7 | Hello world`; 8 | 9 | expect(text).toEqual('Hello world Hello world'); 10 | }); 11 | 12 | it('should strip two line string (string without whitespace at end of first line and indention at second one)', () => { 13 | const text = toSingleLine`Hello world 14 | Hello world`; 15 | 16 | expect(text).toEqual('Hello worldHello world'); 17 | }); 18 | 19 | it('should include literals and not remove whitespaces between them without necessary', () => { 20 | const a = 'Hello'; 21 | const b = 'world'; 22 | const text = toSingleLine`${a} ${b}`; 23 | 24 | expect(text).toEqual('Hello world'); 25 | }); 26 | 27 | it('should remove whitespaces from both sides of a string.', () => { 28 | const a = ' Hello'; 29 | const b = 'world '; 30 | const text = toSingleLine`${a} ${b}`; 31 | 32 | expect(text).toEqual('Hello world'); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/unit/i18n/phraseFormatters/index.spec.js: -------------------------------------------------------------------------------- 1 | import { getAll as getAllFormatters, register as registerPhraseFormatter } from 'handsontable/i18n/phraseFormatters'; 2 | 3 | describe('i18n phraseFormatters', () => { 4 | it('should register formatters at start', () => { 5 | 6 | // Formatter `substituteVariables` isn't registered at the moment. 7 | expect(getAllFormatters().length).toEqual(1); 8 | }); 9 | 10 | it('should register formatter by `register` function', () => { 11 | registerPhraseFormatter('exampleFormatterName', () => {}); 12 | 13 | expect(getAllFormatters().length).toEqual(2); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /update.json: -------------------------------------------------------------------------------- 1 | { 2 | "COMMENT": "This is a update.json file used by jsDelivr.com CDN", 3 | "packageManager": "github", 4 | "name": "handsontable", 5 | "repo": "handsontable/handsontable", 6 | "files": { 7 | "include": ["./dist/*.js", "./plugins/**/*.js", "./plugins/**/*.css"], 8 | "exclude": ["./plugins/**/*spec*"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | var env = process.env.NODE_ENV; 6 | var configFactory = require('./.config/' + env); 7 | 8 | // In some cases, npm env variables become rewritten to lower case names. To prevent this it is rewritten to the 9 | // original variable name so the --testPathPattern work in any case. 10 | if (process.env.npm_config_testpathpattern) { 11 | process.env.npm_config_testPathPattern = process.env.npm_config_testpathpattern; 12 | } 13 | 14 | module.exports = function() { 15 | return configFactory.create({ 16 | testPathPattern: process.env.npm_config_testPathPattern, 17 | }); 18 | }; 19 | --------------------------------------------------------------------------------