chore(types): allow containers to constrain children

This commit is contained in:
Elian Doran 2025-01-05 12:21:01 +02:00
parent 4cfb0d6161
commit 6d41af98fd
No known key found for this signature in database
9 changed files with 69 additions and 41 deletions

View File

@ -12,12 +12,12 @@ import { CommandMappings, CommandNames } from './app_context.js';
* - although the execution is async, we are collecting all the promises, and therefore it is possible to wait until the * - although the execution is async, we are collecting all the promises, and therefore it is possible to wait until the
* event / command is executed in all components - by simply awaiting the `triggerEvent()`. * event / command is executed in all components - by simply awaiting the `triggerEvent()`.
*/ */
export default class Component { export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
$widget!: JQuery<HTMLElement>; $widget!: JQuery<HTMLElement>;
componentId: string; componentId: string;
children: Component[]; children: ChildT[];
initialized: Promise<void> | null; initialized: Promise<void> | null;
parent?: Component; parent?: TypedComponent<any>;
position!: number; position!: number;
constructor() { constructor() {
@ -31,12 +31,12 @@ export default class Component {
return this.constructor.name.replace(/[^A-Z0-9]/ig, "_"); return this.constructor.name.replace(/[^A-Z0-9]/ig, "_");
} }
setParent(parent: Component) { setParent(parent: TypedComponent<any>) {
this.parent = parent; this.parent = parent;
return this; return this;
} }
child(...components: Component[]) { child(...components: ChildT[]) {
for (const component of components) { for (const component of components) {
component.setParent(this); component.setParent(this);
@ -122,3 +122,5 @@ export default class Component {
return promise; return promise;
} }
} }
export default class Component extends TypedComponent<Component> {}

View File

@ -1,14 +1,9 @@
import Component from "../components/component.js"; import Component, { TypedComponent } from "../components/component.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
import toastService from "../services/toast.js"; import toastService from "../services/toast.js";
/** export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedComponent<T> {
* This is the base widget for all other widgets.
*
* For information on using widgets, see the tutorial {@tutorial widget_basics}.
*/
class BasicWidget extends Component {
protected attrs: Record<string, string>; protected attrs: Record<string, string>;
private classes: string[]; private classes: string[];
private childPositionCounter: number; private childPositionCounter: number;
@ -27,7 +22,7 @@ class BasicWidget extends Component {
this.childPositionCounter = 10; this.childPositionCounter = 10;
} }
child(...components: Component[]) { child(...components: T[]) {
if (!components) { if (!components) {
return this; return this;
} }
@ -53,7 +48,7 @@ class BasicWidget extends Component {
* @param components the components to be added as children to this component provided the condition is truthy. * @param components the components to be added as children to this component provided the condition is truthy.
* @returns self for chaining. * @returns self for chaining.
*/ */
optChild(condition: boolean, ...components: Component[]) { optChild(condition: boolean, ...components: T[]) {
if (condition) { if (condition) {
return this.child(...components); return this.child(...components);
} else { } else {
@ -259,4 +254,11 @@ class BasicWidget extends Component {
cleanup() {} cleanup() {}
} }
export default BasicWidget; /**
* This is the base widget for all other widgets.
*
* For information on using widgets, see the tutorial {@tutorial widget_basics}.
*/
export default class BasicWidget extends TypedBasicWidget<Component> {
}

View File

@ -1,18 +0,0 @@
import BasicWidget from "../basic_widget.js";
export default class Container extends BasicWidget {
doRender() {
this.$widget = $(`<div>`);
this.renderChildren();
}
renderChildren() {
for (const widget of this.children) {
try {
this.$widget.append(widget.render());
} catch (e) {
widget.logRenderingError(e);
}
}
}
}

View File

@ -0,0 +1,30 @@
import Component, { TypedComponent } from "../../components/component.js";
import BasicWidget, { TypedBasicWidget } from "../basic_widget.js";
export default class Container<T extends TypedComponent<any>> extends TypedBasicWidget<T> {
doRender() {
this.$widget = $(`<div>`);
this.renderChildren();
}
renderChildren() {
for (const widget of this.children) {
if (!("render" in widget)) {
throw "Non-renderable widget encountered.";
}
const typedWidget = widget as unknown as TypedBasicWidget<any>;
try {
if ("render" in widget) {
this.$widget.append(typedWidget.render());
}
} catch (e: any) {
typedWidget.logRenderingError(e);
}
}
}
}

View File

@ -1,8 +1,9 @@
import { TypedComponent } from "../../components/component.js";
import Container from "./container.js"; import Container from "./container.js";
export type FlexDirection = "row" | "column"; export type FlexDirection = "row" | "column";
export default class FlexContainer extends Container { export default class FlexContainer<T extends TypedComponent<any>> extends Container<T> {
constructor(direction: FlexDirection) { constructor(direction: FlexDirection) {
super(); super();
@ -13,4 +14,5 @@ export default class FlexContainer extends Container {
this.attrs.style = `display: flex; flex-direction: ${direction};`; this.attrs.style = `display: flex; flex-direction: ${direction};`;
} }
} }

View File

@ -4,7 +4,7 @@ import appContext, { EventData } from "../../components/app_context.js";
import LauncherWidget from "./launcher.js"; import LauncherWidget from "./launcher.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
export default class LauncherContainer extends FlexContainer { export default class LauncherContainer extends FlexContainer<LauncherWidget> {
private isHorizontalLayout: boolean; private isHorizontalLayout: boolean;
constructor(isHorizontalLayout: boolean) { constructor(isHorizontalLayout: boolean) {

View File

@ -1,7 +1,11 @@
import FlexContainer from "./flex_container.js"; import FlexContainer from "./flex_container.js";
import splitService from "../../services/resizer.js"; import splitService from "../../services/resizer.js";
import RightPanelWidget from "../right_panel_widget.js";
export default class RightPaneContainer extends FlexContainer<RightPanelWidget> {
private rightPaneHidden: boolean;
export default class RightPaneContainer extends FlexContainer {
constructor() { constructor() {
super('column'); super('column');
@ -19,7 +23,7 @@ export default class RightPaneContainer extends FlexContainer {
&& !!this.children.find(ch => ch.isEnabled() && ch.canBeShown()); && !!this.children.find(ch => ch.isEnabled() && ch.canBeShown());
} }
handleEventInChildren(name, data) { handleEventInChildren(name: string, data: unknown) {
const promise = super.handleEventInChildren(name, data); const promise = super.handleEventInChildren(name, data);
if (['activeContextChanged', 'noteSwitchedAndActivated', 'noteSwitched'].includes(name)) { if (['activeContextChanged', 'noteSwitchedAndActivated', 'noteSwitched'].includes(name)) {

View File

@ -1,5 +1,6 @@
import { EventData } from "../../components/app_context.js"; import { EventData } from "../../components/app_context.js";
import { Screen } from "../../components/mobile_screen_switcher.js"; import { Screen } from "../../components/mobile_screen_switcher.js";
import BasicWidget from "../basic_widget.js";
import FlexContainer, { FlexDirection } from "../containers/flex_container.js"; import FlexContainer, { FlexDirection } from "../containers/flex_container.js";
const DRAG_STATE_NONE = 0; const DRAG_STATE_NONE = 0;
@ -14,7 +15,7 @@ const DRAG_CLOSED_START_THRESHOLD = 10;
/** The number of pixels the user has to drag across the screen to the left when the sidebar is opened to trigger the drag close animation. */ /** The number of pixels the user has to drag across the screen to the left when the sidebar is opened to trigger the drag close animation. */
const DRAG_OPENED_START_THRESHOLD = 80; const DRAG_OPENED_START_THRESHOLD = 80;
export default class SidebarContainer extends FlexContainer { export default class SidebarContainer extends FlexContainer<BasicWidget> {
private screenName: Screen; private screenName: Screen;
/** The screen name that is currently active, according to the screen changed event. */ /** The screen name that is currently active, according to the screen changed event. */

View File

@ -1,6 +1,5 @@
import BasicWidget from "./basic_widget.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js"; import NoteContextAwareWidget from "./note_context_aware_widget.js";
import toastService from "../services/toast.js";
import { t } from "../services/i18n.js";
const WIDGET_TPL = ` const WIDGET_TPL = `
<div class="card widget"> <div class="card widget">
@ -19,6 +18,12 @@ const WIDGET_TPL = `
* @extends {NoteContextAwareWidget} * @extends {NoteContextAwareWidget}
*/ */
class RightPanelWidget extends NoteContextAwareWidget { class RightPanelWidget extends NoteContextAwareWidget {
private $bodyWrapper!: JQuery<HTMLElement>;
$body!: JQuery<HTMLElement>;
private $title!: JQuery<HTMLElement>;
private $buttons!: JQuery<HTMLElement>;
/** Title to show in the panel. */ /** Title to show in the panel. */
get widgetTitle() { return "Untitled widget"; } get widgetTitle() { return "Untitled widget"; }
@ -53,7 +58,7 @@ class RightPanelWidget extends NoteContextAwareWidget {
this.$buttons.empty(); this.$buttons.empty();
for (const buttonWidget of this.children) { for (const buttonWidget of this.children) {
this.$buttons.append(buttonWidget.render()); this.$buttons.append((buttonWidget as BasicWidget).render());
} }
this.initialized = this.doRenderBody().catch(e => { this.initialized = this.doRenderBody().catch(e => {