Replaced the function-based JavaScript with the object-oriented AccountForm class for the account form.

This commit is contained in:
依瑪貓 2023-03-12 18:01:11 +08:00
parent 50dc79d865
commit 0a658a76e8
2 changed files with 317 additions and 153 deletions

View File

@ -24,171 +24,335 @@
// Initializes the page JavaScript. // Initializes the page JavaScript.
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
initializeBaseAccountSelector(); AccountForm.initialize();
document.getElementById("accounting-base-code")
.onchange = validateBase;
document.getElementById("accounting-title")
.onchange = validateTitle;
document.getElementById("accounting-form")
.onsubmit = validateForm;
}); });
/** /**
* Initializes the base account selector. * The account form.
* *
* @private * @private
*/ */
function initializeBaseAccountSelector() { class AccountForm {
const selector = document.getElementById("accounting-base-selector-modal");
const base = document.getElementById("accounting-base"); /**
const baseCode = document.getElementById("accounting-base-code"); * The base account selector
const baseContent = document.getElementById("accounting-base-content"); * @type {BaseAccountSelector}
const isOffsetNeededControl = document.getElementById("accounting-is-offset-needed-control"); */
const isOffsetNeeded = document.getElementById("accounting-is-offset-needed"); #baseAccountSelector;
const options = Array.from(document.getElementsByClassName("accounting-base-option"));
const btnClear = document.getElementById("accounting-btn-clear-base"); /**
base.onclick = () => { * The form element
base.classList.add("accounting-not-empty"); * @type {HTMLFormElement}
for (const option of options) { */
option.classList.remove("active"); #formElement;
}
const selected = document.getElementById("accounting-base-option-" + baseCode.value); /**
if (selected !== null) { * The control of the base account
selected.classList.add("active"); * @type {HTMLDivElement}
} */
}; #baseControl;
selector.addEventListener("hidden.bs.modal", () => {
if (baseCode.value === "") { /**
base.classList.remove("accounting-not-empty"); * The input of the base account
} * @type {HTMLInputElement}
}); */
for (const option of options) { #baseCode;
option.onclick = () => {
baseCode.value = option.dataset.code; /**
baseContent.innerText = option.dataset.content; * The base account
if (["1", "2"].includes(option.dataset.content.substring(0, 1))) { * @type {HTMLDivElement}
isOffsetNeededControl.classList.remove("d-none"); */
isOffsetNeeded.disabled = false; #base;
} else {
isOffsetNeededControl.classList.add("d-none"); /**
isOffsetNeeded.disabled = true; * The error message for the base account
isOffsetNeeded.checked = false; * @type {HTMLDivElement}
} */
btnClear.classList.add("btn-danger"); #baseError;
btnClear.classList.remove("btn-secondary")
btnClear.disabled = false; /**
validateBase(); * The title
bootstrap.Modal.getInstance(selector).hide(); * @type {HTMLInputElement}
*/
#title;
/**
* The error message of the title
* @type {HTMLDivElement}
*/
#titleError;
/**
* The control of the is-offset-needed option
* @type {HTMLDivElement}
*/
#isOffsetNeededControl;
/**
* The is-offset-needed option
* @type {HTMLInputElement}
*/
#isOffsetNeeded;
/**
* Constructs the account form.
*
*/
constructor() {
this.#baseAccountSelector = new BaseAccountSelector(this);
this.#formElement = document.getElementById("accounting-form");
this.#baseControl = document.getElementById("accounting-base-control");
this.#baseCode = document.getElementById("accounting-base-code");
this.#base = document.getElementById("accounting-base");
this.#baseError = document.getElementById("accounting-base-error");
this.#title = document.getElementById("accounting-title");
this.#titleError = document.getElementById("accounting-title-error");
this.#isOffsetNeededControl = document.getElementById("accounting-is-offset-needed-control");
this.#isOffsetNeeded = document.getElementById("accounting-is-offset-needed");
this.#formElement.onsubmit = () => {
return this.#validateForm();
};
this.#baseControl.onclick = () => {
this.#baseControl.classList.add("accounting-not-empty");
this.#baseAccountSelector.onOpen(this.#baseCode.value);
}; };
} }
btnClear.onclick = () => {
baseCode.value = ""; /**
baseContent.innerText = ""; * The callback when the base account selector is closed.
btnClear.classList.add("btn-secondary") *
btnClear.classList.remove("btn-danger"); */
btnClear.disabled = true; onBaseAccountSelectorClosed() {
validateBase(); if (this.#baseCode.value === "") {
bootstrap.Modal.getInstance(selector).hide(); this.#baseControl.classList.remove("accounting-not-empty");
}
}
/**
* Sets the base account.
*
* @param code {string} the base account code
* @param text {string} the text for the base account
*/
setBaseAccount(code, text) {
this.#baseCode.value = code;
this.#base.innerText = text;
if (["1", "2"].includes(code.substring(0, 1))) {
this.#isOffsetNeededControl.classList.remove("d-none");
this.#isOffsetNeeded.disabled = false;
} else {
this.#isOffsetNeededControl.classList.add("d-none");
this.#isOffsetNeeded.disabled = true;
this.#isOffsetNeeded.checked = false;
}
this.#validateBase();
}
/**
* Clears the base account.
*
*/
clearBaseAccount() {
this.#baseCode.value = "";
this.#base.innerText = "";
this.#validateBase();
}
/**
* Validates the form.
*
* @returns {boolean} true if valid, or false otherwise
*/
#validateForm() {
let isValid = true;
isValid = this.#validateBase() && isValid;
isValid = this.#validateTitle() && isValid;
return isValid;
}
/**
* Validates the base account.
*
* @returns {boolean} true if valid, or false otherwise
*/
#validateBase() {
if (this.#baseCode.value === "") {
this.#baseControl.classList.add("is-invalid");
this.#baseError.innerText = A_("Please select the base account.");
return false;
}
this.#baseControl.classList.remove("is-invalid");
this.#baseError.innerText = "";
return true;
}
/**
* Validates the title.
*
* @returns {boolean} true if valid, or false otherwise
*/
#validateTitle() {
this.#title.value = this.#title.value.trim();
if (this.#title.value === "") {
this.#title.classList.add("is-invalid");
this.#titleError.innerText = A_("Please fill in the title.");
return false;
}
this.#title.classList.remove("is-invalid");
this.#titleError.innerText = "";
return true;
}
/**
* The account form
* @type {AccountForm} the form
*/
static #form;
static initialize() {
this.#form = new AccountForm();
} }
initializeBaseAccountQuery();
} }
/** /**
* Initializes the query on the base account options. * The base account selector.
* *
* @private * @private
*/ */
function initializeBaseAccountQuery() { class BaseAccountSelector {
const query = document.getElementById("accounting-base-selector-query");
const optionList = document.getElementById("accounting-base-option-list"); /**
const options = Array.from(document.getElementsByClassName("accounting-base-option")); * The account form
const queryNoResult = document.getElementById("accounting-base-option-no-result"); * @type {AccountForm}
query.addEventListener("input", () => { */
if (query.value === "") { #form;
for (const option of options) {
option.classList.remove("d-none"); /**
} * The selector modal
optionList.classList.remove("d-none"); * @type {HTMLDivElement}
queryNoResult.classList.add("d-none"); */
return #modal;
/**
* The query input
* @type {HTMLInputElement}
*/
#query;
/**
* The error message when the query has no result
* @type {HTMLParagraphElement}
*/
#queryNoResult;
/**
* The option list
* @type {HTMLUListElement}
*/
#optionList;
/**
* The options
* @type {HTMLLIElement[]}
*/
#options;
/**
* The button to clear the base account value
* @type {HTMLButtonElement}
*/
#clearButton;
/**
* Constructs the base account selector.
*
* @param form {AccountForm} the form
*/
constructor(form) {
this.#form = form;
this.#modal = document.getElementById("accounting-base-selector-modal");
this.#query = document.getElementById("accounting-base-selector-query");
this.#optionList = document.getElementById("accounting-base-selector-option-list");
// noinspection JSValidateTypes
this.#options = Array.from(document.getElementsByClassName("accounting-base-selector-option"));
this.#clearButton = document.getElementById("accounting-base-selector-clear");
this.#queryNoResult = document.getElementById("accounting-base-selector-option-no-result");
this.#modal.addEventListener("hidden.bs.modal", () => {
this.#form.onBaseAccountSelectorClosed();
});
for (const option of this.#options) {
option.onclick = () => {
this.#form.setBaseAccount(option.dataset.code, option.dataset.content);
};
} }
let hasAnyMatched = false; this.#clearButton.onclick = () => {
for (const option of options) { this.#form.clearBaseAccount();
const queryValues = JSON.parse(option.dataset.queryValues); };
let isMatched = false; this.#initializeBaseAccountQuery();
for (const queryValue of queryValues) { }
if (queryValue.includes(query.value)) {
isMatched = true; /**
break; * Initializes the query.
*
*/
#initializeBaseAccountQuery() {
this.#query.addEventListener("input", () => {
if (this.#query.value === "") {
for (const option of this.#options) {
option.classList.remove("d-none");
}
this.#optionList.classList.remove("d-none");
this.#queryNoResult.classList.add("d-none");
return
}
let hasAnyMatched = false;
for (const option of this.#options) {
const queryValues = JSON.parse(option.dataset.queryValues);
let isMatched = false;
for (const queryValue of queryValues) {
if (queryValue.includes(this.#query.value)) {
isMatched = true;
break;
}
}
if (isMatched) {
option.classList.remove("d-none");
hasAnyMatched = true;
} else {
option.classList.add("d-none");
} }
} }
if (isMatched) { if (!hasAnyMatched) {
option.classList.remove("d-none"); this.#optionList.classList.add("d-none");
hasAnyMatched = true; this.#queryNoResult.classList.remove("d-none");
} else { } else {
option.classList.add("d-none"); this.#optionList.classList.remove("d-none");
this.#queryNoResult.classList.add("d-none");
}
});
}
/**
* The callback when the base account selector is shown.
*
* @param baseCode {string} the active base code
*/
onOpen(baseCode) {
for (const option of this.#options) {
if (option.dataset.code === baseCode) {
option.classList.add("active");
} else {
option.classList.remove("active");
} }
} }
if (!hasAnyMatched) { if (baseCode === "") {
optionList.classList.add("d-none"); this.#clearButton.classList.add("btn-secondary")
queryNoResult.classList.remove("d-none"); this.#clearButton.classList.remove("btn-danger");
this.#clearButton.disabled = true;
} else { } else {
optionList.classList.remove("d-none"); this.#clearButton.classList.add("btn-danger");
queryNoResult.classList.add("d-none"); this.#clearButton.classList.remove("btn-secondary")
this.#clearButton.disabled = false;
} }
});
}
/**
* Validates the form.
*
* @returns {boolean} true if valid, or false otherwise
* @private
*/
function validateForm() {
let isValid = true;
isValid = validateBase() && isValid;
isValid = validateTitle() && isValid;
return isValid;
}
/**
* Validates the base account.
*
* @returns {boolean} true if valid, or false otherwise
* @private
*/
function validateBase() {
const field = document.getElementById("accounting-base-code");
const error = document.getElementById("accounting-base-code-error");
const displayField = document.getElementById("accounting-base");
field.value = field.value.trim();
if (field.value === "") {
displayField.classList.add("is-invalid");
error.innerText = A_("Please select the base account.");
return false;
} }
displayField.classList.remove("is-invalid");
error.innerText = "";
return true;
}
/**
* Validates the title.
*
* @returns {boolean} true if valid, or false otherwise
* @private
*/
function validateTitle() {
const field = document.getElementById("accounting-title");
const error = document.getElementById("accounting-title-error");
field.value = field.value.trim();
if (field.value === "") {
field.classList.add("is-invalid");
error.innerText = A_("Please fill in the title.");
return false;
}
field.classList.remove("is-invalid");
error.innerText = "";
return true;
} }

View File

@ -41,9 +41,9 @@ First written: 2023/2/1
{% endif %} {% endif %}
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input id="accounting-base-code" type="hidden" name="base_code" value="{{ form.base_code.data|accounting_default }}"> <input id="accounting-base-code" type="hidden" name="base_code" value="{{ form.base_code.data|accounting_default }}">
<div id="accounting-base" class="form-control accounting-clickable accounting-material-text-field {% if form.base_code.data %} accounting-not-empty {% endif %} {% if form.base_code.errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-base-selector-modal"> <div id="accounting-base-control" class="form-control accounting-clickable accounting-material-text-field {% if form.base_code.data %} accounting-not-empty {% endif %} {% if form.base_code.errors %} is-invalid {% endif %}" data-bs-toggle="modal" data-bs-target="#accounting-base-selector-modal">
<label class="form-label" for="accounting-base">{{ A_("Base account") }}</label> <label class="form-label" for="accounting-base">{{ A_("Base account") }}</label>
<div id="accounting-base-content"> <div id="accounting-base">
{% if form.base_code.data %} {% if form.base_code.data %}
{% if form.base_code.errors %} {% if form.base_code.errors %}
{{ A_("(Unknown)") }} {{ A_("(Unknown)") }}
@ -53,7 +53,7 @@ First written: 2023/2/1
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div id="accounting-base-code-error" class="invalid-feedback">{% if form.base_code.errors %}{{ form.base_code.errors[0] }}{% endif %}</div> <div id="accounting-base-error" class="invalid-feedback">{% if form.base_code.errors %}{{ form.base_code.errors[0] }}{% endif %}</div>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
@ -99,21 +99,21 @@ First written: 2023/2/1
</label> </label>
</div> </div>
<ul id="accounting-base-option-list" class="list-group accounting-selector-list"> <ul id="accounting-base-selector-option-list" class="list-group accounting-selector-list">
{% for base in form.base_options %} {% for base in form.base_options %}
<li id="accounting-base-option-{{ base.code }}" class="list-group-item accounting-base-option accounting-clickable" data-code="{{ base.code }}" data-content="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}"> <li class="list-group-item accounting-clickable accounting-base-selector-option" data-code="{{ base.code }}" data-content="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}" data-bs-dismiss="modal">
{{ base }} {{ base }}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<p id="accounting-base-option-no-result" class="d-none">{{ A_("There is no data.") }}</p> <p id="accounting-base-selector-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ A_("Cancel") }}</button>
{% if form.base_code.data %} {% if form.base_code.data %}
<button id="accounting-btn-clear-base" type="button" class="btn btn-danger">{{ A_("Clear") }}</button> <button id="accounting-base-selector-clear" type="button" class="btn btn-danger" data-bs-dismiss="modal">{{ A_("Clear") }}</button>
{% else %} {% else %}
<button id="accounting-btn-clear-base" type="button" class="btn btn-secondary" disabled="disabled">{{ A_("Clear") }}</button> <button id="accounting-base-selector-clear" type="button" class="btn btn-secondary" disabled="disabled" data-bs-dismiss="modal">{{ A_("Clear") }}</button>
{% endif %} {% endif %}
</div> </div>
</div> </div>