Add BaseCombobox base class with listbox/option ARIA pattern
This commit is contained in:
@@ -225,15 +225,16 @@ class AccountForm {
|
|||||||
/**
|
/**
|
||||||
* The base account selector.
|
* The base account selector.
|
||||||
*
|
*
|
||||||
|
* @extends {BaseCombobox<BaseAccountOption>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class BaseAccountSelector {
|
class BaseAccountSelector extends BaseCombobox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account form
|
* The account form
|
||||||
* @type {AccountForm}
|
* @type {AccountForm}
|
||||||
*/
|
*/
|
||||||
form;
|
#form;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The selector modal
|
* The selector modal
|
||||||
@@ -241,12 +242,6 @@ class BaseAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#modal;
|
#modal;
|
||||||
|
|
||||||
/**
|
|
||||||
* The query input
|
|
||||||
* @type {HTMLInputElement}
|
|
||||||
*/
|
|
||||||
#query;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The error message when the query has no result
|
* The error message when the query has no result
|
||||||
* @type {HTMLParagraphElement}
|
* @type {HTMLParagraphElement}
|
||||||
@@ -259,12 +254,6 @@ class BaseAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#optionList;
|
#optionList;
|
||||||
|
|
||||||
/**
|
|
||||||
* The options
|
|
||||||
* @type {BaseAccountOption[]}
|
|
||||||
*/
|
|
||||||
#options;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The button to clear the base account value
|
* The button to clear the base account value
|
||||||
* @type {HTMLButtonElement}
|
* @type {HTMLButtonElement}
|
||||||
@@ -277,29 +266,32 @@ class BaseAccountSelector {
|
|||||||
* @param form {AccountForm} the form
|
* @param form {AccountForm} the form
|
||||||
*/
|
*/
|
||||||
constructor(form) {
|
constructor(form) {
|
||||||
this.form = form;
|
|
||||||
const prefix = "accounting-base-selector";
|
const prefix = "accounting-base-selector";
|
||||||
|
const query = document.getElementById(`${prefix}-query`);
|
||||||
|
const options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new BaseAccountOption(element, form.saveBaseAccount.bind(form)));
|
||||||
|
super(query, options);
|
||||||
|
this.#form = form;
|
||||||
this.#modal = document.getElementById(`${prefix}-modal`);
|
this.#modal = document.getElementById(`${prefix}-modal`);
|
||||||
this.#query = document.getElementById(`${prefix}-query`);
|
this.#modal.addEventListener("hidden.bs.modal", () => this.#form.onBaseAccountSelectorClosed());
|
||||||
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
||||||
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
||||||
this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new BaseAccountOption(this, element));
|
|
||||||
this.#clearButton = document.getElementById(`${prefix}-clear`);
|
|
||||||
|
|
||||||
this.#modal.addEventListener("hidden.bs.modal", () => this.form.onBaseAccountSelectorClosed());
|
this.#clearButton = document.getElementById(`${prefix}-clear`);
|
||||||
this.#query.oninput = () => this.#filterOptions();
|
this.#clearButton.onclick = () => this.#form.clearBaseAccount();
|
||||||
this.#clearButton.onclick = () => this.form.clearBaseAccount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters the options.
|
* Filters the options.
|
||||||
*
|
*
|
||||||
|
* @override
|
||||||
*/
|
*/
|
||||||
#filterOptions() {
|
filterOptions() {
|
||||||
|
this.shownOptions = [];
|
||||||
let isAnyMatched = false;
|
let isAnyMatched = false;
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
if (option.isMatched(this.#query.value)) {
|
if (option.isMatched(this.query.value)) {
|
||||||
option.setShown(true);
|
option.setShown(true);
|
||||||
|
this.shownOptions.push(option);
|
||||||
isAnyMatched = true;
|
isAnyMatched = true;
|
||||||
} else {
|
} else {
|
||||||
option.setShown(false);
|
option.setShown(false);
|
||||||
@@ -319,12 +311,11 @@ class BaseAccountSelector {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.#query.value = "";
|
this.query.value = "";
|
||||||
this.#filterOptions();
|
this.filterOptions();
|
||||||
for (const option of this.#options) {
|
this.query.removeAttribute("aria-activedescendant");
|
||||||
option.setActive(option.code === this.form.baseCode);
|
this.selectOption(this.shownOptions.find((option) => option.code === this.#form.baseCode));
|
||||||
}
|
if (this.#form.baseCode === null) {
|
||||||
if (this.form.baseCode === null) {
|
|
||||||
this.#clearButton.classList.add("btn-secondary")
|
this.#clearButton.classList.add("btn-secondary")
|
||||||
this.#clearButton.classList.remove("btn-danger");
|
this.#clearButton.classList.remove("btn-danger");
|
||||||
this.#clearButton.disabled = true;
|
this.#clearButton.disabled = true;
|
||||||
@@ -341,13 +332,7 @@ class BaseAccountSelector {
|
|||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class BaseAccountOption {
|
class BaseAccountOption extends BaseOption {
|
||||||
|
|
||||||
/**
|
|
||||||
* The element
|
|
||||||
* @type {HTMLLIElement}
|
|
||||||
*/
|
|
||||||
#element;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account code
|
* The account code
|
||||||
@@ -370,16 +355,16 @@ class BaseAccountOption {
|
|||||||
/**
|
/**
|
||||||
* Constructs the account in the base account selector.
|
* Constructs the account in the base account selector.
|
||||||
*
|
*
|
||||||
* @param selector {BaseAccountSelector} the base account selector
|
|
||||||
* @param element {HTMLLIElement} the element
|
* @param element {HTMLLIElement} the element
|
||||||
|
* @param save {function(BaseAccountOption): void} the callback to save the option
|
||||||
*/
|
*/
|
||||||
constructor(selector, element) {
|
constructor(element, save) {
|
||||||
this.#element = element;
|
super(element);
|
||||||
this.code = element.dataset.code;
|
this.code = element.dataset.code;
|
||||||
this.text = element.dataset.text;
|
this.text = element.dataset.text;
|
||||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||||
|
|
||||||
this.#element.onclick = () => selector.form.saveBaseAccount(this);
|
element.onclick = () => save(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -399,30 +384,4 @@ class BaseAccountOption {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is shown.
|
|
||||||
*
|
|
||||||
* @param isShown {boolean} true to show, or false otherwise
|
|
||||||
*/
|
|
||||||
setShown(isShown) {
|
|
||||||
if (isShown) {
|
|
||||||
this.#element.classList.remove("d-none");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.add("d-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is active.
|
|
||||||
*
|
|
||||||
* @param isActive {boolean} true if active, or false otherwise
|
|
||||||
*/
|
|
||||||
setActive(isActive) {
|
|
||||||
if (isActive) {
|
|
||||||
this.#element.classList.add("active");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.remove("active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
/* The Mia! Accounting Project
|
||||||
|
* base-combobox.js: The JavaScript for the base abstract combobox
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Copyright (c) 2026 imacat.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Author: imacat@mail.imacat.idv.tw (imacat)
|
||||||
|
* First written: 2026/4/16
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base abstract combobox.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @template {BaseOption} T
|
||||||
|
*/
|
||||||
|
class BaseCombobox {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The query input
|
||||||
|
* @type {HTMLInputElement}
|
||||||
|
*/
|
||||||
|
query;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options
|
||||||
|
* @type {T[]}
|
||||||
|
*/
|
||||||
|
options;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The options that are shown
|
||||||
|
* @type {T[]}
|
||||||
|
*/
|
||||||
|
shownOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a base abstract combobox.
|
||||||
|
*
|
||||||
|
* @param query {HTMLInputElement} the query input
|
||||||
|
* @param options {T[]} the options
|
||||||
|
*/
|
||||||
|
constructor(query, options) {
|
||||||
|
this.query = query;
|
||||||
|
this.query.oninput = () => this.filterOptions();
|
||||||
|
this.query.onkeydown = this.onQueryKeyDown.bind(this);
|
||||||
|
this.options = options;
|
||||||
|
this.shownOptions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions when keys are pressed on the query input.
|
||||||
|
*
|
||||||
|
* @param event {KeyboardEvent} the key event
|
||||||
|
*/
|
||||||
|
onQueryKeyDown(event) {
|
||||||
|
if (this.shownOptions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentID = this.query.getAttribute("aria-activedescendant");
|
||||||
|
const currentIndex = this.shownOptions.findIndex((option) => option.elementID === currentID);
|
||||||
|
|
||||||
|
let newIndex;
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowUp":
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
newIndex = this.shownOptions.length - 1;
|
||||||
|
} else {
|
||||||
|
newIndex = (currentIndex - 1 + this.shownOptions.length) % this.shownOptions.length;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
newIndex = 0;
|
||||||
|
} else {
|
||||||
|
newIndex = (currentIndex + 1) % this.shownOptions.length;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Home":
|
||||||
|
if (this.query.value !== "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newIndex = 0;
|
||||||
|
break;
|
||||||
|
case "End":
|
||||||
|
if (this.query.value !== "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newIndex = this.shownOptions.length - 1;
|
||||||
|
break;
|
||||||
|
case "PageUp":
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
newIndex = this.shownOptions.length - 1;
|
||||||
|
} else {
|
||||||
|
newIndex = Math.max(currentIndex - 10, 0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "PageDown":
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
newIndex = 0;
|
||||||
|
} else {
|
||||||
|
newIndex = Math.min(currentIndex + 10, this.shownOptions.length - 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Enter":
|
||||||
|
event.preventDefault();
|
||||||
|
if (currentIndex !== -1) {
|
||||||
|
this.shownOptions[currentIndex].click();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case "Escape":
|
||||||
|
if (this.query.value !== "") {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.query.value = "";
|
||||||
|
this.filterOptions();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
this.selectOption(this.shownOptions[newIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the options.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
filterOptions() {
|
||||||
|
throw new Error("Method not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects an option.
|
||||||
|
*
|
||||||
|
* @param option {T|undefined} the option.
|
||||||
|
*/
|
||||||
|
selectOption(option) {
|
||||||
|
this.options.forEach((opt) => opt.setActive(false));
|
||||||
|
if (option === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
option.setActive(true);
|
||||||
|
this.query.setAttribute("aria-activedescendant", option.elementID);
|
||||||
|
option.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base abstract option
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
*/
|
||||||
|
class BaseOption {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The element
|
||||||
|
* @type {HTMLLIElement}
|
||||||
|
*/
|
||||||
|
#element;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The element ID
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
elementID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the base abstract option.
|
||||||
|
*
|
||||||
|
* @param element {HTMLLIElement} the element
|
||||||
|
*/
|
||||||
|
constructor(element) {
|
||||||
|
this.#element = element;
|
||||||
|
this.elementID = element.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the option is shown.
|
||||||
|
*
|
||||||
|
* @param isShown {boolean} true to show, or false otherwise
|
||||||
|
*/
|
||||||
|
setShown(isShown) {
|
||||||
|
if (isShown) {
|
||||||
|
this.#element.classList.remove("d-none");
|
||||||
|
} else {
|
||||||
|
this.#element.classList.add("d-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the option is active.
|
||||||
|
*
|
||||||
|
* @param isActive {boolean} true if active, or false otherwise
|
||||||
|
*/
|
||||||
|
setActive(isActive) {
|
||||||
|
if (isActive) {
|
||||||
|
this.#element.classList.add("active");
|
||||||
|
this.#element.ariaSelected = "true";
|
||||||
|
} else {
|
||||||
|
this.#element.classList.remove("active");
|
||||||
|
this.#element.ariaSelected = "false";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicks the option.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
click() {
|
||||||
|
this.#element.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolls the option into view.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
scrollIntoView() {
|
||||||
|
this.#element.scrollIntoView({block: "nearest"});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,15 +25,16 @@
|
|||||||
/**
|
/**
|
||||||
* The account selector.
|
* The account selector.
|
||||||
*
|
*
|
||||||
|
* @extends {BaseCombobox<BaseJournalEntryAccountOption>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class JournalEntryAccountSelector {
|
class JournalEntryAccountSelector extends BaseCombobox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The line item editor
|
* The line item editor
|
||||||
* @type {JournalEntryLineItemEditor}
|
* @type {JournalEntryLineItemEditor}
|
||||||
*/
|
*/
|
||||||
lineItemEditor;
|
#lineItemEditor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Either "debit" or "credit"
|
* Either "debit" or "credit"
|
||||||
@@ -47,12 +48,6 @@ class JournalEntryAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#clearButton
|
#clearButton
|
||||||
|
|
||||||
/**
|
|
||||||
* The query input
|
|
||||||
* @type {HTMLInputElement}
|
|
||||||
*/
|
|
||||||
#query;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The error message when the query has no result
|
* The error message when the query has no result
|
||||||
* @type {HTMLParagraphElement}
|
* @type {HTMLParagraphElement}
|
||||||
@@ -65,15 +60,9 @@ class JournalEntryAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#optionList;
|
#optionList;
|
||||||
|
|
||||||
/**
|
|
||||||
* The options
|
|
||||||
* @type {JournalEntryAccountOption[]}
|
|
||||||
*/
|
|
||||||
#options;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The more item to show all accounts
|
* The more item to show all accounts
|
||||||
* @type {HTMLLIElement}
|
* @type {MoreItems}
|
||||||
*/
|
*/
|
||||||
#more;
|
#more;
|
||||||
|
|
||||||
@@ -90,40 +79,47 @@ class JournalEntryAccountSelector {
|
|||||||
* @param debitCredit {string} either "debit" or "credit"
|
* @param debitCredit {string} either "debit" or "credit"
|
||||||
*/
|
*/
|
||||||
constructor(lineItemEditor, debitCredit) {
|
constructor(lineItemEditor, debitCredit) {
|
||||||
this.lineItemEditor = lineItemEditor;
|
|
||||||
this.#debitCredit = debitCredit;
|
|
||||||
const prefix = `accounting-account-selector-${debitCredit}`;
|
const prefix = `accounting-account-selector-${debitCredit}`;
|
||||||
this.#query = document.getElementById(`${prefix}-query`);
|
const query = document.getElementById(`${prefix}-query`);
|
||||||
|
const options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new JournalEntryAccountOption(element, lineItemEditor.saveAccount.bind(lineItemEditor)));
|
||||||
|
super(query, options);
|
||||||
|
this.#lineItemEditor = lineItemEditor;
|
||||||
|
this.#debitCredit = debitCredit;
|
||||||
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
||||||
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
||||||
this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new JournalEntryAccountOption(this, element));
|
const moreElement = document.getElementById(`${prefix}-more`);
|
||||||
this.#more = document.getElementById(`${prefix}-more`);
|
this.#more = new MoreItems(moreElement);
|
||||||
this.#clearButton = document.getElementById(`${prefix}-btn-clear`);
|
moreElement.onclick = () => {
|
||||||
|
|
||||||
this.#more.onclick = () => {
|
|
||||||
this.#isShowMore = true;
|
this.#isShowMore = true;
|
||||||
this.#more.classList.add("d-none");
|
this.#more.setShown(false);
|
||||||
this.#filterOptions();
|
this.filterOptions();
|
||||||
};
|
};
|
||||||
this.#query.oninput = () => this.#filterOptions();
|
|
||||||
this.#clearButton.onclick = () => this.lineItemEditor.clearAccount();
|
this.#clearButton = document.getElementById(`${prefix}-btn-clear`);
|
||||||
|
this.#clearButton.onclick = () => this.#lineItemEditor.clearAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters the options.
|
* Filters the options.
|
||||||
*
|
*
|
||||||
|
* @override
|
||||||
*/
|
*/
|
||||||
#filterOptions() {
|
filterOptions() {
|
||||||
|
this.shownOptions = [];
|
||||||
const codesInUse = this.#getCodesUsedInForm();
|
const codesInUse = this.#getCodesUsedInForm();
|
||||||
let isAnyMatched = false;
|
let isAnyMatched = false;
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
if (option.isMatched(this.#isShowMore, codesInUse, this.#query.value)) {
|
if (option.isMatched(this.#isShowMore, codesInUse, this.query.value)) {
|
||||||
option.setShown(true);
|
option.setShown(true);
|
||||||
|
this.shownOptions.push(option);
|
||||||
isAnyMatched = true;
|
isAnyMatched = true;
|
||||||
} else {
|
} else {
|
||||||
option.setShown(false);
|
option.setShown(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!this.#isShowMore) {
|
||||||
|
this.shownOptions.push(this.#more);
|
||||||
|
}
|
||||||
if (!isAnyMatched && this.#isShowMore) {
|
if (!isAnyMatched && this.#isShowMore) {
|
||||||
this.#optionList.classList.add("d-none");
|
this.#optionList.classList.add("d-none");
|
||||||
this.#queryNoResult.classList.remove("d-none");
|
this.#queryNoResult.classList.remove("d-none");
|
||||||
@@ -139,9 +135,9 @@ class JournalEntryAccountSelector {
|
|||||||
* @return {string[]} the account codes that are used in the form
|
* @return {string[]} the account codes that are used in the form
|
||||||
*/
|
*/
|
||||||
#getCodesUsedInForm() {
|
#getCodesUsedInForm() {
|
||||||
const inUse = this.lineItemEditor.form.getAccountCodesUsed(this.#debitCredit);
|
const inUse = this.#lineItemEditor.form.getAccountCodesUsed(this.#debitCredit);
|
||||||
if (this.lineItemEditor.account !== null) {
|
if (this.#lineItemEditor.account !== null) {
|
||||||
inUse.push(this.lineItemEditor.account.code);
|
inUse.push(this.#lineItemEditor.account.code);
|
||||||
}
|
}
|
||||||
return inUse
|
return inUse
|
||||||
}
|
}
|
||||||
@@ -151,14 +147,13 @@ class JournalEntryAccountSelector {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.#query.value = "";
|
this.query.value = "";
|
||||||
this.#isShowMore = false;
|
this.#isShowMore = false;
|
||||||
this.#more.classList.remove("d-none");
|
this.#more.setShown(true);
|
||||||
this.#filterOptions();
|
this.filterOptions();
|
||||||
for (const option of this.#options) {
|
this.query.removeAttribute("aria-activedescendant");
|
||||||
option.setActive(this.lineItemEditor.account !== null && option.code === this.lineItemEditor.account.code);
|
this.selectOption(this.shownOptions.find((option) => this.#lineItemEditor.account !== null && option.code === this.#lineItemEditor.account.code));
|
||||||
}
|
if (this.#lineItemEditor.account === null) {
|
||||||
if (this.lineItemEditor.account === null) {
|
|
||||||
this.#clearButton.classList.add("btn-secondary");
|
this.#clearButton.classList.add("btn-secondary");
|
||||||
this.#clearButton.classList.remove("btn-danger");
|
this.#clearButton.classList.remove("btn-danger");
|
||||||
this.#clearButton.disabled = true;
|
this.#clearButton.disabled = true;
|
||||||
@@ -169,6 +164,17 @@ class JournalEntryAccountSelector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects an option.
|
||||||
|
*
|
||||||
|
* @param option {BaseJournalEntryAccountOption|undefined} the option.
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
selectOption(option) {
|
||||||
|
this.#more.setActive(false);
|
||||||
|
super.selectOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the account selector instances.
|
* Returns the account selector instances.
|
||||||
*
|
*
|
||||||
@@ -186,23 +192,37 @@ class JournalEntryAccountSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An account option
|
* The base abstract account option
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class JournalEntryAccountOption {
|
class BaseJournalEntryAccountOption extends BaseOption {
|
||||||
|
|
||||||
/**
|
|
||||||
* The element
|
|
||||||
* @type {HTMLLIElement}
|
|
||||||
*/
|
|
||||||
#element;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account code
|
* The account code
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
code;
|
code = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the account matches the query.
|
||||||
|
*
|
||||||
|
* @param isShowMore {boolean} true to show all accounts, or false to show only those in use
|
||||||
|
* @param codesInUse {string[]} the account codes that are used in the form
|
||||||
|
* @param query {string} the query term
|
||||||
|
* @return {boolean} true if the option matches, or false otherwise
|
||||||
|
*/
|
||||||
|
isMatched(isShowMore, codesInUse, query) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An account option
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class JournalEntryAccountOption extends BaseJournalEntryAccountOption {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account title
|
* The account title
|
||||||
@@ -237,11 +257,11 @@ class JournalEntryAccountOption {
|
|||||||
/**
|
/**
|
||||||
* Constructs the account in the account selector.
|
* Constructs the account in the account selector.
|
||||||
*
|
*
|
||||||
* @param selector {JournalEntryAccountSelector} the account selector
|
|
||||||
* @param element {HTMLLIElement} the element
|
* @param element {HTMLLIElement} the element
|
||||||
|
* @param save {function(JournalEntryAccountOption): void} the callback to save the option
|
||||||
*/
|
*/
|
||||||
constructor(selector, element) {
|
constructor(element, save) {
|
||||||
this.#element = element;
|
super(element);
|
||||||
this.code = element.dataset.code;
|
this.code = element.dataset.code;
|
||||||
this.title = element.dataset.title;
|
this.title = element.dataset.title;
|
||||||
this.text = element.dataset.text;
|
this.text = element.dataset.text;
|
||||||
@@ -249,7 +269,7 @@ class JournalEntryAccountOption {
|
|||||||
this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");
|
this.isNeedOffset = element.classList.contains("accounting-account-is-need-offset");
|
||||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||||
|
|
||||||
this.#element.onclick = () => selector.lineItemEditor.saveAccount(this);
|
element.onclick = () => save(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -259,6 +279,7 @@ class JournalEntryAccountOption {
|
|||||||
* @param codesInUse {string[]} the account codes that are used in the form
|
* @param codesInUse {string[]} the account codes that are used in the form
|
||||||
* @param query {string} the query term
|
* @param query {string} the query term
|
||||||
* @return {boolean} true if the option matches, or false otherwise
|
* @return {boolean} true if the option matches, or false otherwise
|
||||||
|
* @override
|
||||||
*/
|
*/
|
||||||
isMatched(isShowMore, codesInUse, query) {
|
isMatched(isShowMore, codesInUse, query) {
|
||||||
return this.#isInUseMatched(isShowMore, codesInUse) && this.#isQueryMatched(query);
|
return this.#isInUseMatched(isShowMore, codesInUse) && this.#isQueryMatched(query);
|
||||||
@@ -292,30 +313,12 @@ class JournalEntryAccountOption {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Sets whether the option is shown.
|
/**
|
||||||
*
|
* The more item to show all accounts.
|
||||||
* @param isShown {boolean} true to show, or false otherwise
|
*
|
||||||
*/
|
* @private
|
||||||
setShown(isShown) {
|
*/
|
||||||
if (isShown) {
|
class MoreItems extends BaseJournalEntryAccountOption {
|
||||||
this.#element.classList.remove("d-none");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.add("d-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is active.
|
|
||||||
*
|
|
||||||
* @param isActive {boolean} true if active, or false otherwise
|
|
||||||
*/
|
|
||||||
setActive(isActive) {
|
|
||||||
if (isActive) {
|
|
||||||
this.#element.classList.add("active");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.remove("active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -843,15 +843,16 @@ class RecurringItemEditor {
|
|||||||
/**
|
/**
|
||||||
* The account selector for the recurring item editor.
|
* The account selector for the recurring item editor.
|
||||||
*
|
*
|
||||||
|
* @extends {BaseCombobox<RecurringAccount>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class RecurringAccountSelector {
|
class RecurringAccountSelector extends BaseCombobox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The recurring item editor
|
* The recurring item editor
|
||||||
* @type {RecurringItemEditor}
|
* @type {RecurringItemEditor}
|
||||||
*/
|
*/
|
||||||
editor;
|
#editor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Either "expense" or "income"
|
* Either "expense" or "income"
|
||||||
@@ -859,12 +860,6 @@ class RecurringAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#expenseIncome;
|
#expenseIncome;
|
||||||
|
|
||||||
/**
|
|
||||||
* The query input
|
|
||||||
* @type {HTMLInputElement}
|
|
||||||
*/
|
|
||||||
#query;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The error message when the query has no result
|
* The error message when the query has no result
|
||||||
* @type {HTMLParagraphElement}
|
* @type {HTMLParagraphElement}
|
||||||
@@ -877,12 +872,6 @@ class RecurringAccountSelector {
|
|||||||
*/
|
*/
|
||||||
#optionList;
|
#optionList;
|
||||||
|
|
||||||
/**
|
|
||||||
* The account options
|
|
||||||
* @type {RecurringAccount[]}
|
|
||||||
*/
|
|
||||||
#options;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The button to clear the account
|
* The button to clear the account
|
||||||
* @type {HTMLButtonElement}
|
* @type {HTMLButtonElement}
|
||||||
@@ -895,28 +884,31 @@ class RecurringAccountSelector {
|
|||||||
* @param editor {RecurringItemEditor} the recurring item editor
|
* @param editor {RecurringItemEditor} the recurring item editor
|
||||||
*/
|
*/
|
||||||
constructor(editor) {
|
constructor(editor) {
|
||||||
this.editor = editor;
|
|
||||||
this.#expenseIncome = editor.expenseIncome;
|
|
||||||
const prefix = `accounting-recurring-accounting-selector-${editor.expenseIncome}`;
|
const prefix = `accounting-recurring-accounting-selector-${editor.expenseIncome}`;
|
||||||
this.#query = document.getElementById(`${prefix}-query`);
|
const query = document.getElementById(`${prefix}-query`);
|
||||||
|
const options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new RecurringAccount(element, editor.saveAccount.bind(editor)));
|
||||||
|
super(query, options);
|
||||||
|
this.#editor = editor;
|
||||||
|
this.#expenseIncome = editor.expenseIncome;
|
||||||
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
||||||
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
||||||
this.#options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new RecurringAccount(this, element));
|
|
||||||
this.#clearButton = document.getElementById(`${prefix}-clear`);
|
|
||||||
|
|
||||||
this.#query.oninput = () => this.#filterOptions();
|
this.#clearButton = document.getElementById(`${prefix}-clear`);
|
||||||
this.#clearButton.onclick = () => this.editor.clearAccount();
|
this.#clearButton.onclick = () => this.#editor.clearAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters the options.
|
* Filters the options.
|
||||||
*
|
*
|
||||||
|
* @override
|
||||||
*/
|
*/
|
||||||
#filterOptions() {
|
filterOptions() {
|
||||||
|
this.shownOptions = [];
|
||||||
let isAnyMatched = false;
|
let isAnyMatched = false;
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
if (option.isMatched(this.#query.value)) {
|
if (option.isMatched(this.query.value)) {
|
||||||
option.setShown(true);
|
option.setShown(true);
|
||||||
|
this.shownOptions.push(option);
|
||||||
isAnyMatched = true;
|
isAnyMatched = true;
|
||||||
} else {
|
} else {
|
||||||
option.setShown(false);
|
option.setShown(false);
|
||||||
@@ -936,12 +928,11 @@ class RecurringAccountSelector {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.#query.value = "";
|
this.query.value = "";
|
||||||
this.#filterOptions();
|
this.filterOptions();
|
||||||
for (const option of this.#options) {
|
this.query.removeAttribute("aria-activedescendant");
|
||||||
option.setActive(option.code === this.editor.accountCode);
|
this.selectOption(this.shownOptions.find((option) => option.code === this.#editor.accountCode));
|
||||||
}
|
if (this.#editor.accountCode === null) {
|
||||||
if (this.editor.accountCode === null) {
|
|
||||||
this.#clearButton.classList.add("btn-secondary");
|
this.#clearButton.classList.add("btn-secondary");
|
||||||
this.#clearButton.classList.remove("btn-danger");
|
this.#clearButton.classList.remove("btn-danger");
|
||||||
this.#clearButton.disabled = true;
|
this.#clearButton.disabled = true;
|
||||||
@@ -958,13 +949,7 @@ class RecurringAccountSelector {
|
|||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class RecurringAccount {
|
class RecurringAccount extends BaseOption {
|
||||||
|
|
||||||
/**
|
|
||||||
* The element
|
|
||||||
* @type {HTMLLIElement}
|
|
||||||
*/
|
|
||||||
#element;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The account code
|
* The account code
|
||||||
@@ -987,16 +972,16 @@ class RecurringAccount {
|
|||||||
/**
|
/**
|
||||||
* Constructs the account in the account selector for the recurring item editor.
|
* Constructs the account in the account selector for the recurring item editor.
|
||||||
*
|
*
|
||||||
* @param selector {RecurringAccountSelector} the account selector
|
|
||||||
* @param element {HTMLLIElement} the element
|
* @param element {HTMLLIElement} the element
|
||||||
|
* @param save {function(RecurringAccount): void} the callback to save the option
|
||||||
*/
|
*/
|
||||||
constructor(selector, element) {
|
constructor(element, save) {
|
||||||
this.#element = element;
|
super(element);
|
||||||
this.code = element.dataset.code;
|
this.code = element.dataset.code;
|
||||||
this.text = element.dataset.text;
|
this.text = element.dataset.text;
|
||||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||||
|
|
||||||
this.#element.onclick = () => selector.editor.saveAccount(this);
|
element.onclick = () => save(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1016,30 +1001,4 @@ class RecurringAccount {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is shown.
|
|
||||||
*
|
|
||||||
* @param isShown {boolean} true to show, or false otherwise
|
|
||||||
*/
|
|
||||||
setShown(isShown) {
|
|
||||||
if (isShown) {
|
|
||||||
this.#element.classList.remove("d-none");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.add("d-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is active.
|
|
||||||
*
|
|
||||||
* @param isActive {boolean} true if active, or false otherwise
|
|
||||||
*/
|
|
||||||
setActive(isActive) {
|
|
||||||
if (isActive) {
|
|
||||||
this.#element.classList.add("active");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.remove("active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,27 +25,16 @@
|
|||||||
/**
|
/**
|
||||||
* The original line item selector.
|
* The original line item selector.
|
||||||
*
|
*
|
||||||
|
* @extends {BaseCombobox<OriginalLineItem>}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class OriginalLineItemSelector {
|
class OriginalLineItemSelector extends BaseCombobox {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The line item editor
|
* The line item editor
|
||||||
* @type {JournalEntryLineItemEditor}
|
* @type {JournalEntryLineItemEditor}
|
||||||
*/
|
*/
|
||||||
lineItemEditor;
|
#lineItemEditor;
|
||||||
|
|
||||||
/**
|
|
||||||
* The prefix of the HTML ID and class names
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
#prefix = "accounting-original-line-item-selector";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The query input
|
|
||||||
* @type {HTMLInputElement}
|
|
||||||
*/
|
|
||||||
#query;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The error message when the query has no result
|
* The error message when the query has no result
|
||||||
@@ -59,12 +48,6 @@ class OriginalLineItemSelector {
|
|||||||
*/
|
*/
|
||||||
#optionList;
|
#optionList;
|
||||||
|
|
||||||
/**
|
|
||||||
* The options
|
|
||||||
* @type {OriginalLineItem[]}
|
|
||||||
*/
|
|
||||||
#options;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The options by their ID
|
* The options by their ID
|
||||||
* @type {Object.<string, OriginalLineItem>}
|
* @type {Object.<string, OriginalLineItem>}
|
||||||
@@ -88,16 +71,17 @@ class OriginalLineItemSelector {
|
|||||||
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
|
* @param lineItemEditor {JournalEntryLineItemEditor} the line item editor
|
||||||
*/
|
*/
|
||||||
constructor(lineItemEditor) {
|
constructor(lineItemEditor) {
|
||||||
this.lineItemEditor = lineItemEditor;
|
const prefix = "accounting-original-line-item-selector";
|
||||||
this.#query = document.getElementById(`${this.#prefix}-query`);
|
const query = document.getElementById(`${prefix}-query`);
|
||||||
this.#queryNoResult = document.getElementById(`${this.#prefix}-option-no-result`);
|
const options = Array.from(document.getElementsByClassName(`${prefix}-option`)).map((element) => new OriginalLineItem(element, lineItemEditor.saveOriginalLineItem.bind(lineItemEditor), lineItemEditor.form));
|
||||||
this.#optionList = document.getElementById(`${this.#prefix}-option-list`);
|
super(query, options);
|
||||||
this.#options = Array.from(document.getElementsByClassName(`${this.#prefix}-option`)).map((element) => new OriginalLineItem(this, element));
|
this.#lineItemEditor = lineItemEditor;
|
||||||
|
this.#queryNoResult = document.getElementById(`${prefix}-option-no-result`);
|
||||||
|
this.#optionList = document.getElementById(`${prefix}-option-list`);
|
||||||
this.#optionById = {};
|
this.#optionById = {};
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
this.#optionById[option.id] = option;
|
this.#optionById[option.id] = option;
|
||||||
}
|
}
|
||||||
this.#query.oninput = () => this.#filterOptions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -127,7 +111,7 @@ class OriginalLineItemSelector {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
#updateNetBalances() {
|
#updateNetBalances() {
|
||||||
const otherLineItems = this.lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.lineItemEditor.lineItem);
|
const otherLineItems = this.#lineItemEditor.form.getLineItems().filter((lineItem) => lineItem !== this.#lineItemEditor.lineItem);
|
||||||
const otherOffsets = {}
|
const otherOffsets = {}
|
||||||
for (const otherLineItem of otherLineItems) {
|
for (const otherLineItem of otherLineItems) {
|
||||||
const otherOriginalLineItemId = otherLineItem.originalLineItemId;
|
const otherOriginalLineItemId = otherLineItem.originalLineItemId;
|
||||||
@@ -140,7 +124,7 @@ class OriginalLineItemSelector {
|
|||||||
}
|
}
|
||||||
otherOffsets[otherOriginalLineItemId] = otherOffsets[otherOriginalLineItemId].plus(amount);
|
otherOffsets[otherOriginalLineItemId] = otherOffsets[otherOriginalLineItemId].plus(amount);
|
||||||
}
|
}
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
if (option.id in otherOffsets) {
|
if (option.id in otherOffsets) {
|
||||||
option.updateNetBalance(otherOffsets[option.id]);
|
option.updateNetBalance(otherOffsets[option.id]);
|
||||||
} else {
|
} else {
|
||||||
@@ -152,12 +136,15 @@ class OriginalLineItemSelector {
|
|||||||
/**
|
/**
|
||||||
* Filters the options.
|
* Filters the options.
|
||||||
*
|
*
|
||||||
|
* @override
|
||||||
*/
|
*/
|
||||||
#filterOptions() {
|
filterOptions() {
|
||||||
|
this.shownOptions = [];
|
||||||
let isAnyMatched = false;
|
let isAnyMatched = false;
|
||||||
for (const option of this.#options) {
|
for (const option of this.options) {
|
||||||
if (option.isMatched(this.#debitCredit, this.#currencyCode, this.#query.value)) {
|
if (option.isMatched(this.#debitCredit, this.#currencyCode, this.query.value)) {
|
||||||
option.setShown(true);
|
option.setShown(true);
|
||||||
|
this.shownOptions.push(option);
|
||||||
isAnyMatched = true;
|
isAnyMatched = true;
|
||||||
} else {
|
} else {
|
||||||
option.setShown(false);
|
option.setShown(false);
|
||||||
@@ -177,14 +164,13 @@ class OriginalLineItemSelector {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.#currencyCode = this.lineItemEditor.currencyCode;
|
this.#currencyCode = this.#lineItemEditor.currencyCode;
|
||||||
this.#debitCredit = this.lineItemEditor.debitCredit;
|
this.#debitCredit = this.#lineItemEditor.debitCredit;
|
||||||
for (const option of this.#options) {
|
this.query.value = "";
|
||||||
option.setActive(option.id === this.lineItemEditor.originalLineItemId);
|
|
||||||
}
|
|
||||||
this.#query.value = "";
|
|
||||||
this.#updateNetBalances();
|
this.#updateNetBalances();
|
||||||
this.#filterOptions();
|
this.filterOptions();
|
||||||
|
this.query.removeAttribute("aria-activedescendant");
|
||||||
|
this.selectOption(this.shownOptions.find((option) => option.id === this.#lineItemEditor.originalLineItemId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +179,7 @@ class OriginalLineItemSelector {
|
|||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
class OriginalLineItem {
|
class OriginalLineItem extends BaseOption {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The journal entry form
|
* The journal entry form
|
||||||
@@ -201,12 +187,6 @@ class OriginalLineItem {
|
|||||||
*/
|
*/
|
||||||
#form;
|
#form;
|
||||||
|
|
||||||
/**
|
|
||||||
* The element
|
|
||||||
* @type {HTMLLIElement}
|
|
||||||
*/
|
|
||||||
#element;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID
|
* The ID
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -276,12 +256,13 @@ class OriginalLineItem {
|
|||||||
/**
|
/**
|
||||||
* Constructs an original line item.
|
* Constructs an original line item.
|
||||||
*
|
*
|
||||||
* @param selector {OriginalLineItemSelector} the original line item selector
|
|
||||||
* @param element {HTMLLIElement} the element
|
* @param element {HTMLLIElement} the element
|
||||||
|
* @param save {function(OriginalLineItem): void} the callback to save the option
|
||||||
|
* @param form {JournalEntryForm} the journal entry form
|
||||||
*/
|
*/
|
||||||
constructor(selector, element) {
|
constructor(element, save, form) {
|
||||||
this.#form = selector.lineItemEditor.form;
|
super(element);
|
||||||
this.#element = element;
|
this.#form = form;
|
||||||
this.id = element.dataset.id;
|
this.id = element.dataset.id;
|
||||||
this.date = element.dataset.date;
|
this.date = element.dataset.date;
|
||||||
this.#debitCredit = element.dataset.debitCredit;
|
this.#debitCredit = element.dataset.debitCredit;
|
||||||
@@ -293,7 +274,8 @@ class OriginalLineItem {
|
|||||||
this.netBalanceText = document.getElementById(`accounting-original-line-item-selector-option-${this.id}-net-balance`);
|
this.netBalanceText = document.getElementById(`accounting-original-line-item-selector-option-${this.id}-net-balance`);
|
||||||
this.text = element.dataset.text;
|
this.text = element.dataset.text;
|
||||||
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
this.#queryValues = JSON.parse(element.dataset.queryValues);
|
||||||
this.#element.onclick = () => selector.lineItemEditor.saveOriginalLineItem(this);
|
|
||||||
|
element.onclick = () => save(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -382,30 +364,4 @@ class OriginalLineItem {
|
|||||||
const whole = Number(this.netBalance.minus(frac));
|
const whole = Number(this.netBalance.minus(frac));
|
||||||
return String(whole) + String(frac).substring(1);
|
return String(whole) + String(frac).substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is shown.
|
|
||||||
*
|
|
||||||
* @param isShown {boolean} true to show, or false otherwise
|
|
||||||
*/
|
|
||||||
setShown(isShown) {
|
|
||||||
if (isShown) {
|
|
||||||
this.#element.classList.remove("d-none");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.add("d-none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the option is active.
|
|
||||||
*
|
|
||||||
* @param isActive {boolean} true if active, or false otherwise
|
|
||||||
*/
|
|
||||||
setActive(isActive) {
|
|
||||||
if (isActive) {
|
|
||||||
this.#element.classList.add("active");
|
|
||||||
} else {
|
|
||||||
this.#element.classList.remove("active");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ First written: 2023/2/1
|
|||||||
{% extends "accounting/base.html" %}
|
{% extends "accounting/base.html" %}
|
||||||
|
|
||||||
{% block accounting_scripts %}
|
{% block accounting_scripts %}
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/base-combobox.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/account-form.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/account-form.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -92,16 +93,16 @@ First written: 2023/2/1
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
<input id="accounting-base-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
<input id="accounting-base-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required" role="combobox" aria-expanded="true" aria-controls="accounting-base-selector-option-list" aria-autocomplete="list" aria-activedescendant="">
|
||||||
<label class="input-group-text" for="accounting-base-selector-query">
|
<label class="input-group-text" for="accounting-base-selector-query">
|
||||||
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||||
{{ A_("Search") }}
|
{{ A_("Search") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul id="accounting-base-selector-option-list" class="list-group accounting-selector-list">
|
<ul id="accounting-base-selector-option-list" class="list-group accounting-selector-list" role="listbox" tabindex="-1" aria-labelledby="accounting-base-selector-modal-label">
|
||||||
{% for base in form.base_options %}
|
{% for base in form.base_options %}
|
||||||
<li class="list-group-item accounting-clickable accounting-base-selector-option" data-code="{{ base.code }}" data-text="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}" data-bs-dismiss="modal">
|
<li id="accounting-base-selector-option-{{ base.code }}" class="list-group-item accounting-clickable accounting-base-selector-option" role="option" aria-selected="false" data-code="{{ base.code }}" data-text="{{ base }}" data-query-values="{{ base.query_values|tojson|forceescape }}" data-bs-dismiss="modal">
|
||||||
{{ base }}
|
{{ base }}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
+4
-4
@@ -28,20 +28,20 @@ First written: 2023/2/25
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
<input id="accounting-account-selector-{{ debit_credit }}-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
<input id="accounting-account-selector-{{ debit_credit }}-query" class="form-control form-control-sm" type="search" placeholder=" " required="required" role="combobox" aria-expanded="true" aria-controls="accounting-account-selector-{{ debit_credit }}-option-list" aria-autocomplete="list" aria-activedescendant="">
|
||||||
<label class="input-group-text" for="accounting-account-selector-{{ debit_credit }}-query">
|
<label class="input-group-text" for="accounting-account-selector-{{ debit_credit }}-query">
|
||||||
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||||
{{ A_("Search") }}
|
{{ A_("Search") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list">
|
<ul id="accounting-account-selector-{{ debit_credit }}-option-list" class="list-group accounting-selector-list" role="listbox" tabindex="-1" aria-labelledby="accounting-account-selector-{{ debit_credit }}-modal-label">
|
||||||
{% for account in account_options %}
|
{% for account in account_options %}
|
||||||
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-is-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" data-code="{{ account.code }}" data-title="{{ account.title }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
<li id="accounting-account-selector-{{ debit_credit }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-account-selector-{{ debit_credit }}-option {% if account.is_in_use %} accounting-account-is-in-use {% endif %} {% if account.is_need_offset %} accounting-account-is-need-offset {% endif %}" role="option" aria-selected="false" data-code="{{ account.code }}" data-title="{{ account.title }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||||
{{ account }}
|
{{ account }}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li id="accounting-account-selector-{{ debit_credit }}-more" class="list-group-item accounting-clickable">{{ A_("More…") }}</li>
|
<li id="accounting-account-selector-{{ debit_credit }}-more" class="list-group-item accounting-clickable" role="option" aria-selected="false">{{ A_("More…") }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p id="accounting-account-selector-{{ debit_credit }}-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
<p id="accounting-account-selector-{{ debit_credit }}-option-no-result" class="d-none">{{ A_("There is no data.") }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ First written: 2023/2/26
|
|||||||
|
|
||||||
{% block accounting_scripts %}
|
{% block accounting_scripts %}
|
||||||
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/base-combobox.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/journal-entry-form.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/journal-entry-form.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/journal-entry-line-item-editor.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/journal-entry-line-item-editor.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/journal-entry-account-selector.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/journal-entry-account-selector.js") }}"></script>
|
||||||
|
|||||||
+3
-3
@@ -28,16 +28,16 @@ First written: 2023/2/25
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
<input id="accounting-original-line-item-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
<input id="accounting-original-line-item-selector-query" class="form-control form-control-sm" type="search" placeholder=" " required="required" role="combobox" aria-expanded="true" aria-controls="accounting-original-line-item-selector-option-list" aria-autocomplete="list" aria-activedescendant="">
|
||||||
<label class="input-group-text" for="accounting-original-line-item-selector-query">
|
<label class="input-group-text" for="accounting-original-line-item-selector-query">
|
||||||
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||||
{{ A_("Search") }}
|
{{ A_("Search") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul id="accounting-original-line-item-selector-option-list" class="list-group accounting-selector-list">
|
<ul id="accounting-original-line-item-selector-option-list" class="list-group accounting-selector-list" role="listbox" tabindex="-1" aria-labelledby="accounting-original-line-item-selector-modal-label">
|
||||||
{% for line_item in form.original_line_item_options %}
|
{% for line_item in form.original_line_item_options %}
|
||||||
<li id="accounting-original-line-item-selector-option-{{ line_item.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-line-item-selector-option" data-id="{{ line_item.id }}" data-date="{{ line_item.journal_entry.date }}" data-debit-credit="{{ "debit" if line_item.is_debit else "credit" }}" data-currency-code="{{ line_item.currency.code }}" data-account-code="{{ line_item.account_code }}" data-account-title="{{ line_item.account.title }}" data-account-text="{{ line_item.account }}" data-description="{{ line_item.description|accounting_default }}" data-net-balance="{{ line_item.net_balance|accounting_journal_entry_format_amount_input }}" data-text="{{ line_item }}" data-query-values="{{ line_item.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
<li id="accounting-original-line-item-selector-option-{{ line_item.id }}" class="list-group-item d-flex justify-content-between accounting-clickable accounting-original-line-item-selector-option" data-id="{{ line_item.id }}" data-date="{{ line_item.journal_entry.date }}" data-debit-credit="{{ "debit" if line_item.is_debit else "credit" }}" data-currency-code="{{ line_item.currency.code }}" data-account-code="{{ line_item.account_code }}" data-account-title="{{ line_item.account.title }}" data-account-text="{{ line_item.account }}" data-description="{{ line_item.description|accounting_default }}" data-net-balance="{{ line_item.net_balance|accounting_journal_entry_format_amount_input }}" data-text="{{ line_item }}" data-query-values="{{ line_item.query_values|tojson|forceescape }}" role="option" aria-selected="false" data-bs-toggle="modal" data-bs-target="#accounting-line-item-editor-modal">
|
||||||
<div>
|
<div>
|
||||||
<div class="small">
|
<div class="small">
|
||||||
{{ line_item.journal_entry.date|accounting_format_date }}
|
{{ line_item.journal_entry.date|accounting_format_date }}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ First written: 2023/3/22
|
|||||||
|
|
||||||
{% block accounting_scripts %}
|
{% block accounting_scripts %}
|
||||||
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/drag-and-drop-reorder.js") }}"></script>
|
||||||
|
<script src="{{ url_for("accounting.static", filename="js/base-combobox.js") }}"></script>
|
||||||
<script src="{{ url_for("accounting.static", filename="js/option-form.js") }}"></script>
|
<script src="{{ url_for("accounting.static", filename="js/option-form.js") }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -28,16 +28,16 @@ First written: 2023/3/22
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
<input id="accounting-recurring-accounting-selector-{{ expense_income }}-query" class="form-control form-control-sm" type="search" placeholder=" " required="required">
|
<input id="accounting-recurring-accounting-selector-{{ expense_income }}-query" class="form-control form-control-sm" type="search" placeholder=" " required="required" role="combobox" aria-expanded="true" aria-controls="accounting-recurring-accounting-selector-{{ expense_income }}-option-list" aria-autocomplete="list" aria-activedescendant="">
|
||||||
<label class="input-group-text" for="accounting-recurring-accounting-selector-{{ expense_income }}-query">
|
<label class="input-group-text" for="accounting-recurring-accounting-selector-{{ expense_income }}-query">
|
||||||
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
<i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
|
||||||
{{ A_("Search") }}
|
{{ A_("Search") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul id="accounting-recurring-accounting-selector-{{ expense_income }}-option-list" class="list-group accounting-selector-list">
|
<ul id="accounting-recurring-accounting-selector-{{ expense_income }}-option-list" class="list-group accounting-selector-list" role="listbox" tabindex="-1" aria-labelledby="accounting-recurring-accounting-selector-{{ expense_income }}-modal-label">
|
||||||
{% for account in accounts %}
|
{% for account in accounts %}
|
||||||
<li id="accounting-recurring-accounting-selector-{{ expense_income }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-recurring-accounting-selector-{{ expense_income }}-option" data-code="{{ account.code }}" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-recurring-item-editor-{{ expense_income }}-modal">
|
<li id="accounting-recurring-accounting-selector-{{ expense_income }}-option-{{ account.code }}" class="list-group-item accounting-clickable accounting-recurring-accounting-selector-{{ expense_income }}-option" data-code="{{ account.code }}" role="option" aria-selected="false" data-text="{{ account }}" data-query-values="{{ account.query_values|tojson|forceescape }}" data-bs-toggle="modal" data-bs-target="#accounting-recurring-item-editor-{{ expense_income }}-modal">
|
||||||
{{ account }}
|
{{ account }}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Reference in New Issue
Block a user