Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
65 changes: 37 additions & 28 deletions packages/kg-default-nodes/src/generate-decorator-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {KoenigDecoratorNode} from './KoenigDecoratorNode.js';
import type {ExportDOMOptions, ExportDOMOutput} from './export-dom.js';
import readTextContent from './utils/read-text-content.js';
import {buildDefaultVisibility, isVisibilityRestricted, migrateOldVisibilityFormat} from './utils/visibility.js';
import type {LexicalEditor} from 'lexical';
import type {LexicalEditor, SerializedLexicalNode} from 'lexical';
import type {Visibility} from './utils/visibility.js';

type RenderFn<TOutput extends ExportDOMOutput = ExportDOMOutput> = (node: any, options: ExportDOMOptions) => TOutput;
type VersionedRenderFn<TOutput extends ExportDOMOutput = ExportDOMOutput> = Record<string | number, RenderFn<TOutput>>;
type RenderFn<TNode = unknown, TOutput extends ExportDOMOutput = ExportDOMOutput> = {
bivarianceHack(node: TNode, options: ExportDOMOptions): TOutput;
}['bivarianceHack'];
type VersionedRenderFn<TOutput extends ExportDOMOutput = ExportDOMOutput> = Record<string | number, RenderFn<unknown, TOutput>>;
type WidenLiteral<T> =
T extends string ? string :
T extends number ? number :
Expand Down Expand Up @@ -62,14 +64,16 @@ export interface DecoratorNodeProperty<Name extends string = string, Default = u

export type DecoratorNodeValueMap<Props extends readonly DecoratorNodeProperty[], HasVisibility extends boolean = false> = {
[Prop in Props[number] as Prop['name']]: WidenLiteral<Prop['default']>;
} & (HasVisibility extends true ? {visibility: Visibility} : {});
} & (HasVisibility extends true ? {visibility: Visibility} : unknown);

export type DecoratorNodeData<Props extends readonly DecoratorNodeProperty[], HasVisibility extends boolean = false> = Partial<DecoratorNodeValueMap<Props, HasVisibility>>;

type GeneratedDecoratorNodeInstance<TDataset extends Record<string, unknown>, TOutput extends ExportDOMOutput = ExportDOMOutput> = GeneratedDecoratorNodeBase<TDataset> & TDataset & {
exportDOM(editor: LexicalEditor, options?: ExportDOMOptions): TOutput;
};

export type SerializedGeneratedDecoratorNode<TDataset extends Record<string, unknown> = Record<string, unknown>> = SerializedLexicalNode & TDataset;

export interface GeneratedDecoratorNodeClass<TDataset extends Record<string, unknown>, TOutput extends ExportDOMOutput = ExportDOMOutput> {
new (data?: Partial<TDataset>, key?: string): GeneratedDecoratorNodeInstance<TDataset, TOutput>;
prototype: GeneratedDecoratorNodeInstance<TDataset, TOutput>;
Expand Down Expand Up @@ -135,19 +139,26 @@ export class GeneratedDecoratorNodeBase<TDataset extends Record<string, unknown>
export function generateDecoratorNode<
Props extends readonly DecoratorNodeProperty[] = readonly [],
HasVisibility extends boolean = false,
TOutput extends ExportDOMOutput = ExportDOMOutput
>({nodeType, properties = [] as unknown as Props, defaultRenderFn, version = 1, hasVisibility = false as HasVisibility}: {
TOutput extends ExportDOMOutput = ExportDOMOutput,
TRenderNode = GeneratedDecoratorNodeInstance<DecoratorNodeValueMap<Props, HasVisibility>, TOutput>
>({nodeType, properties, defaultRenderFn, version = 1, hasVisibility}: {
nodeType: string;
properties?: Props;
defaultRenderFn?: RenderFn<TOutput> | VersionedRenderFn<TOutput>;
defaultRenderFn?: RenderFn<TRenderNode, TOutput> | VersionedRenderFn<TOutput>;
version?: number;
hasVisibility?: HasVisibility;
}): GeneratedDecoratorNodeClass<DecoratorNodeValueMap<Props, HasVisibility>, TOutput> {
validateArguments(nodeType, properties);
type GeneratedDataset = DecoratorNodeValueMap<Props, HasVisibility>;
type GeneratedRenderFn = RenderFn<TRenderNode, TOutput>;
type GeneratedVersionedRenderFn = VersionedRenderFn<TOutput>;

const nodeProperties = properties ?? [];

validateArguments(nodeType, nodeProperties);

// Adds a `privateName` field to the properties for convenience (e.g. `__name`):
// properties: [{name: 'name', privateName: '__name', type: 'string', default: 'hello'}, {...}]
const internalProps = properties.map((prop) => {
const internalProps = nodeProperties.map((prop) => {
return Object.defineProperties({}, {
...Object.getOwnPropertyDescriptors(prop),
privateName: {
Expand All @@ -174,7 +185,7 @@ export function generateDecoratorNode<
class GeneratedDecoratorNode extends KoenigDecoratorNode {
[key: string]: unknown;

constructor(data: Partial<DecoratorNodeValueMap<Props, HasVisibility>> = {}, key?: string) {
constructor(data: Partial<GeneratedDataset> = {}, key?: string) {
super(key);
const dataset = data as Record<string, unknown>;
internalProps.forEach((prop) => {
Expand Down Expand Up @@ -206,7 +217,7 @@ export function generateDecoratorNode<
* @see https://lexical.dev/docs/concepts/nodes#extending-decoratornode
*/
static clone(node: GeneratedDecoratorNodeInstance<DecoratorNodeValueMap<Props, HasVisibility>, TOutput>) {
return new this(node.getDataset() as Partial<DecoratorNodeValueMap<Props, HasVisibility>>, node.__key);
return new this(node.getDataset() as Partial<GeneratedDataset>, node.__key);
}

/**
Expand All @@ -217,7 +228,7 @@ export function generateDecoratorNode<
return internalProps.reduce((obj: Record<string, unknown>, prop) => {
obj[prop.name] = prop.default;
return obj;
}, {}) as DecoratorNodeValueMap<Props, HasVisibility>;
}, {}) as GeneratedDataset;
}

/**
Expand Down Expand Up @@ -272,62 +283,60 @@ export function generateDecoratorNode<
data[prop.name] = serializedNode[prop.name];
});

return new this(data as Partial<DecoratorNodeValueMap<Props, HasVisibility>>);
return new this(data as Partial<GeneratedDataset>);
}

/**
* Serializes a Lexical node to JSON. The JSON content is then saved to the database.
* @extends DecoratorNode
* @see https://lexical.dev/docs/concepts/serialization#lexicalnodeexportjson
*/
// @ts-expect-error -- strict mode migration
exportJSON() {
const dataset: Record<string, unknown> = {
exportJSON(): SerializedGeneratedDecoratorNode<GeneratedDataset> {
const dataset = {
type: nodeType,
version: version,
...internalProps.reduce((obj: Record<string, unknown>, prop) => {
obj[prop.name] = this[prop.name];
return obj;
}, {})
};
} as SerializedGeneratedDecoratorNode<GeneratedDataset>;
return dataset;
}

exportDOM(_editor: LexicalEditor, options: ExportDOMOptions = {}): TOutput {
// this.__version is used when a node has a version property which
// means it's set from the serialized version data at runtime
const nodeVersion = this.__version || version;
const nodeVersion = typeof this.__version === 'string' || typeof this.__version === 'number' ? this.__version : version;
const node = this as unknown as TRenderNode;

const nodeRenderers = options.nodeRenderers as Record<string, RenderFn<TOutput> | VersionedRenderFn<TOutput>> | undefined;
const nodeRenderers = options.nodeRenderers as Record<string, GeneratedRenderFn | GeneratedVersionedRenderFn> | undefined;
if (nodeRenderers?.[nodeType]) {
const render = nodeRenderers[nodeType];

if (typeof render === 'object') {
const versionRenderer = (render as VersionedRenderFn<TOutput>)[nodeVersion as number];
const versionRenderer = render[nodeVersion];
if (!versionRenderer) {
throw new Error(`[generateDecoratorNode] ${nodeType}: options.nodeRenderers['${nodeType}'] for version ${nodeVersion} is required`);
}
return versionRenderer(this, options);
return versionRenderer(node, options);
} else {
return (render as RenderFn<TOutput>)(this, options);
return render(node, options);
}
}

if (typeof defaultRenderFn === 'object') {
const render = (defaultRenderFn as VersionedRenderFn<TOutput>)[nodeVersion as number];
const render = defaultRenderFn[nodeVersion];
if (!render) {
throw new Error(`[generateDecoratorNode] ${nodeType}: "defaultRenderFn" for version ${nodeVersion} is required`);
}
return render(this, options);
return render(node, options);
}

if (!defaultRenderFn) {
throw new Error(`[generateDecoratorNode] ${nodeType}: "defaultRenderFn" is required`);
}

const render = defaultRenderFn as RenderFn<TOutput>;

return render(this, options);
return defaultRenderFn(node, options);
}

/* c8 ignore start */
Expand Down Expand Up @@ -380,7 +389,7 @@ export function generateDecoratorNode<
*/
getTextContent() {
const self = this.getLatest();
const propertiesWithText = properties.filter(prop => !!prop.wordCount);
const propertiesWithText = nodeProperties.filter(prop => !!prop.wordCount);

const text = propertiesWithText.map(
prop => readTextContent(self, prop.name)
Expand Down
1 change: 1 addition & 0 deletions packages/kg-default-nodes/src/kg-default-nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export * from './nodes/ExtendedQuoteNode.js';
export * from './nodes/TKNode.js';
export * from './nodes/at-link/index.js';
export * from './nodes/zwnj/ZWNJNode.js';
export * from './utils/card-widths.js';

// export utility functions that are useful in other packages or tests
import * as visibilityUtils from './utils/visibility.js';
Expand Down
11 changes: 11 additions & 0 deletions packages/kg-default-nodes/src/utils/card-widths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const CARD_WIDTHS = ['regular', 'wide', 'full'] as const;

export type CardWidth = typeof CARD_WIDTHS[number];

export function isCardWidth(width: unknown): width is CardWidth {
return typeof width === 'string' && (CARD_WIDTHS as readonly string[]).includes(width);
}

export function normalizeCardWidth(width: unknown): CardWidth | undefined {
return isCardWidth(width) ? width : undefined;
}
2 changes: 1 addition & 1 deletion packages/kg-default-nodes/src/utils/visibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function isNullish(value: unknown) {
}

// ensure we always work with a deep copy to avoid accidental ref mutations
export function buildDefaultVisibility() {
export function buildDefaultVisibility(): typeof DEFAULT_VISIBILITY {
return JSON.parse(JSON.stringify(DEFAULT_VISIBILITY));
}

Expand Down
3 changes: 3 additions & 0 deletions packages/koenig-lexical/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { dirname, join } from "path";
import { createRequire } from "module";
import { mergeConfig } from 'vite';
import type {StorybookConfig} from '@storybook/react-vite';

const require = createRequire(import.meta.url);

const config: StorybookConfig = {
framework: {
name: getAbsolutePath("@storybook/react-vite"),
Expand Down
69 changes: 0 additions & 69 deletions packages/koenig-lexical/.storybook/preview.jsx

This file was deleted.

70 changes: 70 additions & 0 deletions packages/koenig-lexical/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import '../src/styles/index.css';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import type {FC} from 'react';

export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
backgrounds: {
default: 'light',
values: [
{
name: 'light',
value: '#fff',
},
{
name: 'dark',
value: '#15171A',
},
],
},
status: {
statuses: {
toDo: {
background: '#AEB7C1',
color: '#ffffff',
description: 'This component has not yet been created',
},
inProgress: {
background: '#FFB41F',
color: '#ffffff',
description: 'The UI for this component is in progress',
},
uiReady: {
background: '#30CF43',
color: '#ffffff',
description: 'This component is ready to be wired up',
},
functional: {
background: '#14B8FF',
color: '#ffffff',
description: 'This component is live and functional',
},
uiBlocked: {
background: '#F50B23',
color: '#ffffff',
description: 'The UI for this component is blocked',
},
},
},
}

export const decorators = [
(Story: FC) => {
return (
<LexicalComposer initialConfig={{namespace: 'Storybook editor'}}>
<div className="koenig-lexical">
<div>
<Story />
</div>
</div>
</LexicalComposer>
)
}
];
export const tags = ['autodocs'];
Loading
Loading