diff --git a/src/accounting/static/js/base-tablist.js b/src/accounting/static/js/base-tablist.js new file mode 100644 index 0000000..d83ac45 --- /dev/null +++ b/src/accounting/static/js/base-tablist.js @@ -0,0 +1,177 @@ +/* The Mia! Accounting Project + * base-tablist.js: The JavaScript for base abstract tablist + */ + +/* 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 tablist. + * + * @abstract + * @template {BaseTab} T + */ +class BaseTablist { + + /** + * The tabs. + * @type {T[]} + */ + tabs; + + /** + * The current tab. + * @type {T} + */ + currentTab; + + /** + * Constructs a new base abstract tablist. + * + * @param tablist {HTMLUListElement} the tab list + */ + constructor(tablist) { + tablist.onkeydown = this.onTabKeyDown.bind(this); + } + + /** + * Actions when keys are pressed on the tabs. + * + * @param event {KeyboardEvent} the key event + */ + onTabKeyDown(event) { + const currentIndex = this.tabs.indexOf(this.currentTab); + if (currentIndex === -1) { + return; + } + + let newIndex = currentIndex; + switch (event.key) { + case "ArrowRight": + newIndex = (newIndex + 1) % this.tabs.length; + break; + case "ArrowLeft": + newIndex = (newIndex - 1 + this.tabs.length) % this.tabs.length; + break; + case "Home": + newIndex = 0; + break; + case "End": + newIndex = this.tabs.length - 1; + break; + default: + return; + } + event.preventDefault(); + this.tabs[newIndex].focus(); + this.onTabFocus(this.tabs[newIndex]); + } + + /** + * Actions when a tab is focused. + * + * @param tab {T} the tab + */ + onTabFocus(tab) { /* Do nothing */ } + + /** + * Switches to a tab. + * + * @param tab {T} the tab + */ + switchTo(tab) { + this.tabs.forEach(t => t.setActive(t === tab)); + this.currentTab = tab; + this.currentTab.onActivated(); + } +} + +/** + * The base abstract tab. + * + * @abstract + */ +class BaseTab { + + /** + * The tab element. + * @type {HTMLButtonElement} + */ + #tab; + + /** + * The panel element. + * @type {HTMLDivElement} + */ + #panel; + + /** + * Constructs a new base abstract tab. + * + * @param tab {HTMLButtonElement} The tab element. + * @param panel {HTMLDivElement} The panel element. + * @param switchTo {function(BaseTab): void} The function to switch to the tab. + */ + constructor(tab, panel, switchTo) { + this.#tab = tab; + this.#panel = panel; + this.#tab.onclick = () => switchTo(this); + } + + /** + * Sets the active state of the tab. + * + * @param isActive {boolean} true if the tab is active, false otherwise + */ + setActive(isActive) { + if (isActive) { + this.#tab.classList.add("active"); + this.#tab.tabIndex = 0; + this.#tab.ariaSelected = "true"; + this.#panel.classList.remove("d-none"); + } else { + this.#tab.classList.remove("active"); + this.#tab.tabIndex = -1; + this.#tab.ariaSelected = "false"; + this.#panel.classList.add("d-none"); + } + } + + /** + * Returns whether the tab is active. + * + * @returns {boolean} true if the tab is active, false otherwise + */ + isActive() { + return this.#tab.classList.contains("active"); + } + + /** + * Actions when the tab is activated. + */ + onActivated() { /* Do nothing */ } + + /** + * Focuses the tab. + */ + focus() { + this.#tab.focus(); + } +} diff --git a/src/accounting/static/js/description-editor.js b/src/accounting/static/js/description-editor.js index 933ed21..176b319 100644 --- a/src/accounting/static/js/description-editor.js +++ b/src/accounting/static/js/description-editor.js @@ -25,8 +25,9 @@ /** * A description editor. * + * @extends {BaseTablist} */ -class DescriptionEditor { +class DescriptionEditor extends BaseTablist { /** * The line item editor @@ -58,12 +59,6 @@ class DescriptionEditor { */ debitCredit; - /** - * The current tab - * @type {DescriptionEditorTabPlane} - */ - currentTab; - /** * The description input * @type {HTMLInputElement} @@ -125,10 +120,10 @@ class DescriptionEditor { selectedAccount = null; /** - * The tab planes - * @type {{general: DescriptionEditorGeneralTagTab, travel: DescriptionEditorGeneralTripTab, bus: DescriptionEditorBusTripTab, recurring: DescriptionEditorRecurringTab, annotation: DescriptionEditorAnnotationTab}} + * The tabs by their ID. + * @type {DescriptionEditorTabFactory} */ - tabPlanes = {}; + #tabsByID; /** * Constructs a description editor. @@ -137,23 +132,23 @@ class DescriptionEditor { * @param debitCredit {string} either "debit" or "credit" */ constructor(lineItemEditor, debitCredit) { + const prefix = `accounting-description-editor-${debitCredit}`; + super(document.getElementById(`${prefix}-tab-list`)); + this.prefix = prefix; this.lineItemEditor = lineItemEditor; this.debitCredit = debitCredit; - this.prefix = `accounting-description-editor-${debitCredit}`; - this.#form = document.getElementById(this.prefix); - this.#modal = document.getElementById(`${this.prefix}-modal`); - this.#descriptionInput = document.getElementById(`${this.prefix}-description`); - this.#offsetButton = document.getElementById(`${this.prefix}-offset`); - this.number = document.getElementById(`${this.prefix}-annotation-number`); - this.note = document.getElementById(`${this.prefix}-annotation-note`); - this.#confirmedAccountPlaceholder = new DescriptionEditorConfirmedAccount(this, document.getElementById(`${this.prefix}-account-confirmed`)); - this.#allSuggestedAccounts = Array.from(document.getElementsByClassName(`${this.prefix}-account`)).map((button) => new DescriptionEditorSuggestedAccount(this, button)); + this.#form = document.getElementById(prefix); + this.#modal = document.getElementById(`${prefix}-modal`); + this.#descriptionInput = document.getElementById(`${prefix}-description`); + this.#offsetButton = document.getElementById(`${prefix}-offset`); + this.number = document.getElementById(`${prefix}-annotation-number`); + this.note = document.getElementById(`${prefix}-annotation-note`); + this.#confirmedAccountPlaceholder = new DescriptionEditorConfirmedAccount(this, document.getElementById(`${prefix}-account-confirmed`)); + this.#allSuggestedAccounts = Array.from(document.getElementsByClassName(`${prefix}-account`)).map((button) => new DescriptionEditorSuggestedAccount(this, button)); - for (const cls of [DescriptionEditorGeneralTagTab, DescriptionEditorGeneralTripTab, DescriptionEditorBusTripTab, DescriptionEditorRecurringTab, DescriptionEditorAnnotationTab]) { - const tab = new cls(this); - this.tabPlanes[tab.tabId()] = tab; - } - this.currentTab = this.tabPlanes.general; + this.#tabsByID = new DescriptionEditorTabFactory(this); + this.tabs = [this.#tabsByID.general, this.#tabsByID.travel, this.#tabsByID.bus, this.#tabsByID.recurring, this.#tabsByID.annotation]; + this.currentTab = this.tabs[0]; this.#descriptionInput.onchange = () => this.#onDescriptionChange(); this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen(); this.#form.onsubmit = () => { @@ -199,26 +194,26 @@ class DescriptionEditor { * */ #onDescriptionChange() { - this.#resetTabPlanes(); + this.#resetTabs(); this.selectedAccount = null; this.description = this.description.trim(); - for (const tabPlane of [this.tabPlanes.recurring, this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) { - if (tabPlane.populate()) { + for (const tab of [this.#tabsByID.recurring, this.#tabsByID.bus, this.#tabsByID.travel, this.#tabsByID.general]) { + if (tab.populate()) { break; } } - this.tabPlanes.annotation.populate(); + this.#tabsByID.annotation.populate(); } /** - * Resets the tab planes. + * Resets the tabs. * */ - #resetTabPlanes() { - for (const tabPlane of Object.values(this.tabPlanes)) { - tabPlane.reset(); + #resetTabs() { + for (const tab of this.tabs) { + tab.reset(); } - this.tabPlanes.general.switchToMe(); + this.switchTo(this.tabs[0]); } /** @@ -463,12 +458,63 @@ class DescriptionEditorConfirmedAccount extends DescriptionEditorAccount { } /** - * A tab plane. + * The tab factory. + * + * @private + */ +class DescriptionEditorTabFactory { + + /** + * The general tag tab + * @type {GeneralTagTab} + */ + general; + + /** + * The general trip tab + * @type {GeneralTripTab} + */ + travel; + + /** + * The bus trip tab + * @type {BusTripTab} + */ + bus; + + /** + * The recurring transactions tab + * @type {RecurringTab} + */ + recurring; + + /** + * The annotation tab + * @type {AnnotationTab} + */ + annotation; + + /** + * Constructs the tab factory + * + * @param editor {DescriptionEditor} the parent description editor + */ + constructor(editor) { + this.general = new GeneralTagTab(editor); + this.travel = new GeneralTripTab(editor); + this.bus = new BusTripTab(editor); + this.recurring = new RecurringTab(editor); + this.annotation = new AnnotationTab(editor); + } +} + +/** + * The base abstract tab in the description editor. * * @abstract * @private */ -class DescriptionEditorTabPlane { +class BaseDescriptionEditorTab extends BaseTab { /** * The parent description editor @@ -483,47 +529,29 @@ class DescriptionEditorTabPlane { prefix; /** - * The tab - * @type {HTMLSpanElement} - */ - #tab; - - /** - * The page - * @type {HTMLDivElement} - */ - #page; - - /** - * Constructs a tab plane. + * Constructs a base abstract tab in the description editor. * + * @param tabID {string} the tab ID * @param editor {DescriptionEditor} the parent description editor */ - constructor(editor) { + constructor(tabID, editor) { + const prefix = `${editor.prefix}-${tabID}`; + const tab = document.getElementById(`${prefix}-tab`); + const panel = document.getElementById(`${prefix}-panel`); + super(tab, panel, editor.switchTo.bind(editor)); this.editor = editor; - this.prefix = `${this.editor.prefix}-${this.tabId()}`; - this.#tab = document.getElementById(`${this.prefix}-tab`); - this.#page = document.getElementById(`${this.prefix}-page`); - this.#tab.onclick = () => this.switchToMe(); + this.prefix = prefix; } /** - * The tab ID - * - * @return {string} - * @abstract - */ - tabId() { throw new Error("Method not implemented.") }; - - /** - * Resets the tab plane input. + * Resets the tab panel input. * * @abstract */ reset() { throw new Error("Method not implemented."); } /** - * Populates the tab plane with the description input. + * Populates the tab panel with the description input. * * @return {boolean} true if the description input matches this tab, or false otherwise * @abstract @@ -531,39 +559,21 @@ class DescriptionEditorTabPlane { populate() { throw new Error("Method not implemented."); } /** - * Validates the input in the tab plane. + * Validates the input in the tab panel. * * @return {boolean} true if valid, or false otherwise * @abstract */ validate() { throw new Error("Method not implemented."); } - - /** - * Switches to the tab plane. - * - */ - switchToMe() { - for (const tabPlane of Object.values(this.editor.tabPlanes)) { - tabPlane.#tab.classList.remove("active") - tabPlane.#tab.ariaCurrent = "false"; - tabPlane.#page.classList.add("d-none"); - tabPlane.#page.ariaCurrent = "false"; - } - this.#tab.classList.add("active"); - this.#tab.ariaCurrent = "page"; - this.#page.classList.remove("d-none"); - this.#page.ariaCurrent = "page"; - this.editor.currentTab = this; - } } /** - * A tag plane with selectable tags. + * The base abstract tab with selectable tags. * * @abstract * @private */ -class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane { +class BaseTagTab extends BaseDescriptionEditorTab { /** * The tag input @@ -581,20 +591,21 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane { * The tag buttons * @type {HTMLButtonElement[]} */ - tagButtons; + #tagButtons; /** - * Constructs a tab plane. + * Constructs a base abstract tab with selectable tags. * + * @param tabID {string} the tab ID * @param editor {DescriptionEditor} the parent description editor * @override */ - constructor(editor) { - super(editor); + constructor(tabID, editor) { + super(tabID, editor); this.tag = document.getElementById(`${this.prefix}-tag`); this.tagError = document.getElementById(`${this.prefix}-tag-error`); // noinspection JSValidateTypes - this.tagButtons = Array.from(document.getElementsByClassName(`${this.prefix}-btn-tag`)); + this.#tagButtons = Array.from(document.getElementsByClassName(`${this.prefix}-btn-tag`)); this.initializeTagButtons(); this.tag.onchange = () => { this.onTagChange(); @@ -609,7 +620,7 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane { onTagChange() { this.tag.value = this.tag.value.trim(); let isMatched = false; - for (const tagButton of this.tagButtons) { + for (const tagButton of this.#tagButtons) { if (tagButton.dataset.value === this.tag.value) { tagButton.classList.remove("btn-outline-primary"); tagButton.classList.add("btn-primary"); @@ -627,19 +638,18 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane { } /** - * Updates the description according to the input in the tab plane. + * Updates the description according to the input in the tab panel. * * @abstract */ updateDescription() { throw new Error("Method not implemented."); } /** - * Switches to the tab plane. - * + * @inheritDoc + * @override */ - switchToMe() { - super.switchToMe(); - for (const tagButton of this.tagButtons) { + onActivated() { + for (const tagButton of this.#tagButtons) { if (tagButton.classList.contains("btn-primary")) { this.editor.updateCurrentSuggestedAccounts(tagButton); return; @@ -653,9 +663,9 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane { * */ initializeTagButtons() { - for (const tagButton of this.tagButtons) { + for (const tagButton of this.#tagButtons) { tagButton.onclick = () => { - for (const otherButton of this.tagButtons) { + for (const otherButton of this.#tagButtons) { otherButton.classList.remove("btn-primary"); otherButton.classList.add("btn-outline-primary"); } @@ -701,7 +711,7 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane { } /** - * Resets the tab plane input. + * Resets the tab panel input. * * @override */ @@ -709,7 +719,7 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane { this.tag.value = ""; this.tag.classList.remove("is-invalid"); this.tagError.innerText = ""; - for (const tagButton of this.tagButtons) { + for (const tagButton of this.#tagButtons) { tagButton.classList.remove("btn-primary"); tagButton.classList.add("btn-outline-primary"); } @@ -717,24 +727,24 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane { } /** - * The general tag tab plane. + * The general tag tab. * * @private */ -class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane { +class GeneralTagTab extends BaseTagTab { /** - * The tab ID + * Constructs a general tag tab. * - * @return {string} - * @abstract + * @param editor {DescriptionEditor} the parent description editor + * @override */ - tabId() { - return "general"; - }; + constructor(editor) { + super("general", editor); + } /** - * Updates the description according to the input in the tab plane. + * Updates the description according to the input in the tab panel. * * @override */ @@ -749,7 +759,7 @@ class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane { } /** - * Populates the tab plane with the description input. + * Populates the tab panel with the description input. * * @return {boolean} true if the description input matches this tab, or false otherwise * @override @@ -763,12 +773,12 @@ class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane { this.tag.value = found[1]; this.onTagChange(); } - this.switchToMe(); + this.editor.switchTo(this); return true; } /** - * Validates the input in the tab plane. + * Validates the input in the tab panel. * * @return {boolean} true if valid, or false otherwise */ @@ -778,11 +788,11 @@ class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane { } /** - * The general trip tab plane. + * The general trip tab. * * @private */ -class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane { +class GeneralTripTab extends BaseTagTab { /** * The origin @@ -815,13 +825,13 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane { #directionButtons; /** - * Constructs a tab plane. + * Constructs a general trip tab. * * @param editor {DescriptionEditor} the parent description editor * @override */ constructor(editor) { - super(editor); + super("travel", editor); this.#from = document.getElementById(`${this.prefix}-from`); this.#fromError = document.getElementById(`${this.prefix}-from-error`); this.#to = document.getElementById(`${this.prefix}-to`); @@ -852,17 +862,7 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane { } /** - * The tab ID - * - * @return {string} - * @abstract - */ - tabId() { - return "travel"; - }; - - /** - * Updates the description according to the input in the tab plane. + * Updates the description according to the input in the tab panel. * * @override */ @@ -878,7 +878,7 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane { } /** - * Resets the tab plane input. + * Resets the tab panel input. * * @override */ @@ -902,7 +902,7 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane { } /** - * Populates the tab plane with the description input. + * Populates the tab panel with the description input. * * @return {boolean} true if the description input matches this tab, or false otherwise * @override @@ -927,12 +927,12 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane { } } this.#to.value = found[4]; - this.switchToMe(); + this.editor.switchTo(this); return true; } /** - * Validates the input in the tab plane. + * Validates the input in the tab panel. * * @return {boolean} true if valid, or false otherwise * @override @@ -977,11 +977,11 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane { } /** - * The bus trip tab plane. + * The bus trip tab. * * @private */ -class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane { +class BusTripTab extends BaseTagTab { /** * The route @@ -1020,13 +1020,13 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane { #toError; /** - * Constructs a tab plane. + * Constructs a bus trip tab. * * @param editor {DescriptionEditor} the parent description editor * @override */ constructor(editor) { - super(editor); + super("bus", editor); this.#route = document.getElementById(`${this.prefix}-route`); this.#routeError = document.getElementById(`${this.prefix}-route-error`); this.#from = document.getElementById(`${this.prefix}-from`); @@ -1051,17 +1051,7 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane { } /** - * The tab ID - * - * @return {string} - * @abstract - */ - tabId() { - return "bus"; - }; - - /** - * Updates the description according to the input in the tab plane. + * Updates the description according to the input in the tab panel. * * @override */ @@ -1070,7 +1060,7 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane { } /** - * Resets the tab plane input. + * Resets the tab panel input. * * @override */ @@ -1088,7 +1078,7 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane { } /** - * Populates the tab plane with the description input. + * Populates the tab panel with the description input. * * @return {boolean} true if the description input matches this tab, or false otherwise * @override @@ -1105,12 +1095,12 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane { this.#route.value = found[2]; this.#from.value = found[3]; this.#to.value = found[4]; - this.switchToMe(); + this.editor.switchTo(this); return true; } /** - * Validates the input in the tab plane. + * Validates the input in the tab panel. * * @return {boolean} true if valid, or false otherwise */ @@ -1165,11 +1155,11 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane { } /** - * The recurring transaction tab plane. + * The recurring transaction tab. * * @private */ -class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane { +class RecurringTab extends BaseDescriptionEditorTab { /** * The month names @@ -1184,13 +1174,13 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane { #itemButtons; /** - * Constructs a tab plane. + * Constructs a recurring transaction tab. * * @param editor {DescriptionEditor} the parent description editor * @override */ constructor(editor) { - super(editor); + super("recurring", editor); this.#monthNames = [ "", A_("January"), A_("February"), A_("March"), A_("April"), @@ -1232,17 +1222,7 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane { } /** - * The tab ID - * - * @return {string} - * @abstract - */ - tabId() { - return "recurring"; - }; - - /** - * Resets the tab plane input. + * Resets the tab panel input. * * @override */ @@ -1254,7 +1234,7 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane { } /** - * Populates the tab plane with the description input. + * Populates the tab panel with the description input. * * @return {boolean} true if the description input matches this tab, or false otherwise * @override @@ -1264,7 +1244,7 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane { if (this.#getDescription(itemButton) === this.editor.description) { itemButton.classList.add("btn-primary"); itemButton.classList.remove("btn-outline-primary"); - this.switchToMe(); + this.editor.switchTo(this); return true; } } @@ -1272,11 +1252,10 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane { } /** - * Switches to the tab plane. - * + * @inheritDoc + * @override */ - switchToMe() { - super.switchToMe(); + onActivated() { for (const itemButton of this.#itemButtons) { if (itemButton.classList.contains("btn-primary")) { this.editor.updateCurrentSuggestedAccounts(itemButton); @@ -1287,7 +1266,7 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane { } /** - * Validates the input in the tab plane. + * Validates the input in the tab panel. * * @return {boolean} true if valid, or false otherwise * @override @@ -1298,20 +1277,20 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane { } /** - * The annotation tab plane. + * The annotation tab. * * @private */ -class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane { +class AnnotationTab extends BaseDescriptionEditorTab { /** - * Constructs a tab plane. + * Constructs an annotation tab. * * @param editor {DescriptionEditor} the parent description editor * @override */ constructor(editor) { - super(editor); + super("annotation", editor); this.editor.number.onchange = () => this.updateDescription(); this.editor.note.onchange = () => { this.editor.note.value = this.editor.note.value.trim(); @@ -1320,17 +1299,7 @@ class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane { } /** - * The tab ID - * - * @return {string} - * @abstract - */ - tabId() { - return "annotation"; - }; - - /** - * Updates the description according to the input in the tab plane. + * Updates the description according to the input in the tab panel. * * @override */ @@ -1348,7 +1317,7 @@ class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane { } /** - * Resets the tab plane input. + * Resets the tab panel input. * * @override */ @@ -1358,7 +1327,7 @@ class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane { } /** - * Populates the tab plane with the description input. + * Populates the tab panel with the description input. * * @return {boolean} true if the description input matches this tab, or false otherwise * @override @@ -1382,7 +1351,7 @@ class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane { } /** - * Validates the input in the tab plane. + * Validates the input in the tab panel. * * @return {boolean} true if valid, or false otherwise * @override diff --git a/src/accounting/static/js/period-chooser.js b/src/accounting/static/js/period-chooser.js index 7561bd9..a58619c 100644 --- a/src/accounting/static/js/period-chooser.js +++ b/src/accounting/static/js/period-chooser.js @@ -30,21 +30,16 @@ document.addEventListener("DOMContentLoaded", () => { /** * The period chooser. * + * @extends {BaseTablist} * @private */ -class PeriodChooser { +class PeriodChooser extends BaseTablist { /** - * The modal of the period chooser - * @type {HTMLDivElement} + * The URL template for different periods. + * @type {string} */ - modal; - - /** - * The tab planes - * @type {{month: MonthTab, year: YearTab, day: DayTab, custom: CustomTab}} - */ - tabPlanes = {}; + urlTemplate; /** * Constructs the period chooser. @@ -52,13 +47,24 @@ class PeriodChooser { */ constructor() { const prefix = "accounting-period-chooser"; - this.modal = document.getElementById(`${prefix}-modal`); - for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) { - const tab = new cls(this); - this.tabPlanes[tab.tabId()] = tab; + super(document.getElementById(`${prefix}-tab-list`)); + this.tabs = [new MonthTab(this), new YearTab(this), new DayTab(this), new CustomTab(this)]; + const modal = document.getElementById(`${prefix}-modal`); + this.urlTemplate = modal.dataset.urlTemplate; + for (const tab of this.tabs) { + if (tab.isActive()) { + this.currentTab = tab; + break; + } } } + /** + * @inheritDoc + * @override + */ + onTabFocus(tab) { this.switchTo(tab); } + /** * The period chooser. * @type {PeriodChooser} @@ -75,12 +81,12 @@ class PeriodChooser { } /** - * A tab plane. + * A base abstract period tab. * * @abstract * @private */ -class TabPlane { +class BasePeriodTab extends BaseTab { /** * The period chooser @@ -95,62 +101,27 @@ class TabPlane { prefix; /** - * The tab - * @type {HTMLSpanElement} - */ - #tab; - - /** - * The page - * @type {HTMLDivElement} - */ - #page; - - /** - * Constructs a tab plane. + * Constructs a base abstract period tab. * + * @param tabID {string} the tab ID * @param chooser {PeriodChooser} the period chooser */ - constructor(chooser) { + constructor(tabID, chooser) { + const prefix = `accounting-period-chooser-${tabID}`; + const tab = document.getElementById(`${prefix}-tab`); + const panel = document.getElementById(`${prefix}-panel`); + super(tab, panel, chooser.switchTo.bind(chooser)); this.chooser = chooser; - this.prefix = `accounting-period-chooser-${this.tabId()}`; - this.#tab = document.getElementById(`${this.prefix}-tab`); - this.#page = document.getElementById(`${this.prefix}-page`); - this.#tab.onclick = () => this.#switchToMe(); - } - - /** - * The tab ID - * - * @return {string} - * @abstract - */ - tabId() { throw new Error("Method not implemented.") }; - - /** - * Switches to the tab plane. - * - */ - #switchToMe() { - for (const tabPlane of Object.values(this.chooser.tabPlanes)) { - tabPlane.#tab.classList.remove("active") - tabPlane.#tab.ariaCurrent = "false"; - tabPlane.#page.classList.add("d-none"); - tabPlane.#page.ariaCurrent = "false"; - } - this.#tab.classList.add("active"); - this.#tab.ariaCurrent = "page"; - this.#page.classList.remove("d-none"); - this.#page.ariaCurrent = "page"; + this.prefix = prefix; } } /** - * The month tab plane. + * The month tab. * * @private */ -class MonthTab extends TabPlane { +class MonthTab extends BasePeriodTab { /** * The month chooser. @@ -159,12 +130,12 @@ class MonthTab extends TabPlane { #monthChooser /** - * Constructs a tab plane. + * Constructs a month tab. * * @param chooser {PeriodChooser} the period chooser */ constructor(chooser) { - super(chooser); + super("month", chooser); const monthChooser = document.getElementById(`${this.prefix}-chooser`); if (monthChooser !== null) { this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, { @@ -184,45 +155,36 @@ class MonthTab extends TabPlane { const date = e.detail.date; const zeroPaddedMonth = `0${date.month + 1}`.slice(-2) const period = `${date.year}-${zeroPaddedMonth}`; - window.location = chooser.modal.dataset.urlTemplate + window.location = chooser.urlTemplate .replaceAll("PERIOD", period); }); } } +} + +/** + * The year tab. + * + * @private + */ +class YearTab extends BasePeriodTab { /** - * The tab ID + * Constructs a year tab. * - * @return {string} + * @param chooser {PeriodChooser} the period chooser */ - tabId() { - return "month"; + constructor(chooser) { + super("year", chooser); } } /** - * The year tab plane. + * The day tab. * * @private */ -class YearTab extends TabPlane { - - /** - * The tab ID - * - * @return {string} - */ - tabId() { - return "year"; - } -} - -/** - * The day tab plane. - * - * @private - */ -class DayTab extends TabPlane { +class DayTab extends BasePeriodTab { /** * The day input @@ -237,18 +199,18 @@ class DayTab extends TabPlane { #dateError; /** - * Constructs a tab plane. + * Constructs a day tab. * * @param chooser {PeriodChooser} the period chooser */ constructor(chooser) { - super(chooser); + super("day", chooser); this.#date = document.getElementById(`${this.prefix}-date`); this.#dateError = document.getElementById(`${this.prefix}-date-error`); if (this.#date !== null) { this.#date.onchange = () => { if (this.#validateDate()) { - window.location = chooser.modal.dataset.urlTemplate + window.location = chooser.urlTemplate .replaceAll("PERIOD", this.#date.value); } }; @@ -275,23 +237,14 @@ class DayTab extends TabPlane { this.#dateError.innerText = ""; return true; } - - /** - * The tab ID - * - * @return {string} - */ - tabId() { - return "day"; - } } /** - * The custom tab plane. + * The custom tab. * * @private */ -class CustomTab extends TabPlane { +class CustomTab extends BasePeriodTab { /** * The start of the period @@ -324,12 +277,12 @@ class CustomTab extends TabPlane { #confirm; /** - * Constructs a tab plane. + * Constructs a custom tab. * * @param chooser {PeriodChooser} the period chooser */ constructor(chooser) { - super(chooser); + super("custom", chooser); this.#start = document.getElementById(`${this.prefix}-start`); this.#startError = document.getElementById(`${this.prefix}-start-error`); this.#end = document.getElementById(`${this.prefix}-end`); @@ -351,7 +304,7 @@ class CustomTab extends TabPlane { isValid = this.#validateStart() && isValid; isValid = this.#validateEnd() && isValid; if (isValid) { - window.location = chooser.modal.dataset.urlTemplate + window.location = chooser.urlTemplate .replaceAll("PERIOD", `${this.#start.value}-${this.#end.value}`); } }; @@ -407,13 +360,4 @@ class CustomTab extends TabPlane { this.#endError.innerText = ""; return true; } - - /** - * The tab ID - * - * @return {string} - */ - tabId() { - return "custom"; - } } diff --git a/src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html b/src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html index d7d31ed..1eba8a2 100644 --- a/src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html +++ b/src/accounting/templates/accounting/journal-entry/include/description-editor-modal.html @@ -39,36 +39,36 @@ First written: 2023/2/28 {# Tab navigation #} -