diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/import/content/csv-field-map.js | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mailnews/import/content/csv-field-map.js')
-rw-r--r-- | comm/mailnews/import/content/csv-field-map.js | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/comm/mailnews/import/content/csv-field-map.js b/comm/mailnews/import/content/csv-field-map.js new file mode 100644 index 0000000000..544a9cf50c --- /dev/null +++ b/comm/mailnews/import/content/csv-field-map.js @@ -0,0 +1,280 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { exportAttributes } = ChromeUtils.import( + "resource:///modules/AddrBookUtils.jsm" +); + +/** + * A component to config the mapping between CSV fields and address book fields. + * For each CSV field, there is a <select> with address book fields as options. + * If an address book field is selected for one CSV field, it can't be used for + * another CSV field. + */ +class CsvFieldMap extends HTMLElement { + /** Render the first two rows from the source CSV data. */ + DATA_ROWS_LIMIT = 2; + + /** @type {string[]} - The indexes of target address book fields. */ + get value() { + return [...this._elTbody.querySelectorAll("select")].map( + select => select.value + ); + } + + /** @type {string[][]} - An array of rows, each row is an array of columns. */ + set data(rows) { + this._init(); + this._rows = rows.slice(0, this.DATA_ROWS_LIMIT); + this._render(); + } + + /** + * Init internal states. + */ + _init() { + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/importMsgs.properties" + ); + this._supportedFields = []; + for (let [, stringId] of exportAttributes) { + if (stringId) { + this._supportedFields.push(bundle.GetStringFromID(stringId)); + } + } + // Create an index array ["0", "1", "2", ..., "<length -1>"]. + this._allFieldIndexes = Array.from({ + length: this._supportedFields.length, + }).map((_, index) => index.toString()); + } + + /** + * Init <option> list for all <select> elements. + */ + _initSelectOptions() { + let fields; + let fieldIndexes = Services.prefs.getCharPref("mail.import.csv.fields", ""); + if (fieldIndexes) { + // If the user has done CSV importing before, show the same field mapping. + fieldIndexes = fieldIndexes.split(","); + fields = fieldIndexes.map(i => (i == "" ? i : this._supportedFields[+i])); + } else { + // Show the same field orders as in an exported CSV file. + fields = this._supportedFields; + fieldIndexes = this._allFieldIndexes; + } + + let i = 0; + for (let select of this.querySelectorAll("select")) { + if (fields[i]) { + let option = document.createElement("option"); + option.value = fieldIndexes[i]; + option.textContent = fields[i]; + select.add(option); + } else { + select.disabled = true; + select + .closest("tr") + .querySelector("input[type=checkbox]").checked = false; + } + i++; + } + + this._updateSelectOptions(); + } + + /** + * When a <select> is disabled, we remove all its options. This function is to + * add all available options back. + * + * @param {HTMLSelectElement} select - The <select> element. + */ + _enableSelect(select) { + let selects = [...this._elTbody.querySelectorAll("select")]; + let selectedFieldIndexes = selects.map(select => select.value); + let availableFieldIndexes = this._allFieldIndexes.filter( + index => !selectedFieldIndexes.includes(index) + ); + for (let i = 0; i < availableFieldIndexes.length; i++) { + let option = document.createElement("option"); + option.value = availableFieldIndexes[i]; + option.textContent = this._supportedFields[option.value]; + select.add(option); + } + } + + /** + * Update the options of all <select> elements. The result is if an option is + * selected by a <select>, this option should no longer be shown as an option + * for other <select>. + * + * @param {HTMLSelectElement} [changedSelect] - This param is present only + * when an option is selected, we don't need to update the options of this + * <select> element. + */ + _updateSelectOptions(changedSelect) { + let selects = [...this._elTbody.querySelectorAll("select")]; + let selectedFieldIndexes = selects.map(select => select.value); + let availableFieldIndexes = this._allFieldIndexes.filter( + index => !selectedFieldIndexes.includes(index) + ); + + for (let select of selects) { + if (select.disabled || select == changedSelect) { + continue; + } + for (let i = select.options.length - 1; i >= 0; i--) { + // Remove unselected options first. + if (i != select.selectedIndex) { + select.remove(i); + } + } + for (let i = 0; i < availableFieldIndexes.length; i++) { + // Add all available options. + let option = document.createElement("option"); + option.value = availableFieldIndexes[i]; + option.textContent = this._supportedFields[option.value]; + select.add(option); + } + } + } + + /** + * Handle the change event of <select> and <input type="checkbox">. + */ + _bindEvents() { + this._elTbody.addEventListener("change", e => { + let el = e.target; + if (el.tagName == "select") { + this._updateSelectOptions(el); + } else if (el.tagName == "input" && el.type == "checkbox") { + let select = el.closest("tr").querySelector("select"); + select.disabled = !el.checked; + if (select.disabled) { + // Because it's disabled, remove all the options. + for (let i = select.options.length - 1; i >= 0; i--) { + select.remove(i); + } + } else { + this._enableSelect(select); + } + this._updateSelectOptions(); + } + }); + } + + /** + * Render the table structure. + */ + async _renderLayout() { + this.innerHTML = ""; + let [ + firstRowContainsHeaders, + sourceField, + sourceFirstRecord, + sourceSecondRecord, + targetField, + ] = await document.l10n.formatValues([ + "csv-first-row-contains-headers", + "csv-source-field", + "csv-source-first-record", + "csv-source-second-record", + "csv-target-field", + ]); + + let label = document.createElement("label"); + label.className = "toggle-container-with-text"; + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = Services.prefs.getBoolPref( + "mail.import.csv.skipfirstrow", + true + ); + let labelText = document.createElement("span"); + labelText.textContent = firstRowContainsHeaders; + label.appendChild(checkbox); + label.appendChild(labelText); + this.appendChild(label); + + let table = document.createElement("table"); + + let thead = document.createElement("thead"); + let tr = document.createElement("tr"); + let headers = []; + for (let colName of [sourceField, sourceFirstRecord, targetField, ""]) { + let th = document.createElement("th"); + th.textContent = colName; + tr.appendChild(th); + headers.push(th); + } + thead.appendChild(tr); + table.appendChild(thead); + + this._elTbody = document.createElement("tbody"); + table.appendChild(this._elTbody); + + this.appendChild(table); + this._bindEvents(); + + checkbox.addEventListener("change", () => { + if (checkbox.checked) { + headers[0].textContent = sourceField; + headers[1].textContent = sourceFirstRecord; + } else { + headers[0].textContent = sourceFirstRecord; + headers[1].textContent = sourceSecondRecord; + } + Services.prefs.setBoolPref( + "mail.import.csv.skipfirstrow", + checkbox.checked + ); + }); + } + + /** + * Render the table content. Each row contains four columns: + * Source field | Source Data | Address book field | <checkbox> + */ + _renderTable() { + let colCount = this._rows[0].length; + for (let i = 0; i < colCount; i++) { + let tr = document.createElement("tr"); + + // Render the source field name and source data. + for (let j = 0; j < this.DATA_ROWS_LIMIT; j++) { + let td = document.createElement("td"); + td.textContent = this._rows[j]?.[i] || ""; + tr.appendChild(td); + } + + // Render a <select> for target field name. + let td = document.createElement("td"); + let select = document.createElement("select"); + td.appendChild(select); + tr.appendChild(td); + + // Render a checkbox. + td = document.createElement("td"); + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = true; + td.appendChild(checkbox); + tr.appendChild(td); + + this._elTbody.appendChild(tr); + } + + this._initSelectOptions(); + } + + /** + * Render the table layout and content. + */ + async _render() { + await this._renderLayout(); + this._renderTable(); + } +} + +customElements.define("csv-field-map", CsvFieldMap); |