mirror of
https://github.com/pdfme/pdfme.git
synced 2026-06-03 20:06:26 -04:00
feat(schemas): edit split inline markdown mvt variables (#1474)
This commit is contained in:
26
PLAN.md
26
PLAN.md
@@ -34,8 +34,8 @@ layout/builder の考え方を `converter` package の `md2pdf` に応用し、M
|
||||
|
||||
- リンク基盤、`@pdfme/jsx` MVP、text / MVT の `overflow: "expand"`、行単位 page split、
|
||||
custom `basePdf` 制御、dynamic layout docs は main に入った。
|
||||
- plain `multiVariableText` の split chunk は Form 上でも編集できる。
|
||||
- inline-markdown の split chunk は、plain text / MVT ともに Form 上では read-only のまま残す。
|
||||
- `multiVariableText` の split chunk は、plain / inline-markdown ともに Form 上で変数値だけ編集できる。
|
||||
- `text` schema の inline-markdown split chunk は Form 上では read-only のまま残す。
|
||||
- 次の大きな判断は、dynamic layout の編集体験をどこまで広げるかと、`@pdfme/jsx` / `md2pdf`
|
||||
に進む前にどの schema 表現を追加するか。
|
||||
|
||||
@@ -94,9 +94,8 @@ layout/builder の考え方を `converter` package の `md2pdf` に応用し、M
|
||||
`__splitRange: { unit: "textLine", start, end }` を持つ split schema に分割し、次ページへ続ける。
|
||||
plain text と inline-markdown は同じ line layout を使い、PDF / UI preview で split chunk が
|
||||
同じ input 全体を重複描画しないようにする。
|
||||
- inline-markdown の split chunk は markdown 記法が行境界で分断される可能性があるため、Form 上では
|
||||
初回は read-only 表示に寄せる。将来的に編集可能にする場合は rich text AST と selection/editing
|
||||
model を合わせて設計する。
|
||||
- inline-markdown の split chunk は markdown 記法が行境界で分断される可能性があるため、`text`
|
||||
schema は Form 上では read-only 表示に寄せる。編集対象は Designer / template authoring 側に寄せる。
|
||||
|
||||
### PR #1469: dynamic layout split range の共通化
|
||||
|
||||
@@ -132,13 +131,21 @@ layout/builder の考え方を `converter` package の `md2pdf` に応用し、M
|
||||
- inline-markdown の split chunk は、rich text AST / selection editing の設計が必要なため read-only
|
||||
のまま残す。
|
||||
|
||||
### PR #1474: inline-markdown `multiVariableText` split chunk の変数編集
|
||||
|
||||
- inline-markdown `multiVariableText` の split chunk は、Form 上で表示範囲内の変数値だけ編集できる。
|
||||
- template の markdown 記法、static text、link は Form では編集対象にしない。
|
||||
- 変数値に `**` や `` ` `` などの markdown delimiter が含まれても、変数値として literal に扱う。
|
||||
計測時は escape 済みの値を使い、再描画時に変数値が markdown として再解釈されないようにする。
|
||||
- link は Form 上では clickable にしない。link の見た目を Form mode でも維持するかは follow-up とする。
|
||||
|
||||
## 次 PR 候補
|
||||
|
||||
### 1. inline-markdown split chunk 編集方針
|
||||
### 1. `text` schema inline-markdown split chunk 編集方針
|
||||
|
||||
直近で一番悩ましい残論点。すぐ実装するより、先に仕様を固定した方がよい。
|
||||
|
||||
- split 後の inline-markdown text / MVT を Form 上でも編集可能にするか、read-only 表示に限定するか決める。
|
||||
- split 後の inline-markdown `text` を Form 上でも編集可能にするか、read-only 表示に限定するか決める。
|
||||
- 編集可能にする場合は、markdown source string を直接編集するのか、rich text AST / run model を編集して
|
||||
source へ戻すのか決める。
|
||||
- link / bold / italic / inline code が行境界で分断された場合の selection / blur / input merge 方針を決める。
|
||||
@@ -235,8 +242,7 @@ layout/builder の考え方を `converter` package の `md2pdf` に応用し、M
|
||||
|
||||
Dynamic layout / editing:
|
||||
|
||||
- inline-markdown の `multiVariableText` split chunk を Form 上で編集可能にするべきか、read-only chunk のままでよいか。
|
||||
- split 後の inline-markdown 編集をサポートするか、read-only 表示に限定するか。
|
||||
- `text` schema の split inline-markdown 編集をサポートするか、read-only 表示に限定するか。
|
||||
- split chunk 内の複数 variable span を連続編集した場合、blur の順序と reflow 後の最新 input を
|
||||
どう同期するか。必要なら live pagination / editing session の設計と合わせて扱う。
|
||||
- custom `basePdf` では dynamic layout を無効にする現行方針で固定するか、将来的に限定的な reflow
|
||||
@@ -246,6 +252,8 @@ Dynamic layout / editing:
|
||||
Rich content / link:
|
||||
|
||||
- MVT の inline link 対応をどのタイミングで入れるか。
|
||||
- Form mode の inline link は clickable にしないまま、色や underline などの視覚表現だけを
|
||||
viewer と揃えるべきか決める。
|
||||
- table cell / list item の rich inline content を schema 拡張で扱うか、複数 schema に分解するか。
|
||||
- link の見た目をデフォルトで青 + 下線にするか、明示的 styling に任せるか。
|
||||
- Designer で通常のテキスト編集を難しくせずにリンク編集 UI をどう出すか。
|
||||
|
||||
@@ -330,7 +330,7 @@ describe('multiVariableText inline markdown UI rendering', () => {
|
||||
expect(newValue.name).not.toBe('edited chunk');
|
||||
});
|
||||
|
||||
it('keeps split inline markdown form chunks as read-only resolved text', async () => {
|
||||
it('writes split inline markdown form chunk edits back into the full variable value', async () => {
|
||||
const rootElement = document.createElement('div');
|
||||
const onChange = vi.fn();
|
||||
const schema: MultiVariableTextSchema = {
|
||||
@@ -341,6 +341,44 @@ describe('multiVariableText inline markdown UI rendering', () => {
|
||||
__splitRange: { unit: 'textLine', start: 0, end: 1 },
|
||||
};
|
||||
|
||||
await uiRender({
|
||||
value: JSON.stringify({ name: 'first **line**\nsecond line' }),
|
||||
schema,
|
||||
rootElement,
|
||||
mode: 'form',
|
||||
onChange,
|
||||
options: { font: getSampleFont() },
|
||||
_cache: new Map(),
|
||||
theme: { colorPrimary: '#1677ff' },
|
||||
} as Parameters<typeof uiRender>[0]);
|
||||
|
||||
const textBlock = rootElement.querySelector(`#text-${schema.id}`) as HTMLDivElement;
|
||||
const variableSpan = textBlock.querySelector('span') as HTMLSpanElement;
|
||||
expect(textBlock.textContent).toBe('first **line**');
|
||||
expect(variableSpan.contentEditable).toBe('plaintext-only');
|
||||
expect(variableSpan.style.fontWeight).toBe('800');
|
||||
expect(variableSpan.style.textShadow).not.toBe('');
|
||||
|
||||
variableSpan.textContent = 'edited **first** line';
|
||||
variableSpan.dispatchEvent(new Event('blur'));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
key: 'content',
|
||||
value: JSON.stringify({ name: 'edited **first** line\nsecond line' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('writes later split inline markdown chunk edits back into the correct variable range', async () => {
|
||||
const rootElement = document.createElement('div');
|
||||
const onChange = vi.fn();
|
||||
const schema: MultiVariableTextSchema = {
|
||||
...getSchema(),
|
||||
text: '**{name}**',
|
||||
variables: ['name'],
|
||||
width: 100,
|
||||
__splitRange: { unit: 'textLine', start: 1, end: 2 },
|
||||
};
|
||||
|
||||
await uiRender({
|
||||
value: JSON.stringify({ name: 'first line\nsecond line' }),
|
||||
schema,
|
||||
@@ -352,11 +390,87 @@ describe('multiVariableText inline markdown UI rendering', () => {
|
||||
theme: { colorPrimary: '#1677ff' },
|
||||
} as Parameters<typeof uiRender>[0]);
|
||||
|
||||
const textBlock = rootElement.querySelector(`#text-${schema.id}`) as HTMLDivElement;
|
||||
const variableSpan = textBlock.querySelector('span') as HTMLSpanElement;
|
||||
expect(textBlock.textContent).toBe('second line');
|
||||
|
||||
variableSpan.textContent = 'edited second line';
|
||||
variableSpan.dispatchEvent(new Event('blur'));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
key: 'content',
|
||||
value: JSON.stringify({ name: 'first line\nedited second line' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not create an editable split inline markdown span for an empty variable value', async () => {
|
||||
const rootElement = document.createElement('div');
|
||||
const onChange = vi.fn();
|
||||
const schema: MultiVariableTextSchema = {
|
||||
...getSchema(),
|
||||
text: '**{name}**',
|
||||
variables: ['name'],
|
||||
width: 100,
|
||||
__splitRange: { unit: 'textLine', start: 0, end: 1 },
|
||||
};
|
||||
|
||||
await uiRender({
|
||||
value: JSON.stringify({ name: '' }),
|
||||
schema,
|
||||
rootElement,
|
||||
mode: 'form',
|
||||
onChange,
|
||||
options: { font: getSampleFont() },
|
||||
_cache: new Map(),
|
||||
theme: { colorPrimary: '#1677ff' },
|
||||
} as Parameters<typeof uiRender>[0]);
|
||||
|
||||
const textBlock = rootElement.querySelector(`#text-${schema.id}`) as HTMLDivElement;
|
||||
textBlock.dispatchEvent(new Event('blur'));
|
||||
|
||||
expect(textBlock.textContent).toBe('');
|
||||
expect(textBlock.querySelector('span')).toBeNull();
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
expect(textBlock.querySelector('span')?.contentEditable).not.toBe('plaintext-only');
|
||||
expect(textBlock.textContent).toBe('first line');
|
||||
});
|
||||
|
||||
it('keeps split inline markdown links non-clickable while editing variables', async () => {
|
||||
const rootElement = document.createElement('div');
|
||||
const onChange = vi.fn();
|
||||
const schema: MultiVariableTextSchema = {
|
||||
...getSchema(),
|
||||
text: '[docs](https://pdfme.com) for **{name}**',
|
||||
variables: ['name'],
|
||||
width: 100,
|
||||
__splitRange: { unit: 'textLine', start: 0, end: 1 },
|
||||
};
|
||||
|
||||
await uiRender({
|
||||
value: JSON.stringify({ name: 'Alice\nBob' }),
|
||||
schema,
|
||||
rootElement,
|
||||
mode: 'form',
|
||||
onChange,
|
||||
options: { font: getSampleFont() },
|
||||
_cache: new Map(),
|
||||
theme: { colorPrimary: '#1677ff' },
|
||||
} as Parameters<typeof uiRender>[0]);
|
||||
|
||||
const textBlock = rootElement.querySelector(`#text-${schema.id}`) as HTMLDivElement;
|
||||
const variableSpan = Array.from(textBlock.querySelectorAll('span')).find(
|
||||
(span) => span.contentEditable === 'plaintext-only',
|
||||
) as HTMLSpanElement;
|
||||
|
||||
expect(textBlock.querySelector('a')).toBeNull();
|
||||
expect(textBlock.textContent).toBe('docs for Alice');
|
||||
expect(variableSpan.textContent).toBe('Alice');
|
||||
expect(variableSpan.style.fontWeight).toBe('800');
|
||||
|
||||
variableSpan.textContent = 'Carol';
|
||||
variableSpan.dispatchEvent(new Event('blur'));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
key: 'content',
|
||||
value: JSON.stringify({ name: 'Carol\nBob' }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,17 +30,6 @@ export const uiRender = async (arg: UIRenderProps<MultiVariableTextSchema>) => {
|
||||
: substituteVariables(text, value);
|
||||
|
||||
if (mode === 'form' && numVariables > 0 && !renderResolvedValue) {
|
||||
if (getTextLineRange(schema) && isInlineMarkdownTextSchema(schema)) {
|
||||
await parentUiRender({
|
||||
value: renderValue,
|
||||
schema,
|
||||
mode: 'viewer',
|
||||
rootElement,
|
||||
...rest,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await formUiRender(arg);
|
||||
return;
|
||||
}
|
||||
@@ -115,6 +104,9 @@ const formUiRender = async (arg: UIRenderProps<MultiVariableTextSchema>) => {
|
||||
_cache as Map<string, import('fontkit').Font>,
|
||||
);
|
||||
|
||||
// The split/non-split form paths rebuild child spans after this call. We still create
|
||||
// the parent text block through the shared helper so sizing, alignment, and font
|
||||
// setup stay identical to the normal text renderer.
|
||||
const textBlock = buildStyledTextContainer(
|
||||
arg,
|
||||
fontKitFont,
|
||||
@@ -126,18 +118,25 @@ const formUiRender = async (arg: UIRenderProps<MultiVariableTextSchema>) => {
|
||||
const lineRange = getTextLineRange(schema);
|
||||
if (lineRange) {
|
||||
const { lines } = await measureTextLines({
|
||||
value: substitutedText,
|
||||
// Variable values are literal Form inputs. Escape markdown delimiters before
|
||||
// measuring so a value like "**name**" is not reinterpreted as template markdown
|
||||
// after blur/reflow.
|
||||
value: inlineMarkdownRuns
|
||||
? substituteVariablesAsInlineMarkdownLiterals(rawText, variables)
|
||||
: substitutedText,
|
||||
schema,
|
||||
font,
|
||||
_cache,
|
||||
ignoreDynamicFontSize: true,
|
||||
});
|
||||
renderSplitPlainVariableSpans({
|
||||
renderSplitVariableSpans({
|
||||
textBlock,
|
||||
lines,
|
||||
runs: inlineMarkdownRuns,
|
||||
rawText,
|
||||
variables,
|
||||
schema,
|
||||
font,
|
||||
theme,
|
||||
onChange,
|
||||
stopEditing,
|
||||
@@ -203,28 +202,36 @@ type SplitChunkSegment = {
|
||||
variableName?: string;
|
||||
variableStart?: number;
|
||||
variableEnd?: number;
|
||||
run?: RichTextRun;
|
||||
};
|
||||
|
||||
type ResolvedPlainChar = {
|
||||
type ResolvedChunkChar = {
|
||||
char: string;
|
||||
variableName?: string;
|
||||
variableOffset?: number;
|
||||
run?: RichTextRun;
|
||||
};
|
||||
|
||||
const renderSplitPlainVariableSpans = (arg: {
|
||||
type RenderFont = NonNullable<UIRenderProps<MultiVariableTextSchema>['options']['font']>;
|
||||
|
||||
const renderSplitVariableSpans = (arg: {
|
||||
textBlock: HTMLDivElement;
|
||||
lines: string[];
|
||||
runs?: RichTextRun[];
|
||||
rawText: string;
|
||||
variables: Record<string, string>;
|
||||
schema: MultiVariableTextSchema;
|
||||
font: RenderFont;
|
||||
theme: UIRenderProps<MultiVariableTextSchema>['theme'];
|
||||
onChange: UIRenderProps<MultiVariableTextSchema>['onChange'];
|
||||
stopEditing: UIRenderProps<MultiVariableTextSchema>['stopEditing'];
|
||||
}) => {
|
||||
const { textBlock, lines, rawText, variables, schema, theme, onChange, stopEditing } = arg;
|
||||
const { textBlock, lines, runs, rawText, variables, schema, font, theme, onChange, stopEditing } =
|
||||
arg;
|
||||
const lineRange = getTextLineRange(schema);
|
||||
const lineSegments = getSplitPlainLineSegments({
|
||||
const lineSegments = getSplitLineSegments({
|
||||
lines,
|
||||
runs,
|
||||
rawText,
|
||||
variables,
|
||||
start: lineRange?.start ?? 0,
|
||||
@@ -239,6 +246,8 @@ const renderSplitPlainVariableSpans = (arg: {
|
||||
textBlock,
|
||||
segment,
|
||||
variables,
|
||||
schema,
|
||||
font,
|
||||
theme,
|
||||
onChange,
|
||||
stopEditing,
|
||||
@@ -249,6 +258,9 @@ const renderSplitPlainVariableSpans = (arg: {
|
||||
const span = document.createElement('span');
|
||||
span.style.letterSpacing = lineIndex === lineSegments.length - 1 ? '0' : 'inherit';
|
||||
span.textContent = segment.text;
|
||||
if (segment.run) {
|
||||
applyInlineMarkdownStyle({ element: span, run: segment.run, schema, font });
|
||||
}
|
||||
textBlock.appendChild(span);
|
||||
});
|
||||
|
||||
@@ -258,24 +270,29 @@ const renderSplitPlainVariableSpans = (arg: {
|
||||
});
|
||||
};
|
||||
|
||||
const getSplitPlainLineSegments = (arg: {
|
||||
const getSplitLineSegments = (arg: {
|
||||
lines: string[];
|
||||
runs?: RichTextRun[];
|
||||
rawText: string;
|
||||
variables: Record<string, string>;
|
||||
start: number;
|
||||
end: number;
|
||||
}): SplitChunkSegment[][] => {
|
||||
const { lines, rawText, variables, start, end } = arg;
|
||||
const resolvedChars = buildResolvedPlainChars(rawText, variables);
|
||||
const allLineSegments = consumeMeasuredLineSegments(lines, resolvedChars);
|
||||
const { lines, runs, rawText, variables, start, end } = arg;
|
||||
const resolvedChars = runs
|
||||
? buildResolvedInlineMarkdownChars(runs, variables)
|
||||
: buildResolvedPlainChars(rawText, variables);
|
||||
const allLineSegments = consumeMeasuredLineSegments(lines, resolvedChars, {
|
||||
dropUnmappedTargets: Boolean(runs),
|
||||
});
|
||||
return allLineSegments.slice(start, end);
|
||||
};
|
||||
|
||||
const buildResolvedPlainChars = (
|
||||
rawText: string,
|
||||
variables: Record<string, string>,
|
||||
): ResolvedPlainChar[] => {
|
||||
const chars: ResolvedPlainChar[] = [];
|
||||
): ResolvedChunkChar[] => {
|
||||
const chars: ResolvedChunkChar[] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
visitVariables(rawText, ({ name, startIndex, endIndex }) => {
|
||||
@@ -294,15 +311,40 @@ const buildResolvedPlainChars = (
|
||||
return chars;
|
||||
};
|
||||
|
||||
const appendTextChars = (chars: ResolvedPlainChar[], text: string) => {
|
||||
const buildResolvedInlineMarkdownChars = (
|
||||
runs: RichTextRun[],
|
||||
variables: Record<string, string>,
|
||||
): ResolvedChunkChar[] => {
|
||||
const chars: ResolvedChunkChar[] = [];
|
||||
|
||||
runs.forEach((run) => {
|
||||
let lastIndex = 0;
|
||||
|
||||
visitVariables(run.text, ({ name, startIndex, endIndex }) => {
|
||||
appendTextChars(chars, run.text.slice(lastIndex, startIndex), run);
|
||||
const value = variables[name] ?? '';
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
chars.push({ char: value[i], variableName: name, variableOffset: i, run });
|
||||
}
|
||||
lastIndex = endIndex + 1;
|
||||
});
|
||||
|
||||
appendTextChars(chars, run.text.slice(lastIndex), run);
|
||||
});
|
||||
|
||||
return chars;
|
||||
};
|
||||
|
||||
const appendTextChars = (chars: ResolvedChunkChar[], text: string, run?: RichTextRun) => {
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
chars.push({ char: text[i] });
|
||||
chars.push({ char: text[i], run });
|
||||
}
|
||||
};
|
||||
|
||||
const consumeMeasuredLineSegments = (
|
||||
lines: string[],
|
||||
resolvedChars: ResolvedPlainChar[],
|
||||
resolvedChars: ResolvedChunkChar[],
|
||||
options: { dropUnmappedTargets?: boolean } = {},
|
||||
): SplitChunkSegment[][] => {
|
||||
const lineSegments: SplitChunkSegment[][] = [];
|
||||
let cursor = 0;
|
||||
@@ -311,9 +353,10 @@ const consumeMeasuredLineSegments = (
|
||||
const segments: SplitChunkSegment[] = [];
|
||||
const lineText = stripTrailingLineBreaks(line);
|
||||
|
||||
// `lines` must come from measuring the substituted plain MVT value. We map those
|
||||
// measured characters back to the same substituted value, annotated with variable
|
||||
// offsets, so each editable span can update only the touched variable range.
|
||||
// `lines` must come from measuring the substituted MVT value. We map those measured
|
||||
// characters back to the same substituted value, annotated with variable offsets
|
||||
// and optional inline markdown run styles, so each editable span updates only the
|
||||
// touched variable range.
|
||||
for (let i = 0; i < lineText.length; i += 1) {
|
||||
const target = lineText[i];
|
||||
while (
|
||||
@@ -326,6 +369,7 @@ const consumeMeasuredLineSegments = (
|
||||
}
|
||||
|
||||
if (cursor >= resolvedChars.length) {
|
||||
if (options.dropUnmappedTargets) continue;
|
||||
appendSegment(segments, { char: target });
|
||||
continue;
|
||||
}
|
||||
@@ -338,6 +382,7 @@ const consumeMeasuredLineSegments = (
|
||||
// If a future text measurement path normalizes characters differently, keep
|
||||
// rendering the chunk but do not attach the mismatched character to a variable
|
||||
// offset. Advancing the cursor here would corrupt all following mappings.
|
||||
if (options.dropUnmappedTargets) continue;
|
||||
appendSegment(segments, { char: target });
|
||||
}
|
||||
}
|
||||
@@ -362,7 +407,7 @@ const consumeMeasuredLineSegments = (
|
||||
|
||||
const absorbHiddenTrailingWhitespace = (
|
||||
segments: SplitChunkSegment[],
|
||||
resolvedChars: ResolvedPlainChar[],
|
||||
resolvedChars: ResolvedChunkChar[],
|
||||
cursor: number,
|
||||
) => {
|
||||
let nextCursor = cursor;
|
||||
@@ -376,6 +421,7 @@ const absorbHiddenTrailingWhitespace = (
|
||||
lastSegment &&
|
||||
lastSegment.variableName === sourceChar.variableName &&
|
||||
lastSegment.variableEnd === sourceChar.variableOffset &&
|
||||
lastSegment.run === sourceChar.run &&
|
||||
sourceChar.variableOffset !== undefined
|
||||
) {
|
||||
lastSegment.variableEnd = sourceChar.variableOffset + 1;
|
||||
@@ -406,12 +452,13 @@ const isWhitespaceChar = (value: string) =>
|
||||
const isHorizontalWhitespaceChar = (value: string) =>
|
||||
value === ' ' || value === '\t' || value === '\f' || value === '\v';
|
||||
|
||||
const appendSegment = (segments: SplitChunkSegment[], sourceChar: ResolvedPlainChar) => {
|
||||
const appendSegment = (segments: SplitChunkSegment[], sourceChar: ResolvedChunkChar) => {
|
||||
const lastSegment = segments.at(-1);
|
||||
if (
|
||||
lastSegment &&
|
||||
lastSegment.variableName === sourceChar.variableName &&
|
||||
lastSegment.variableEnd === sourceChar.variableOffset
|
||||
lastSegment.variableEnd === sourceChar.variableOffset &&
|
||||
lastSegment.run === sourceChar.run
|
||||
) {
|
||||
lastSegment.text += sourceChar.char;
|
||||
if (sourceChar.variableOffset !== undefined) {
|
||||
@@ -426,6 +473,7 @@ const appendSegment = (segments: SplitChunkSegment[], sourceChar: ResolvedPlainC
|
||||
variableStart: sourceChar.variableOffset,
|
||||
variableEnd:
|
||||
sourceChar.variableOffset === undefined ? undefined : sourceChar.variableOffset + 1,
|
||||
run: sourceChar.run,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -433,15 +481,20 @@ const appendRangedVariableSpan = (arg: {
|
||||
textBlock: HTMLDivElement;
|
||||
segment: SplitChunkSegment;
|
||||
variables: Record<string, string>;
|
||||
schema: MultiVariableTextSchema;
|
||||
font: RenderFont;
|
||||
theme: UIRenderProps<MultiVariableTextSchema>['theme'];
|
||||
onChange: UIRenderProps<MultiVariableTextSchema>['onChange'];
|
||||
stopEditing: UIRenderProps<MultiVariableTextSchema>['stopEditing'];
|
||||
}) => {
|
||||
const { textBlock, segment, variables, theme, onChange, stopEditing } = arg;
|
||||
const { textBlock, segment, variables, schema, font, theme, onChange, stopEditing } = arg;
|
||||
if (!segment.variableName) return;
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.style.outline = `${theme.colorPrimary} dashed 1px`;
|
||||
if (segment.run) {
|
||||
applyInlineMarkdownStyle({ element: span, run: segment.run, schema, font });
|
||||
}
|
||||
makeElementPlainTextContentEditable(span);
|
||||
span.textContent = segment.text;
|
||||
span.addEventListener('blur', (e: Event) => {
|
||||
|
||||
Reference in New Issue
Block a user