core.js 120 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597
  1. import { addClass, empty, isChildOfWebComponentTable, removeClass } from './helpers/dom/element';
  2. import { columnFactory } from './helpers/setting';
  3. import { isFunction } from './helpers/function';
  4. import { warn } from './helpers/console';
  5. import { isDefined, isUndefined, isRegExp, _injectProductInfo, isEmpty } from './helpers/mixed';
  6. import { isMobileBrowser } from './helpers/browser';
  7. import DataMap from './dataMap';
  8. import EditorManager from './editorManager';
  9. import EventManager from './eventManager';
  10. import {
  11. deepClone,
  12. duckSchema,
  13. extend, isObject,
  14. isObjectEqual,
  15. deepObjectSize,
  16. hasOwnProperty,
  17. createObjectPropListener,
  18. objectEach
  19. } from './helpers/object';
  20. import { arrayFlatten, arrayMap, arrayEach, arrayReduce } from './helpers/array';
  21. import { toSingleLine } from './helpers/templateLiteralTag';
  22. // eslint-disable-next-line import/extensions
  23. import { getPlugin } from './plugins.js';
  24. import { getRenderer } from './renderers';
  25. import { getValidator } from './validators';
  26. import { randomString } from './helpers/string';
  27. import { rangeEach, rangeEachReverse } from './helpers/number';
  28. import TableView from './tableView';
  29. import DataSource from './dataSource';
  30. import { translateRowsToColumns, cellMethodLookupFactory, spreadsheetColumnLabel } from './helpers/data';
  31. import { getTranslator } from './utils/recordTranslator';
  32. import { registerAsRootInstance, hasValidParameter, isRootInstance } from './utils/rootInstance';
  33. import { CellCoords, ViewportColumnsCalculator } from './3rdparty/walkontable/src';
  34. import Hooks from './pluginHooks';
  35. import DefaultSettings from './defaultSettings';
  36. import { getCellType } from './cellTypes';
  37. import { getTranslatedPhrase } from './i18n';
  38. import { hasLanguageDictionary } from './i18n/dictionariesManager';
  39. import { warnUserAboutLanguageRegistration, applyLanguageSetting, normalizeLanguageCode } from './i18n/utils';
  40. import { startObserving as keyStateStartObserving, stopObserving as keyStateStopObserving } from './utils/keyStateObserver';
  41. import { Selection } from './selection';
  42. let activeGuid = null;
  43. /**
  44. * Handsontable constructor
  45. *
  46. * @core
  47. * @constructor Core
  48. * @description
  49. *
  50. * After Handsontable is constructed, you can modify the grid behavior using the available public methods.
  51. *
  52. * ---
  53. * ## How to call methods
  54. *
  55. * These are 2 equal ways to call a Handsontable method:
  56. *
  57. * ```js
  58. * // all following examples assume that you constructed Handsontable like this
  59. * const hot = new Handsontable(document.getElementById('example1'), options);
  60. *
  61. * // now, to use setDataAtCell method, you can either:
  62. * ht.setDataAtCell(0, 0, 'new value');
  63. * ```
  64. *
  65. * Alternatively, you can call the method using jQuery wrapper (__obsolete__, requires initialization using our jQuery guide
  66. * ```js
  67. * $('#example1').handsontable('setDataAtCell', 0, 0, 'new value');
  68. * ```
  69. * ---
  70. */
  71. export default function Core(rootElement, userSettings, rootInstanceSymbol = false) {
  72. let preventScrollingToCell = false;
  73. let instance = this;
  74. let GridSettings = function() {
  75. };
  76. const eventManager = new EventManager(instance);
  77. let priv;
  78. let datamap;
  79. let dataSource;
  80. let grid;
  81. let editorManager;
  82. extend(GridSettings.prototype, DefaultSettings.prototype); // create grid settings as a copy of default settings
  83. extend(GridSettings.prototype, userSettings); // overwrite defaults with user settings
  84. extend(GridSettings.prototype, expandType(userSettings));
  85. applyLanguageSetting(GridSettings.prototype, userSettings.language);
  86. if (hasValidParameter(rootInstanceSymbol)) {
  87. registerAsRootInstance(this);
  88. }
  89. keyStateStartObserving();
  90. this.isDestroyed = false;
  91. this.rootElement = rootElement;
  92. this.isHotTableEnv = isChildOfWebComponentTable(this.rootElement);
  93. EventManager.isHotTableEnv = this.isHotTableEnv;
  94. this.container = document.createElement('div');
  95. this.renderCall = false;
  96. rootElement.insertBefore(this.container, rootElement.firstChild);
  97. if (process.env.HOT_PACKAGE_TYPE !== '\x63\x65' && isRootInstance(this)) {
  98. _injectProductInfo(userSettings.licenseKey, rootElement);
  99. }
  100. this.guid = `ht_${randomString()}`; // this is the namespace for global events
  101. const recordTranslator = getTranslator(instance);
  102. dataSource = new DataSource(instance);
  103. if (!this.rootElement.id || this.rootElement.id.substring(0, 3) === 'ht_') {
  104. this.rootElement.id = this.guid; // if root element does not have an id, assign a random id
  105. }
  106. priv = {
  107. cellSettings: [],
  108. columnSettings: [],
  109. columnsSettingConflicts: ['data', 'width', 'language'],
  110. settings: new GridSettings(), // current settings instance
  111. selRange: null, // exposed by public method `getSelectedRange`
  112. isPopulated: null,
  113. scrollable: null,
  114. firstRun: true
  115. };
  116. let selection = new Selection(priv.settings, {
  117. countCols: () => instance.countCols(),
  118. countRows: () => instance.countRows(),
  119. propToCol: prop => datamap.propToCol(prop),
  120. isEditorOpened: () => (instance.getActiveEditor() ? instance.getActiveEditor().isOpened() : false),
  121. });
  122. this.selection = selection;
  123. this.selection.addLocalHook('beforeSetRangeStart', (cellCoords) => {
  124. this.runHooks('beforeSetRangeStart', cellCoords);
  125. });
  126. this.selection.addLocalHook('beforeSetRangeStartOnly', (cellCoords) => {
  127. this.runHooks('beforeSetRangeStartOnly', cellCoords);
  128. });
  129. this.selection.addLocalHook('beforeSetRangeEnd', (cellCoords) => {
  130. this.runHooks('beforeSetRangeEnd', cellCoords);
  131. if (cellCoords.row < 0) {
  132. cellCoords.row = this.view.wt.wtTable.getFirstVisibleRow();
  133. }
  134. if (cellCoords.col < 0) {
  135. cellCoords.col = this.view.wt.wtTable.getFirstVisibleColumn();
  136. }
  137. });
  138. this.selection.addLocalHook('afterSetRangeEnd', (cellCoords) => {
  139. const preventScrolling = createObjectPropListener(false);
  140. const selectionRange = this.selection.getSelectedRange();
  141. const { from, to } = selectionRange.current();
  142. const selectionLayerLevel = selectionRange.size() - 1;
  143. this.runHooks('afterSelection',
  144. from.row, from.col, to.row, to.col, preventScrolling, selectionLayerLevel);
  145. this.runHooks('afterSelectionByProp',
  146. from.row, instance.colToProp(from.col), to.row, instance.colToProp(to.col), preventScrolling, selectionLayerLevel);
  147. const isSelectedByAnyHeader = this.selection.isSelectedByAnyHeader();
  148. const currentSelectedRange = this.selection.selectedRange.current();
  149. let scrollToCell = true;
  150. if (preventScrollingToCell) {
  151. scrollToCell = false;
  152. }
  153. if (preventScrolling.isTouched()) {
  154. scrollToCell = !preventScrolling.value;
  155. }
  156. const isSelectedByRowHeader = this.selection.isSelectedByRowHeader();
  157. const isSelectedByColumnHeader = this.selection.isSelectedByColumnHeader();
  158. if (scrollToCell !== false) {
  159. if (!isSelectedByAnyHeader) {
  160. if (currentSelectedRange && !this.selection.isMultiple()) {
  161. this.view.scrollViewport(currentSelectedRange.from);
  162. } else {
  163. this.view.scrollViewport(cellCoords);
  164. }
  165. } else if (isSelectedByRowHeader) {
  166. this.view.scrollViewportVertically(cellCoords.row);
  167. } else if (isSelectedByColumnHeader) {
  168. this.view.scrollViewportHorizontally(cellCoords.col);
  169. }
  170. }
  171. // @TODO: These CSS classes are no longer needed anymore. They are used only as a indicator of the selected
  172. // rows/columns in the MergedCells plugin (via border.js#L520 in the walkontable module). After fixing
  173. // the Border class this should be removed.
  174. if (isSelectedByRowHeader && isSelectedByColumnHeader) {
  175. addClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
  176. } else if (isSelectedByRowHeader) {
  177. removeClass(this.rootElement, 'ht__selection--columns');
  178. addClass(this.rootElement, 'ht__selection--rows');
  179. } else if (isSelectedByColumnHeader) {
  180. removeClass(this.rootElement, 'ht__selection--rows');
  181. addClass(this.rootElement, 'ht__selection--columns');
  182. } else {
  183. removeClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
  184. }
  185. this._refreshBorders(null);
  186. });
  187. this.selection.addLocalHook('afterSelectionFinished', (cellRanges) => {
  188. const selectionLayerLevel = cellRanges.length - 1;
  189. const { from, to } = cellRanges[selectionLayerLevel];
  190. this.runHooks('afterSelectionEnd',
  191. from.row, from.col, to.row, to.col, selectionLayerLevel);
  192. this.runHooks('afterSelectionEndByProp',
  193. from.row, instance.colToProp(from.col), to.row, instance.colToProp(to.col), selectionLayerLevel);
  194. });
  195. this.selection.addLocalHook('afterIsMultipleSelection', (isMultiple) => {
  196. const changedIsMultiple = this.runHooks('afterIsMultipleSelection', isMultiple.value);
  197. if (isMultiple.value) {
  198. isMultiple.value = changedIsMultiple;
  199. }
  200. });
  201. this.selection.addLocalHook('beforeModifyTransformStart', (cellCoordsDelta) => {
  202. this.runHooks('modifyTransformStart', cellCoordsDelta);
  203. });
  204. this.selection.addLocalHook('afterModifyTransformStart', (coords, rowTransformDir, colTransformDir) => {
  205. this.runHooks('afterModifyTransformStart', coords, rowTransformDir, colTransformDir);
  206. });
  207. this.selection.addLocalHook('beforeModifyTransformEnd', (cellCoordsDelta) => {
  208. this.runHooks('modifyTransformEnd', cellCoordsDelta);
  209. });
  210. this.selection.addLocalHook('afterModifyTransformEnd', (coords, rowTransformDir, colTransformDir) => {
  211. this.runHooks('afterModifyTransformEnd', coords, rowTransformDir, colTransformDir);
  212. });
  213. this.selection.addLocalHook('afterDeselect', () => {
  214. editorManager.destroyEditor();
  215. this._refreshBorders();
  216. removeClass(this.rootElement, ['ht__selection--rows', 'ht__selection--columns']);
  217. this.runHooks('afterDeselect');
  218. });
  219. this.selection.addLocalHook('insertRowRequire', (totalRows) => {
  220. this.alter('insert_row', totalRows, 1, 'auto');
  221. });
  222. this.selection.addLocalHook('insertColRequire', (totalCols) => {
  223. this.alter('insert_col', totalCols, 1, 'auto');
  224. });
  225. grid = {
  226. /**
  227. * Inserts or removes rows and columns.
  228. *
  229. * @memberof Core#
  230. * @function alter
  231. * @private
  232. * @param {String} action Possible values: "insert_row", "insert_col", "remove_row", "remove_col".
  233. * @param {Number|Array} index Row or column visual index which from the alter action will be triggered.
  234. * Alter actions such as "remove_row" and "remove_col" support array indexes in the
  235. * format `[[index, amount], [index, amount]...]` this can be used to remove
  236. * non-consecutive columns or rows in one call.
  237. * @param {Number} [amount=1] Ammount rows or columns to remove.
  238. * @param {String} [source] Optional. Source of hook runner.
  239. * @param {Boolean} [keepEmptyRows] Optional. Flag for preventing deletion of empty rows.
  240. */
  241. alter(action, index, amount = 1, source, keepEmptyRows) {
  242. let delta;
  243. function spliceWith(data, startIndex, count, toInject) {
  244. const valueFactory = () => {
  245. let result;
  246. if (toInject === 'array') {
  247. result = [];
  248. } else if (toInject === 'object') {
  249. result = {};
  250. }
  251. return result;
  252. };
  253. const spliceArgs = arrayMap(new Array(count), () => valueFactory());
  254. spliceArgs.unshift(startIndex, 0);
  255. data.splice(...spliceArgs);
  256. }
  257. const normalizeIndexesGroup = (indexes) => {
  258. if (indexes.length === 0) {
  259. return [];
  260. }
  261. const sortedIndexes = [...indexes];
  262. // Sort the indexes in ascending order.
  263. sortedIndexes.sort(([indexA], [indexB]) => {
  264. if (indexA === indexB) {
  265. return 0;
  266. }
  267. return indexA > indexB ? 1 : -1;
  268. });
  269. // Normalize the {index, amount} groups into bigger groups.
  270. const normalizedIndexes = arrayReduce(sortedIndexes, (acc, [groupIndex, groupAmount]) => {
  271. const previousItem = acc[acc.length - 1];
  272. const [prevIndex, prevAmount] = previousItem;
  273. const prevLastIndex = prevIndex + prevAmount;
  274. if (groupIndex <= prevLastIndex) {
  275. const amountToAdd = Math.max(groupAmount - (prevLastIndex - groupIndex), 0);
  276. previousItem[1] += amountToAdd;
  277. } else {
  278. acc.push([groupIndex, groupAmount]);
  279. }
  280. return acc;
  281. }, [sortedIndexes[0]]);
  282. return normalizedIndexes;
  283. };
  284. /* eslint-disable no-case-declarations */
  285. switch (action) {
  286. case 'insert_row':
  287. const numberOfSourceRows = instance.countSourceRows();
  288. if (instance.getSettings().maxRows === numberOfSourceRows) {
  289. return;
  290. }
  291. // eslint-disable-next-line no-param-reassign
  292. index = (isDefined(index)) ? index : numberOfSourceRows;
  293. delta = datamap.createRow(index, amount, source);
  294. spliceWith(priv.cellSettings, index, amount, 'array');
  295. if (delta) {
  296. if (selection.isSelected() && selection.selectedRange.current().from.row >= index) {
  297. selection.selectedRange.current().from.row += delta;
  298. selection.transformEnd(delta, 0); // will call render() internally
  299. } else {
  300. instance._refreshBorders(); // it will call render and prepare methods
  301. }
  302. }
  303. break;
  304. case 'insert_col':
  305. delta = datamap.createCol(index, amount, source);
  306. for (let row = 0, len = instance.countSourceRows(); row < len; row++) {
  307. if (priv.cellSettings[row]) {
  308. spliceWith(priv.cellSettings[row], index, amount);
  309. }
  310. }
  311. if (delta) {
  312. if (Array.isArray(instance.getSettings().colHeaders)) {
  313. const spliceArray = [index, 0];
  314. spliceArray.length += delta; // inserts empty (undefined) elements at the end of an array
  315. Array.prototype.splice.apply(instance.getSettings().colHeaders, spliceArray); // inserts empty (undefined) elements into the colHeader array
  316. }
  317. if (selection.isSelected() && selection.selectedRange.current().from.col >= index) {
  318. selection.selectedRange.current().from.col += delta;
  319. selection.transformEnd(0, delta); // will call render() internally
  320. } else {
  321. instance._refreshBorders(); // it will call render and prepare methods
  322. }
  323. }
  324. break;
  325. case 'remove_row':
  326. const removeRow = (indexes) => {
  327. let offset = 0;
  328. // Normalize the {index, amount} groups into bigger groups.
  329. arrayEach(indexes, ([groupIndex, groupAmount]) => {
  330. const calcIndex = isEmpty(groupIndex) ? instance.countRows() - 1 : Math.max(groupIndex - offset, 0);
  331. // If the 'index' is an integer decrease it by 'offset' otherwise pass it through to make the value
  332. // compatible with datamap.removeCol method.
  333. if (Number.isInteger(groupIndex)) {
  334. // eslint-disable-next-line no-param-reassign
  335. groupIndex = Math.max(groupIndex - offset, 0);
  336. }
  337. // TODO: for datamap.removeRow index should be passed as it is (with undefined and null values). If not, the logic
  338. // inside the datamap.removeRow breaks the removing functionality.
  339. datamap.removeRow(groupIndex, groupAmount, source);
  340. priv.cellSettings.splice(calcIndex, amount);
  341. const totalRows = instance.countRows();
  342. const fixedRowsTop = instance.getSettings().fixedRowsTop;
  343. if (fixedRowsTop >= calcIndex + 1) {
  344. instance.getSettings().fixedRowsTop -= Math.min(groupAmount, fixedRowsTop - calcIndex);
  345. }
  346. const fixedRowsBottom = instance.getSettings().fixedRowsBottom;
  347. if (fixedRowsBottom && calcIndex >= totalRows - fixedRowsBottom) {
  348. instance.getSettings().fixedRowsBottom -= Math.min(groupAmount, fixedRowsBottom);
  349. }
  350. offset += groupAmount;
  351. });
  352. };
  353. if (Array.isArray(index)) {
  354. removeRow(normalizeIndexesGroup(index));
  355. } else {
  356. removeRow([[index, amount]]);
  357. }
  358. grid.adjustRowsAndCols();
  359. instance._refreshBorders(); // it will call render and prepare methods
  360. break;
  361. case 'remove_col':
  362. const removeCol = (indexes) => {
  363. let offset = 0;
  364. // Normalize the {index, amount} groups into bigger groups.
  365. arrayEach(indexes, ([groupIndex, groupAmount]) => {
  366. const calcIndex = isEmpty(groupIndex) ? instance.countCols() - 1 : Math.max(groupIndex - offset, 0);
  367. let visualColumnIndex = recordTranslator.toPhysicalColumn(calcIndex);
  368. // If the 'index' is an integer decrease it by 'offset' otherwise pass it through to make the value
  369. // compatible with datamap.removeCol method.
  370. if (Number.isInteger(groupIndex)) {
  371. // eslint-disable-next-line no-param-reassign
  372. groupIndex = Math.max(groupIndex - offset, 0);
  373. }
  374. // TODO: for datamap.removeCol index should be passed as it is (with undefined and null values). If not, the logic
  375. // inside the datamap.removeCol breaks the removing functionality.
  376. datamap.removeCol(groupIndex, groupAmount, source);
  377. for (let row = 0, len = instance.countSourceRows(); row < len; row++) {
  378. if (priv.cellSettings[row]) { // if row hasn't been rendered it wouldn't have cellSettings
  379. priv.cellSettings[row].splice(visualColumnIndex, groupAmount);
  380. }
  381. }
  382. const fixedColumnsLeft = instance.getSettings().fixedColumnsLeft;
  383. if (fixedColumnsLeft >= calcIndex + 1) {
  384. instance.getSettings().fixedColumnsLeft -= Math.min(groupAmount, fixedColumnsLeft - calcIndex);
  385. }
  386. if (Array.isArray(instance.getSettings().colHeaders)) {
  387. if (typeof visualColumnIndex === 'undefined') {
  388. visualColumnIndex = -1;
  389. }
  390. instance.getSettings().colHeaders.splice(visualColumnIndex, groupAmount);
  391. }
  392. offset += groupAmount;
  393. });
  394. };
  395. if (Array.isArray(index)) {
  396. removeCol(normalizeIndexesGroup(index));
  397. } else {
  398. removeCol([[index, amount]]);
  399. }
  400. grid.adjustRowsAndCols();
  401. instance._refreshBorders(); // it will call render and prepare methods
  402. break;
  403. default:
  404. throw new Error(`There is no such action "${action}"`);
  405. }
  406. if (!keepEmptyRows) {
  407. grid.adjustRowsAndCols(); // makes sure that we did not add rows that will be removed in next refresh
  408. }
  409. },
  410. /**
  411. * Makes sure there are empty rows at the bottom of the table
  412. */
  413. adjustRowsAndCols() {
  414. if (priv.settings.minRows) {
  415. // should I add empty rows to data source to meet minRows?
  416. const rows = instance.countRows();
  417. if (rows < priv.settings.minRows) {
  418. for (let r = 0, minRows = priv.settings.minRows; r < minRows - rows; r++) {
  419. datamap.createRow(instance.countRows(), 1, 'auto');
  420. }
  421. }
  422. }
  423. if (priv.settings.minSpareRows) {
  424. let emptyRows = instance.countEmptyRows(true);
  425. // should I add empty rows to meet minSpareRows?
  426. if (emptyRows < priv.settings.minSpareRows) {
  427. for (; emptyRows < priv.settings.minSpareRows && instance.countSourceRows() < priv.settings.maxRows; emptyRows++) {
  428. datamap.createRow(instance.countRows(), 1, 'auto');
  429. }
  430. }
  431. }
  432. {
  433. let emptyCols;
  434. // count currently empty cols
  435. if (priv.settings.minCols || priv.settings.minSpareCols) {
  436. emptyCols = instance.countEmptyCols(true);
  437. }
  438. // should I add empty cols to meet minCols?
  439. if (priv.settings.minCols && !priv.settings.columns && instance.countCols() < priv.settings.minCols) {
  440. for (; instance.countCols() < priv.settings.minCols; emptyCols++) {
  441. datamap.createCol(instance.countCols(), 1, 'auto');
  442. }
  443. }
  444. // should I add empty cols to meet minSpareCols?
  445. if (priv.settings.minSpareCols && !priv.settings.columns && instance.dataType === 'array' &&
  446. emptyCols < priv.settings.minSpareCols) {
  447. for (; emptyCols < priv.settings.minSpareCols && instance.countCols() < priv.settings.maxCols; emptyCols++) {
  448. datamap.createCol(instance.countCols(), 1, 'auto');
  449. }
  450. }
  451. }
  452. const rowCount = instance.countRows();
  453. const colCount = instance.countCols();
  454. if (rowCount === 0 || colCount === 0) {
  455. selection.deselect();
  456. }
  457. if (selection.isSelected()) {
  458. arrayEach(selection.selectedRange, (range) => {
  459. let selectionChanged = false;
  460. let fromRow = range.from.row;
  461. let fromCol = range.from.col;
  462. let toRow = range.to.row;
  463. let toCol = range.to.col;
  464. // if selection is outside, move selection to last row
  465. if (fromRow > rowCount - 1) {
  466. fromRow = rowCount - 1;
  467. selectionChanged = true;
  468. if (toRow > fromRow) {
  469. toRow = fromRow;
  470. }
  471. } else if (toRow > rowCount - 1) {
  472. toRow = rowCount - 1;
  473. selectionChanged = true;
  474. if (fromRow > toRow) {
  475. fromRow = toRow;
  476. }
  477. }
  478. // if selection is outside, move selection to last row
  479. if (fromCol > colCount - 1) {
  480. fromCol = colCount - 1;
  481. selectionChanged = true;
  482. if (toCol > fromCol) {
  483. toCol = fromCol;
  484. }
  485. } else if (toCol > colCount - 1) {
  486. toCol = colCount - 1;
  487. selectionChanged = true;
  488. if (fromCol > toCol) {
  489. fromCol = toCol;
  490. }
  491. }
  492. if (selectionChanged) {
  493. instance.selectCell(fromRow, fromCol, toRow, toCol);
  494. }
  495. });
  496. }
  497. if (instance.view) {
  498. instance.view.wt.wtOverlays.adjustElementsSize();
  499. }
  500. },
  501. /**
  502. * Populate the data from the provided 2d array from the given cell coordinates.
  503. *
  504. * @private
  505. * @param {Object} start Start selection position. Visual indexes.
  506. * @param {Array} input 2d data array.
  507. * @param {Object} [end] End selection position (only for drag-down mode). Visual indexes.
  508. * @param {String} [source="populateFromArray"] Source information string.
  509. * @param {String} [method="overwrite"] Populate method. Possible options: `shift_down`, `shift_right`, `overwrite`.
  510. * @param {String} direction (left|right|up|down) String specifying the direction.
  511. * @param {Array} deltas The deltas array. A difference between values of adjacent cells.
  512. * Useful **only** when the type of handled cells is `numeric`.
  513. * @returns {Object|undefined} ending td in pasted area (only if any cell was changed).
  514. */
  515. populateFromArray(start, input, end, source, method, direction, deltas) {
  516. // TODO: either remove or implement the `direction` argument. Currently it's not working at all.
  517. let r;
  518. let rlen;
  519. let c;
  520. let clen;
  521. const setData = [];
  522. const current = {};
  523. rlen = input.length;
  524. if (rlen === 0) {
  525. return false;
  526. }
  527. let repeatCol;
  528. let repeatRow;
  529. let cmax;
  530. let rmax;
  531. /* eslint-disable no-case-declarations */
  532. // insert data with specified pasteMode method
  533. switch (method) {
  534. case 'shift_down' :
  535. repeatCol = end ? end.col - start.col + 1 : 0;
  536. repeatRow = end ? end.row - start.row + 1 : 0;
  537. // eslint-disable-next-line no-param-reassign
  538. input = translateRowsToColumns(input);
  539. for (c = 0, clen = input.length, cmax = Math.max(clen, repeatCol); c < cmax; c++) {
  540. if (c < clen) {
  541. for (r = 0, rlen = input[c].length; r < repeatRow - rlen; r++) {
  542. input[c].push(input[c][r % rlen]);
  543. }
  544. input[c].unshift(start.col + c, start.row, 0);
  545. instance.spliceCol(...input[c]);
  546. } else {
  547. input[c % clen][0] = start.col + c;
  548. instance.spliceCol(...input[c % clen]);
  549. }
  550. }
  551. break;
  552. case 'shift_right':
  553. repeatCol = end ? end.col - start.col + 1 : 0;
  554. repeatRow = end ? end.row - start.row + 1 : 0;
  555. for (r = 0, rlen = input.length, rmax = Math.max(rlen, repeatRow); r < rmax; r++) {
  556. if (r < rlen) {
  557. for (c = 0, clen = input[r].length; c < repeatCol - clen; c++) {
  558. input[r].push(input[r][c % clen]);
  559. }
  560. input[r].unshift(start.row + r, start.col, 0);
  561. instance.spliceRow(...input[r]);
  562. } else {
  563. input[r % rlen][0] = start.row + r;
  564. instance.spliceRow(...input[r % rlen]);
  565. }
  566. }
  567. break;
  568. case 'overwrite':
  569. default:
  570. // overwrite and other not specified options
  571. current.row = start.row;
  572. current.col = start.col;
  573. const selected = { // selected range
  574. row: (end && start) ? (end.row - start.row + 1) : 1,
  575. col: (end && start) ? (end.col - start.col + 1) : 1
  576. };
  577. let skippedRow = 0;
  578. let skippedColumn = 0;
  579. let pushData = true;
  580. let cellMeta;
  581. const getInputValue = function getInputValue(row, col = null) {
  582. const rowValue = input[row % input.length];
  583. if (col !== null) {
  584. return rowValue[col % rowValue.length];
  585. }
  586. return rowValue;
  587. };
  588. const rowInputLength = input.length;
  589. const rowSelectionLength = end ? end.row - start.row + 1 : 0;
  590. if (end) {
  591. rlen = rowSelectionLength;
  592. } else {
  593. rlen = Math.max(rowInputLength, rowSelectionLength);
  594. }
  595. for (r = 0; r < rlen; r++) {
  596. if ((end && current.row > end.row && rowSelectionLength > rowInputLength) ||
  597. (!priv.settings.allowInsertRow && current.row > instance.countRows() - 1) ||
  598. (current.row >= priv.settings.maxRows)) {
  599. break;
  600. }
  601. const visualRow = r - skippedRow;
  602. const colInputLength = getInputValue(visualRow).length;
  603. const colSelectionLength = end ? end.col - start.col + 1 : 0;
  604. if (end) {
  605. clen = colSelectionLength;
  606. } else {
  607. clen = Math.max(colInputLength, colSelectionLength);
  608. }
  609. current.col = start.col;
  610. cellMeta = instance.getCellMeta(current.row, current.col);
  611. if ((source === 'CopyPaste.paste' || source === 'Autofill.autofill') && cellMeta.skipRowOnPaste) {
  612. skippedRow += 1;
  613. current.row += 1;
  614. rlen += 1;
  615. /* eslint-disable no-continue */
  616. continue;
  617. }
  618. skippedColumn = 0;
  619. for (c = 0; c < clen; c++) {
  620. if ((end && current.col > end.col && colSelectionLength > colInputLength) ||
  621. (!priv.settings.allowInsertColumn && current.col > instance.countCols() - 1) ||
  622. (current.col >= priv.settings.maxCols)) {
  623. break;
  624. }
  625. cellMeta = instance.getCellMeta(current.row, current.col);
  626. if ((source === 'CopyPaste.paste' || source === 'Autofill.fill') && cellMeta.skipColumnOnPaste) {
  627. skippedColumn += 1;
  628. current.col += 1;
  629. clen += 1;
  630. continue;
  631. }
  632. if (cellMeta.readOnly) {
  633. current.col += 1;
  634. /* eslint-disable no-continue */
  635. continue;
  636. }
  637. const visualColumn = c - skippedColumn;
  638. let value = getInputValue(visualRow, visualColumn);
  639. const orgValue = instance.getDataAtCell(current.row, current.col);
  640. const index = {
  641. row: visualRow,
  642. col: visualColumn
  643. };
  644. if (source === 'Autofill.fill') {
  645. const result = instance.runHooks('beforeAutofillInsidePopulate', index, direction, input, deltas, {}, selected);
  646. if (result) {
  647. value = isUndefined(result.value) ? value : result.value;
  648. }
  649. }
  650. if (value !== null && typeof value === 'object') {
  651. if (orgValue === null || typeof orgValue !== 'object') {
  652. pushData = false;
  653. } else {
  654. const orgValueSchema = duckSchema(orgValue[0] || orgValue);
  655. const valueSchema = duckSchema(value[0] || value);
  656. /* eslint-disable max-depth */
  657. if (isObjectEqual(orgValueSchema, valueSchema)) {
  658. value = deepClone(value);
  659. } else {
  660. pushData = false;
  661. }
  662. }
  663. } else if (orgValue !== null && typeof orgValue === 'object') {
  664. pushData = false;
  665. }
  666. if (pushData) {
  667. setData.push([current.row, current.col, value]);
  668. }
  669. pushData = true;
  670. current.col += 1;
  671. }
  672. current.row += 1;
  673. }
  674. instance.setDataAtCell(setData, null, null, source || 'populateFromArray');
  675. break;
  676. }
  677. },
  678. };
  679. /**
  680. * Internal function to set `language` key of settings.
  681. *
  682. * @private
  683. * @param {String} languageCode Language code for specific language i.e. 'en-US', 'pt-BR', 'de-DE'
  684. * @fires Hooks#afterLanguageChange
  685. */
  686. function setLanguage(languageCode) {
  687. const normalizedLanguageCode = normalizeLanguageCode(languageCode);
  688. if (hasLanguageDictionary(normalizedLanguageCode)) {
  689. instance.runHooks('beforeLanguageChange', normalizedLanguageCode);
  690. GridSettings.prototype.language = normalizedLanguageCode;
  691. instance.runHooks('afterLanguageChange', normalizedLanguageCode);
  692. } else {
  693. warnUserAboutLanguageRegistration(languageCode);
  694. }
  695. }
  696. this.init = function() {
  697. dataSource.setData(priv.settings.data);
  698. instance.runHooks('beforeInit');
  699. if (isMobileBrowser()) {
  700. addClass(instance.rootElement, 'mobile');
  701. }
  702. this.updateSettings(priv.settings, true);
  703. this.view = new TableView(this);
  704. editorManager = EditorManager.getInstance(instance, priv, selection, datamap);
  705. this.forceFullRender = true; // used when data was changed
  706. instance.runHooks('init');
  707. this.view.render();
  708. if (typeof priv.firstRun === 'object') {
  709. instance.runHooks('afterChange', priv.firstRun[0], priv.firstRun[1]);
  710. priv.firstRun = false;
  711. }
  712. instance.runHooks('afterInit');
  713. };
  714. function ValidatorsQueue() { // moved this one level up so it can be used in any function here. Probably this should be moved to a separate file
  715. let resolved = false;
  716. return {
  717. validatorsInQueue: 0,
  718. valid: true,
  719. addValidatorToQueue() {
  720. this.validatorsInQueue += 1;
  721. resolved = false;
  722. },
  723. removeValidatorFormQueue() {
  724. this.validatorsInQueue = this.validatorsInQueue - 1 < 0 ? 0 : this.validatorsInQueue - 1;
  725. this.checkIfQueueIsEmpty();
  726. },
  727. onQueueEmpty() {
  728. },
  729. checkIfQueueIsEmpty() {
  730. if (this.validatorsInQueue === 0 && resolved === false) {
  731. resolved = true;
  732. this.onQueueEmpty(this.valid);
  733. }
  734. }
  735. };
  736. }
  737. /**
  738. * Get parsed number from numeric string.
  739. *
  740. * @private
  741. * @param {String} numericData Float (separated by a dot or a comma) or integer.
  742. * @returns {Number} Number if we get data in parsable format, not changed value otherwise.
  743. */
  744. function getParsedNumber(numericData) {
  745. // Unifying "float like" string. Change from value with comma determiner to value with dot determiner,
  746. // for example from `450,65` to `450.65`.
  747. const unifiedNumericData = numericData.replace(',', '.');
  748. if (isNaN(parseFloat(unifiedNumericData)) === false) {
  749. return parseFloat(unifiedNumericData);
  750. }
  751. return numericData;
  752. }
  753. function validateChanges(changes, source, callback) {
  754. const waitingForValidator = new ValidatorsQueue();
  755. const isNumericData = value => value.length > 0 && /^\s*[+-.]?\s*(?:(?:\d+(?:(\.|,)\d+)?(?:e[+-]?\d+)?)|(?:0x[a-f\d]+))\s*$/.test(value);
  756. waitingForValidator.onQueueEmpty = resolve;
  757. for (let i = changes.length - 1; i >= 0; i--) {
  758. if (changes[i] === null) {
  759. changes.splice(i, 1);
  760. } else {
  761. const [row, prop, , newValue] = changes[i];
  762. const col = datamap.propToCol(prop);
  763. const cellProperties = instance.getCellMeta(row, col);
  764. if (cellProperties.type === 'numeric' && typeof newValue === 'string' && isNumericData(newValue)) {
  765. changes[i][3] = getParsedNumber(newValue);
  766. }
  767. /* eslint-disable no-loop-func */
  768. if (instance.getCellValidator(cellProperties)) {
  769. waitingForValidator.addValidatorToQueue();
  770. instance.validateCell(changes[i][3], cellProperties, (function(index, cellPropertiesReference) {
  771. return function(result) {
  772. if (typeof result !== 'boolean') {
  773. throw new Error('Validation error: result is not boolean');
  774. }
  775. if (result === false && cellPropertiesReference.allowInvalid === false) {
  776. changes.splice(index, 1); // cancel the change
  777. cellPropertiesReference.valid = true; // we cancelled the change, so cell value is still valid
  778. const cell = instance.getCell(cellPropertiesReference.visualRow, cellPropertiesReference.visualCol);
  779. if (cell !== null) {
  780. removeClass(cell, instance.getSettings().invalidCellClassName);
  781. }
  782. // index -= 1;
  783. }
  784. waitingForValidator.removeValidatorFormQueue();
  785. };
  786. }(i, cellProperties)), source);
  787. }
  788. }
  789. }
  790. waitingForValidator.checkIfQueueIsEmpty();
  791. function resolve() {
  792. let beforeChangeResult;
  793. if (changes.length) {
  794. beforeChangeResult = instance.runHooks('beforeChange', changes, source || 'edit');
  795. if (isFunction(beforeChangeResult)) {
  796. warn('Your beforeChange callback returns a function. It\'s not supported since Handsontable 0.12.1 (and the returned function will not be executed).');
  797. } else if (beforeChangeResult === false) {
  798. changes.splice(0, changes.length); // invalidate all changes (remove everything from array)
  799. }
  800. }
  801. callback(); // called when async validators are resolved and beforeChange was not async
  802. }
  803. }
  804. /**
  805. * Internal function to apply changes. Called after validateChanges
  806. *
  807. * @private
  808. * @param {Array} changes Array in form of [row, prop, oldValue, newValue]
  809. * @param {String} source String that identifies how this change will be described in changes array (useful in onChange callback)
  810. * @fires Hooks#beforeChangeRender
  811. * @fires Hooks#afterChange
  812. */
  813. function applyChanges(changes, source) {
  814. let i = changes.length - 1;
  815. if (i < 0) {
  816. return;
  817. }
  818. for (; i >= 0; i--) {
  819. let skipThisChange = false;
  820. if (changes[i] === null) {
  821. changes.splice(i, 1);
  822. /* eslint-disable no-continue */
  823. continue;
  824. }
  825. if ((changes[i][2] === null || changes[i][2] === void 0)
  826. && (changes[i][3] === null || changes[i][3] === void 0)) {
  827. /* eslint-disable no-continue */
  828. continue;
  829. }
  830. if (priv.settings.allowInsertRow) {
  831. while (changes[i][0] > instance.countRows() - 1) {
  832. const numberOfCreatedRows = datamap.createRow(void 0, void 0, source);
  833. if (numberOfCreatedRows === 0) {
  834. skipThisChange = true;
  835. break;
  836. }
  837. }
  838. }
  839. if (skipThisChange) {
  840. /* eslint-disable no-continue */
  841. continue;
  842. }
  843. if (instance.dataType === 'array' && (!priv.settings.columns || priv.settings.columns.length === 0) && priv.settings.allowInsertColumn) {
  844. while (datamap.propToCol(changes[i][1]) > instance.countCols() - 1) {
  845. datamap.createCol(void 0, void 0, source);
  846. }
  847. }
  848. datamap.set(changes[i][0], changes[i][1], changes[i][3]);
  849. }
  850. instance.forceFullRender = true; // used when data was changed
  851. grid.adjustRowsAndCols();
  852. instance.runHooks('beforeChangeRender', changes, source);
  853. editorManager.lockEditor();
  854. instance._refreshBorders(null);
  855. editorManager.unlockEditor();
  856. instance.view.wt.wtOverlays.adjustElementsSize();
  857. instance.runHooks('afterChange', changes, source || 'edit');
  858. const activeEditor = instance.getActiveEditor();
  859. if (activeEditor && isDefined(activeEditor.refreshValue)) {
  860. activeEditor.refreshValue();
  861. }
  862. }
  863. /**
  864. * Validate a single cell.
  865. *
  866. * @param {String|Number} value
  867. * @param cellProperties
  868. * @param callback
  869. * @param source
  870. */
  871. this.validateCell = function(value, cellProperties, callback, source) {
  872. let validator = instance.getCellValidator(cellProperties);
  873. // the `canBeValidated = false` argument suggests, that the cell passes validation by default.
  874. function done(valid, canBeValidated = true) {
  875. // Fixes GH#3903
  876. if (!canBeValidated || cellProperties.hidden === true) {
  877. callback(valid);
  878. return;
  879. }
  880. const col = cellProperties.visualCol;
  881. const row = cellProperties.visualRow;
  882. const td = instance.getCell(row, col, true);
  883. if (td && td.nodeName !== 'TH') {
  884. instance.view.wt.wtSettings.settings.cellRenderer(row, col, td);
  885. }
  886. callback(valid);
  887. }
  888. if (isRegExp(validator)) {
  889. validator = (function(expression) {
  890. return function(cellValue, validatorCallback) {
  891. validatorCallback(expression.test(cellValue));
  892. };
  893. }(validator));
  894. }
  895. if (isFunction(validator)) {
  896. // eslint-disable-next-line no-param-reassign
  897. value = instance.runHooks('beforeValidate', value, cellProperties.visualRow, cellProperties.prop, source);
  898. // To provide consistent behaviour, validation should be always asynchronous
  899. instance._registerTimeout(setTimeout(() => {
  900. validator.call(cellProperties, value, (valid) => {
  901. // eslint-disable-next-line no-param-reassign
  902. valid = instance.runHooks('afterValidate', valid, value, cellProperties.visualRow, cellProperties.prop, source);
  903. cellProperties.valid = valid;
  904. done(valid);
  905. instance.runHooks('postAfterValidate', valid, value, cellProperties.visualRow, cellProperties.prop, source);
  906. });
  907. }, 0));
  908. } else {
  909. // resolve callback even if validator function was not found
  910. instance._registerTimeout(setTimeout(() => {
  911. cellProperties.valid = true;
  912. done(cellProperties.valid, false);
  913. }, 0));
  914. }
  915. };
  916. function setDataInputToArray(row, propOrCol, value) {
  917. if (typeof row === 'object') { // is it an array of changes
  918. return row;
  919. }
  920. return [
  921. [row, propOrCol, value]
  922. ];
  923. }
  924. /**
  925. * @description
  926. * Set new value to a cell. To change many cells at once (recommended way), pass an array of `changes` in format
  927. * `[[row, col, value],...]` as the first argument.
  928. *
  929. * @memberof Core#
  930. * @function setDataAtCell
  931. * @param {Number|Array} row Visual row index or array of changes in format `[[row, col, value],...]`.
  932. * @param {Number} [column] Visual column index.
  933. * @param {String} [value] New value.
  934. * @param {String} [source] String that identifies how this change will be described in the changes array (useful in onAfterChange or onBeforeChange callback).
  935. */
  936. this.setDataAtCell = function(row, column, value, source) {
  937. const input = setDataInputToArray(row, column, value);
  938. const changes = [];
  939. let changeSource = source;
  940. let i;
  941. let ilen;
  942. let prop;
  943. for (i = 0, ilen = input.length; i < ilen; i++) {
  944. if (typeof input[i] !== 'object') {
  945. throw new Error('Method `setDataAtCell` accepts row number or changes array of arrays as its first parameter');
  946. }
  947. if (typeof input[i][1] !== 'number') {
  948. throw new Error('Method `setDataAtCell` accepts row and column number as its parameters. If you want to use object property name, use method `setDataAtRowProp`');
  949. }
  950. prop = datamap.colToProp(input[i][1]);
  951. changes.push([
  952. input[i][0],
  953. prop,
  954. dataSource.getAtCell(recordTranslator.toPhysicalRow(input[i][0]), input[i][1]),
  955. input[i][2],
  956. ]);
  957. }
  958. if (!changeSource && typeof row === 'object') {
  959. changeSource = column;
  960. }
  961. instance.runHooks('afterSetDataAtCell', changes, changeSource);
  962. validateChanges(changes, changeSource, () => {
  963. applyChanges(changes, changeSource);
  964. });
  965. };
  966. /**
  967. * @description
  968. * Set new value to a cell. To change many cells at once (recommended way), pass an array of `changes` in format
  969. * `[[row, prop, value],...]` as the first argument.
  970. *
  971. * @memberof Core#
  972. * @function setDataAtRowProp
  973. * @param {Number|Array} row Visual row index or array of changes in format `[[row, prop, value], ...]`.
  974. * @param {String} prop Property name or the source string (e.g. `'first.name'` or `'0'`).
  975. * @param {String} value Value to be set.
  976. * @param {String} [source] String that identifies how this change will be described in changes array (useful in onChange callback).
  977. */
  978. this.setDataAtRowProp = function(row, prop, value, source) {
  979. const input = setDataInputToArray(row, prop, value);
  980. const changes = [];
  981. let changeSource = source;
  982. let i;
  983. let ilen;
  984. for (i = 0, ilen = input.length; i < ilen; i++) {
  985. changes.push([
  986. input[i][0],
  987. input[i][1],
  988. dataSource.getAtCell(recordTranslator.toPhysicalRow(input[i][0]), input[i][1]),
  989. input[i][2],
  990. ]);
  991. }
  992. if (!changeSource && typeof row === 'object') {
  993. changeSource = prop;
  994. }
  995. instance.runHooks('afterSetDataAtRowProp', changes, changeSource);
  996. validateChanges(changes, changeSource, () => {
  997. applyChanges(changes, changeSource);
  998. });
  999. };
  1000. /**
  1001. * Listen to the keyboard input on document body. This allows Handsontable to capture keyboard events and respond
  1002. * in the right way.
  1003. *
  1004. * @memberof Core#
  1005. * @function listen
  1006. * @param {Boolean} [modifyDocumentFocus=true] If `true`, currently focused element will be blured (which returns focus
  1007. * to the document.body). Otherwise the active element does not lose its focus.
  1008. * @fires Hooks#afterListen
  1009. */
  1010. this.listen = function(modifyDocumentFocus = true) {
  1011. if (modifyDocumentFocus) {
  1012. const invalidActiveElement = !document.activeElement || (document.activeElement && document.activeElement.nodeName === void 0);
  1013. if (document.activeElement && document.activeElement !== document.body && !invalidActiveElement) {
  1014. document.activeElement.blur();
  1015. } else if (invalidActiveElement) { // IE
  1016. document.body.focus();
  1017. }
  1018. }
  1019. if (instance && !instance.isListening()) {
  1020. activeGuid = instance.guid;
  1021. instance.runHooks('afterListen');
  1022. }
  1023. };
  1024. /**
  1025. * Stop listening to keyboard input on the document body. Calling this method makes the Handsontable inactive for
  1026. * any keyboard events.
  1027. *
  1028. * @memberof Core#
  1029. * @function unlisten
  1030. */
  1031. this.unlisten = function() {
  1032. if (this.isListening()) {
  1033. activeGuid = null;
  1034. instance.runHooks('afterUnlisten');
  1035. }
  1036. };
  1037. /**
  1038. * Returns `true` if the current Handsontable instance is listening to keyboard input on document body.
  1039. *
  1040. * @memberof Core#
  1041. * @function isListening
  1042. * @returns {Boolean} `true` if the instance is listening, `false` otherwise.
  1043. */
  1044. this.isListening = function() {
  1045. return activeGuid === instance.guid;
  1046. };
  1047. /**
  1048. * Destroys the current editor, render the table and prepares the editor of the newly selected cell.
  1049. *
  1050. * @memberof Core#
  1051. * @function destroyEditor
  1052. * @param {Boolean} [revertOriginal=false] If `true`, the previous value will be restored. Otherwise, the edited value will be saved.
  1053. * @param {Boolean} [prepareEditorIfNeeded=true] If `true` the editor under the selected cell will be prepared to open.
  1054. */
  1055. this.destroyEditor = function(revertOriginal = false, prepareEditorIfNeeded = true) {
  1056. instance._refreshBorders(revertOriginal, prepareEditorIfNeeded);
  1057. };
  1058. /**
  1059. * Populate cells at position with 2D input array (e.g. `[[1, 2], [3, 4]]`). Use `endRow`, `endCol` when you
  1060. * want to cut input when a certain row is reached.
  1061. *
  1062. * Optional `method` argument has the same effect as pasteMode option (see {@link Options#pasteMode}).
  1063. *
  1064. * @memberof Core#
  1065. * @function populateFromArray
  1066. * @param {Number} row Start visual row index.
  1067. * @param {Number} column Start visual column index.
  1068. * @param {Array} input 2d array
  1069. * @param {Number} [endRow] End visual row index (use when you want to cut input when certain row is reached).
  1070. * @param {Number} [endCol] End visual column index (use when you want to cut input when certain column is reached).
  1071. * @param {String} [source=populateFromArray] Used to identify this call in the resulting events (beforeChange, afterChange).
  1072. * @param {String} [method=overwrite] Populate method, possible values: `'shift_down'`, `'shift_right'`, `'overwrite'`.
  1073. * @param {String} direction Populate direction, possible values: `'left'`, `'right'`, `'up'`, `'down'`.
  1074. * @param {Array} deltas The deltas array. A difference between values of adjacent cells.
  1075. * Useful **only** when the type of handled cells is `numeric`.
  1076. */
  1077. this.populateFromArray = function(row, column, input, endRow, endCol, source, method, direction, deltas) {
  1078. if (!(typeof input === 'object' && typeof input[0] === 'object')) {
  1079. throw new Error('populateFromArray parameter `input` must be an array of arrays'); // API changed in 0.9-beta2, let's check if you use it correctly
  1080. }
  1081. const c = typeof endRow === 'number' ? new CellCoords(endRow, endCol) : null;
  1082. return grid.populateFromArray(new CellCoords(row, column), input, c, source, method, direction, deltas);
  1083. };
  1084. /**
  1085. * Adds/removes data from the column. This method works the same as Array.splice for arrays (see {@link DataMap#spliceCol}).
  1086. *
  1087. * @memberof Core#
  1088. * @function spliceCol
  1089. * @param {Number} column Index of the column in which do you want to do splice.
  1090. * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end.
  1091. * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed.
  1092. * @param {...Number} [elements] The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array.
  1093. */
  1094. this.spliceCol = function(column, index, amount, ...elements) {
  1095. return datamap.spliceCol(column, index, amount, ...elements);
  1096. };
  1097. /**
  1098. * Adds/removes data from the row. This method works the same as Array.splice for arrays (see {@link DataMap#spliceRow}).
  1099. *
  1100. * @memberof Core#
  1101. * @function spliceRow
  1102. * @param {Number} row Index of column in which do you want to do splice.
  1103. * @param {Number} index Index at which to start changing the array. If negative, will begin that many elements from the end.
  1104. * @param {Number} amount An integer indicating the number of old array elements to remove. If amount is 0, no elements are removed.
  1105. * @param {...Number} [elements] The elements to add to the array. If you don't specify any elements, spliceCol simply removes elements from the array.
  1106. */
  1107. this.spliceRow = function(row, index, amount, ...elements) {
  1108. return datamap.spliceRow(row, index, amount, ...elements);
  1109. };
  1110. /**
  1111. * Returns indexes of the currently selected cells as an array of arrays `[[startRow, startCol, endRow, endCol],...]`.
  1112. *
  1113. * Start row and start column are the coordinates of the active cell (where the selection was started).
  1114. *
  1115. * The version 0.36.0 adds a non-consecutive selection feature. Since this version, the method returns an array of arrays.
  1116. * Additionally to collect the coordinates of the currently selected area (as it was previously done by the method)
  1117. * you need to use `getSelectedLast` method.
  1118. *
  1119. * @memberof Core#
  1120. * @function getSelected
  1121. * @returns {Array[]|undefined} An array of arrays of the selection's coordinates.
  1122. */
  1123. this.getSelected = function() { // https://github.com/handsontable/handsontable/issues/44 //cjl
  1124. if (selection.isSelected()) {
  1125. return arrayMap(selection.getSelectedRange(), ({ from, to }) => [from.row, from.col, to.row, to.col]);
  1126. }
  1127. };
  1128. /**
  1129. * Returns the last coordinates applied to the table as a an array `[startRow, startCol, endRow, endCol]`.
  1130. *
  1131. * @since 0.36.0
  1132. * @memberof Core#
  1133. * @function getSelectedLast
  1134. * @returns {Array|undefined} An array of the selection's coordinates.
  1135. */
  1136. this.getSelectedLast = function() {
  1137. const selected = this.getSelected();
  1138. let result;
  1139. if (selected && selected.length > 0) {
  1140. result = selected[selected.length - 1];
  1141. }
  1142. return result;
  1143. };
  1144. /**
  1145. * Returns the current selection as an array of CellRange objects.
  1146. *
  1147. * The version 0.36.0 adds a non-consecutive selection feature. Since this version, the method returns an array of arrays.
  1148. * Additionally to collect the coordinates of the currently selected area (as it was previously done by the method)
  1149. * you need to use `getSelectedRangeLast` method.
  1150. *
  1151. * @memberof Core#
  1152. * @function getSelectedRange
  1153. * @returns {CellRange[]|undefined} Selected range object or undefined if there is no selection.
  1154. */
  1155. this.getSelectedRange = function() { // https://github.com/handsontable/handsontable/issues/44 //cjl
  1156. if (selection.isSelected()) {
  1157. return Array.from(selection.getSelectedRange());
  1158. }
  1159. };
  1160. /**
  1161. * Returns the last coordinates applied to the table as a CellRange object.
  1162. *
  1163. * @memberof Core#
  1164. * @function getSelectedRangeLast
  1165. * @since 0.36.0
  1166. * @returns {CellRange|undefined} Selected range object or undefined` if there is no selection.
  1167. */
  1168. this.getSelectedRangeLast = function() {
  1169. const selectedRange = this.getSelectedRange();
  1170. let result;
  1171. if (selectedRange && selectedRange.length > 0) {
  1172. result = selectedRange[selectedRange.length - 1];
  1173. }
  1174. return result;
  1175. };
  1176. /**
  1177. * Erases content from cells that have been selected in the table.
  1178. *
  1179. * @memberof Core#
  1180. * @function emptySelectedCells
  1181. * @since 0.36.0
  1182. */
  1183. this.emptySelectedCells = function() {
  1184. if (!selection.isSelected()) {
  1185. return;
  1186. }
  1187. const changes = [];
  1188. arrayEach(selection.getSelectedRange(), (cellRange) => {
  1189. const topLeft = cellRange.getTopLeftCorner();
  1190. const bottomRight = cellRange.getBottomRightCorner();
  1191. rangeEach(topLeft.row, bottomRight.row, (row) => {
  1192. rangeEach(topLeft.col, bottomRight.col, (column) => {
  1193. if (!this.getCellMeta(row, column).readOnly) {
  1194. changes.push([row, column, '']);
  1195. }
  1196. });
  1197. });
  1198. });
  1199. if (changes.length > 0) {
  1200. this.setDataAtCell(changes);
  1201. }
  1202. };
  1203. /**
  1204. * Rerender the table. Calling this method starts the process of recalculating, redrawing and applying the changes
  1205. * to the DOM. While rendering the table all cell renderers are recalled.
  1206. *
  1207. * Calling this method manually is not recommended. Handsontable tries to render itself by choosing the most
  1208. * optimal moments in its lifecycle.
  1209. *
  1210. * @memberof Core#
  1211. * @function render
  1212. */
  1213. this.render = function() {
  1214. if (instance.view) {
  1215. instance.renderCall = true;
  1216. instance.forceFullRender = true; // used when data was changed
  1217. editorManager.lockEditor();
  1218. instance._refreshBorders(null);
  1219. editorManager.unlockEditor();
  1220. }
  1221. };
  1222. /**
  1223. * Loads new data to Handsontable. Loading new data resets the cell meta.
  1224. *
  1225. * @memberof Core#
  1226. * @function loadData
  1227. * @param {Array} data Array of arrays or array of objects containing data.
  1228. * @fires Hooks#afterLoadData
  1229. * @fires Hooks#afterChange
  1230. */
  1231. this.loadData = function(data) {
  1232. if (Array.isArray(priv.settings.dataSchema)) {
  1233. instance.dataType = 'array';
  1234. } else if (isFunction(priv.settings.dataSchema)) {
  1235. instance.dataType = 'function';
  1236. } else {
  1237. instance.dataType = 'object';
  1238. }
  1239. if (datamap) {
  1240. datamap.destroy();
  1241. }
  1242. datamap = new DataMap(instance, priv, GridSettings);
  1243. if (typeof data === 'object' && data !== null) {
  1244. if (!(data.push && data.splice)) { // check if data is array. Must use duck-type check so Backbone Collections also pass it
  1245. // when data is not an array, attempt to make a single-row array of it
  1246. // eslint-disable-next-line no-param-reassign
  1247. data = [data];
  1248. }
  1249. } else if (data === null) {
  1250. const dataSchema = datamap.getSchema();
  1251. // eslint-disable-next-line no-param-reassign
  1252. data = [];
  1253. let row;
  1254. let r = 0;
  1255. let rlen = 0;
  1256. for (r = 0, rlen = priv.settings.startRows; r < rlen; r++) {
  1257. if ((instance.dataType === 'object' || instance.dataType === 'function') && priv.settings.dataSchema) {
  1258. row = deepClone(dataSchema);
  1259. data.push(row);
  1260. } else if (instance.dataType === 'array') {
  1261. row = deepClone(dataSchema[0]);
  1262. data.push(row);
  1263. } else {
  1264. row = [];
  1265. for (let c = 0, clen = priv.settings.startCols; c < clen; c++) {
  1266. row.push(null);
  1267. }
  1268. data.push(row);
  1269. }
  1270. }
  1271. } else {
  1272. throw new Error(`loadData only accepts array of objects or array of arrays (${typeof data} given)`);
  1273. }
  1274. priv.isPopulated = false;
  1275. GridSettings.prototype.data = data;
  1276. if (Array.isArray(data[0])) {
  1277. instance.dataType = 'array';
  1278. }
  1279. datamap.dataSource = data;
  1280. dataSource.data = data;
  1281. dataSource.dataType = instance.dataType;
  1282. dataSource.colToProp = datamap.colToProp.bind(datamap);
  1283. dataSource.propToCol = datamap.propToCol.bind(datamap);
  1284. clearCellSettingCache();
  1285. grid.adjustRowsAndCols();
  1286. instance.runHooks('afterLoadData', priv.firstRun);
  1287. if (priv.firstRun) {
  1288. priv.firstRun = [null, 'loadData'];
  1289. } else {
  1290. instance.runHooks('afterChange', null, 'loadData');
  1291. instance.render();
  1292. }
  1293. priv.isPopulated = true;
  1294. function clearCellSettingCache() {
  1295. priv.cellSettings.length = 0;
  1296. }
  1297. };
  1298. /**
  1299. * Returns the current data object (the same one that was passed by `data` configuration option or `loadData` method,
  1300. * unless the `modifyRow` hook was used to trim some of the rows. If that's the case - use the {@link Core#getSourceData} method.).
  1301. *
  1302. * Optionally you can provide cell range by defining `row`, `column`, `row2`, `column2` to get only a fragment of table data.
  1303. *
  1304. * @memberof Core#
  1305. * @function getData
  1306. * @param {Number} [row] From visual row index.
  1307. * @param {Number} [column] From visual column index.
  1308. * @param {Number} [row2] To visual row index.
  1309. * @param {Number} [column2] To visual column index.
  1310. * @returns {Array[]} Array with the data.
  1311. * @example
  1312. * ```js
  1313. * // Get all data (in order how it is rendered in the table).
  1314. * hot.getData();
  1315. * // Get data fragment (from top-left 0, 0 to bottom-right 3, 3).
  1316. * hot.getData(3, 3);
  1317. * // Get data fragment (from top-left 2, 1 to bottom-right 3, 3).
  1318. * hot.getData(2, 1, 3, 3);
  1319. * ```
  1320. */
  1321. this.getData = function(row, column, row2, column2) {
  1322. if (isUndefined(row)) {
  1323. return datamap.getAll();
  1324. }
  1325. return datamap.getRange(new CellCoords(row, column), new CellCoords(row2, column2), datamap.DESTINATION_RENDERER);
  1326. };
  1327. /**
  1328. * Returns a string value of the selected range. Each column is separated by tab, each row is separated by a new
  1329. * line character (see {@link DataMap#getCopyableText}).
  1330. *
  1331. * @memberof Core#
  1332. * @function getCopyableText
  1333. * @param {Number} startRow From visual row index.
  1334. * @param {Number} startCol From visual column index.
  1335. * @param {Number} endRow To visual row index.
  1336. * @param {Number} endCol To visual column index.
  1337. * @returns {String}
  1338. */
  1339. this.getCopyableText = function(startRow, startCol, endRow, endCol) {
  1340. return datamap.getCopyableText(new CellCoords(startRow, startCol), new CellCoords(endRow, endCol));
  1341. };
  1342. /**
  1343. * Returns the data's copyable value at specified `row` and `column` index (see {@link DataMap#getCopyable}).
  1344. *
  1345. * @memberof Core#
  1346. * @function getCopyableData
  1347. * @param {Number} row Visual row index.
  1348. * @param {Number} column Visual column index.
  1349. * @returns {String}
  1350. */
  1351. this.getCopyableData = function(row, column) {
  1352. return datamap.getCopyable(row, datamap.colToProp(column));
  1353. };
  1354. /**
  1355. * Returns schema provided by constructor settings. If it doesn't exist then it returns the schema based on the data
  1356. * structure in the first row.
  1357. *
  1358. * @memberof Core#
  1359. * @function getSchema
  1360. * @returns {Object} Schema object.
  1361. */
  1362. this.getSchema = function() {
  1363. return datamap.getSchema();
  1364. };
  1365. /**
  1366. * Use it if you need to change configuration after initialization. The `settings` argument is an object containing the new
  1367. * settings, declared the same way as in the initial settings object.
  1368. *
  1369. * __Note__, that although the `updateSettings` method doesn't overwrite the previously declared settings, it might reset
  1370. * the settings made post-initialization. (for example - ignore changes made using the columnResize feature).
  1371. *
  1372. * @memberof Core#
  1373. * @function updateSettings
  1374. * @param {Object} settings New settings object (see {@link Options}).
  1375. * @param {Boolean} [init=false] Internally used for in initialization mode.
  1376. * @example
  1377. * ```js
  1378. * hot.updateSettings({
  1379. * contextMenu: true,
  1380. * colHeaders: true,
  1381. * fixedRowsTop: 2
  1382. * });
  1383. * ```
  1384. * @fires Hooks#afterCellMetaReset
  1385. * @fires Hooks#afterUpdateSettings
  1386. */
  1387. this.updateSettings = function(settings, init = false) {
  1388. let columnsAsFunc = false;
  1389. let i;
  1390. let j;
  1391. let clen;
  1392. if (isDefined(settings.rows)) {
  1393. throw new Error('"rows" setting is no longer supported. do you mean startRows, minRows or maxRows?');
  1394. }
  1395. if (isDefined(settings.cols)) {
  1396. throw new Error('"cols" setting is no longer supported. do you mean startCols, minCols or maxCols?');
  1397. }
  1398. // eslint-disable-next-line no-restricted-syntax
  1399. for (i in settings) {
  1400. if (i === 'data') {
  1401. /* eslint-disable-next-line no-continue */
  1402. continue; // loadData will be triggered later
  1403. } else if (i === 'language') {
  1404. setLanguage(settings.language);
  1405. /* eslint-disable-next-line no-continue */
  1406. continue;
  1407. } else if (Hooks.getSingleton().getRegistered().indexOf(i) > -1) {
  1408. if (isFunction(settings[i]) || Array.isArray(settings[i])) {
  1409. settings[i].initialHook = true;
  1410. instance.addHook(i, settings[i]);
  1411. }
  1412. } else if (!init && hasOwnProperty(settings, i)) { // Update settings
  1413. GridSettings.prototype[i] = settings[i];
  1414. }
  1415. }
  1416. // Load data or create data map
  1417. if (settings.data === void 0 && priv.settings.data === void 0) {
  1418. instance.loadData(null); // data source created just now
  1419. } else if (settings.data !== void 0) {
  1420. instance.loadData(settings.data); // data source given as option
  1421. } else if (settings.columns !== void 0) {
  1422. datamap.createMap();
  1423. }
  1424. clen = instance.countCols();
  1425. const columnSetting = settings.columns || GridSettings.prototype.columns;
  1426. // Init columns constructors configuration
  1427. if (columnSetting && isFunction(columnSetting)) {
  1428. clen = instance.countSourceCols();
  1429. columnsAsFunc = true;
  1430. }
  1431. // Clear cellSettings cache
  1432. if (settings.cell !== void 0 || settings.cells !== void 0 || settings.columns !== void 0) {
  1433. priv.cellSettings.length = 0;
  1434. }
  1435. if (clen > 0) {
  1436. let proto;
  1437. let column;
  1438. for (i = 0, j = 0; i < clen; i++) {
  1439. if (columnsAsFunc && !columnSetting(i)) {
  1440. /* eslint-disable no-continue */
  1441. continue;
  1442. }
  1443. priv.columnSettings[j] = columnFactory(GridSettings, priv.columnsSettingConflicts);
  1444. // shortcut for prototype
  1445. proto = priv.columnSettings[j].prototype;
  1446. // Use settings provided by user
  1447. if (columnSetting) {
  1448. if (columnsAsFunc) {
  1449. column = columnSetting(i);
  1450. } else {
  1451. column = columnSetting[j];
  1452. }
  1453. if (column) {
  1454. extend(proto, column);
  1455. extend(proto, expandType(column));
  1456. }
  1457. }
  1458. j += 1;
  1459. }
  1460. }
  1461. if (isDefined(settings.cell)) {
  1462. objectEach(settings.cell, (cell) => {
  1463. instance.setCellMetaObject(cell.row, cell.col, cell);
  1464. });
  1465. }
  1466. instance.runHooks('afterCellMetaReset');
  1467. if (isDefined(settings.className)) {
  1468. if (GridSettings.prototype.className) {
  1469. removeClass(instance.rootElement, GridSettings.prototype.className);
  1470. }
  1471. if (settings.className) {
  1472. addClass(instance.rootElement, settings.className);
  1473. }
  1474. }
  1475. let currentHeight = instance.rootElement.style.height;
  1476. if (currentHeight !== '') {
  1477. currentHeight = parseInt(instance.rootElement.style.height, 10);
  1478. }
  1479. let height = settings.height;
  1480. if (isFunction(height)) {
  1481. height = height();
  1482. }
  1483. if (init) {
  1484. const initialStyle = instance.rootElement.getAttribute('style');
  1485. if (initialStyle) {
  1486. instance.rootElement.setAttribute('data-initialstyle', instance.rootElement.getAttribute('style'));
  1487. }
  1488. }
  1489. if (height === null) {
  1490. const initialStyle = instance.rootElement.getAttribute('data-initialstyle');
  1491. if (initialStyle && (initialStyle.indexOf('height') > -1 || initialStyle.indexOf('overflow') > -1)) {
  1492. instance.rootElement.setAttribute('style', initialStyle);
  1493. } else {
  1494. instance.rootElement.style.height = '';
  1495. instance.rootElement.style.overflow = '';
  1496. }
  1497. } else if (height !== void 0) {
  1498. instance.rootElement.style.height = `${height}px`;
  1499. instance.rootElement.style.overflow = 'hidden';
  1500. }
  1501. if (typeof settings.width !== 'undefined') {
  1502. let width = settings.width;
  1503. if (isFunction(width)) {
  1504. width = width();
  1505. }
  1506. instance.rootElement.style.width = `${width}px`;
  1507. }
  1508. if (!init) {
  1509. datamap.clearLengthCache(); // force clear cache length on updateSettings() #3416
  1510. if (instance.view) {
  1511. instance.view.wt.wtViewport.resetHasOversizedColumnHeadersMarked();
  1512. }
  1513. instance.runHooks('afterUpdateSettings', settings);
  1514. }
  1515. grid.adjustRowsAndCols();
  1516. if (instance.view && !priv.firstRun) {
  1517. instance.forceFullRender = true; // used when data was changed
  1518. editorManager.lockEditor();
  1519. instance._refreshBorders(null);
  1520. editorManager.unlockEditor();
  1521. }
  1522. if (!init && instance.view && (currentHeight === '' || height === '' || height === void 0) && currentHeight !== height) {
  1523. instance.view.wt.wtOverlays.updateMainScrollableElements();
  1524. }
  1525. };
  1526. /**
  1527. * Get value from the selected cell.
  1528. *
  1529. * @memberof Core#
  1530. * @function getValue
  1531. * @returns {*} Value of selected cell.
  1532. */
  1533. this.getValue = function() {
  1534. const sel = instance.getSelectedLast();
  1535. if (GridSettings.prototype.getValue) {
  1536. if (isFunction(GridSettings.prototype.getValue)) {
  1537. return GridSettings.prototype.getValue.call(instance);
  1538. } else if (sel) {
  1539. return instance.getData()[sel[0][0]][GridSettings.prototype.getValue];
  1540. }
  1541. } else if (sel) {
  1542. return instance.getDataAtCell(sel[0], sel[1]);
  1543. }
  1544. };
  1545. function expandType(obj) {
  1546. if (!hasOwnProperty(obj, 'type')) {
  1547. // ignore obj.prototype.type
  1548. return;
  1549. }
  1550. const expandedType = {};
  1551. let type;
  1552. if (typeof obj.type === 'object') {
  1553. type = obj.type;
  1554. } else if (typeof obj.type === 'string') {
  1555. type = getCellType(obj.type);
  1556. }
  1557. // eslint-disable-next-line no-restricted-syntax
  1558. for (const i in type) {
  1559. if (hasOwnProperty(type, i) && !hasOwnProperty(obj, i)) {
  1560. expandedType[i] = type[i];
  1561. }
  1562. }
  1563. return expandedType;
  1564. }
  1565. /**
  1566. * Returns the object settings.
  1567. *
  1568. * @memberof Core#
  1569. * @function getSettings
  1570. * @returns {Object} Object containing the current table settings.
  1571. */
  1572. this.getSettings = function() {
  1573. return priv.settings;
  1574. };
  1575. /**
  1576. * Clears the data from the table (the table settings remain intact).
  1577. *
  1578. * @memberof Core#
  1579. * @function clear
  1580. */
  1581. this.clear = function() {
  1582. this.selectAll();
  1583. this.emptySelectedCells();
  1584. };
  1585. /**
  1586. * Allows altering the table structure by either inserting/removing rows or columns.
  1587. *
  1588. * @memberof Core#
  1589. * @function alter
  1590. * @param {String} action Possible alter operations:
  1591. * * `'insert_row'`
  1592. * * `'insert_col'`
  1593. * * `'remove_row'`
  1594. * * `'remove_col'`
  1595. * @param {Number|Number[]} index Visual index of the row/column before which the new row/column will be
  1596. * inserted/removed or an array of arrays in format `[[index, amount],...]`.
  1597. * @param {Number} [amount=1] Amount of rows/columns to be inserted or removed.
  1598. * @param {String} [source] Source indicator.
  1599. * @param {Boolean} [keepEmptyRows] Flag for preventing deletion of empty rows.
  1600. * @example
  1601. * ```js
  1602. * // Insert new row above the row at given visual index.
  1603. * hot.alter('insert_row', 10);
  1604. * // Insert 3 new columns before 10th column.
  1605. * hot.alter('insert_col', 10, 3);
  1606. * // Remove 2 rows starting from 10th row.
  1607. * hot.alter('remove_row', 10, 2);
  1608. * // Remove 5 non-contiquous rows (it removes 3 rows from visual index 1 and 2 rows from visual index 5).
  1609. * hot.alter('remove_row', [[1, 3], [5, 2]]);
  1610. * ```
  1611. */
  1612. this.alter = function(action, index, amount, source, keepEmptyRows) {
  1613. grid.alter(action, index, amount, source, keepEmptyRows);
  1614. };
  1615. /**
  1616. * Returns a TD element for the given `row` and `column` arguments, if it is rendered on screen.
  1617. * Returns `null` if the TD is not rendered on screen (probably because that part of the table is not visible).
  1618. *
  1619. * @memberof Core#
  1620. * @function getCell
  1621. * @param {Number} row Visual row index.
  1622. * @param {Number} column Visual column index.
  1623. * @param {Boolean} [topmost=false] If set to `true`, it returns the TD element from the topmost overlay. For example,
  1624. * if the wanted cell is in the range of fixed rows, it will return a TD element from the `top` overlay.
  1625. * @returns {HTMLTableCellElement|null} The cell's TD element.
  1626. */
  1627. this.getCell = function(row, column, topmost = false) {
  1628. return instance.view.getCellAtCoords(new CellCoords(row, column), topmost);
  1629. };
  1630. /**
  1631. * Returns the coordinates of the cell, provided as a HTML table cell element.
  1632. *
  1633. * @memberof Core#
  1634. * @function getCoords
  1635. * @param {HTMLTableCellElement} element The HTML Element representing the cell.
  1636. * @returns {CellCoords} Visual coordinates object.
  1637. * @example
  1638. * ```js
  1639. * hot.getCoords(hot.getCell(1, 1));
  1640. * // it returns CellCoords object instance with props row: 1 and col: 1.
  1641. * ```
  1642. */
  1643. this.getCoords = function(element) {
  1644. return this.view.wt.wtTable.getCoords.call(this.view.wt.wtTable, element);
  1645. };
  1646. /**
  1647. * Returns the property name that corresponds with the given column index (see {@link DataMap#colToProp}).
  1648. * If the data source is an array of arrays, it returns the columns index.
  1649. *
  1650. * @memberof Core#
  1651. * @function colToProp
  1652. * @param {Number} column Visual column index.
  1653. * @returns {String|Number} Column property or physical column index.
  1654. */
  1655. this.colToProp = function(column) {
  1656. return datamap.colToProp(column);
  1657. };
  1658. /**
  1659. * Returns column index that corresponds with the given property (see {@link DataMap#propToCol}).
  1660. *
  1661. * @memberof Core#
  1662. * @function propToCol
  1663. * @param {String|Number} prop Property name or physical column index.
  1664. * @returns {Number} Visual column index.
  1665. */
  1666. this.propToCol = function(prop) {
  1667. return datamap.propToCol(prop);
  1668. };
  1669. /**
  1670. * Translate physical row index into visual.
  1671. *
  1672. * This method is useful when you want to retrieve visual row index which can be reordered, moved or trimmed
  1673. * based on a physical index
  1674. *
  1675. * @memberof Core#
  1676. * @function toVisualRow
  1677. * @param {Number} row Physical row index.
  1678. * @returns {Number} Returns visual row index.
  1679. */
  1680. this.toVisualRow = row => recordTranslator.toVisualRow(row);
  1681. /**
  1682. * Translate physical column index into visual.
  1683. *
  1684. * This method is useful when you want to retrieve visual column index which can be reordered, moved or trimmed
  1685. * based on a physical index
  1686. *
  1687. * @memberof Core#
  1688. * @function toVisualColumn
  1689. * @param {Number} column Physical column index.
  1690. * @returns {Number} Returns visual column index.
  1691. */
  1692. this.toVisualColumn = column => recordTranslator.toVisualColumn(column);
  1693. /**
  1694. * Translate visual row index into physical.
  1695. *
  1696. * This method is useful when you want to retrieve physical row index based on a visual index which can be
  1697. * reordered, moved or trimmed.
  1698. *
  1699. * @memberof Core#
  1700. * @function toPhysicalRow
  1701. * @param {Number} row Visual row index.
  1702. * @returns {Number} Returns physical row index.
  1703. */
  1704. this.toPhysicalRow = row => recordTranslator.toPhysicalRow(row);
  1705. /**
  1706. * Translate visual column index into physical.
  1707. *
  1708. * This method is useful when you want to retrieve physical column index based on a visual index which can be
  1709. * reordered, moved or trimmed.
  1710. *
  1711. * @memberof Core#
  1712. * @function toPhysicalColumn
  1713. * @param {Number} column Visual column index.
  1714. * @returns {Number} Returns physical column index.
  1715. */
  1716. this.toPhysicalColumn = column => recordTranslator.toPhysicalColumn(column);
  1717. /**
  1718. * @description
  1719. * Returns the cell value at `row`, `column`.
  1720. *
  1721. * __Note__: If data is reordered, sorted or trimmed, the currently visible order will be used.
  1722. *
  1723. * @memberof Core#
  1724. * @function getDataAtCell
  1725. * @param {Number} row Visual row index.
  1726. * @param {Number} column Visual column index.
  1727. * @returns {*} Data at cell.
  1728. */
  1729. this.getDataAtCell = function(row, column) {
  1730. return datamap.get(row, datamap.colToProp(column));
  1731. };
  1732. /**
  1733. * Returns value at visual `row` and `prop` indexes (see {@link DataMap#get}).
  1734. *
  1735. * __Note__: If data is reordered, sorted or trimmed, the currently visible order will be used.
  1736. *
  1737. * @memberof Core#
  1738. * @function getDataAtRowProp
  1739. * @param {Number} row Visual row index.
  1740. * @param {String} prop Property name.
  1741. * @returns {*} Cell value.
  1742. */
  1743. this.getDataAtRowProp = function(row, prop) {
  1744. return datamap.get(row, prop);
  1745. };
  1746. /**
  1747. * @description
  1748. * Returns array of column values from the data source.
  1749. *
  1750. * __Note__: If columns were reordered or sorted, the currently visible order will be used.
  1751. *
  1752. * @memberof Core#
  1753. * @function getDataAtCol
  1754. * @param {Number} column Visual column index.
  1755. * @returns {Array} Array of cell values.
  1756. */
  1757. this.getDataAtCol = function(column) {
  1758. return [].concat(...datamap.getRange(new CellCoords(0, column), new CellCoords(priv.settings.data.length - 1, column), datamap.DESTINATION_RENDERER));
  1759. };
  1760. /**
  1761. * Given the object property name (e.g. `'first.name'` or `'0'`), returns an array of column's values from the table data.
  1762. * You can also provide a column index as the first argument.
  1763. *
  1764. * @memberof Core#
  1765. * @function getDataAtProp
  1766. * @param {String|Number} prop Property name or physical column index.
  1767. * @returns {Array} Array of cell values.
  1768. */
  1769. // TODO: Getting data from `datamap` should work on visual indexes.
  1770. this.getDataAtProp = function(prop) {
  1771. const range = datamap.getRange(
  1772. new CellCoords(0, datamap.propToCol(prop)),
  1773. new CellCoords(priv.settings.data.length - 1, datamap.propToCol(prop)),
  1774. datamap.DESTINATION_RENDERER);
  1775. return [].concat(...range);
  1776. };
  1777. /**
  1778. * Returns the source data object (the same that was passed by `data` configuration option or `loadData` method).
  1779. * Optionally you can provide a cell range by using the `row`, `column`, `row2`, `column2` arguments, to get only a
  1780. * fragment of the table data.
  1781. *
  1782. * __Note__: This method does not participate in data transformation. If the visual data of the table is reordered,
  1783. * sorted or trimmed only physical indexes are correct.
  1784. *
  1785. * @memberof Core#
  1786. * @function getSourceData
  1787. * @param {Number} [row] From physical row index.
  1788. * @param {Number} [column] From physical column index (or visual index, if data type is an array of objects).
  1789. * @param {Number} [row2] To physical row index.
  1790. * @param {Number} [column2] To physical column index (or visual index, if data type is an array of objects).
  1791. * @returns {Array[]|Object[]} The table data.
  1792. */
  1793. this.getSourceData = function(row, column, row2, column2) {
  1794. let data;
  1795. if (row === void 0) {
  1796. data = dataSource.getData();
  1797. } else {
  1798. data = dataSource.getByRange(new CellCoords(row, column), new CellCoords(row2, column2));
  1799. }
  1800. return data;
  1801. };
  1802. /**
  1803. * Returns the source data object as an arrays of arrays format even when source data was provided in another format.
  1804. * Optionally you can provide a cell range by using the `row`, `column`, `row2`, `column2` arguments, to get only a
  1805. * fragment of the table data.
  1806. *
  1807. * __Note__: This method does not participate in data transformation. If the visual data of the table is reordered,
  1808. * sorted or trimmed only physical indexes are correct.
  1809. *
  1810. * @memberof Core#
  1811. * @function getSourceDataArray
  1812. * @param {Number} [row] From physical row index.
  1813. * @param {Number} [column] From physical column index (or visual index, if data type is an array of objects).
  1814. * @param {Number} [row2] To physical row index.
  1815. * @param {Number} [column2] To physical column index (or visual index, if data type is an array of objects).
  1816. * @returns {Array} An array of arrays.
  1817. */
  1818. this.getSourceDataArray = function(row, column, row2, column2) {
  1819. let data;
  1820. if (row === void 0) {
  1821. data = dataSource.getData(true);
  1822. } else {
  1823. data = dataSource.getByRange(new CellCoords(row, column), new CellCoords(row2, column2), true);
  1824. }
  1825. return data;
  1826. };
  1827. /**
  1828. * Returns an array of column values from the data source.
  1829. *
  1830. * @memberof Core#
  1831. * @function getSourceDataAtCol
  1832. * @param {Number} column Visual column index.
  1833. * @returns {Array} Array of the column's cell values.
  1834. */
  1835. // TODO: Getting data from `sourceData` should work always on physical indexes.
  1836. this.getSourceDataAtCol = function(column) {
  1837. return dataSource.getAtColumn(column);
  1838. };
  1839. /**
  1840. * Returns a single row of the data (array or object, depending on what data format you use).
  1841. *
  1842. * __Note__: This method does not participate in data transformation. If the visual data of the table is reordered,
  1843. * sorted or trimmed only physical indexes are correct.
  1844. *
  1845. * @memberof Core#
  1846. * @function getSourceDataAtRow
  1847. * @param {Number} row Physical row index.
  1848. * @returns {Array|Object} Single row of data.
  1849. */
  1850. this.getSourceDataAtRow = function(row) {
  1851. return dataSource.getAtRow(row);
  1852. };
  1853. /**
  1854. * Returns a single value from the data source.
  1855. *
  1856. * @memberof Core#
  1857. * @function getSourceDataAtCell
  1858. * @param {Number} row Physical row index.
  1859. * @param {Number} column Visual column index.
  1860. * @returns {*} Cell data.
  1861. */
  1862. // TODO: Getting data from `sourceData` should work always on physical indexes.
  1863. this.getSourceDataAtCell = function(row, column) {
  1864. return dataSource.getAtCell(row, column);
  1865. };
  1866. /**
  1867. * @description
  1868. * Returns a single row of the data.
  1869. *
  1870. * __Note__: If rows were reordered, sorted or trimmed, the currently visible order will be used.
  1871. *
  1872. * @memberof Core#
  1873. * @function getDataAtRow
  1874. * @param {Number} row Visual row index.
  1875. * @returns {Array} Array of row's cell data.
  1876. */
  1877. this.getDataAtRow = function(row) {
  1878. const data = datamap.getRange(new CellCoords(row, 0), new CellCoords(row, this.countCols() - 1), datamap.DESTINATION_RENDERER);
  1879. return data[0] || [];
  1880. };
  1881. /**
  1882. * @description
  1883. * Returns a data type defined in the Handsontable settings under the `type` key ([Options#type](http://docs.handsontable.com/Options.html#type)).
  1884. * If there are cells with different types in the selected range, it returns `'mixed'`.
  1885. *
  1886. * __Note__: If data is reordered, sorted or trimmed, the currently visible order will be used.
  1887. *
  1888. * @memberof Core#
  1889. * @function getDataType
  1890. * @param {Number} rowFrom From visual row index.
  1891. * @param {Number} columnFrom From visual column index.
  1892. * @param {Number} rowTo To visual row index.
  1893. * @param {Number} columnTo To visual column index.
  1894. * @returns {String} Cell type (e.q: `'mixed'`, `'text'`, `'numeric'`, `'autocomplete'`).
  1895. */
  1896. this.getDataType = function(rowFrom, columnFrom, rowTo, columnTo) {
  1897. const coords = rowFrom === void 0 ? [0, 0, this.countRows(), this.countCols()] : [rowFrom, columnFrom, rowTo, columnTo];
  1898. const [rowStart, columnStart] = coords;
  1899. let [, , rowEnd, columnEnd] = coords;
  1900. let previousType = null;
  1901. let currentType = null;
  1902. if (rowEnd === void 0) {
  1903. rowEnd = rowStart;
  1904. }
  1905. if (columnEnd === void 0) {
  1906. columnEnd = columnStart;
  1907. }
  1908. let type = 'mixed';
  1909. rangeEach(Math.min(rowStart, rowEnd), Math.max(rowStart, rowEnd), (row) => {
  1910. let isTypeEqual = true;
  1911. rangeEach(Math.min(columnStart, columnEnd), Math.max(columnStart, columnEnd), (column) => {
  1912. const cellType = this.getCellMeta(row, column);
  1913. currentType = cellType.type;
  1914. if (previousType) {
  1915. isTypeEqual = previousType === currentType;
  1916. } else {
  1917. previousType = currentType;
  1918. }
  1919. return isTypeEqual;
  1920. });
  1921. type = isTypeEqual ? currentType : 'mixed';
  1922. return isTypeEqual;
  1923. });
  1924. return type;
  1925. };
  1926. /**
  1927. * Remove a property defined by the `key` argument from the cell meta object for the provided `row` and `column` coordinates.
  1928. *
  1929. * @memberof Core#
  1930. * @function removeCellMeta
  1931. * @param {Number} row Visual row index.
  1932. * @param {Number} column Visual column index.
  1933. * @param {String} key Property name.
  1934. * @fires Hooks#beforeRemoveCellMeta
  1935. * @fires Hooks#afterRemoveCellMeta
  1936. */
  1937. this.removeCellMeta = function(row, column, key) {
  1938. const [physicalRow, physicalColumn] = recordTranslator.toPhysical(row, column);
  1939. let cachedValue = priv.cellSettings[physicalRow][physicalColumn][key];
  1940. const hookResult = instance.runHooks('beforeRemoveCellMeta', row, column, key, cachedValue);
  1941. if (hookResult !== false) {
  1942. delete priv.cellSettings[physicalRow][physicalColumn][key];
  1943. instance.runHooks('afterRemoveCellMeta', row, column, key, cachedValue);
  1944. }
  1945. cachedValue = null;
  1946. };
  1947. /**
  1948. * Remove one or more rows from the cell meta object.
  1949. *
  1950. * @since 0.30.0
  1951. * @param {Number} index An integer that specifies at what position to add/remove items, Use negative values to specify the position from the end of the array.
  1952. * @param {Number} deleteAmount The number of items to be removed. If set to 0, no items will be removed.
  1953. * @param {Array} items The new items to be added to the array.
  1954. */
  1955. this.spliceCellsMeta = function(index, deleteAmount, ...items) {
  1956. priv.cellSettings.splice(index, deleteAmount, ...items);
  1957. };
  1958. /**
  1959. * Set cell meta data object defined by `prop` to the corresponding params `row` and `column`.
  1960. *
  1961. * @memberof Core#
  1962. * @function setCellMetaObject
  1963. * @param {Number} row Visual row index.
  1964. * @param {Number} column Visual column index.
  1965. * @param {Object} prop Meta object.
  1966. */
  1967. this.setCellMetaObject = function(row, column, prop) {
  1968. if (typeof prop === 'object') {
  1969. objectEach(prop, (value, key) => {
  1970. this.setCellMeta(row, column, key, value);
  1971. });
  1972. }
  1973. };
  1974. /**
  1975. * Sets a property defined by the `key` property to the meta object of a cell corresponding to params `row` and `column`.
  1976. *
  1977. * @memberof Core#
  1978. * @function setCellMeta
  1979. * @param {Number} row Visual row index.
  1980. * @param {Number} column Visual column index.
  1981. * @param {String} key Property name.
  1982. * @param {String} value Property value.
  1983. * @fires Hooks#afterSetCellMeta
  1984. */
  1985. this.setCellMeta = function(row, column, key, value) {
  1986. const [physicalRow, physicalColumn] = recordTranslator.toPhysical(row, column);
  1987. if (!priv.columnSettings[physicalColumn]) {
  1988. priv.columnSettings[physicalColumn] = columnFactory(GridSettings, priv.columnsSettingConflicts);
  1989. }
  1990. if (!priv.cellSettings[physicalRow]) {
  1991. priv.cellSettings[physicalRow] = [];
  1992. }
  1993. if (!priv.cellSettings[physicalRow][physicalColumn]) {
  1994. priv.cellSettings[physicalRow][physicalColumn] = new priv.columnSettings[physicalColumn]();
  1995. }
  1996. priv.cellSettings[physicalRow][physicalColumn][key] = value;
  1997. instance.runHooks('afterSetCellMeta', row, column, key, value);
  1998. };
  1999. /**
  2000. * Get all the cells meta settings at least once generated in the table (in order of cell initialization).
  2001. *
  2002. * @memberof Core#
  2003. * @function getCellsMeta
  2004. * @returns {Array} Returns an array of ColumnSettings object instances.
  2005. */
  2006. this.getCellsMeta = function() {
  2007. return arrayFlatten(priv.cellSettings);
  2008. };
  2009. /**
  2010. * Returns the cell properties object for the given `row` and `column` coordinates.
  2011. *
  2012. * @memberof Core#
  2013. * @function getCellMeta
  2014. * @param {Number} row Visual row index.
  2015. * @param {Number} column Visual column index.
  2016. * @returns {Object} The cell properties object.
  2017. * @fires Hooks#beforeGetCellMeta
  2018. * @fires Hooks#afterGetCellMeta
  2019. */
  2020. this.getCellMeta = function(row, column) {
  2021. const prop = datamap.colToProp(column);
  2022. const [potentialPhysicalRow, physicalColumn] = recordTranslator.toPhysical(row, column);
  2023. let physicalRow = potentialPhysicalRow;
  2024. // Workaround for #11. Connected also with #3849. It should be fixed within #4497.
  2025. if (physicalRow === null) {
  2026. physicalRow = row;
  2027. }
  2028. if (!priv.columnSettings[physicalColumn]) {
  2029. priv.columnSettings[physicalColumn] = columnFactory(GridSettings, priv.columnsSettingConflicts);
  2030. }
  2031. if (!priv.cellSettings[physicalRow]) {
  2032. priv.cellSettings[physicalRow] = [];
  2033. }
  2034. if (!priv.cellSettings[physicalRow][physicalColumn]) {
  2035. priv.cellSettings[physicalRow][physicalColumn] = new priv.columnSettings[physicalColumn]();
  2036. }
  2037. const cellProperties = priv.cellSettings[physicalRow][physicalColumn]; // retrieve cellProperties from cache
  2038. cellProperties.row = physicalRow;
  2039. cellProperties.col = physicalColumn;
  2040. cellProperties.visualRow = row;
  2041. cellProperties.visualCol = column;
  2042. cellProperties.prop = prop;
  2043. cellProperties.instance = instance;
  2044. instance.runHooks('beforeGetCellMeta', row, column, cellProperties);
  2045. extend(cellProperties, expandType(cellProperties)); // for `type` added in beforeGetCellMeta
  2046. if (cellProperties.cells) {
  2047. const settings = cellProperties.cells.call(cellProperties, physicalRow, physicalColumn, prop);
  2048. if (settings) {
  2049. extend(cellProperties, settings);
  2050. extend(cellProperties, expandType(settings)); // for `type` added in cells
  2051. }
  2052. }
  2053. instance.runHooks('afterGetCellMeta', row, column, cellProperties);
  2054. return cellProperties;
  2055. };
  2056. /**
  2057. * Returns an array of cell meta objects for specyfied physical row index.
  2058. *
  2059. * @memberof Core#
  2060. * @function getCellMetaAtRow
  2061. * @param {Number} row Physical row index.
  2062. * @returns {Array}
  2063. */
  2064. this.getCellMetaAtRow = function(row) {
  2065. return priv.cellSettings[row];
  2066. };
  2067. /**
  2068. * Checks if the data format and config allows user to modify the column structure.
  2069. *
  2070. * @memberof Core#
  2071. * @function isColumnModificationAllowed
  2072. * @returns {Boolean}
  2073. */
  2074. this.isColumnModificationAllowed = function() {
  2075. return !(instance.dataType === 'object' || instance.getSettings().columns);
  2076. };
  2077. const rendererLookup = cellMethodLookupFactory('renderer');
  2078. /**
  2079. * Returns the cell renderer function by given `row` and `column` arguments.
  2080. *
  2081. * @memberof Core#
  2082. * @function getCellRenderer
  2083. * @param {Number|Object} row Visual row index or cell meta object (see {@link Core#getCellMeta}).
  2084. * @param {Number} column Visual column index.
  2085. * @returns {Function} The renderer function.
  2086. * @example
  2087. * ```js
  2088. * // Get cell renderer using `row` and `column` coordinates.
  2089. * hot.getCellRenderer(1, 1);
  2090. * // Get cell renderer using cell meta object.
  2091. * hot.getCellRenderer(hot.getCellMeta(1, 1));
  2092. * ```
  2093. */
  2094. this.getCellRenderer = function(row, column) {
  2095. return getRenderer(rendererLookup.call(this, row, column));
  2096. };
  2097. /**
  2098. * Returns the cell editor class by the provided `row` and `column` arguments.
  2099. *
  2100. * @memberof Core#
  2101. * @function getCellEditor
  2102. * @param {Number} row Visual row index or cell meta object (see {@link Core#getCellMeta}).
  2103. * @param {Number} column Visual column index.
  2104. * @returns {Function} The editor class.
  2105. * @example
  2106. * ```js
  2107. * // Get cell editor class using `row` and `column` coordinates.
  2108. * hot.getCellEditor(1, 1);
  2109. * // Get cell editor class using cell meta object.
  2110. * hot.getCellEditor(hot.getCellMeta(1, 1));
  2111. * ```
  2112. */
  2113. this.getCellEditor = cellMethodLookupFactory('editor');
  2114. const validatorLookup = cellMethodLookupFactory('validator');
  2115. /**
  2116. * Returns the cell validator by `row` and `column`.
  2117. *
  2118. * @memberof Core#
  2119. * @function getCellValidator
  2120. * @param {Number|Object} row Visual row index or cell meta object (see {@link Core#getCellMeta}).
  2121. * @param {Number} column Visual column index.
  2122. * @returns {Function|RegExp|undefined} The validator function.
  2123. * @example
  2124. * ```js
  2125. * // Get cell valiator using `row` and `column` coordinates.
  2126. * hot.getCellValidator(1, 1);
  2127. * // Get cell valiator using cell meta object.
  2128. * hot.getCellValidator(hot.getCellMeta(1, 1));
  2129. * ```
  2130. */
  2131. this.getCellValidator = function(row, column) {
  2132. let validator = validatorLookup.call(this, row, column);
  2133. if (typeof validator === 'string') {
  2134. validator = getValidator(validator);
  2135. }
  2136. return validator;
  2137. };
  2138. /**
  2139. * Validates all cells using their validator functions and calls callback when finished.
  2140. *
  2141. * If one of the cells is invalid, the callback will be fired with `'valid'` arguments as `false` - otherwise it
  2142. * would equal `true`.
  2143. *
  2144. * @memberof Core#
  2145. * @function validateCells
  2146. * @param {Function} [callback] The callback function.
  2147. * @example
  2148. * ```js
  2149. * hot.validateCells((valid) => {
  2150. * if (valid) {
  2151. * // ... code for validated cells
  2152. * }
  2153. * })
  2154. * ```
  2155. */
  2156. this.validateCells = function(callback) {
  2157. this._validateCells(callback);
  2158. };
  2159. /**
  2160. * Validates rows using their validator functions and calls callback when finished.
  2161. *
  2162. * If one of the cells is invalid, the callback will be fired with `'valid'` arguments as `false` - otherwise it
  2163. * would equal `true`.
  2164. *
  2165. * @memberof Core#
  2166. * @function validateRows
  2167. * @param {Array} [rows] Array of validation target visual row indexes.
  2168. * @param {Function} [callback] The callback function.
  2169. * @example
  2170. * ```js
  2171. * hot.validateRows([3, 4, 5], (valid) => {
  2172. * if (valid) {
  2173. * // ... code for validated rows
  2174. * }
  2175. * })
  2176. * ```
  2177. */
  2178. this.validateRows = function(rows, callback) {
  2179. if (!Array.isArray(rows)) {
  2180. throw new Error('validateRows parameter `rows` must be an array');
  2181. }
  2182. this._validateCells(callback, rows);
  2183. };
  2184. /**
  2185. * Validates columns using their validator functions and calls callback when finished.
  2186. *
  2187. * If one of the cells is invalid, the callback will be fired with `'valid'` arguments as `false` - otherwise it
  2188. * would equal `true`.
  2189. *
  2190. * @memberof Core#
  2191. * @function validateColumns
  2192. * @param {Array} [columns] Array of validation target visual columns indexes.
  2193. * @param {Function} [callback] The callback function.
  2194. * @example
  2195. * ```js
  2196. * hot.validateColumns([3, 4, 5], (valid) => {
  2197. * if (valid) {
  2198. * // ... code for validated columns
  2199. * }
  2200. * })
  2201. * ```
  2202. */
  2203. this.validateColumns = function(columns, callback) {
  2204. if (!Array.isArray(columns)) {
  2205. throw new Error('validateColumns parameter `columns` must be an array');
  2206. }
  2207. this._validateCells(callback, undefined, columns);
  2208. };
  2209. /**
  2210. * Validates all cells using their validator functions and calls callback when finished.
  2211. *
  2212. * If one of the cells is invalid, the callback will be fired with `'valid'` arguments as `false` - otherwise it would equal `true`.
  2213. *
  2214. * Private use intended.
  2215. *
  2216. * @private
  2217. * @memberof Core#
  2218. * @function _validateCells
  2219. * @param {Function} [callback] The callback function.
  2220. * @param {Array} [rows] An array of validation target visual row indexes.
  2221. * @param {Array} [columns] An array of validation target visual column indexes.
  2222. */
  2223. this._validateCells = function(callback, rows, columns) {
  2224. const waitingForValidator = new ValidatorsQueue();
  2225. if (callback) {
  2226. waitingForValidator.onQueueEmpty = callback;
  2227. }
  2228. let i = instance.countRows() - 1;
  2229. while (i >= 0) {
  2230. if (rows !== undefined && rows.indexOf(i) === -1) {
  2231. i -= 1;
  2232. continue;
  2233. }
  2234. let j = instance.countCols() - 1;
  2235. while (j >= 0) {
  2236. if (columns !== undefined && columns.indexOf(j) === -1) {
  2237. j -= 1;
  2238. continue;
  2239. }
  2240. waitingForValidator.addValidatorToQueue();
  2241. instance.validateCell(instance.getDataAtCell(i, j), instance.getCellMeta(i, j), (result) => {
  2242. if (typeof result !== 'boolean') {
  2243. throw new Error('Validation error: result is not boolean');
  2244. }
  2245. if (result === false) {
  2246. waitingForValidator.valid = false;
  2247. }
  2248. waitingForValidator.removeValidatorFormQueue();
  2249. }, 'validateCells');
  2250. j -= 1;
  2251. }
  2252. i -= 1;
  2253. }
  2254. waitingForValidator.checkIfQueueIsEmpty();
  2255. };
  2256. /**
  2257. * Returns an array of row headers' values (if they are enabled). If param `row` was given, it returns the header of the given row as a string.
  2258. *
  2259. * @memberof Core#
  2260. * @function getRowHeader
  2261. * @param {Number} [row] Visual row index.
  2262. * @fires Hooks#modifyRowHeader
  2263. * @returns {Array|String|Number} Array of header values / single header value.
  2264. */
  2265. this.getRowHeader = function(row) {
  2266. let rowHeader = priv.settings.rowHeaders;
  2267. let physicalRow = row;
  2268. if (physicalRow !== void 0) {
  2269. physicalRow = instance.runHooks('modifyRowHeader', physicalRow);
  2270. }
  2271. if (physicalRow === void 0) {
  2272. rowHeader = [];
  2273. rangeEach(instance.countRows() - 1, (i) => {
  2274. rowHeader.push(instance.getRowHeader(i));
  2275. });
  2276. } else if (Array.isArray(rowHeader) && rowHeader[physicalRow] !== void 0) {
  2277. rowHeader = rowHeader[physicalRow];
  2278. } else if (isFunction(rowHeader)) {
  2279. rowHeader = rowHeader(physicalRow);
  2280. } else if (rowHeader && typeof rowHeader !== 'string' && typeof rowHeader !== 'number') {
  2281. rowHeader = physicalRow + 1;
  2282. }
  2283. return rowHeader;
  2284. };
  2285. /**
  2286. * Returns information about if this table is configured to display row headers.
  2287. *
  2288. * @memberof Core#
  2289. * @function hasRowHeaders
  2290. * @returns {Boolean} `true` if the instance has the row headers enabled, `false` otherwise.
  2291. */
  2292. this.hasRowHeaders = function() {
  2293. return !!priv.settings.rowHeaders;
  2294. };
  2295. /**
  2296. * Returns information about if this table is configured to display column headers.
  2297. *
  2298. * @memberof Core#
  2299. * @function hasColHeaders
  2300. * @returns {Boolean} `true` if the instance has the column headers enabled, `false` otherwise.
  2301. */
  2302. this.hasColHeaders = function() {
  2303. if (priv.settings.colHeaders !== void 0 && priv.settings.colHeaders !== null) { // Polymer has empty value = null
  2304. return !!priv.settings.colHeaders;
  2305. }
  2306. for (let i = 0, ilen = instance.countCols(); i < ilen; i++) {
  2307. if (instance.getColHeader(i)) {
  2308. return true;
  2309. }
  2310. }
  2311. return false;
  2312. };
  2313. /**
  2314. * Returns an array of column headers (in string format, if they are enabled). If param `column` is given, it
  2315. * returns the header at the given column.
  2316. *
  2317. * @memberof Core#
  2318. * @function getColHeader
  2319. * @param {Number} [column] Visual column index.
  2320. * @fires Hooks#modifyColHeader
  2321. * @returns {Array|String|Number} The column header(s).
  2322. */
  2323. this.getColHeader = function(column) {
  2324. const columnsAsFunc = priv.settings.columns && isFunction(priv.settings.columns);
  2325. const columnIndex = instance.runHooks('modifyColHeader', column);
  2326. let result = priv.settings.colHeaders;
  2327. if (columnIndex === void 0) {
  2328. const out = [];
  2329. const ilen = columnsAsFunc ? instance.countSourceCols() : instance.countCols();
  2330. for (let i = 0; i < ilen; i++) {
  2331. out.push(instance.getColHeader(i));
  2332. }
  2333. result = out;
  2334. } else {
  2335. const translateVisualIndexToColumns = function(visualColumnIndex) {
  2336. const arr = [];
  2337. const columnsLen = instance.countSourceCols();
  2338. let index = 0;
  2339. for (; index < columnsLen; index++) {
  2340. if (isFunction(instance.getSettings().columns) && instance.getSettings().columns(index)) {
  2341. arr.push(index);
  2342. }
  2343. }
  2344. return arr[visualColumnIndex];
  2345. };
  2346. const baseCol = columnIndex;
  2347. const physicalColumn = instance.runHooks('modifyCol', baseCol);
  2348. const prop = translateVisualIndexToColumns(physicalColumn);
  2349. if (priv.settings.colHeaders === false) {
  2350. result = null;
  2351. } else if (priv.settings.columns && isFunction(priv.settings.columns) && priv.settings.columns(prop) && priv.settings.columns(prop).title) {
  2352. result = priv.settings.columns(prop).title;
  2353. } else if (priv.settings.columns && priv.settings.columns[physicalColumn] && priv.settings.columns[physicalColumn].title) {
  2354. result = priv.settings.columns[physicalColumn].title;
  2355. } else if (Array.isArray(priv.settings.colHeaders) && priv.settings.colHeaders[physicalColumn] !== void 0) {
  2356. result = priv.settings.colHeaders[physicalColumn];
  2357. } else if (isFunction(priv.settings.colHeaders)) {
  2358. result = priv.settings.colHeaders(physicalColumn);
  2359. } else if (priv.settings.colHeaders && typeof priv.settings.colHeaders !== 'string' && typeof priv.settings.colHeaders !== 'number') {
  2360. result = spreadsheetColumnLabel(baseCol); // see #1458
  2361. }
  2362. }
  2363. return result;
  2364. };
  2365. /**
  2366. * Return column width from settings (no guessing). Private use intended.
  2367. *
  2368. * @private
  2369. * @memberof Core#
  2370. * @function _getColWidthFromSettings
  2371. * @param {Number} col Visual col index.
  2372. * @returns {Number}
  2373. */
  2374. this._getColWidthFromSettings = function(col) {
  2375. const cellProperties = instance.getCellMeta(0, col);
  2376. let width = cellProperties.width;
  2377. if (width === void 0 || width === priv.settings.width) {
  2378. width = cellProperties.colWidths;
  2379. }
  2380. if (width !== void 0 && width !== null) {
  2381. switch (typeof width) {
  2382. case 'object': // array
  2383. width = width[col];
  2384. break;
  2385. case 'function':
  2386. width = width(col);
  2387. break;
  2388. default:
  2389. break;
  2390. }
  2391. if (typeof width === 'string') {
  2392. width = parseInt(width, 10);
  2393. }
  2394. }
  2395. return width;
  2396. };
  2397. /**
  2398. * Returns the width of the requested column.
  2399. *
  2400. * @memberof Core#
  2401. * @function getColWidth
  2402. * @param {Number} column Visual column index.
  2403. * @returns {Number} Column width.
  2404. * @fires Hooks#modifyColWidth
  2405. */
  2406. this.getColWidth = function(column) {
  2407. let width = instance._getColWidthFromSettings(column);
  2408. width = instance.runHooks('modifyColWidth', width, column);
  2409. if (width === void 0) {
  2410. width = ViewportColumnsCalculator.DEFAULT_WIDTH;
  2411. }
  2412. return width;
  2413. };
  2414. /**
  2415. * Return row height from settings (no guessing). Private use intended.
  2416. *
  2417. * @private
  2418. * @memberof Core#
  2419. * @function _getRowHeightFromSettings
  2420. * @param {Number} row Visual row index.
  2421. * @returns {Number}
  2422. */
  2423. this._getRowHeightFromSettings = function(row) {
  2424. // let cellProperties = instance.getCellMeta(row, 0);
  2425. // let height = cellProperties.height;
  2426. //
  2427. // if (height === void 0 || height === priv.settings.height) {
  2428. // height = cellProperties.rowHeights;
  2429. // }
  2430. let height = priv.settings.rowHeights;
  2431. if (height !== void 0 && height !== null) {
  2432. switch (typeof height) {
  2433. case 'object': // array
  2434. height = height[row];
  2435. break;
  2436. case 'function':
  2437. height = height(row);
  2438. break;
  2439. default:
  2440. break;
  2441. }
  2442. if (typeof height === 'string') {
  2443. height = parseInt(height, 10);
  2444. }
  2445. }
  2446. return height;
  2447. };
  2448. /**
  2449. * Returns the row height.
  2450. *
  2451. * @memberof Core#
  2452. * @function getRowHeight
  2453. * @param {Number} row Visual row index.
  2454. * @returns {Number} The given row's height.
  2455. * @fires Hooks#modifyRowHeight
  2456. */
  2457. this.getRowHeight = function(row) {
  2458. let height = instance._getRowHeightFromSettings(row);
  2459. height = instance.runHooks('modifyRowHeight', height, row);
  2460. return height;
  2461. };
  2462. /**
  2463. * Returns the total number of rows in the data source.
  2464. *
  2465. * @memberof Core#
  2466. * @function countSourceRows
  2467. * @returns {Number} Total number of rows.
  2468. */
  2469. this.countSourceRows = function() {
  2470. const sourceLength = instance.runHooks('modifySourceLength');
  2471. return sourceLength || (instance.getSourceData() ? instance.getSourceData().length : 0);
  2472. };
  2473. /**
  2474. * Returns the total number of columns in the data source.
  2475. *
  2476. * @memberof Core#
  2477. * @function countSourceCols
  2478. * @returns {Number} Total number of columns.
  2479. */
  2480. this.countSourceCols = function() {
  2481. let len = 0;
  2482. const obj = instance.getSourceData() && instance.getSourceData()[0] ? instance.getSourceData()[0] : [];
  2483. if (isObject(obj)) {
  2484. len = deepObjectSize(obj);
  2485. } else {
  2486. len = obj.length || 0;
  2487. }
  2488. return len;
  2489. };
  2490. /**
  2491. * Returns the total number of visual rows in the table.
  2492. *
  2493. * @memberof Core#
  2494. * @function countRows
  2495. * @returns {Number} Total number of rows.
  2496. */
  2497. this.countRows = function() {
  2498. return datamap.getLength();
  2499. };
  2500. /**
  2501. * Returns the total number of visible columns in the table.
  2502. *
  2503. * @memberof Core#
  2504. * @function countCols
  2505. * @returns {Number} Total number of columns.
  2506. */
  2507. this.countCols = function() {
  2508. const maxCols = this.getSettings().maxCols;
  2509. let dataHasLength = false;
  2510. let dataLen = 0;
  2511. if (instance.dataType === 'array') {
  2512. dataHasLength = priv.settings.data && priv.settings.data[0] && priv.settings.data[0].length;
  2513. }
  2514. if (dataHasLength) {
  2515. dataLen = priv.settings.data[0].length;
  2516. }
  2517. if (priv.settings.columns) {
  2518. const columnsIsFunction = isFunction(priv.settings.columns);
  2519. if (columnsIsFunction) {
  2520. if (instance.dataType === 'array') {
  2521. let columnLen = 0;
  2522. for (let i = 0; i < dataLen; i++) {
  2523. if (priv.settings.columns(i)) {
  2524. columnLen += 1;
  2525. }
  2526. }
  2527. dataLen = columnLen;
  2528. } else if (instance.dataType === 'object' || instance.dataType === 'function') {
  2529. dataLen = datamap.colToPropCache.length;
  2530. }
  2531. } else {
  2532. dataLen = priv.settings.columns.length;
  2533. }
  2534. } else if (instance.dataType === 'object' || instance.dataType === 'function') {
  2535. dataLen = datamap.colToPropCache.length;
  2536. }
  2537. return Math.min(maxCols, dataLen);
  2538. };
  2539. /**
  2540. * Returns an visual index of the first rendered row.
  2541. *
  2542. * @memberof Core#
  2543. * @function rowOffset
  2544. * @returns {Number} Visual index of first rendered row.
  2545. */
  2546. this.rowOffset = function() {
  2547. return instance.view.wt.wtTable.getFirstRenderedRow();
  2548. };
  2549. /**
  2550. * Returns the visual index of the first rendered column.
  2551. *
  2552. * @memberof Core#
  2553. * @function colOffset
  2554. * @returns {Number} Visual index of the first visible column.
  2555. */
  2556. this.colOffset = function() {
  2557. return instance.view.wt.wtTable.getFirstRenderedColumn();
  2558. };
  2559. /**
  2560. * Returns the number of rendered rows (including rows partially or fully rendered outside viewport).
  2561. *
  2562. * @memberof Core#
  2563. * @function countRenderedRows
  2564. * @returns {Number} Returns -1 if table is not visible.
  2565. */
  2566. this.countRenderedRows = function() {
  2567. return instance.view.wt.drawn ? instance.view.wt.wtTable.getRenderedRowsCount() : -1;
  2568. };
  2569. /**
  2570. * Returns the number of visible rows (rendered rows that fully fit inside viewport).
  2571. *
  2572. * @memberof Core#
  2573. * @function countVisibleRows
  2574. * @returns {Number} Number of visible rows or -1.
  2575. */
  2576. this.countVisibleRows = function() {
  2577. return instance.view.wt.drawn ? instance.view.wt.wtTable.getVisibleRowsCount() : -1;
  2578. };
  2579. /**
  2580. * Returns the number of rendered columns (including columns partially or fully rendered outside viewport).
  2581. *
  2582. * @memberof Core#
  2583. * @function countRenderedCols
  2584. * @returns {Number} Returns -1 if table is not visible.
  2585. */
  2586. this.countRenderedCols = function() {
  2587. return instance.view.wt.drawn ? instance.view.wt.wtTable.getRenderedColumnsCount() : -1;
  2588. };
  2589. /**
  2590. * Returns the number of visible columns. Returns -1 if table is not visible
  2591. *
  2592. * @memberof Core#
  2593. * @function countVisibleCols
  2594. * @return {Number} Number of visible columns or -1.
  2595. */
  2596. this.countVisibleCols = function() {
  2597. return instance.view.wt.drawn ? instance.view.wt.wtTable.getVisibleColumnsCount() : -1;
  2598. };
  2599. /**
  2600. * Returns the number of empty rows. If the optional ending parameter is `true`, returns the
  2601. * number of empty rows at the bottom of the table.
  2602. *
  2603. * @memberof Core#
  2604. * @function countEmptyRows
  2605. * @param {Boolean} [ending=false] If `true`, will only count empty rows at the end of the data source.
  2606. * @returns {Number} Count empty rows.
  2607. */
  2608. this.countEmptyRows = function(ending = false) {
  2609. let emptyRows = 0;
  2610. rangeEachReverse(instance.countRows() - 1, (visualIndex) => {
  2611. if (instance.isEmptyRow(visualIndex)) {
  2612. emptyRows += 1;
  2613. } else if (ending === true) {
  2614. return false;
  2615. }
  2616. });
  2617. return emptyRows;
  2618. };
  2619. /**
  2620. * Returns the number of empty columns. If the optional ending parameter is `true`, returns the number of empty
  2621. * columns at right hand edge of the table.
  2622. *
  2623. * @memberof Core#
  2624. * @function countEmptyCols
  2625. * @param {Boolean} [ending=false] If `true`, will only count empty columns at the end of the data source row.
  2626. * @returns {Number} Count empty cols.
  2627. */
  2628. this.countEmptyCols = function(ending = false) {
  2629. if (instance.countRows() < 1) {
  2630. return 0;
  2631. }
  2632. let emptyColumns = 0;
  2633. rangeEachReverse(instance.countCols() - 1, (visualIndex) => {
  2634. if (instance.isEmptyCol(visualIndex)) {
  2635. emptyColumns += 1;
  2636. } else if (ending === true) {
  2637. return false;
  2638. }
  2639. });
  2640. return emptyColumns;
  2641. };
  2642. /**
  2643. * Check if all cells in the row declared by the `row` argument are empty.
  2644. *
  2645. * @memberof Core#
  2646. * @function isEmptyRow
  2647. * @param {Number} row Visual row index.
  2648. * @returns {Boolean} `true` if the row at the given `row` is empty, `false` otherwise.
  2649. */
  2650. this.isEmptyRow = function(row) {
  2651. return priv.settings.isEmptyRow.call(instance, row);
  2652. };
  2653. /**
  2654. * Check if all cells in the the column declared by the `column` argument are empty.
  2655. *
  2656. * @memberof Core#
  2657. * @function isEmptyCol
  2658. * @param {Number} column Column index.
  2659. * @returns {Boolean} `true` if the column at the given `col` is empty, `false` otherwise.
  2660. */
  2661. this.isEmptyCol = function(column) {
  2662. return priv.settings.isEmptyCol.call(instance, column);
  2663. };
  2664. /**
  2665. * Select cell specified by `row` and `column` values or a range of cells finishing at `endRow`, `endCol`. If the table
  2666. * was configured to support data column properties that properties can be used to making a selection.
  2667. *
  2668. * By default, viewport will be scrolled to the selection. After the `selectCell` method had finished, the instance
  2669. * will be listening to keyboard input on the document.
  2670. *
  2671. * @example
  2672. * ```js
  2673. * // select a single cell
  2674. * hot.selectCell(2, 4);
  2675. * // select a single cell using column property
  2676. * hot.selectCell(2, 'address');
  2677. * // select a range of cells
  2678. * hot.selectCell(2, 4, 3, 5);
  2679. * // select a range of cells using column properties
  2680. * hot.selectCell(2, 'address', 3, 'phone_number');
  2681. * // select a range of cells without scrolling to them
  2682. * hot.selectCell(2, 'address', 3, 'phone_number', false);
  2683. * ```
  2684. *
  2685. * @memberof Core#
  2686. * @function selectCell
  2687. * @param {Number} row Visual row index.
  2688. * @param {Number|String} column Visual column index or column property.
  2689. * @param {Number} [endRow] Visual end row index (if selecting a range).
  2690. * @param {Number|String} [endColumn] Visual end column index or column property (if selecting a range).
  2691. * @param {Boolean} [scrollToCell=true] If `true`, the viewport will be scrolled to the selection.
  2692. * @param {Boolean} [changeListener=true] If `false`, Handsontable will not change keyboard events listener to himself.
  2693. * @returns {Boolean} `true` if selection was successful, `false` otherwise.
  2694. */
  2695. this.selectCell = function(row, column, endRow, endColumn, scrollToCell = true, changeListener = true) {
  2696. if (isUndefined(row) || isUndefined(column)) {
  2697. return false;
  2698. }
  2699. return this.selectCells([[row, column, endRow, endColumn]], scrollToCell, changeListener);
  2700. };
  2701. /**
  2702. * Make multiple, non-contiguous selection specified by `row` and `column` values or a range of cells
  2703. * finishing at `endRow`, `endColumn`. The method supports two input formats which are the same as that
  2704. * produces by `getSelected` and `getSelectedRange` methods.
  2705. *
  2706. * By default, viewport will be scrolled to selection. After the `selectCells` method had finished, the instance
  2707. * will be listening to keyboard input on the document.
  2708. *
  2709. * @example
  2710. * ```js
  2711. * // Using an array of arrays.
  2712. * hot.selectCells([[1, 1, 2, 2], [3, 3], [6, 2, 0, 2]]);
  2713. * // Using an array of arrays with defined columns as props.
  2714. * hot.selectCells([[1, 'id', 2, 'first_name'], [3, 'full_name'], [6, 'last_name', 0, 'first_name']]);
  2715. * // Using an array of CellRange objects (produced by `.getSelectedRange()` method).
  2716. * const selected = hot.getSelectedRange();
  2717. *
  2718. * selected[0].from.row = 0;
  2719. * selected[0].from.col = 0;
  2720. *
  2721. * hot.selectCells(selected);
  2722. * ```
  2723. *
  2724. * @memberof Core#
  2725. * @since 0.38.0
  2726. * @function selectCells
  2727. * @param {Array[]|CellRange[]} coords Visual coords passed as an array of array (`[[rowStart, columnStart, rowEnd, columnEnd], ...]`)
  2728. * the same format as `getSelected` method returns or as an CellRange objects
  2729. * which is the same format what `getSelectedRange` method returns.
  2730. * @param {Boolean} [scrollToCell=true] If `true`, the viewport will be scrolled to the selection.
  2731. * @param {Boolean} [changeListener=true] If `false`, Handsontable will not change keyboard events listener to himself.
  2732. * @returns {Boolean} `true` if selection was successful, `false` otherwise.
  2733. */
  2734. this.selectCells = function(coords = [[]], scrollToCell = true, changeListener = true) {
  2735. if (scrollToCell === false) {
  2736. preventScrollingToCell = true;
  2737. }
  2738. const wasSelected = selection.selectCells(coords);
  2739. if (wasSelected && changeListener) {
  2740. instance.listen();
  2741. }
  2742. preventScrollingToCell = false;
  2743. return wasSelected;
  2744. };
  2745. /**
  2746. * Select the cell specified by the `row` and `prop` arguments, or a range finishing at `endRow`, `endProp`.
  2747. * By default, viewport will be scrolled to selection.
  2748. *
  2749. * @deprecated
  2750. * @memberof Core#
  2751. * @function selectCellByProp
  2752. * @param {Number} row Visual row index.
  2753. * @param {String} prop Property name.
  2754. * @param {Number} [endRow] visual end row index (if selecting a range).
  2755. * @param {String} [endProp] End property name (if selecting a range).
  2756. * @param {Boolean} [scrollToCell=true] If `true`, viewport will be scrolled to the selection.
  2757. * @param {Boolean} [changeListener=true] If `false`, Handsontable will not change keyboard events listener to himself.
  2758. * @returns {Boolean} `true` if selection was successful, `false` otherwise.
  2759. */
  2760. this.selectCellByProp = function(row, prop, endRow, endProp, scrollToCell = true, changeListener = true) {
  2761. warn(toSingleLine`Deprecation warning: This method is going to be removed in the next release.
  2762. If you want to select a cell using props, please use the \`selectCell\` method.`);
  2763. return this.selectCells([[row, prop, endRow, endProp]], scrollToCell, changeListener);
  2764. };
  2765. /**
  2766. * Select column specified by `startColumn` visual index, column property or a range of columns finishing at `endColumn`.
  2767. *
  2768. * @example
  2769. * ```js
  2770. * // Select column using visual index.
  2771. * hot.selectColumns(1);
  2772. * // Select column using column property.
  2773. * hot.selectColumns('id');
  2774. * // Select range of columns using visual indexes.
  2775. * hot.selectColumns(1, 4);
  2776. * // Select range of columns using column properties.
  2777. * hot.selectColumns('id', 'last_name');
  2778. * ```
  2779. *
  2780. * @memberof Core#
  2781. * @since 0.38.0
  2782. * @function selectColumns
  2783. * @param {Number} startColumn The visual column index from which the selection starts.
  2784. * @param {Number} [endColumn=startColumn] The visual column index to which the selection finishes. If `endColumn`
  2785. * is not defined the column defined by `startColumn` will be selected.
  2786. * @returns {Boolean} `true` if selection was successful, `false` otherwise.
  2787. */
  2788. this.selectColumns = function(startColumn, endColumn = startColumn) {
  2789. return selection.selectColumns(startColumn, endColumn);
  2790. };
  2791. /**
  2792. * Select row specified by `startRow` visual index or a range of rows finishing at `endRow`.
  2793. *
  2794. * @example
  2795. * ```js
  2796. * // Select row using visual index.
  2797. * hot.selectRows(1);
  2798. * // Select range of rows using visual indexes.
  2799. * hot.selectRows(1, 4);
  2800. * ```
  2801. *
  2802. * @memberof Core#
  2803. * @since 0.38.0
  2804. * @function selectRows
  2805. * @param {Number} startRow The visual row index from which the selection starts.
  2806. * @param {Number} [endRow=startRow] The visual row index to which the selection finishes. If `endRow`
  2807. * is not defined the row defined by `startRow` will be selected.
  2808. * @returns {Boolean} `true` if selection was successful, `false` otherwise.
  2809. */
  2810. this.selectRows = function(startRow, endRow = startRow) {
  2811. return selection.selectRows(startRow, endRow);
  2812. };
  2813. /**
  2814. * Deselects the current cell selection on the table.
  2815. *
  2816. * @memberof Core#
  2817. * @function deselectCell
  2818. */
  2819. this.deselectCell = function() {
  2820. selection.deselect();
  2821. };
  2822. /**
  2823. * Select the whole table. The previous selection will be overwritten.
  2824. *
  2825. * @since 0.38.2
  2826. * @memberof Core#
  2827. * @function selectAll
  2828. */
  2829. this.selectAll = function() {
  2830. preventScrollingToCell = true;
  2831. selection.selectAll();
  2832. preventScrollingToCell = false;
  2833. };
  2834. /**
  2835. * Scroll viewport to coordinates specified by the `row` and `column` arguments.
  2836. *
  2837. * @memberof Core#
  2838. * @function scrollViewportTo
  2839. * @param {Number} [row] Visual row index.
  2840. * @param {Number} [column] Visual column index.
  2841. * @param {Boolean} [snapToBottom = false] If `true`, viewport is scrolled to show the cell on the bottom of the table.
  2842. * @param {Boolean} [snapToRight = false] If `true`, viewport is scrolled to show the cell on the right side of the table.
  2843. * @returns {Boolean} `true` if scroll was successful, `false` otherwise.
  2844. */
  2845. this.scrollViewportTo = function(row, column, snapToBottom = false, snapToRight = false) {
  2846. const snapToTop = !snapToBottom;
  2847. const snapToLeft = !snapToRight;
  2848. let result = false;
  2849. if (row !== void 0 && column !== void 0) {
  2850. result = instance.view.scrollViewport(new CellCoords(row, column), snapToTop, snapToRight, snapToBottom, snapToLeft);
  2851. }
  2852. if (typeof row === 'number' && typeof column !== 'number') {
  2853. result = instance.view.scrollViewportVertically(row, snapToTop, snapToBottom);
  2854. }
  2855. if (typeof column === 'number' && typeof row !== 'number') {
  2856. result = instance.view.scrollViewportHorizontally(column, snapToRight, snapToLeft);
  2857. }
  2858. return result;
  2859. };
  2860. /**
  2861. * Removes the table from the DOM and destroys the instance of the Handsontable.
  2862. *
  2863. * @memberof Core#
  2864. * @function destroy
  2865. * @fires Hooks#afterDestroy
  2866. */
  2867. this.destroy = function() {
  2868. instance._clearTimeouts();
  2869. instance._clearImmediates();
  2870. if (instance.view) { // in case HT is destroyed before initialization has finished
  2871. instance.view.destroy();
  2872. }
  2873. if (dataSource) {
  2874. dataSource.destroy();
  2875. }
  2876. dataSource = null;
  2877. keyStateStopObserving();
  2878. if (process.env.HOT_PACKAGE_TYPE !== '\x63\x65' && isRootInstance(instance)) {
  2879. const licenseInfo = document.querySelector('#hot-display-license-info');
  2880. if (licenseInfo) {
  2881. licenseInfo.parentNode.removeChild(licenseInfo);
  2882. }
  2883. }
  2884. empty(instance.rootElement);
  2885. eventManager.destroy();
  2886. if (editorManager) {
  2887. editorManager.destroy();
  2888. }
  2889. instance.runHooks('afterDestroy');
  2890. Hooks.getSingleton().destroy(instance);
  2891. objectEach(instance, (property, key, obj) => {
  2892. // replace instance methods with post mortem
  2893. if (isFunction(property)) {
  2894. obj[key] = postMortem(key);
  2895. } else if (key !== 'guid') {
  2896. // replace instance properties with null (restores memory)
  2897. // it should not be necessary but this prevents a memory leak side effects that show itself in Jasmine tests
  2898. obj[key] = null;
  2899. }
  2900. });
  2901. instance.isDestroyed = true;
  2902. // replace private properties with null (restores memory)
  2903. // it should not be necessary but this prevents a memory leak side effects that show itself in Jasmine tests
  2904. if (datamap) {
  2905. datamap.destroy();
  2906. }
  2907. datamap = null;
  2908. priv = null;
  2909. grid = null;
  2910. selection = null;
  2911. editorManager = null;
  2912. instance = null;
  2913. GridSettings = null;
  2914. };
  2915. /**
  2916. * Replacement for all methods after Handsotnable was destroyed.
  2917. *
  2918. * @private
  2919. */
  2920. function postMortem(method) {
  2921. return () => {
  2922. throw new Error(`The "${method}" method cannot be called because this Handsontable instance has been destroyed`);
  2923. };
  2924. }
  2925. /**
  2926. * Returns the active editor class instance.
  2927. *
  2928. * @memberof Core#
  2929. * @function getActiveEditor
  2930. * @returns {BaseEditor} The active editor instance.
  2931. */
  2932. this.getActiveEditor = function() {
  2933. return editorManager.getActiveEditor();
  2934. };
  2935. /**
  2936. * Returns plugin instance by provided its name.
  2937. *
  2938. * @memberof Core#
  2939. * @function getPlugin
  2940. * @param {String} pluginName The plugin name.
  2941. * @returns {BasePlugin} The plugin instance.
  2942. */
  2943. this.getPlugin = function(pluginName) {
  2944. return getPlugin(this, pluginName);
  2945. };
  2946. /**
  2947. * Returns the Handsontable instance.
  2948. *
  2949. * @memberof Core#
  2950. * @function getInstance
  2951. * @returns {Handsontable} The Handsontable instance.
  2952. */
  2953. this.getInstance = function() {
  2954. return instance;
  2955. };
  2956. /**
  2957. * Adds listener to the specified hook name (only for this Handsontable instance).
  2958. *
  2959. * @memberof Core#
  2960. * @function addHook
  2961. * @see Hooks#add
  2962. * @param {String} key Hook name (see {@link Hooks}).
  2963. * @param {Function|Array} callback Function or array of functions.
  2964. * @example
  2965. * ```js
  2966. * hot.addHook('beforeInit', myCallback);
  2967. * ```
  2968. */
  2969. this.addHook = function(key, callback) {
  2970. Hooks.getSingleton().add(key, callback, instance);
  2971. };
  2972. /**
  2973. * Check if for a specified hook name there are added listeners (only for this Handsontable instance). All available
  2974. * hooks you will find {@link Hooks}.
  2975. *
  2976. * @memberof Core#
  2977. * @function hasHook
  2978. * @see Hooks#has
  2979. * @param {String} key Hook name
  2980. * @return {Boolean}
  2981. *
  2982. * @example
  2983. * ```js
  2984. * const hasBeforeInitListeners = hot.hasHook('beforeInit');
  2985. * ```
  2986. */
  2987. this.hasHook = function(key) {
  2988. return Hooks.getSingleton().has(key, instance);
  2989. };
  2990. /**
  2991. * Adds listener to specified hook name (only for this Handsontable instance). After the listener is triggered,
  2992. * it will be automatically removed.
  2993. *
  2994. * @memberof Core#
  2995. * @function addHookOnce
  2996. * @see Hooks#once
  2997. * @param {String} key Hook name (see {@link Hooks}).
  2998. * @param {Function|Array} callback Function or array of functions.
  2999. * @example
  3000. * ```js
  3001. * hot.addHookOnce('beforeInit', myCallback);
  3002. * ```
  3003. */
  3004. this.addHookOnce = function(key, callback) {
  3005. Hooks.getSingleton().once(key, callback, instance);
  3006. };
  3007. /**
  3008. * Removes the hook listener previously registered with {@link Core#addHook}.
  3009. *
  3010. * @memberof Core#
  3011. * @function removeHook
  3012. * @see Hooks#remove
  3013. * @param {String} key Hook name.
  3014. * @param {Function} callback Reference to the function which has been registered using {@link Core#addHook}.
  3015. *
  3016. * @example
  3017. * ```js
  3018. * hot.removeHook('beforeInit', myCallback);
  3019. * ```
  3020. */
  3021. this.removeHook = function(key, callback) {
  3022. Hooks.getSingleton().remove(key, callback, instance);
  3023. };
  3024. /**
  3025. * Run the callbacks for the hook provided in the `key` argument using the parameters given in the other arguments.
  3026. *
  3027. * @memberof Core#
  3028. * @function runHooks
  3029. * @see Hooks#run
  3030. * @param {String} key Hook name.
  3031. * @param {*} [p1] Argument passed to the callback.
  3032. * @param {*} [p2] Argument passed to the callback.
  3033. * @param {*} [p3] Argument passed to the callback.
  3034. * @param {*} [p4] Argument passed to the callback.
  3035. * @param {*} [p5] Argument passed to the callback.
  3036. * @param {*} [p6] Argument passed to the callback.
  3037. * @returns {*}
  3038. *
  3039. * @example
  3040. * ```js
  3041. * // Run built-in hook
  3042. * hot.runHooks('beforeInit');
  3043. * // Run custom hook
  3044. * hot.runHooks('customAction', 10, 'foo');
  3045. * ```
  3046. */
  3047. this.runHooks = function(key, p1, p2, p3, p4, p5, p6) {
  3048. return Hooks.getSingleton().run(instance, key, p1, p2, p3, p4, p5, p6);
  3049. };
  3050. /**
  3051. * Get language phrase for specified dictionary key.
  3052. *
  3053. * @memberof Core#
  3054. * @function getTranslatedPhrase
  3055. * @since 0.35.0
  3056. * @param {String} dictionaryKey Constant which is dictionary key.
  3057. * @param {*} extraArguments Arguments which will be handled by formatters.
  3058. * @returns {String}
  3059. */
  3060. this.getTranslatedPhrase = function(dictionaryKey, extraArguments) {
  3061. return getTranslatedPhrase(priv.settings.language, dictionaryKey, extraArguments);
  3062. };
  3063. this.timeouts = [];
  3064. /**
  3065. * Sets timeout. Purpose of this method is to clear all known timeouts when `destroy` method is called.
  3066. *
  3067. * @param {Number|Function} handle Handler returned from setTimeout or function to execute (it will be automatically wraped
  3068. * by setTimeout function).
  3069. * @param {Number} [delay=0] If first argument is passed as a function this argument set delay of the execution of that function.
  3070. * @private
  3071. */
  3072. this._registerTimeout = function(handle, delay = 0) {
  3073. let handleFunc = handle;
  3074. if (typeof handleFunc === 'function') {
  3075. handleFunc = setTimeout(handleFunc, delay);
  3076. }
  3077. this.timeouts.push(handleFunc);
  3078. };
  3079. /**
  3080. * Clears all known timeouts.
  3081. *
  3082. * @private
  3083. */
  3084. this._clearTimeouts = function() {
  3085. arrayEach(this.timeouts, (handler) => {
  3086. clearTimeout(handler);
  3087. });
  3088. };
  3089. this.immediates = [];
  3090. /**
  3091. * Execute function execution to the next event loop cycle. Purpose of this method is to clear all known timeouts when `destroy` method is called.
  3092. *
  3093. * @param {Function} callback Function to be delayed in execution.
  3094. * @private
  3095. */
  3096. this._registerImmediate = function(callback) {
  3097. this.immediates.push(setImmediate(callback));
  3098. };
  3099. /**
  3100. * Clears all known timeouts.
  3101. *
  3102. * @private
  3103. */
  3104. this._clearImmediates = function() {
  3105. arrayEach(this.immediates, (handler) => {
  3106. clearImmediate(handler);
  3107. });
  3108. };
  3109. /**
  3110. * Refresh selection borders. This is temporary method relic after selection rewrite.
  3111. *
  3112. * @private
  3113. * @param {Boolean} [revertOriginal=false] If `true`, the previous value will be restored. Otherwise, the edited value will be saved.
  3114. * @param {Boolean} [prepareEditorIfNeeded=true] If `true` the editor under the selected cell will be prepared to open.
  3115. */
  3116. this._refreshBorders = function(revertOriginal = false, prepareEditorIfNeeded = true) {
  3117. editorManager.destroyEditor(revertOriginal);
  3118. instance.view.render();
  3119. if (prepareEditorIfNeeded && selection.isSelected()) {
  3120. editorManager.prepareEditor();
  3121. }
  3122. };
  3123. Hooks.getSingleton().run(instance, 'construct');
  3124. }