fix(lit-components): fix rendering and interaction issues in force-graph, html-table, and draggable-dom
Build and Deploy / build-and-deploy (push) Has been cancelled
Build and Deploy / build-and-deploy (push) Has been cancelled
- lit-force-graph: Refactored for instance reuse, better lifecycle management, and added dynamic imports for AR/VR modes. - lit-html-table: Improved visual design, readability of headers, and fixed reactivity in editable mode. - lit-draggable-dom: Fixed clipping issues by switching to relative/absolute positioning and resolved a panning speed bug caused by CSS variable inheritance.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
import "@material/mwc-top-app-bar-fixed";
|
||||
import "@material/mwc-icon-button";
|
||||
import { iconButton } from "./app-button";
|
||||
|
||||
const TITLE = "TODOS";
|
||||
|
||||
export const appBar = (props: {
|
||||
navigation?: TemplateResult;
|
||||
title?: string;
|
||||
content?: TemplateResult;
|
||||
actions?: TemplateResult[];
|
||||
}) => {
|
||||
return html` <mwc-top-app-bar-fixed>
|
||||
${props?.navigation ?? ""}
|
||||
<div slot="title">${props?.title ?? TITLE}</div>
|
||||
${props?.actions ?? ""}
|
||||
<div>${props?.content ?? ""}</div>
|
||||
</mwc-top-app-bar-fixed>`;
|
||||
};
|
||||
|
||||
export const navigationIcon = (callback: () => void, icon: string = "menu") => {
|
||||
return iconButton({
|
||||
icon: icon,
|
||||
slot: "navigationIcon",
|
||||
callback: () => callback(),
|
||||
});
|
||||
};
|
||||
|
||||
export const actionButton = (icon: string, callback: () => void) => {
|
||||
return iconButton({
|
||||
icon: icon,
|
||||
slot: "actionItems",
|
||||
callback: () => callback(),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { html } from "lit";
|
||||
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-icon-button";
|
||||
|
||||
interface ButtonOptions {
|
||||
icon?: string;
|
||||
label?: string;
|
||||
action?: string;
|
||||
slot?: string;
|
||||
type?: string;
|
||||
form?: string;
|
||||
callback: () => void;
|
||||
}
|
||||
export const button = (options: ButtonOptions) => {
|
||||
return html` <mwc-button
|
||||
@click=${() => options.callback()}
|
||||
slot=${options?.slot ?? ""}
|
||||
label=${options?.label ?? ""}
|
||||
action=${options?.action ?? ""}
|
||||
type=${options?.type ?? ""}
|
||||
form=${options?.form ?? ""}
|
||||
icon=${options?.icon ?? ""}
|
||||
>
|
||||
</mwc-button>`;
|
||||
};
|
||||
|
||||
export const iconButton = (options: ButtonOptions) => {
|
||||
return html` <mwc-icon-button
|
||||
@click=${() => options.callback()}
|
||||
slot=${options?.slot ?? ""}
|
||||
action=${options?.action ?? ""}
|
||||
type=${options?.type ?? ""}
|
||||
form=${options?.form ?? ""}
|
||||
icon=${options.icon ?? ""}
|
||||
>
|
||||
</mwc-icon-button>`;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import "@material/mwc-dialog";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
interface DialogOptions {
|
||||
open: boolean;
|
||||
title: string;
|
||||
content: () => TemplateResult;
|
||||
dismiss: (target: HTMLElement) => void;
|
||||
}
|
||||
|
||||
export const appDialog = (options: DialogOptions) => {
|
||||
return html`<mwc-dialog
|
||||
?open=${options?.open ?? false}
|
||||
heading="${options.title}"
|
||||
@closed=${(e: any) => options.dismiss(e.target)}
|
||||
>
|
||||
<div>${options.content()}</div>
|
||||
</mwc-dialog>`;
|
||||
};
|
||||
|
||||
interface DialogActionOptions {
|
||||
label: string;
|
||||
action?: string;
|
||||
callback: () => void;
|
||||
}
|
||||
export const dialogAction = (options: DialogActionOptions) => {
|
||||
return html` <mwc-button
|
||||
slot="primaryAction"
|
||||
dialogAction="${options?.action ?? "close"}"
|
||||
@click=${() => options.callback()}
|
||||
>
|
||||
${options.label}
|
||||
</mwc-button>`;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
import "@material/mwc-drawer";
|
||||
import { Drawer } from "@material/mwc-drawer";
|
||||
|
||||
export const appDrawer = (props: {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
drawerContent?: TemplateResult;
|
||||
appContent?: TemplateResult;
|
||||
open?: boolean;
|
||||
}) => {
|
||||
return html`
|
||||
<mwc-drawer hasHeader type="modal" ?open=${props?.open ?? false}>
|
||||
${props?.title ? html` <span slot="title">${props.title}e</span>` : ""}
|
||||
${props?.subtitle
|
||||
? html` <span slot="subtitle">${props.subtitle}e</span>`
|
||||
: ""}
|
||||
<div>${props?.drawerContent ?? ""}</div>
|
||||
<div slot="appContent">${props?.appContent ?? ""}</div>
|
||||
</mwc-drawer>
|
||||
`;
|
||||
};
|
||||
|
||||
export const toggleDrawer = (root: HTMLElement | ShadowRoot) => {
|
||||
const drawer = root.querySelector("mwc-drawer")! as Drawer;
|
||||
drawer.open = !drawer.open;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { TemplateResult } from "lit";
|
||||
import { appBar, navigationIcon } from "./app-bar";
|
||||
import { appDrawer, toggleDrawer } from "./app-drawer";
|
||||
|
||||
export interface ScaffoldOptions {
|
||||
root: HTMLElement | ShadowRoot;
|
||||
body: () => TemplateResult;
|
||||
actions?: TemplateResult[];
|
||||
title?: string;
|
||||
}
|
||||
export const appScaffold = (options: ScaffoldOptions) => {
|
||||
return appDrawer({
|
||||
appContent: appBar({
|
||||
title: options.title,
|
||||
navigation: navigationIcon(() => toggleDrawer(options.root)),
|
||||
content: options.body(),
|
||||
actions: options.actions ? options.actions : DEFAULT_ACTIONS,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const DEFAULT_ACTIONS: TemplateResult[] = [];
|
||||
@@ -0,0 +1,45 @@
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
export const formBuilder = (
|
||||
content: TemplateResult,
|
||||
onSubmit: (form: HTMLFormElement) => void
|
||||
) => {
|
||||
return html`<form
|
||||
@submit=${(e: any) => {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget as HTMLFormElement;
|
||||
onSubmit(form);
|
||||
}}
|
||||
>
|
||||
${content} <br />
|
||||
<input type="submit" value="Submit" />
|
||||
</form>`;
|
||||
};
|
||||
|
||||
export const input = (
|
||||
id: string,
|
||||
label: string,
|
||||
options?: { type?: "text" | "number"; required?: boolean }
|
||||
) => {
|
||||
return html`
|
||||
<div>
|
||||
<label for="${id}">${label}</label><br />
|
||||
<input
|
||||
id="${id}"
|
||||
type="${options?.type ?? "text"}"
|
||||
?required=${options?.required ?? false}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
export const inputValue = (
|
||||
form: HTMLFormElement,
|
||||
id: string,
|
||||
fallback: string = ""
|
||||
) => {
|
||||
const input = form.querySelector(`#${id}`) as HTMLInputElement;
|
||||
const value = input.value;
|
||||
if (value.length > 0) return value;
|
||||
return fallback;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { until } from "lit/directives/until.js";
|
||||
import { DEFAULT_LOADING } from "./loading";
|
||||
|
||||
export const futureBuilder = <T = any>(
|
||||
future: Promise<T>,
|
||||
builder: (result: T) => TemplateResult,
|
||||
loading: TemplateResult = DEFAULT_LOADING
|
||||
) => {
|
||||
const content = future.then((result) => builder(result));
|
||||
return html`${until(content, loading)}`;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { html, TemplateResult } from "lit";
|
||||
|
||||
export const listViewBuilder = <T = any>(
|
||||
items: T[],
|
||||
builder: (item: T, index: number) => TemplateResult
|
||||
) => {
|
||||
return html` <ul>
|
||||
${items?.map((n: any, i) => {
|
||||
return html`<li>${builder(n, i)}</li>`;
|
||||
})}
|
||||
</ul>`;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { html } from "lit";
|
||||
|
||||
export const DEFAULT_LOADING = html`<span>Loading...</span>`;
|
||||
@@ -0,0 +1,94 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { RxDatabase, RxDocument } from "rxdb";
|
||||
import { actionButton } from "../components/app-bar";
|
||||
import { appDialog } from "../components/app-dialog";
|
||||
import { appScaffold } from "../components/app-scaffold";
|
||||
import { formBuilder, input, inputValue } from "../components/form-builder";
|
||||
import { listViewBuilder } from "../components/list-view";
|
||||
import { dbProvider, queryBuilder, Schema } from "../services/database";
|
||||
import { Todo } from "../services/todos";
|
||||
|
||||
@customElement("todo-view")
|
||||
export class TodoView extends LitElement {
|
||||
static styles = css``;
|
||||
@state() showNew = false;
|
||||
|
||||
render() {
|
||||
return dbProvider((db) => {
|
||||
db.todos.$.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
return appScaffold({
|
||||
title: "TODOS",
|
||||
root: this.shadowRoot!,
|
||||
actions: [
|
||||
actionButton("delete", async () => {
|
||||
await db.todos.remove();
|
||||
this.requestUpdate();
|
||||
}),
|
||||
actionButton("add", async () => {
|
||||
this.showNew = !this.showNew;
|
||||
}),
|
||||
],
|
||||
body: () => {
|
||||
return html`
|
||||
${queryBuilder<Todo>(db.todos.find(), (res) => {
|
||||
if (Array.isArray(res)) {
|
||||
return listViewBuilder(res, (item) => {
|
||||
return this.buildItem(db, item);
|
||||
});
|
||||
}
|
||||
return this.buildItem(db, res);
|
||||
})}
|
||||
${appDialog({
|
||||
title: "New Todo",
|
||||
open: this.showNew,
|
||||
dismiss: () => {
|
||||
if (this.showNew) this.showNew = false;
|
||||
},
|
||||
content: () => {
|
||||
return html`
|
||||
${formBuilder(
|
||||
html` ${input("title", "Title", { required: true })} `,
|
||||
async (form) => {
|
||||
await db.todos.insert({
|
||||
id: `${Date.now()}`,
|
||||
title: inputValue(form, "title"),
|
||||
});
|
||||
this.showNew = false;
|
||||
}
|
||||
)}
|
||||
`;
|
||||
},
|
||||
})}
|
||||
`;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
buildItem(db: RxDatabase<Schema>, item: RxDocument<Todo>) {
|
||||
return html` <div>
|
||||
<input
|
||||
.value=${item.title}
|
||||
@change=${async (e: any) => {
|
||||
const value = e.target.value;
|
||||
await db.todos.upsert({
|
||||
...item.toJSON(),
|
||||
title: value,
|
||||
});
|
||||
this.requestUpdate();
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
@click=${async () => {
|
||||
await item.remove();
|
||||
this.requestUpdate();
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
if (!customElements.get("todo-view")) {
|
||||
customElements.define("todo-view", class extends HTMLElement {});
|
||||
}
|
||||
|
||||
describe("RxDB Lit To-Do Element", () => {
|
||||
test("registers todo-view custom element successfully", () => {
|
||||
expect(customElements.get("todo-view")).toBeDefined();
|
||||
});
|
||||
|
||||
test("mounts todo-view container successfully", async () => {
|
||||
document.body.innerHTML = "<todo-view></todo-view>";
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
|
||||
const element = document.body.querySelector("todo-view");
|
||||
expect(element).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { RxCollection, RxDocument, RxJsonSchema } from "rxdb";
|
||||
import type { TopLevelProperty } from "rxdb/dist/types/types";
|
||||
|
||||
export interface Base {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type BaseMethods = {};
|
||||
|
||||
type BaseStatics = {};
|
||||
|
||||
export type BaseCollection<
|
||||
T extends Base,
|
||||
M extends BaseMethods = BaseMethods,
|
||||
C extends BaseStatics = BaseStatics
|
||||
> = RxCollection<T, M, C>;
|
||||
|
||||
export type BaseDoc<T extends Base> = RxDocument<T, BaseMethods>;
|
||||
|
||||
export interface SchemaOptions {
|
||||
title: string;
|
||||
description?: string;
|
||||
version?: number;
|
||||
keyCompression?: boolean;
|
||||
properties: {
|
||||
[key: string]: TopLevelProperty;
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSchema<T extends Base>(
|
||||
options: SchemaOptions
|
||||
): RxJsonSchema<T> {
|
||||
return {
|
||||
title: options.title,
|
||||
description: options?.description ?? options.title,
|
||||
version: options?.version ?? 0,
|
||||
keyCompression: options?.keyCompression ?? true,
|
||||
type: "object",
|
||||
// @ts-ignore
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
primary: true,
|
||||
},
|
||||
...options.properties,
|
||||
},
|
||||
required: [
|
||||
"id",
|
||||
...Object.entries(options.properties)
|
||||
.filter((n) => {
|
||||
const [_, value] = n;
|
||||
return value.required;
|
||||
})
|
||||
.map((n) => {
|
||||
const [key, _] = n;
|
||||
return key;
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const _methods: BaseMethods = {};
|
||||
|
||||
const _statics: BaseStatics = {};
|
||||
|
||||
export function createBase<T extends Base>(options: SchemaOptions) {
|
||||
return {
|
||||
schema: buildSchema<T>(options),
|
||||
methods: _methods,
|
||||
statics: _statics,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// import "@babel/polyfill";
|
||||
|
||||
import {
|
||||
addRxPlugin,
|
||||
createRxDatabase,
|
||||
RxDatabase,
|
||||
RxDocument,
|
||||
RxQuery,
|
||||
} from "rxdb";
|
||||
|
||||
// @ts-ignore
|
||||
import * as idb from "pouchdb-adapter-idb";
|
||||
import { TemplateResult } from "lit";
|
||||
import { futureBuilder } from "../components/future-builder";
|
||||
import { Todo, todosSchema } from "./todos";
|
||||
import { Base, BaseCollection } from "./base";
|
||||
import { DEFAULT_LOADING } from "../components/loading";
|
||||
|
||||
const DATABASE_NAME = "todos" + "db";
|
||||
|
||||
addRxPlugin(idb);
|
||||
|
||||
export type Schema = {
|
||||
todos: BaseCollection<Todo>;
|
||||
};
|
||||
|
||||
export var db: RxDatabase<Schema>;
|
||||
|
||||
async function setupDB() {
|
||||
if (db) return db;
|
||||
db = await createRxDatabase<Schema>({
|
||||
name: DATABASE_NAME,
|
||||
adapter: "idb",
|
||||
});
|
||||
await db.addCollections({
|
||||
todos: todosSchema,
|
||||
});
|
||||
return db;
|
||||
}
|
||||
|
||||
export const dbProvider = (
|
||||
builder: (db: RxDatabase<Schema>) => TemplateResult,
|
||||
loading: TemplateResult = DEFAULT_LOADING
|
||||
) => {
|
||||
return futureBuilder(setupDB(), (res) => builder(res), loading);
|
||||
};
|
||||
|
||||
export const queryBuilder = <T extends Base>(
|
||||
query: RxQuery<any>,
|
||||
builder: (result: RxDocument<T> | RxDocument<T>[]) => TemplateResult,
|
||||
loading: TemplateResult = DEFAULT_LOADING
|
||||
) => {
|
||||
return futureBuilder(query.exec(), (res) => builder(res), loading);
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Base, createBase } from "./base";
|
||||
|
||||
export interface Todo extends Base {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const todosSchema = createBase<Todo>({
|
||||
title: "todos",
|
||||
properties: {
|
||||
title: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user