diff --git a/lib/DataHarmonizer.js b/lib/DataHarmonizer.js index 0101889b..4589d08b 100644 --- a/lib/DataHarmonizer.js +++ b/lib/DataHarmonizer.js @@ -5,7 +5,7 @@ import $ from 'jquery'; import i18next from 'i18next'; import { utils as XlsxUtils, read as xlsxRead } from 'xlsx/xlsx.mjs'; import { renderContent, urlToClickableAnchor } from './utils/content'; - +import { MULTIVALUED_DELIMITER } from './utils/fields'; import { wait, isValidHeaderRow } from '@/lib/utils/general'; import { readFileAsync, updateSheetRange } from '@/lib/utils/files'; import { @@ -415,7 +415,7 @@ class DataHarmonizer { console.log( `TEMPLATE ERROR: Check the regular expression syntax for "${new_field.title}".` ); - console.log(err); + console.error(err); // Allow anything until regex fixed. new_field.pattern = new RegExp(/.*/); } @@ -464,7 +464,7 @@ class DataHarmonizer { this.loadSpreadsheetData(contentBuffer); } } catch (err) { - console.log(err); + console.error(err); } } @@ -487,9 +487,6 @@ class DataHarmonizer { */ createHot() { - this.clipboardCache = ''; - this.sheetclip = new SheetClip(); - this.invalid_cells = {}; if (this.hot) { this.hot.destroy(); // handles already existing data @@ -504,32 +501,40 @@ class DataHarmonizer { const self = this; if (fields.length) { - const hot_settings = { - data: [], // Enables true reset - nestedHeaders: this.getNestedHeaders(), - columns: this.getColumns(), - colHeaders: true, - rowHeaders: true, - copyPaste: true, - manualColumnResize: true, - //colWidths: [100], //Just fixes first column width - afterCopy: function (changes) { + + this.clipboardCache = ''; + this.clipboardCoordCache = { + 'CopyPaste.copy': {}, + 'CopyPaste.cut': {}, + 'CopyPaste.paste': {}, + 'Action:CopyPaste': '' + }; // { startCol, startRow, endCol, endRow } + this.sheetclip = new SheetClip(); + + const hot_copy_paste_settings = { + afterCopy: function (changes, coords) { self.clipboardCache = self.sheetclip.stringify(changes); + self.clipboardCoordCache['CopyPaste.copy'] = coords[0]; + self.clipboardCoordCache['Action:CopyPaste'] = 'copy'; }, - afterCut: function (changes) { + afterCut: function (changes, coords) { + self.clipboardCache = self.sheetclip.stringify(changes); - }, - afterPaste: function (changes, ...rest) { - const { startRow, startCol, endRow, endCol } = rest; + self.clipboardCoordCache['CopyPaste.cut'] = coords[0]; + self.clipboardCoordCache['Action:CopyPaste'] = 'cut'; - // TODO: copy over internationalization data - // self.hot.getCellMeta(startRow, startCol).getAttribute('data-i18n') - // self.hot.getCellMeta(startRow, startCol).getAttribute('data-i18n-options') - // self.hot.getCell(endRow, endCol)//.setAttribute('data-i18n', null); - // self.hot.getCell(endRow, endCol)//.setAttribute('data-i18n-options', null); + // clear internationalization data + const TD_source = self.hot.getCell(coords[0].startRow, coords[0].startCol); + if (TD_source.getAttribute('data-i18n').includes('multiselect')) { + TD_source.setAttribute('data-i18n-options', JSON.stringify({ value: [] })); + }; + }, + afterPaste: function (changes, coords) { // we want to be sure that our cache is up to date, even if someone pastes data from another source than our tables. self.clipboardCache = self.sheetclip.stringify(changes); + self.clipboardCoordCache['CopyPaste.paste'] = coords[0]; + self.clipboardCoordCache['Action:CopyPaste'] = 'paste'; }, contextMenu: [ 'copy', @@ -555,7 +560,19 @@ class DataHarmonizer { 'remove_row', 'row_above', 'row_below', - ], + ] + }; + + const hot_settings = { + ...hot_copy_paste_settings, + data: [], // Enables true reset + nestedHeaders: this.getNestedHeaders(), + columns: this.getColumns(), + colHeaders: true, + rowHeaders: true, + copyPaste: true, + manualColumnResize: true, + //colWidths: [100], //Just fixes first column width minRows: 100, minSpareRows: 100, width: '100%', @@ -645,6 +662,7 @@ class DataHarmonizer { } }, afterRenderer: (TD, row, col) => { + if (Object.prototype.hasOwnProperty.call(self.invalid_cells, row)) { if ( Object.prototype.hasOwnProperty.call(self.invalid_cells[row], col) @@ -660,28 +678,71 @@ class DataHarmonizer { this.enableMultiSelection(); } else { - console.log( + console.warn( 'This template had no sections and fields: ' + this.template_name ); } + + // this.hot.addHook('beforeChange', function (changes, source) { + + // }) + // hooks, since passing them as callbacks would result in recurisve dependencies this.hot.addHook( 'afterChange', function (changes, source) { - if (source !== 'loadData') { + + if (source === 'CopyPaste.paste') { + + const previousAction = `CopyPaste.${self.clipboardCoordCache['Action:CopyPaste']}`; + + // take the value from the cut/copy cell + // NOTE: assuming it doesn't change in between pastes! + // shouldn't matter... since under this logic pastes are idempotent upto the next cut/copy + const { startRow, startCol } = self.clipboardCoordCache[previousAction]; + const TD_source = self.hot.getCell(startRow, startCol); + + for (let i = 0; i < changes.length; i++) { + const [endRow, prop] = changes[i]; + const endCol = self.hot.propToCol(prop); + if (startRow !== endRow || startCol !== endCol) { + let TD_target = self.hot.getCell(endRow, endCol); + + // shared column-type should allow transfer of data options, else don't bother (will cause problems) + // the existence of the multiselect formatter means the paste can't be a naive one for data-i18n (which would otherwise just be the innerText of the cell/the label) + // for safety let's equivocate them if they share a column and formatter (so guaranteed to be the same type and schema) + // TODO: ISSUE: more semantic way to do this, like with classes? + if (startCol === endCol && TD_source.getAttribute('data-i18n').includes('multiselect')) { + TD_target.setAttribute('data-i18n', 'multiselect.label;[append]multiselect.arrow'); + TD_target.setAttribute('data-i18n-options', JSON.stringify({ + value: self.clipboardCache.split(MULTIVALUED_DELIMITER) + })); + } + + } + } + + } + + + if (source !== 'loadData' && source !== 'CopyPaste.paste') { // Not triggered by the initial load for (let i = 0; i < changes.length; i++) { - let [row, prop] = changes[i]; + + const [row, prop] = changes[i]; const col = self.hot.propToCol(prop); - const td = self.hot.getCell(row, col); + + const TD = self.hot.getCell(row, col); // Assuming the new value is a translation key: - const localizedText = i18next.t($(td).attr('data-i18n')); + const localizedText = i18next.t($(TD).attr('data-i18n')); // // Set the localized text to the cell - td.innerText = localizedText; - // $(td).localize(); + TD.innerText = localizedText; + self.hot.render(); + } } + }.bind(self) ); } @@ -1304,6 +1365,7 @@ class DataHarmonizer { ret.push(col); } + return ret; } @@ -1318,20 +1380,20 @@ class DataHarmonizer { */ enableMultiSelection() { const fields = this.getFields(); - const that = this; this.hot.updateSettings({ afterBeginEditing: function (row, col) { - - that.hot.getCell(row, col).setAttribute('data-i18n', 'multiselect.label;[append]multiselect.arrow'); - if (that.hot.getCell(row, col).getAttribute('data-i18n-options') == null) { - that.hot.getCell(row, col).setAttribute('data-i18n-options', JSON.stringify({ value: [] })); - } else { - that.hot.getCell(row, col).setAttribute('data-i18n-options', JSON.stringify({ value: $('#field-description-text .multiselect').val() })); - } + const self = this; if (fields[col].flatVocabulary && fields[col].multivalued === true) { - const self = this; + + this.getCell(row, col).setAttribute('data-i18n', 'multiselect.label;[append]multiselect.arrow'); + if (this.getCell(row, col).getAttribute('data-i18n-options') == null) { + this.getCell(row, col).setAttribute('data-i18n-options', JSON.stringify({ value: [] })); + } else { + this.getCell(row, col).setAttribute('data-i18n-options', JSON.stringify({ value: $('#field-description-text .multiselect').val() })); + } + const value = this.getDataAtCell(row, col); const selections = parseMultivaluedValue(value); const formattedValue = formatMultivaluedValue(selections); @@ -1339,6 +1401,7 @@ class DataHarmonizer { if (value !== formattedValue) { this.setDataAtCell(row, col, formattedValue, 'thisChange'); } + let content = ''; if (fields[col].flatVocabulary) { fields[col].flatVocabulary.forEach(function (field) { @@ -1381,16 +1444,19 @@ class DataHarmonizer { }); // add localization information to cell - that.hot.getCell(row, col).setAttribute('data-i18n', 'multiselect.label;[append]multiselect.arrow'); - that.hot.getCell(row, col) + self.getCell(row, col) + .setAttribute('data-i18n', 'multiselect.label;[append]multiselect.arrow'); + self.getCell(row, col) .setAttribute('data-i18n-options', JSON.stringify({ value: $('#field-description-text .multiselect').val() })); self.setDataAtCell(row, col, newValCsv, 'thisChange'); + }); // Saves users a click: $('#field-description-text .multiselect')[0].selectize.focus(); + } }, }); diff --git a/lib/editors/KeyValueEditor.js b/lib/editors/KeyValueEditor.js index 6c1babf7..784639b6 100644 --- a/lib/editors/KeyValueEditor.js +++ b/lib/editors/KeyValueEditor.js @@ -148,7 +148,7 @@ export const keyValueListRenderer = function ( } // Use the ID as the translation key for the data-i18n attribute - item ? TD.setAttribute('data-i18n', item._id) : undefined ; + item ? TD.setAttribute('data-i18n', item._id) : undefined; Handsontable.renderers .getRenderer('autocomplete') diff --git a/web/index.js b/web/index.js index 966aedb1..fec436ec 100644 --- a/web/index.js +++ b/web/index.js @@ -23,8 +23,7 @@ document.addEventListener('DOMContentLoaded', function () { // Takes `lang` as argument (unused) initI18n((lang) => { console.log(lang); - //dh.hot.render(); - console.log('localizing in render') + // dh.hot.render(); $(document).localize(); });