Replaced the function-based JavaScript account selector with the AccountSelector class that does things better.

This commit is contained in:
依瑪貓 2023-02-28 23:30:38 +08:00
parent 319f0aed90
commit a31ce3c400
7 changed files with 205 additions and 137 deletions

View File

@ -23,54 +23,54 @@
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
initializeAccountSelectors(); AccountSelector.initialize();
}); });
/** /**
* Initializes the account selectors. * The account selector.
* *
* @private
*/ */
function initializeAccountSelectors() { class AccountSelector {
const selectors = Array.from(document.getElementsByClassName("accounting-account-selector-modal"));
const formAccountControl = document.getElementById("accounting-entry-form-account-control"); /**
const formAccount = document.getElementById("accounting-entry-form-account"); * The entry type
formAccountControl.onclick = function () { * @type {string}
const entryForm = document.getElementById("accounting-entry-form"); */
const prefix = "accounting-account-selector-" + entryForm.dataset.entryType; #entryType;
const query = document.getElementById(prefix + "-query")
const more = document.getElementById(prefix + "-more"); /**
const options = Array.from(document.getElementsByClassName(prefix + "-option")); * The prefix of the HTML ID and class
const btnClear = document.getElementById(prefix + "-btn-clear"); * @type {string}
query.value = ""; */
more.classList.remove("d-none"); #prefix;
filterAccountOptions(prefix);
for (const option of options) { /**
if (option.dataset.code === formAccount.dataset.code) { * Constructs an account selector.
option.classList.add("active"); *
} else { * @param modal {HTMLFormElement} the account selector modal
option.classList.remove("active"); */
} constructor(modal) {
} this.#entryType = modal.dataset.entryType;
if (formAccount.dataset.code === "") { this.#prefix = "accounting-account-selector-" + modal.dataset.entryType;
btnClear.classList.add("btn-secondary"); this.#init();
btnClear.classList.remove("btn-danger"); }
btnClear.disabled = true;
} else { /**
btnClear.classList.add("btn-danger"); * Initializes the account selector.
btnClear.classList.remove("btn-secondary"); *
btnClear.disabled = false; */
} #init() {
}; const formAccountControl = document.getElementById("accounting-entry-form-account-control");
for (const selector of selectors) { const formAccount = document.getElementById("accounting-entry-form-account");
const more = document.getElementById(selector.dataset.prefix + "-more"); const more = document.getElementById(this.#prefix + "-more");
const btnClear = document.getElementById(selector.dataset.prefix + "-btn-clear"); const btnClear = document.getElementById(this.#prefix + "-btn-clear");
const options = Array.from(document.getElementsByClassName(selector.dataset.prefix + "-option")); const options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
const selector1 = this
more.onclick = function () { more.onclick = function () {
more.classList.add("d-none"); more.classList.add("d-none");
filterAccountOptions(selector.dataset.prefix); selector1.#filterAccountOptions();
}; };
initializeAccountQuery(selector); this.#initializeAccountQuery();
btnClear.onclick = function () { btnClear.onclick = function () {
formAccountControl.classList.remove("accounting-not-empty"); formAccountControl.classList.remove("accounting-not-empty");
formAccount.innerText = ""; formAccount.innerText = "";
@ -88,100 +88,168 @@ function initializeAccountSelectors() {
}; };
} }
} }
}
/** /**
* Initializes the query on the account options. * Initializes the query on the account options.
* *
* @param selector {HTMLDivElement} the selector modal */
* @private #initializeAccountQuery() {
*/ const query = document.getElementById(this.#prefix + "-query");
function initializeAccountQuery(selector) { const helper = this;
const query = document.getElementById(selector.dataset.prefix + "-query"); query.addEventListener("input", function () {
query.addEventListener("input", function () { helper.#filterAccountOptions();
filterAccountOptions(selector.dataset.prefix); });
});
}
/**
* Filters the account options.
*
* @param prefix {string} the HTML ID and class prefix
* @private
*/
function filterAccountOptions(prefix) {
const query = document.getElementById(prefix + "-query");
const optionList = document.getElementById(prefix + "-option-list");
if (optionList === null) {
console.log(prefix + "-option-list");
} }
const options = Array.from(document.getElementsByClassName(prefix + "-option"));
const more = document.getElementById(prefix + "-more"); /**
const queryNoResult = document.getElementById(prefix + "-option-no-result"); * Filters the account options.
const codesInUse = getAccountCodeUsedInForm(); *
let shouldAnyShow = false; */
for (const option of options) { #filterAccountOptions() {
const shouldShow = shouldAccountOptionShow(option, more, codesInUse, query); const query = document.getElementById(this.#prefix + "-query");
if (shouldShow) { const optionList = document.getElementById(this.#prefix + "-option-list");
option.classList.remove("d-none"); if (optionList === null) {
shouldAnyShow = true; console.log(this.#prefix + "-option-list");
} else {
option.classList.add("d-none");
} }
} const options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
if (!shouldAnyShow && more.classList.contains("d-none")) { const more = document.getElementById(this.#prefix + "-more");
optionList.classList.add("d-none"); const queryNoResult = document.getElementById(this.#prefix + "-option-no-result");
queryNoResult.classList.remove("d-none"); const codesInUse = this.#getAccountCodeUsedInForm();
} else { let shouldAnyShow = false;
optionList.classList.remove("d-none"); for (const option of options) {
queryNoResult.classList.add("d-none"); const shouldShow = this.#shouldAccountOptionShow(option, more, codesInUse, query);
} if (shouldShow) {
} option.classList.remove("d-none");
shouldAnyShow = true;
/** } else {
* Returns whether an account option should show. option.classList.add("d-none");
*
* @param option {HTMLLIElement} the account option
* @param more {HTMLLIElement} the more account element
* @param inUse {string[]} the account codes that are used in the form
* @param query {HTMLInputElement} the query element, if any
* @return {boolean} true if the account option should show, or false otherwise
* @private
*/
function shouldAccountOptionShow(option, more, inUse, query) {
const isQueryMatched = function () {
if (query.value === "") {
return true;
}
const queryValues = JSON.parse(option.dataset.queryValues);
for (const queryValue of queryValues) {
if (queryValue.includes(query.value)) {
return true;
} }
} }
return false; if (!shouldAnyShow && more.classList.contains("d-none")) {
}; optionList.classList.add("d-none");
const isMoreMatched = function () { queryNoResult.classList.remove("d-none");
if (more.classList.contains("d-none")) { } else {
return true; optionList.classList.remove("d-none");
queryNoResult.classList.add("d-none");
} }
return option.classList.contains("accounting-account-in-use") || inUse.includes(option.dataset.code);
};
return isMoreMatched() && isQueryMatched();
}
/**
* Returns the account codes that are used in the form.
*
* @return {string[]} the account codes that are used in the form
* @private
*/
function getAccountCodeUsedInForm() {
const accountCodes = Array.from(document.getElementsByClassName("accounting-account-code"));
const formAccount = document.getElementById("accounting-entry-form-account");
const inUse = [formAccount.dataset.code];
for (const accountCode of accountCodes) {
inUse.push(accountCode.value);
} }
return inUse
/**
* Returns the account codes that are used in the form.
*
* @return {string[]} the account codes that are used in the form
*/
#getAccountCodeUsedInForm() {
const accountCodes = Array.from(document.getElementsByClassName("accounting-account-code"));
const formAccount = document.getElementById("accounting-entry-form-account");
const inUse = [formAccount.dataset.code];
for (const accountCode of accountCodes) {
inUse.push(accountCode.value);
}
return inUse
}
/**
* Returns whether an account option should show.
*
* @param option {HTMLLIElement} the account option
* @param more {HTMLLIElement} the more account element
* @param inUse {string[]} the account codes that are used in the form
* @param query {HTMLInputElement} the query element, if any
* @return {boolean} true if the account option should show, or false otherwise
*/
#shouldAccountOptionShow(option, more, inUse, query) {
const isQueryMatched = function () {
if (query.value === "") {
return true;
}
const queryValues = JSON.parse(option.dataset.queryValues);
for (const queryValue of queryValues) {
if (queryValue.includes(query.value)) {
return true;
}
}
return false;
};
const isMoreMatched = function () {
if (more.classList.contains("d-none")) {
return true;
}
return option.classList.contains("accounting-account-in-use") || inUse.includes(option.dataset.code);
};
return isMoreMatched() && isQueryMatched();
}
/**
* Initializes the account selector when it is shown.
*
*/
initShow() {
const formAccount = document.getElementById("accounting-entry-form-account");
const query = document.getElementById(this.#prefix + "-query")
const more = document.getElementById(this.#prefix + "-more");
const options = Array.from(document.getElementsByClassName(this.#prefix + "-option"));
const btnClear = document.getElementById(this.#prefix + "-btn-clear");
query.value = "";
more.classList.remove("d-none");
this.#filterAccountOptions();
for (const option of options) {
if (option.dataset.code === formAccount.dataset.code) {
option.classList.add("active");
} else {
option.classList.remove("active");
}
}
if (formAccount.dataset.code === "") {
btnClear.classList.add("btn-secondary");
btnClear.classList.remove("btn-danger");
btnClear.disabled = true;
} else {
btnClear.classList.add("btn-danger");
btnClear.classList.remove("btn-secondary");
btnClear.disabled = false;
}
}
/**
* The account selectors.
* @type {{debit: AccountSelector, credit: AccountSelector}}
*/
static #selectors = {}
/**
* Initializes the account selectors.
*
*/
static initialize() {
const modals = Array.from(document.getElementsByClassName("accounting-account-selector-modal"));
for (const modal of modals) {
const selector = new AccountSelector(modal);
this.#selectors[selector.#entryType] = selector;
}
this.#initializeTransactionForm();
}
/**
* Initializes the transaction form.
*
*/
static #initializeTransactionForm() {
const entryForm = document.getElementById("accounting-entry-form");
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
const selectors = this.#selectors;
formAccountControl.onclick = function () {
selectors[entryForm.dataset.entryType].initShow();
};
}
/**
* Initializes the account selector for the journal entry form.
*x
*/
static initializeJournalEntryForm() {
const entryForm = document.getElementById("accounting-entry-form");
const formAccountControl = document.getElementById("accounting-entry-form-account-control");
formAccountControl.dataset.bsTarget = "#accounting-account-selector-" + entryForm.dataset.entryType + "-modal";
}
} }

View File

@ -167,7 +167,6 @@ function initializeNewEntryButton(button) {
entryForm.dataset.entryIndex = button.dataset.entryIndex; entryForm.dataset.entryIndex = button.dataset.entryIndex;
formAccountControl.classList.remove("accounting-not-empty"); formAccountControl.classList.remove("accounting-not-empty");
formAccountControl.classList.remove("is-invalid"); formAccountControl.classList.remove("is-invalid");
formAccountControl.dataset.bsTarget = button.dataset.accountModal;
formAccount.innerText = ""; formAccount.innerText = "";
formAccount.dataset.code = ""; formAccount.dataset.code = "";
formAccount.dataset.text = ""; formAccount.dataset.text = "";
@ -181,6 +180,7 @@ function initializeNewEntryButton(button) {
formAmount.value = ""; formAmount.value = "";
formAmount.classList.remove("is-invalid"); formAmount.classList.remove("is-invalid");
formAmountError.innerText = ""; formAmountError.innerText = "";
AccountSelector.initializeJournalEntryForm();
SummaryHelper.initializeNewJournalEntry(button.dataset.entryType); SummaryHelper.initializeNewJournalEntry(button.dataset.entryType);
}; };
} }
@ -225,7 +225,6 @@ function initializeJournalEntry(entry) {
} else { } else {
formAccountControl.classList.add("accounting-not-empty"); formAccountControl.classList.add("accounting-not-empty");
} }
formAccountControl.dataset.bsTarget = entry.dataset.accountModal;
formAccount.innerText = accountCode.dataset.text; formAccount.innerText = accountCode.dataset.text;
formAccount.dataset.code = accountCode.value; formAccount.dataset.code = accountCode.value;
formAccount.dataset.text = accountCode.dataset.text; formAccount.dataset.text = accountCode.dataset.text;
@ -238,6 +237,7 @@ function initializeJournalEntry(entry) {
formSummary.dataset.value = summary.value; formSummary.dataset.value = summary.value;
formSummary.innerText = summary.value; formSummary.innerText = summary.value;
formAmount.value = amount.value; formAmount.value = amount.value;
AccountSelector.initializeJournalEntryForm();
validateJournalEntryForm(); validateJournalEntryForm();
}; };
} }

View File

@ -70,7 +70,7 @@ First written: 2023/2/25
</div> </div>
<div> <div>
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="debit" data-entry-index="new" data-account-modal="#accounting-account-selector-debit-modal" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-debit-modal"> <button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="debit" data-entry-index="new" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-debit-modal">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{{ A_("New") }} {{ A_("New") }}
</button> </button>

View File

@ -19,7 +19,7 @@ account-selector-modal.html: The modal for the account selector
Author: imacat@mail.imacat.idv.tw (imacat) Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25 First written: 2023/2/25
#} #}
<div id="accounting-account-selector-{{ entry_type }}-modal" class="modal fade accounting-account-selector-modal" data-prefix="accounting-account-selector-{{ entry_type }}" tabindex="-1" aria-labelledby="accounting-account-selector-{{ entry_type }}-modal-label" aria-hidden="true"> <div id="accounting-account-selector-{{ entry_type }}-modal" class="modal fade accounting-account-selector-modal" data-entry-type="{{ entry_type }}" tabindex="-1" aria-labelledby="accounting-account-selector-{{ entry_type }}-modal-label" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">

View File

@ -20,7 +20,7 @@ Author: imacat@mail.imacat.idv.tw (imacat)
First written: 2023/2/25 First written: 2023/2/25
#} #}
{# <ul> For SonarQube not to complain about incorrect HTML #} {# <ul> For SonarQube not to complain about incorrect HTML #}
<li id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}" class="list-group-item list-group-item-action d-flex justify-content-between accounting-entry accounting-currency-{{ currency_index }}-{{ entry_type }}" data-currency-index="{{ currency_index }}" data-entry-type="{{ entry_type }}" data-entry-index="{{ entry_index }}" data-account-modal="#accounting-account-selector-{{ entry_type }}-modal" data-prefix="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}"> <li id="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}" class="list-group-item list-group-item-action d-flex justify-content-between accounting-entry accounting-currency-{{ currency_index }}-{{ entry_type }}" data-currency-index="{{ currency_index }}" data-entry-type="{{ entry_type }}" data-entry-index="{{ entry_index }}" data-prefix="accounting-currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}">
{% if entry_id %} {% if entry_id %}
<input type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-eid" value="{{ entry_id }}"> <input type="hidden" name="currency-{{ currency_index }}-{{ entry_type }}-{{ entry_index }}-eid" value="{{ entry_id }}">
{% endif %} {% endif %}

View File

@ -70,7 +70,7 @@ First written: 2023/2/25
</div> </div>
<div> <div>
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="credit" data-entry-index="new" data-account-modal="#accounting-account-selector-credit-modal" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-credit-modal"> <button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="credit" data-entry-index="new" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-credit-modal">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{{ A_("New") }} {{ A_("New") }}
</button> </button>

View File

@ -72,7 +72,7 @@ First written: 2023/2/25
</div> </div>
<div> <div>
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="debit" data-entry-index="new" data-account-modal="#accounting-account-selector-debit-modal" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-debit-modal"> <button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="debit" data-entry-index="new" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-debit-modal">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{{ A_("New") }} {{ A_("New") }}
</button> </button>
@ -112,7 +112,7 @@ First written: 2023/2/25
</div> </div>
<div> <div>
<button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="credit" data-entry-index="new" data-account-modal="#accounting-account-selector-credit-modal" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-credit-modal"> <button class="btn btn-primary accounting-btn-new-entry accounting-currency-{{ currency_index }}-btn-new-entry" type="button" data-currency-index="{{ currency_index }}" data-entry-type="credit" data-entry-index="new" data-bs-toggle="modal" data-bs-target="#accounting-summary-helper-credit-modal">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
{{ A_("New") }} {{ A_("New") }}
</button> </button>