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
* 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>;
componentId: string;
children: Component[];
children: ChildT[];
initialized: Promise<void> | null;
parent?: Component;
parent?: TypedComponent<any>;
position!: number;
constructor() {
@ -31,12 +31,12 @@ export default class Component {
return this.constructor.name.replace(/[^A-Z0-9]/ig, "_");
}
setParent(parent: Component) {
setParent(parent: TypedComponent<any>) {
this.parent = parent;
return this;
}
child(...components: Component[]) {
child(...components: ChildT[]) {
for (const component of components) {
component.setParent(this);
@ -122,3 +122,5 @@ export default class Component {
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 { t } from "../services/i18n.js";
import toastService from "../services/toast.js";
/**
* This is the base widget for all other widgets.
*
* For information on using widgets, see the tutorial {@tutorial widget_basics}.
*/
class BasicWidget extends Component {
export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedComponent<T> {
protected attrs: Record<string, string>;
private classes: string[];
private childPositionCounter: number;
@ -27,7 +22,7 @@ class BasicWidget extends Component {
this.childPositionCounter = 10;
}
child(...components: Component[]) {
child(...components: T[]) {
if (!components) {
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.
* @returns self for chaining.
*/
optChild(condition: boolean, ...components: Component[]) {
optChild(condition: boolean, ...components: T[]) {
if (condition) {
return this.child(...components);
} else {
@ -259,4 +254,11 @@ class BasicWidget extends Component {
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";
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) {
super();
@ -13,4 +14,5 @@ export default class FlexContainer extends Container {
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 utils from "../../services/utils.js";
export default class LauncherContainer extends FlexContainer {
export default class LauncherContainer extends FlexContainer<LauncherWidget> {
private isHorizontalLayout: boolean;
constructor(isHorizontalLayout: boolean) {

View File

@ -1,7 +1,11 @@
import FlexContainer from "./flex_container.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() {
super('column');
@ -19,7 +23,7 @@ export default class RightPaneContainer extends FlexContainer {
&& !!this.children.find(ch => ch.isEnabled() && ch.canBeShown());
}
handleEventInChildren(name, data) {
handleEventInChildren(name: string, data: unknown) {
const promise = super.handleEventInChildren(name, data);
if (['activeContextChanged', 'noteSwitchedAndActivated', 'noteSwitched'].includes(name)) {

View File

@ -1,5 +1,6 @@
import { EventData } from "../../components/app_context.js";
import { Screen } from "../../components/mobile_screen_switcher.js";
import BasicWidget from "../basic_widget.js";
import FlexContainer, { FlexDirection } from "../containers/flex_container.js";
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. */
const DRAG_OPENED_START_THRESHOLD = 80;
export default class SidebarContainer extends FlexContainer {
export default class SidebarContainer extends FlexContainer<BasicWidget> {
private screenName: Screen;
/** 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 toastService from "../services/toast.js";
import { t } from "../services/i18n.js";
const WIDGET_TPL = `
<div class="card widget">
@ -19,6 +18,12 @@ const WIDGET_TPL = `
* @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. */
get widgetTitle() { return "Untitled widget"; }
@ -53,7 +58,7 @@ class RightPanelWidget extends NoteContextAwareWidget {
this.$buttons.empty();
for (const buttonWidget of this.children) {
this.$buttons.append(buttonWidget.render());
this.$buttons.append((buttonWidget as BasicWidget).render());
}
this.initialized = this.doRenderBody().catch(e => {