Page MenuHomeSealhub

jdd-page.ts
No OneTemporary

jdd-page.ts

/* eslint-disable @typescript-eslint/restrict-template-expressions */
import type { Readable } from "node:stream";
import type { Component, JDDContext, RawJDDocument } from "@sealcode/jdd";
import { documentContainerFromParsed, Registry } from "@sealcode/jdd";
import { JDD } from "@sealcode/jdd";
import { StatefulPage } from "@sealcode/sealgen";
import { hasFieldOfType, hasShape, predicates } from "@sealcode/ts-predicates";
import type { Context } from "koa";
import type { FlatTemplatable, Templatable } from "tempstream";
import { tempstream } from "tempstream";
import { ComponentInput } from "./inputs/component-input.js";
import { ComponentPreviewActions } from "./component-preview-actions.js";
export const actionName = "Components";
export type JDDPageState = {
components: RawJDDocument;
preview_size?: string;
messages?: string[];
};
export default abstract class JDDPage extends StatefulPage<
JDDPageState,
typeof ComponentPreviewActions
> {
actions = ComponentPreviewActions;
previewSizes = ["320", "600", "800", "1024", "1300", "1920"];
classes: string[] = [];
public registry: Registry;
public makeJDDContext: (ctx: Context) => JDDContext;
public html: (
args: unknown
) => string | Promise<string> | Readable | Promise<Readable>;
public defaultHead: (
...args: unknown[]
) => string | Promise<string> | Readable | Promise<Readable>;
public makeAssetURL: (asset: string) => string;
constructor(args: {
registry: Registry;
makeJDDContext: (ctx: Context) => JDDContext;
html: (
args: unknown
) => string | Promise<string> | Readable | Promise<Readable>;
defaultHead: (
...args: unknown[]
) => string | Promise<string> | Readable | Promise<Readable>;
makeAssetURL?: (asset: string) => string;
}) {
super();
this.registry = args.registry;
this.makeJDDContext = args.makeJDDContext;
this.defaultHead = args.defaultHead;
this.html = args.html;
this.makeAssetURL =
args.makeAssetURL ||
((str) =>
`/dist/jdd-page/${str.startsWith("/") ? str.slice(1) : str}`);
}
getRegistryComponents() {
return this.registry.getAll();
}
async getInitialState(ctx: Context) {
const all_components = Object.entries(this.getRegistryComponents());
const first_component = all_components[0];
if (!first_component) {
throw new Error("No defined components!");
}
const [component_name, component] = first_component;
const initial_state = {
components: [
{
component_name: component_name,
args: await component.getExampleValues(
this.makeJDDContext(ctx)
),
},
],
};
return initial_state;
}
wrapInLayout(
ctx: Context,
content: Templatable,
state: JDDPageState
): Templatable {
const jdd = new JDD(
this.registry,
this.makeJDDContext(ctx),
documentContainerFromParsed(state.components)
);
return this.html({
ctx,
title: "Components",
body: content,
description: "",
css_clumps: ["jdd-page", ...jdd.getAllCSSClumps()],
htmlOptions: {
morphing: true,
preserveScroll: true,
autoRefreshCSS: false,
navbar: () => ``,
bodyClasses: ["jdd-editor"],
showBottomNavbar: false,
showBanner: false,
showFooter: false,
loadHamburgerMenu: false,
loadSearchModal: false,
},
makeHead: (...args: unknown[]) =>
tempstream/* HTML */ `${this.defaultHead(...args)}
<link
href="/dist/jdd-page.entrypoint.css"
rel="stylesheet"
type="text/css"
/>
${jdd.renderEarlyAssets()}`,
});
}
async preprocessOverrides(
_ctx: Context,
state: JDDPageState,
overrides: Record<string, unknown>
) {
const jdd_context = this.makeJDDContext(_ctx);
if (
!hasFieldOfType(
"components",
overrides,
predicates.array(
predicates.shape({
args: predicates.object,
})
)
)
) {
return {};
}
for (const [component_index, { component_name }] of Object.entries(
state.components
)) {
const component = this.registry.get(component_name);
if (!component) {
throw new Error(`Unknown component: ${component_name}`);
}
const overrides_for_component = overrides.components[
parseInt(component_index)
] || { args: {} };
const promises = Object.entries(component.getArguments()).map(
async ([arg_name, arg]) => {
const value = overrides_for_component.args[arg_name];
if (value) {
const new_value = await arg.receivedToParsed(
jdd_context,
value
);
overrides_for_component.args[arg_name] = new_value;
}
}
);
// eslint-disable-next-line no-await-in-loop
await Promise.all(promises);
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return overrides;
}
// eslint-disable-next-line no-unused-vars
abstract renderParameterButtons(_state: JDDPageState): FlatTemplatable;
renderComponentArgs<C extends Component>(
ctx: Context,
state: JDDPageState,
component: C,
args: Record<string, unknown>,
index: number
): FlatTemplatable {
const jdd_context = this.makeJDDContext(ctx);
return tempstream/* HTML */ `<div
class="component-preview-parameters"
id="${`component-preview-parameters--${index}`}"
>
${Object.entries(component.getArguments()).map(
async ([arg_name, arg]) =>
ComponentInput({
state,
arg_path: [
"components",
index.toString(),
"args",
arg_name,
],
ctx,
arg,
value:
args[arg_name] === undefined
? arg.getExampleValue(jdd_context)
: args[arg_name],
page: this,
makeJDDContext: this.makeJDDContext,
makeAssetURL: this.makeAssetURL,
})
)}
</div>`;
}
renderComponentBlock(
ctx: Context,
state: JDDPageState,
{
component_name,
args: component_args,
}: {
component_name: string;
args: Record<string, unknown>;
},
component_index: number
) {
const component = this.registry.get(component_name);
if (!component) {
return null;
}
return this.renderComponentArgs(
ctx,
state,
component,
component_args,
component_index
);
}
async serializeState(ctx: Context, state: JDDPageState, pretty = false) {
const serialized_components = await Promise.all(
state.components.map(async ({ component_name, args }) => {
const component = this.registry.get(component_name);
const single_result = {
component_name,
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
args: component
? await component.convertParsedToStorage(
this.makeJDDContext(ctx),
args
)
: {},
};
return single_result;
})
);
const serialized_state = JSON.stringify(
{ ...state, components: serialized_components },
null,
pretty ? 4 : ""
);
return serialized_state;
}
async deserializeState(ctx: Context, state_string: string) {
const jdd_context = this.makeJDDContext(ctx);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const raw = JSON.parse(state_string);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const components_storage = raw.components;
if (!Array.isArray(components_storage)) {
throw new Error(
"'components' key is not an array, got ${components_storage}"
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const components_parsed = await Promise.all(
components_storage.map(async (entry) => {
if (
!hasShape(
{
component_name: predicates.string,
args: predicates.object,
},
entry
)
) {
throw new Error(
`Expected components[] items to be objects with 'component_name' and 'args' keys, got ${entry}`
);
}
const { component_name, args } = entry;
const component = this.registry.get(component_name);
if (!component) {
throw new Error("Unknown component: ${component_name}");
}
return {
component_name,
args: await component.convertStorageToParsed(
jdd_context,
args
),
};
})
);
const result = { ...raw, components: components_parsed };
return result;
}
renderPreParameterButtons(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_ctx: Context,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_state: JDDPageState
): FlatTemplatable | Promise<FlatTemplatable> {
return "";
}
renderMessages(_ctx: Context, state: JDDPageState) {
return /* HTML */ `<ul
class="jdd-editor__messages"
data-controller="toast"
>
${(state.messages || []).map(
(e) => `<li class="jdd-editor__message">${e}</li>`
)}
</ul>`;
}
async render(ctx: Context, state: JDDPageState): Promise<string> {
return tempstream/* HTML */ `<div
class="${["two-column", "component-debugger", ...this.classes].join(
" "
)}"
id="component-debugger"
style="${`--resizable-column-width: ${
state.preview_size ? state.preview_size + "px" : "50vw"
}`}"
data-controller="component-debugger"
>
<div class="component-arguments" id="component-arguments">
${this.renderPreParameterButtons(ctx, state)}
${this.renderParameterButtons(state)}
${this.renderMessages(ctx, state)}
${state.components.map((component, component_index) =>
this.renderComponentBlock(
ctx,
state,
component,
component_index
)
)}
<details
class="component-debugger__json"
data-controller="exportable-textarea"
id="exportable-textarea"
open
>
<summary>Edit/Export raw JSON</summary>
<textarea
name="state_override"
rows="40"
cols="40"
data-controller="json-editor"
id="component-debugger-json-textarea"
autocomplete="off"
>
${(await this.serializeState(ctx, state, true)).replaceAll("<", "&lt;")}
</textarea
>
${this.makeActionButton(state, {
action: "replace_state",
label: "Apply",
})}
<button data-action="exportable-textarea#copy">Copy</button>
<button data-action="exportable-textarea#download">
Download
</button>
<input type="file" />${" "}
<button data-action="exportable-textarea#import">
Import
</button>
</details>
</div>
<div
id="resize-gutter"
class="resize-gutter"
data-component-debugger-target="gutter"
></div>
<div
id="component-preview"
class="component-preview"
data-component-debugger-target="preview"
>
<div
id="component-preview__header"
class="component-preview__header"
>
<span>Preview</span>
<select
name="$[preview_size]"
autocomplete="off"
class="component-preview-size-select"
data-component-debugger-target="sizeSelect"
data-action="change->component-debugger#handleWidthDropdown"
data-turbo-data-turbo-permanent
>
${state.preview_size
? /* HTML */ `<option
class="dynamic"
value="${state.preview_size}"
selected
>
${state.preview_size} px
</option>`
: ""}
${this.previewSizes.map(
(size) => /* HTML */ `<option value="${size}">
${`${size} px`}
</option>`
)}
</select>
<noscript>
${this.makeActionButton(state, "change_size")}
</noscript>
</div>
<div class="jdd-outer-container">
<div class="jdd-container">
${JDD.render(
this.registry,
documentContainerFromParsed(state.components),
this.makeJDDContext(ctx)
)}
</div>
</div>
</div>
</div>`;
}
}

File Metadata

Mime Type
text/x-java
Expires
Fri, Nov 28, 15:11 (1 d, 6 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1063639
Default Alt Text
jdd-page.ts (11 KB)

Event Timeline