Add BaseTablist base class with keyboard navigation

This commit is contained in:
2026-04-18 02:18:45 +08:00
parent 454ff8bb5f
commit 800832d15e
12 changed files with 420 additions and 323 deletions
+177
View File
@@ -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();
}
}
+159 -190
View File
@@ -25,8 +25,9 @@
/** /**
* A description editor. * A description editor.
* *
* @extends {BaseTablist<BaseDescriptionEditorTab>}
*/ */
class DescriptionEditor { class DescriptionEditor extends BaseTablist {
/** /**
* The line item editor * The line item editor
@@ -58,12 +59,6 @@ class DescriptionEditor {
*/ */
debitCredit; debitCredit;
/**
* The current tab
* @type {DescriptionEditorTabPlane}
*/
currentTab;
/** /**
* The description input * The description input
* @type {HTMLInputElement} * @type {HTMLInputElement}
@@ -125,10 +120,10 @@ class DescriptionEditor {
selectedAccount = null; selectedAccount = null;
/** /**
* The tab planes * The tabs by their ID.
* @type {{general: DescriptionEditorGeneralTagTab, travel: DescriptionEditorGeneralTripTab, bus: DescriptionEditorBusTripTab, recurring: DescriptionEditorRecurringTab, annotation: DescriptionEditorAnnotationTab}} * @type {DescriptionEditorTabFactory}
*/ */
tabPlanes = {}; #tabsByID;
/** /**
* Constructs a description editor. * Constructs a description editor.
@@ -137,23 +132,23 @@ class DescriptionEditor {
* @param debitCredit {string} either "debit" or "credit" * @param debitCredit {string} either "debit" or "credit"
*/ */
constructor(lineItemEditor, debitCredit) { constructor(lineItemEditor, debitCredit) {
const prefix = `accounting-description-editor-${debitCredit}`;
super(document.getElementById(`${prefix}-tab-list`));
this.prefix = prefix;
this.lineItemEditor = lineItemEditor; this.lineItemEditor = lineItemEditor;
this.debitCredit = debitCredit; this.debitCredit = debitCredit;
this.prefix = `accounting-description-editor-${debitCredit}`; this.#form = document.getElementById(prefix);
this.#form = document.getElementById(this.prefix); this.#modal = document.getElementById(`${prefix}-modal`);
this.#modal = document.getElementById(`${this.prefix}-modal`); this.#descriptionInput = document.getElementById(`${prefix}-description`);
this.#descriptionInput = document.getElementById(`${this.prefix}-description`); this.#offsetButton = document.getElementById(`${prefix}-offset`);
this.#offsetButton = document.getElementById(`${this.prefix}-offset`); this.number = document.getElementById(`${prefix}-annotation-number`);
this.number = document.getElementById(`${this.prefix}-annotation-number`); this.note = document.getElementById(`${prefix}-annotation-note`);
this.note = document.getElementById(`${this.prefix}-annotation-note`); this.#confirmedAccountPlaceholder = new DescriptionEditorConfirmedAccount(this, document.getElementById(`${prefix}-account-confirmed`));
this.#confirmedAccountPlaceholder = new DescriptionEditorConfirmedAccount(this, document.getElementById(`${this.prefix}-account-confirmed`)); this.#allSuggestedAccounts = Array.from(document.getElementsByClassName(`${prefix}-account`)).map((button) => new DescriptionEditorSuggestedAccount(this, button));
this.#allSuggestedAccounts = Array.from(document.getElementsByClassName(`${this.prefix}-account`)).map((button) => new DescriptionEditorSuggestedAccount(this, button));
for (const cls of [DescriptionEditorGeneralTagTab, DescriptionEditorGeneralTripTab, DescriptionEditorBusTripTab, DescriptionEditorRecurringTab, DescriptionEditorAnnotationTab]) { this.#tabsByID = new DescriptionEditorTabFactory(this);
const tab = new cls(this); this.tabs = [this.#tabsByID.general, this.#tabsByID.travel, this.#tabsByID.bus, this.#tabsByID.recurring, this.#tabsByID.annotation];
this.tabPlanes[tab.tabId()] = tab; this.currentTab = this.tabs[0];
}
this.currentTab = this.tabPlanes.general;
this.#descriptionInput.onchange = () => this.#onDescriptionChange(); this.#descriptionInput.onchange = () => this.#onDescriptionChange();
this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen(); this.#offsetButton.onclick = () => this.lineItemEditor.originalLineItemSelector.onOpen();
this.#form.onsubmit = () => { this.#form.onsubmit = () => {
@@ -199,26 +194,26 @@ class DescriptionEditor {
* *
*/ */
#onDescriptionChange() { #onDescriptionChange() {
this.#resetTabPlanes(); this.#resetTabs();
this.selectedAccount = null; this.selectedAccount = null;
this.description = this.description.trim(); this.description = this.description.trim();
for (const tabPlane of [this.tabPlanes.recurring, this.tabPlanes.bus, this.tabPlanes.travel, this.tabPlanes.general]) { for (const tab of [this.#tabsByID.recurring, this.#tabsByID.bus, this.#tabsByID.travel, this.#tabsByID.general]) {
if (tabPlane.populate()) { if (tab.populate()) {
break; break;
} }
} }
this.tabPlanes.annotation.populate(); this.#tabsByID.annotation.populate();
} }
/** /**
* Resets the tab planes. * Resets the tabs.
* *
*/ */
#resetTabPlanes() { #resetTabs() {
for (const tabPlane of Object.values(this.tabPlanes)) { for (const tab of this.tabs) {
tabPlane.reset(); 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 * @abstract
* @private * @private
*/ */
class DescriptionEditorTabPlane { class BaseDescriptionEditorTab extends BaseTab {
/** /**
* The parent description editor * The parent description editor
@@ -483,47 +529,29 @@ class DescriptionEditorTabPlane {
prefix; prefix;
/** /**
* The tab * Constructs a base abstract tab in the description editor.
* @type {HTMLSpanElement}
*/
#tab;
/**
* The page
* @type {HTMLDivElement}
*/
#page;
/**
* Constructs a tab plane.
* *
* @param tabID {string} the tab ID
* @param editor {DescriptionEditor} the parent description editor * @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.editor = editor;
this.prefix = `${this.editor.prefix}-${this.tabId()}`; this.prefix = prefix;
this.#tab = document.getElementById(`${this.prefix}-tab`);
this.#page = document.getElementById(`${this.prefix}-page`);
this.#tab.onclick = () => this.switchToMe();
} }
/** /**
* The tab ID * Resets the tab panel input.
*
* @return {string}
* @abstract
*/
tabId() { throw new Error("Method not implemented.") };
/**
* Resets the tab plane input.
* *
* @abstract * @abstract
*/ */
reset() { throw new Error("Method not implemented."); } 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 * @return {boolean} true if the description input matches this tab, or false otherwise
* @abstract * @abstract
@@ -531,39 +559,21 @@ class DescriptionEditorTabPlane {
populate() { throw new Error("Method not implemented."); } 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 * @return {boolean} true if valid, or false otherwise
* @abstract * @abstract
*/ */
validate() { throw new Error("Method not implemented."); } 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 * @abstract
* @private * @private
*/ */
class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane { class BaseTagTab extends BaseDescriptionEditorTab {
/** /**
* The tag input * The tag input
@@ -581,20 +591,21 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
* The tag buttons * The tag buttons
* @type {HTMLButtonElement[]} * @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 * @param editor {DescriptionEditor} the parent description editor
* @override * @override
*/ */
constructor(editor) { constructor(tabID, editor) {
super(editor); super(tabID, editor);
this.tag = document.getElementById(`${this.prefix}-tag`); this.tag = document.getElementById(`${this.prefix}-tag`);
this.tagError = document.getElementById(`${this.prefix}-tag-error`); this.tagError = document.getElementById(`${this.prefix}-tag-error`);
// noinspection JSValidateTypes // 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.initializeTagButtons();
this.tag.onchange = () => { this.tag.onchange = () => {
this.onTagChange(); this.onTagChange();
@@ -609,7 +620,7 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
onTagChange() { onTagChange() {
this.tag.value = this.tag.value.trim(); this.tag.value = this.tag.value.trim();
let isMatched = false; let isMatched = false;
for (const tagButton of this.tagButtons) { for (const tagButton of this.#tagButtons) {
if (tagButton.dataset.value === this.tag.value) { if (tagButton.dataset.value === this.tag.value) {
tagButton.classList.remove("btn-outline-primary"); tagButton.classList.remove("btn-outline-primary");
tagButton.classList.add("btn-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 * @abstract
*/ */
updateDescription() { throw new Error("Method not implemented."); } updateDescription() { throw new Error("Method not implemented."); }
/** /**
* Switches to the tab plane. * @inheritDoc
* * @override
*/ */
switchToMe() { onActivated() {
super.switchToMe(); for (const tagButton of this.#tagButtons) {
for (const tagButton of this.tagButtons) {
if (tagButton.classList.contains("btn-primary")) { if (tagButton.classList.contains("btn-primary")) {
this.editor.updateCurrentSuggestedAccounts(tagButton); this.editor.updateCurrentSuggestedAccounts(tagButton);
return; return;
@@ -653,9 +663,9 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
* *
*/ */
initializeTagButtons() { initializeTagButtons() {
for (const tagButton of this.tagButtons) { for (const tagButton of this.#tagButtons) {
tagButton.onclick = () => { tagButton.onclick = () => {
for (const otherButton of this.tagButtons) { for (const otherButton of this.#tagButtons) {
otherButton.classList.remove("btn-primary"); otherButton.classList.remove("btn-primary");
otherButton.classList.add("btn-outline-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 * @override
*/ */
@@ -709,7 +719,7 @@ class DescriptionEditorTagTabPlane extends DescriptionEditorTabPlane {
this.tag.value = ""; this.tag.value = "";
this.tag.classList.remove("is-invalid"); this.tag.classList.remove("is-invalid");
this.tagError.innerText = ""; this.tagError.innerText = "";
for (const tagButton of this.tagButtons) { for (const tagButton of this.#tagButtons) {
tagButton.classList.remove("btn-primary"); tagButton.classList.remove("btn-primary");
tagButton.classList.add("btn-outline-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 * @private
*/ */
class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane { class GeneralTagTab extends BaseTagTab {
/** /**
* The tab ID * Constructs a general tag tab.
* *
* @return {string} * @param editor {DescriptionEditor} the parent description editor
* @abstract * @override
*/ */
tabId() { constructor(editor) {
return "general"; 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 * @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 * @return {boolean} true if the description input matches this tab, or false otherwise
* @override * @override
@@ -763,12 +773,12 @@ class DescriptionEditorGeneralTagTab extends DescriptionEditorTagTabPlane {
this.tag.value = found[1]; this.tag.value = found[1];
this.onTagChange(); this.onTagChange();
} }
this.switchToMe(); this.editor.switchTo(this);
return true; return true;
} }
/** /**
* Validates the input in the tab plane. * Validates the input in the tab panel.
* *
* @return {boolean} true if valid, or false otherwise * @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 * @private
*/ */
class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane { class GeneralTripTab extends BaseTagTab {
/** /**
* The origin * The origin
@@ -815,13 +825,13 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
#directionButtons; #directionButtons;
/** /**
* Constructs a tab plane. * Constructs a general trip tab.
* *
* @param editor {DescriptionEditor} the parent description editor * @param editor {DescriptionEditor} the parent description editor
* @override * @override
*/ */
constructor(editor) { constructor(editor) {
super(editor); super("travel", editor);
this.#from = document.getElementById(`${this.prefix}-from`); this.#from = document.getElementById(`${this.prefix}-from`);
this.#fromError = document.getElementById(`${this.prefix}-from-error`); this.#fromError = document.getElementById(`${this.prefix}-from-error`);
this.#to = document.getElementById(`${this.prefix}-to`); this.#to = document.getElementById(`${this.prefix}-to`);
@@ -852,17 +862,7 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
} }
/** /**
* The tab ID * Updates the description according to the input in the tab panel.
*
* @return {string}
* @abstract
*/
tabId() {
return "travel";
};
/**
* Updates the description according to the input in the tab plane.
* *
* @override * @override
*/ */
@@ -878,7 +878,7 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
} }
/** /**
* Resets the tab plane input. * Resets the tab panel input.
* *
* @override * @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 * @return {boolean} true if the description input matches this tab, or false otherwise
* @override * @override
@@ -927,12 +927,12 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
} }
} }
this.#to.value = found[4]; this.#to.value = found[4];
this.switchToMe(); this.editor.switchTo(this);
return true; return true;
} }
/** /**
* Validates the input in the tab plane. * Validates the input in the tab panel.
* *
* @return {boolean} true if valid, or false otherwise * @return {boolean} true if valid, or false otherwise
* @override * @override
@@ -977,11 +977,11 @@ class DescriptionEditorGeneralTripTab extends DescriptionEditorTagTabPlane {
} }
/** /**
* The bus trip tab plane. * The bus trip tab.
* *
* @private * @private
*/ */
class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane { class BusTripTab extends BaseTagTab {
/** /**
* The route * The route
@@ -1020,13 +1020,13 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
#toError; #toError;
/** /**
* Constructs a tab plane. * Constructs a bus trip tab.
* *
* @param editor {DescriptionEditor} the parent description editor * @param editor {DescriptionEditor} the parent description editor
* @override * @override
*/ */
constructor(editor) { constructor(editor) {
super(editor); super("bus", editor);
this.#route = document.getElementById(`${this.prefix}-route`); this.#route = document.getElementById(`${this.prefix}-route`);
this.#routeError = document.getElementById(`${this.prefix}-route-error`); this.#routeError = document.getElementById(`${this.prefix}-route-error`);
this.#from = document.getElementById(`${this.prefix}-from`); this.#from = document.getElementById(`${this.prefix}-from`);
@@ -1051,17 +1051,7 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
} }
/** /**
* The tab ID * Updates the description according to the input in the tab panel.
*
* @return {string}
* @abstract
*/
tabId() {
return "bus";
};
/**
* Updates the description according to the input in the tab plane.
* *
* @override * @override
*/ */
@@ -1070,7 +1060,7 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
} }
/** /**
* Resets the tab plane input. * Resets the tab panel input.
* *
* @override * @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 * @return {boolean} true if the description input matches this tab, or false otherwise
* @override * @override
@@ -1105,12 +1095,12 @@ class DescriptionEditorBusTripTab extends DescriptionEditorTagTabPlane {
this.#route.value = found[2]; this.#route.value = found[2];
this.#from.value = found[3]; this.#from.value = found[3];
this.#to.value = found[4]; this.#to.value = found[4];
this.switchToMe(); this.editor.switchTo(this);
return true; return true;
} }
/** /**
* Validates the input in the tab plane. * Validates the input in the tab panel.
* *
* @return {boolean} true if valid, or false otherwise * @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 * @private
*/ */
class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane { class RecurringTab extends BaseDescriptionEditorTab {
/** /**
* The month names * The month names
@@ -1184,13 +1174,13 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
#itemButtons; #itemButtons;
/** /**
* Constructs a tab plane. * Constructs a recurring transaction tab.
* *
* @param editor {DescriptionEditor} the parent description editor * @param editor {DescriptionEditor} the parent description editor
* @override * @override
*/ */
constructor(editor) { constructor(editor) {
super(editor); super("recurring", editor);
this.#monthNames = [ this.#monthNames = [
"", "",
A_("January"), A_("February"), A_("March"), A_("April"), A_("January"), A_("February"), A_("March"), A_("April"),
@@ -1232,17 +1222,7 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
} }
/** /**
* The tab ID * Resets the tab panel input.
*
* @return {string}
* @abstract
*/
tabId() {
return "recurring";
};
/**
* Resets the tab plane input.
* *
* @override * @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 * @return {boolean} true if the description input matches this tab, or false otherwise
* @override * @override
@@ -1264,7 +1244,7 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
if (this.#getDescription(itemButton) === this.editor.description) { if (this.#getDescription(itemButton) === this.editor.description) {
itemButton.classList.add("btn-primary"); itemButton.classList.add("btn-primary");
itemButton.classList.remove("btn-outline-primary"); itemButton.classList.remove("btn-outline-primary");
this.switchToMe(); this.editor.switchTo(this);
return true; return true;
} }
} }
@@ -1272,11 +1252,10 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
} }
/** /**
* Switches to the tab plane. * @inheritDoc
* * @override
*/ */
switchToMe() { onActivated() {
super.switchToMe();
for (const itemButton of this.#itemButtons) { for (const itemButton of this.#itemButtons) {
if (itemButton.classList.contains("btn-primary")) { if (itemButton.classList.contains("btn-primary")) {
this.editor.updateCurrentSuggestedAccounts(itemButton); 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 * @return {boolean} true if valid, or false otherwise
* @override * @override
@@ -1298,20 +1277,20 @@ class DescriptionEditorRecurringTab extends DescriptionEditorTabPlane {
} }
/** /**
* The annotation tab plane. * The annotation tab.
* *
* @private * @private
*/ */
class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane { class AnnotationTab extends BaseDescriptionEditorTab {
/** /**
* Constructs a tab plane. * Constructs an annotation tab.
* *
* @param editor {DescriptionEditor} the parent description editor * @param editor {DescriptionEditor} the parent description editor
* @override * @override
*/ */
constructor(editor) { constructor(editor) {
super(editor); super("annotation", editor);
this.editor.number.onchange = () => this.updateDescription(); this.editor.number.onchange = () => this.updateDescription();
this.editor.note.onchange = () => { this.editor.note.onchange = () => {
this.editor.note.value = this.editor.note.value.trim(); this.editor.note.value = this.editor.note.value.trim();
@@ -1320,17 +1299,7 @@ class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane {
} }
/** /**
* The tab ID * Updates the description according to the input in the tab panel.
*
* @return {string}
* @abstract
*/
tabId() {
return "annotation";
};
/**
* Updates the description according to the input in the tab plane.
* *
* @override * @override
*/ */
@@ -1348,7 +1317,7 @@ class DescriptionEditorAnnotationTab extends DescriptionEditorTabPlane {
} }
/** /**
* Resets the tab plane input. * Resets the tab panel input.
* *
* @override * @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 * @return {boolean} true if the description input matches this tab, or false otherwise
* @override * @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 * @return {boolean} true if valid, or false otherwise
* @override * @override
+57 -113
View File
@@ -30,21 +30,16 @@ document.addEventListener("DOMContentLoaded", () => {
/** /**
* The period chooser. * The period chooser.
* *
* @extends {BaseTablist<BasePeriodTab>}
* @private * @private
*/ */
class PeriodChooser { class PeriodChooser extends BaseTablist {
/** /**
* The modal of the period chooser * The URL template for different periods.
* @type {HTMLDivElement} * @type {string}
*/ */
modal; urlTemplate;
/**
* The tab planes
* @type {{month: MonthTab, year: YearTab, day: DayTab, custom: CustomTab}}
*/
tabPlanes = {};
/** /**
* Constructs the period chooser. * Constructs the period chooser.
@@ -52,13 +47,24 @@ class PeriodChooser {
*/ */
constructor() { constructor() {
const prefix = "accounting-period-chooser"; const prefix = "accounting-period-chooser";
this.modal = document.getElementById(`${prefix}-modal`); super(document.getElementById(`${prefix}-tab-list`));
for (const cls of [MonthTab, YearTab, DayTab, CustomTab]) { this.tabs = [new MonthTab(this), new YearTab(this), new DayTab(this), new CustomTab(this)];
const tab = new cls(this); const modal = document.getElementById(`${prefix}-modal`);
this.tabPlanes[tab.tabId()] = tab; 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. * The period chooser.
* @type {PeriodChooser} * @type {PeriodChooser}
@@ -75,12 +81,12 @@ class PeriodChooser {
} }
/** /**
* A tab plane. * A base abstract period tab.
* *
* @abstract * @abstract
* @private * @private
*/ */
class TabPlane { class BasePeriodTab extends BaseTab {
/** /**
* The period chooser * The period chooser
@@ -95,62 +101,27 @@ class TabPlane {
prefix; prefix;
/** /**
* The tab * Constructs a base abstract period tab.
* @type {HTMLSpanElement}
*/
#tab;
/**
* The page
* @type {HTMLDivElement}
*/
#page;
/**
* Constructs a tab plane.
* *
* @param tabID {string} the tab ID
* @param chooser {PeriodChooser} the period chooser * @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.chooser = chooser;
this.prefix = `accounting-period-chooser-${this.tabId()}`; this.prefix = prefix;
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";
} }
} }
/** /**
* The month tab plane. * The month tab.
* *
* @private * @private
*/ */
class MonthTab extends TabPlane { class MonthTab extends BasePeriodTab {
/** /**
* The month chooser. * The month chooser.
@@ -159,12 +130,12 @@ class MonthTab extends TabPlane {
#monthChooser #monthChooser
/** /**
* Constructs a tab plane. * Constructs a month tab.
* *
* @param chooser {PeriodChooser} the period chooser * @param chooser {PeriodChooser} the period chooser
*/ */
constructor(chooser) { constructor(chooser) {
super(chooser); super("month", chooser);
const monthChooser = document.getElementById(`${this.prefix}-chooser`); const monthChooser = document.getElementById(`${this.prefix}-chooser`);
if (monthChooser !== null) { if (monthChooser !== null) {
this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, { this.#monthChooser = new tempusDominus.TempusDominus(monthChooser, {
@@ -184,45 +155,36 @@ class MonthTab extends TabPlane {
const date = e.detail.date; const date = e.detail.date;
const zeroPaddedMonth = `0${date.month + 1}`.slice(-2) const zeroPaddedMonth = `0${date.month + 1}`.slice(-2)
const period = `${date.year}-${zeroPaddedMonth}`; const period = `${date.year}-${zeroPaddedMonth}`;
window.location = chooser.modal.dataset.urlTemplate window.location = chooser.urlTemplate
.replaceAll("PERIOD", period); .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() { constructor(chooser) {
return "month"; super("year", chooser);
} }
} }
/** /**
* The year tab plane. * The day tab.
* *
* @private * @private
*/ */
class YearTab extends TabPlane { class DayTab extends BasePeriodTab {
/**
* The tab ID
*
* @return {string}
*/
tabId() {
return "year";
}
}
/**
* The day tab plane.
*
* @private
*/
class DayTab extends TabPlane {
/** /**
* The day input * The day input
@@ -237,18 +199,18 @@ class DayTab extends TabPlane {
#dateError; #dateError;
/** /**
* Constructs a tab plane. * Constructs a day tab.
* *
* @param chooser {PeriodChooser} the period chooser * @param chooser {PeriodChooser} the period chooser
*/ */
constructor(chooser) { constructor(chooser) {
super(chooser); super("day", chooser);
this.#date = document.getElementById(`${this.prefix}-date`); this.#date = document.getElementById(`${this.prefix}-date`);
this.#dateError = document.getElementById(`${this.prefix}-date-error`); this.#dateError = document.getElementById(`${this.prefix}-date-error`);
if (this.#date !== null) { if (this.#date !== null) {
this.#date.onchange = () => { this.#date.onchange = () => {
if (this.#validateDate()) { if (this.#validateDate()) {
window.location = chooser.modal.dataset.urlTemplate window.location = chooser.urlTemplate
.replaceAll("PERIOD", this.#date.value); .replaceAll("PERIOD", this.#date.value);
} }
}; };
@@ -275,23 +237,14 @@ class DayTab extends TabPlane {
this.#dateError.innerText = ""; this.#dateError.innerText = "";
return true; return true;
} }
/**
* The tab ID
*
* @return {string}
*/
tabId() {
return "day";
}
} }
/** /**
* The custom tab plane. * The custom tab.
* *
* @private * @private
*/ */
class CustomTab extends TabPlane { class CustomTab extends BasePeriodTab {
/** /**
* The start of the period * The start of the period
@@ -324,12 +277,12 @@ class CustomTab extends TabPlane {
#confirm; #confirm;
/** /**
* Constructs a tab plane. * Constructs a custom tab.
* *
* @param chooser {PeriodChooser} the period chooser * @param chooser {PeriodChooser} the period chooser
*/ */
constructor(chooser) { constructor(chooser) {
super(chooser); super("custom", chooser);
this.#start = document.getElementById(`${this.prefix}-start`); this.#start = document.getElementById(`${this.prefix}-start`);
this.#startError = document.getElementById(`${this.prefix}-start-error`); this.#startError = document.getElementById(`${this.prefix}-start-error`);
this.#end = document.getElementById(`${this.prefix}-end`); this.#end = document.getElementById(`${this.prefix}-end`);
@@ -351,7 +304,7 @@ class CustomTab extends TabPlane {
isValid = this.#validateStart() && isValid; isValid = this.#validateStart() && isValid;
isValid = this.#validateEnd() && isValid; isValid = this.#validateEnd() && isValid;
if (isValid) { if (isValid) {
window.location = chooser.modal.dataset.urlTemplate window.location = chooser.urlTemplate
.replaceAll("PERIOD", `${this.#start.value}-${this.#end.value}`); .replaceAll("PERIOD", `${this.#start.value}-${this.#end.value}`);
} }
}; };
@@ -407,13 +360,4 @@ class CustomTab extends TabPlane {
this.#endError.innerText = ""; this.#endError.innerText = "";
return true; return true;
} }
/**
* The tab ID
*
* @return {string}
*/
tabId() {
return "custom";
}
} }
@@ -39,36 +39,36 @@ First written: 2023/2/28
</div> </div>
{# Tab navigation #} {# Tab navigation #}
<ul class="nav nav-tabs mb-2" role="tablist"> <ul id="accounting-description-editor-{{ description_editor.debit_credit }}-tab-list" class="nav nav-tabs mb-2" role="tablist" aria-label="{{ A_("Description Type") }}">
<li class="nav-item"> <li class="nav-item">
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tab" class="nav-link active accounting-clickable" type="button" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-general-page" aria-selected="true" aria-current="page"> <button id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tab" class="nav-link active accounting-clickable" type="button" tabindex="0" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-general-panel" aria-selected="true">
{{ A_("General") }} {{ A_("General") }}
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tab" class="nav-link accounting-clickable" type="button" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-travel-page" aria-selected="false" aria-current="false"> <button id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tab" class="nav-link accounting-clickable" type="button" tabindex="-1" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-travel-panel" aria-selected="false">
{{ A_("Travel") }} {{ A_("Travel") }}
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tab" class="nav-link accounting-clickable" type="button" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-bus-page" aria-selected="false" aria-current="false"> <button id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tab" class="nav-link accounting-clickable" type="button" tabindex="-1" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-bus-panel" aria-selected="false">
{{ A_("Bus") }} {{ A_("Bus") }}
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab" class="nav-link accounting-clickable" type="button" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-page" aria-selected="false" aria-current="false"> <button id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab" class="nav-link accounting-clickable" type="button" tabindex="-1" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-panel" aria-selected="false">
{{ A_("Recurring") }} {{ A_("Recurring") }}
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-tab" class="nav-link accounting-clickable" type="button" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-page" aria-selected="false" aria-current="false"> <button id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-tab" class="nav-link accounting-clickable" type="button" tabindex="-1" role="tab" aria-controls="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-panel" aria-selected="false">
{{ A_("Annotation") }} {{ A_("Annotation") }}
</button> </button>
</li> </li>
</ul> </ul>
{# A general description with a tag #} {# A general description with a tag #}
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-general-page" role="tabpanel" aria-current="page" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-general-tab"> <div id="accounting-description-editor-{{ description_editor.debit_credit }}-general-panel" role="tabpanel" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-general-tab">
<div class="form-floating mb-2"> <div class="form-floating mb-2">
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag" class="form-control" type="text" value="" placeholder=" "> <input id="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag">{{ A_("Tag") }}</label> <label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-general-tag">{{ A_("Tag") }}</label>
@@ -85,7 +85,7 @@ First written: 2023/2/28
</div> </div>
{# A general trip with the origin and distination #} {# A general trip with the origin and distination #}
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-page" class="d-none" role="tabpanel" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tab"> <div id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-panel" class="d-none" role="tabpanel" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tab">
<div class="form-floating mb-2"> <div class="form-floating mb-2">
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag" class="form-control" type="text" value="" placeholder=" "> <input id="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag" class="form-control" type="text" value="" placeholder=" ">
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag">{{ A_("Tag") }}</label> <label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-travel-tag">{{ A_("Tag") }}</label>
@@ -119,7 +119,7 @@ First written: 2023/2/28
</div> </div>
{# A bus trip with the route name or route number, the origin and distination #} {# A bus trip with the route name or route number, the origin and distination #}
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-page" class="d-none" role="tabpanel" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tab"> <div id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-panel" class="d-none" role="tabpanel" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tab">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<div class="form-floating me-2"> <div class="form-floating me-2">
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tag" class="form-control" type="text" value="" placeholder=" "> <input id="accounting-description-editor-{{ description_editor.debit_credit }}-bus-tag" class="form-control" type="text" value="" placeholder=" ">
@@ -156,7 +156,7 @@ First written: 2023/2/28
</div> </div>
{# A recurring transaction #} {# A recurring transaction #}
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-page" class="d-none" role="tabpanel" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab"> <div id="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-panel" class="d-none" role="tabpanel" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-recurring-tab">
<div class="accounting-description-editor-buttons"> <div class="accounting-description-editor-buttons">
{% for recurring in description_editor.recurring %} {% for recurring in description_editor.recurring %}
<button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-recurring-item" type="button" tabindex="-1" data-template="{{ recurring.description_template }}" data-accounts="{{ recurring.account_codes|tojson|forceescape }}"> <button class="btn btn-outline-primary accounting-description-editor-{{ description_editor.debit_credit }}-recurring-item" type="button" tabindex="-1" data-template="{{ recurring.description_template }}" data-accounts="{{ recurring.account_codes|tojson|forceescape }}">
@@ -167,7 +167,7 @@ First written: 2023/2/28
</div> </div>
{# The annotation #} {# The annotation #}
<div id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-page" class="d-none" role="tabpanel" aria-current="false" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-tab"> <div id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-panel" class="d-none" role="tabpanel" aria-labelledby="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-tab">
<div class="form-floating"> <div class="form-floating">
<input id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-number" class="form-control" type="number" min="1" value="" placeholder=" "> <input id="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-number" class="form-control" type="number" min="1" value="" placeholder=" ">
<label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-number">{{ A_("The Number of Items") }}</label> <label class="form-label" for="accounting-description-editor-{{ description_editor.debit_credit }}-annotation-number">{{ A_("The Number of Items") }}</label>
@@ -27,6 +27,7 @@ First written: 2023/2/26
<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>
<script src="{{ url_for("accounting.static", filename="js/original-line-item-selector.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/original-line-item-selector.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/base-tablist.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/description-editor.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/description-editor.js") }}"></script>
{% endblock %} {% endblock %}
@@ -23,6 +23,7 @@ First written: 2023/3/7
{% block accounting_scripts %} {% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/base-tablist.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %} {% endblock %}
@@ -28,31 +28,31 @@ First written: 2023/3/4
</div> </div>
<div class="modal-body"> <div class="modal-body">
{# Tab navigation #} {# Tab navigation #}
<ul class="nav nav-tabs mb-2" role="tablist"> <ul id="accounting-period-chooser-tab-list" class="nav nav-tabs mb-2" role="tablist" aria-label="{{ A_("Period Type") }}">
<li class="nav-item"> <li class="nav-item">
<button id="accounting-period-chooser-month-tab" class="nav-link {% if report.period.is_type_month %} active {% endif %} accounting-clickable" type="button" role="tab" aria-controls="accounting-period-chooser-month-page" aria-selected="true" aria-current="{% if report.period.is_type_month %} page {% else %} false {% endif %}" data-tab-id="month"> <button id="accounting-period-chooser-month-tab" class="nav-link {% if report.period.is_type_month %} active {% endif %} accounting-clickable" type="button" tabindex="{% if report.period.is_type_month %}0{% else %}-1{% endif %}" role="tab" aria-controls="accounting-period-chooser-month-panel" aria-selected="{% if report.period.is_type_month %}true{% else %}false{% endif %}">
{{ A_("Month") }} {{ A_("Month") }}
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button id="accounting-period-chooser-year-tab" class="nav-link {% if report.period.is_a_year %} active {% endif %} accounting-clickable" type="button" role="tab" aria-controls="accounting-period-chooser-year-page" aria-selected="false" aria-current="{% if report.period.is_a_year %} page {% else %} false {% endif %}" data-tab-id="year"> <button id="accounting-period-chooser-year-tab" class="nav-link {% if report.period.is_a_year %} active {% endif %} accounting-clickable" type="button" tabindex="{% if report.period.is_a_year %}0{% else %}-1{% endif %}" role="tab" aria-controls="accounting-period-chooser-year-panel" aria-selected="{% if report.period.is_a_year %}true{% else %}false{% endif %}">
{{ A_("Year") }} {{ A_("Year") }}
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button id="accounting-period-chooser-day-tab" class="nav-link {% if report.period.is_a_day %} active {% endif %} accounting-clickable" type="button" role="tab" aria-controls="accounting-period-chooser-day-page" aria-selected="false" aria-current="{% if report.period.is_a_day %} page {% else %} false {% endif %}" data-tab-id="day"> <button id="accounting-period-chooser-day-tab" class="nav-link {% if report.period.is_a_day %} active {% endif %} accounting-clickable" type="button" tabindex="{% if report.period.is_a_day %}0{% else %}-1{% endif %}" role="tab" aria-controls="accounting-period-chooser-day-panel" aria-selected="{% if report.period.is_a_day %}true{% else %}false{% endif %}">
{{ A_("Day") }} {{ A_("Day") }}
</button> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button id="accounting-period-chooser-custom-tab" class="nav-link {% if report.period.is_type_arbitrary %} active {% endif %} accounting-clickable" type="button" role="tab" aria-controls="accounting-period-chooser-custom-page" aria-selected="false" aria-current="{% if report.period.is_type_arbitrary %} page {% else %} false {% endif %}" data-tab-id="custom"> <button id="accounting-period-chooser-custom-tab" class="nav-link {% if report.period.is_type_arbitrary %} active {% endif %} accounting-clickable" type="button" tabindex="{% if report.period.is_type_arbitrary %}0{% else %}-1{% endif %}" role="tab" aria-controls="accounting-period-chooser-custom-panel" aria-selected="{% if report.period.is_type_arbitrary %}true{% else %}false{% endif %}">
{{ A_("Custom") }} {{ A_("Custom") }}
</button> </button>
</li> </li>
</ul> </ul>
{# The month periods #} {# The month periods #}
<div id="accounting-period-chooser-month-page" {% if report.period.is_type_month %} aria-current="page" {% else %} class="d-none" role="tabpanel" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-month-tab"> <div id="accounting-period-chooser-month-panel" {% if not report.period.is_type_month %} class="d-none" {% endif %} role="tabpanel" aria-labelledby="accounting-period-chooser-month-tab">
<div> <div>
<a class="btn {% if report.period.is_this_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.this_month_url }}"> <a class="btn {% if report.period.is_this_month %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.this_month_url }}">
{{ A_("This Month") }} {{ A_("This Month") }}
@@ -72,7 +72,7 @@ First written: 2023/3/4
</div> </div>
{# The year periods #} {# The year periods #}
<div id="accounting-period-chooser-year-page" {% if report.period.is_a_year %} aria-current="page" {% else %} class="d-none" role="tabpanel" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-year-tab"> <div id="accounting-period-chooser-year-panel" {% if not report.period.is_a_year %} class="d-none" {% endif %} role="tabpanel" aria-labelledby="accounting-period-chooser-year-tab">
<a class="btn {% if report.period.is_this_year %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.this_year_url }}"> <a class="btn {% if report.period.is_this_year %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.this_year_url }}">
{{ A_("This Year") }} {{ A_("This Year") }}
</a> </a>
@@ -93,7 +93,7 @@ First written: 2023/3/4
</div> </div>
{# The day periods #} {# The day periods #}
<div id="accounting-period-chooser-day-page" {% if report.period.is_a_day %} aria-current="page" {% else %} class="d-none" role="tabpanel" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-day-tab"> <div id="accounting-period-chooser-day-panel" {% if not report.period.is_a_day %} class="d-none" {% endif %} role="tabpanel" aria-labelledby="accounting-period-chooser-day-tab">
<div> <div>
<a class="btn {% if report.period.is_today %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.today_url }}"> <a class="btn {% if report.period.is_today %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.today_url }}">
{{ A_("Today") }} {{ A_("Today") }}
@@ -116,7 +116,7 @@ First written: 2023/3/4
</div> </div>
{# The custom periods #} {# The custom periods #}
<div id="accounting-period-chooser-custom-page" {% if report.period.is_type_arbitrary %} aria-current="page" {% else %} class="d-none" role="tabpanel" aria-current="false" {% endif %} aria-labelledby="accounting-period-chooser-custom-tab"> <div id="accounting-period-chooser-custom-panel" {% if not report.period.is_type_arbitrary %} class="d-none" {% endif %} role="tabpanel" aria-labelledby="accounting-period-chooser-custom-tab">
<div> <div>
<a class="btn {% if report.period.is_all %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.all_url }}"> <a class="btn {% if report.period.is_all %} btn-primary {% else %} btn-outline-primary {% endif %}" role="button" href="{{ report.period_chooser.all_url }}">
{{ A_("All") }} {{ A_("All") }}
@@ -23,6 +23,7 @@ First written: 2023/3/5
{% block accounting_scripts %} {% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/base-tablist.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %} {% endblock %}
@@ -23,6 +23,7 @@ First written: 2023/3/7
{% block accounting_scripts %} {% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/base-tablist.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %} {% endblock %}
@@ -23,6 +23,7 @@ First written: 2023/3/4
{% block accounting_scripts %} {% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/base-tablist.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %} {% endblock %}
@@ -23,6 +23,7 @@ First written: 2023/3/5
{% block accounting_scripts %} {% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/base-tablist.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %} {% endblock %}
@@ -23,6 +23,7 @@ First written: 2023/3/5
{% block accounting_scripts %} {% block accounting_scripts %}
<script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/material-fab-speed-dial.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/base-tablist.js") }}"></script>
<script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script> <script src="{{ url_for("accounting.static", filename="js/period-chooser.js") }}"></script>
{% endblock %} {% endblock %}