mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
Closes #5992 ## Summary Expose the existing **Text content** binding control when a directly text-capable selected instance contains expressions alongside other content. The supplied reading-time span keeps its existing child sequence: - static prefix text - bound expression No data migration or new Settings editor is introduced. Normalization happens only when the user explicitly resets a binding. Expression-bearing nodes are excluded from canvas text editing because the canvas editor cannot represent bindings safely. Literal descendants in mixed trees remain canvas-editable, while bindings remain editable through Settings. ## Implementation - Reuse the existing scalar Text content and binding UI. - Target expression children with indexed `instances.updateText` mutations, preserving instance boundaries and unrelated siblings. - Fail closed when the selected instance is missing or no longer has a safe text target. - Evaluate expressions through the binding control's existing live scope. - Centralize text-content target classification in project-build for reuse by the engine and Builder. - Reset every expression atomically through the engine's `instances.setTextContent` operation, validate the complete replacement set, and coalesce adjacent text nodes without crossing nested-instance boundaries. - Centralize expression-free canvas text-editor targeting in the project-build lookup module. - Route double-click, Enter, post-insert editing, cursor generation, and keyboard navigation through the shared lookup rules. - Exclude single-expression and mixed/nested expression targets from the canvas editor in every mode. - Preserve canvas editing for literal descendants inside otherwise mixed content. - Share canvas fixtures and simplify binding-removal capability to an optional handler. ## Verification - [x] Settings exposes and edits mixed text/expression targets without replacing siblings. - [x] Reset binding converts every expression on the instance and normalizes adjacent text. - [x] Missing, unsupported, stale, and incomplete replacement sets fail closed. - [x] Double-click and Enter do not open bound nodes in the canvas editor. - [x] Literal descendants within mixed trees remain canvas-editable. - [x] Complete Builder suite passes (2,497 tests). - [x] Complete project-build suite passes (2,070 tests). - [x] New targeted Builder and project-build regression tests pass (175 tests). - [x] Builder and project-build typechecks pass. - [x] Repository lint passes. - [x] Package-boundary validation passes. - [x] Generated runtime operation contracts are updated from source. - [x] Fixture inputs and outputs are unaffected. - [x] Documentation impact reviewed: no documentation update is needed. - [ ] Interactive browser smoke test was unavailable because the in-app browser failed to initialize. - [ ] Agent evaluations were not run because they require separate approval. ## CI note The previous head's Main workflow failed while pulling the E2E database image with Docker Hub `toomanyrequests: Rate exceeded`. Lost Pixel failed separately with a certificate error. Both are external infrastructure failures; branch formatting, lint, package boundaries, generated contracts/docs, tests, and typechecks were successful in that run. Supersedes #5993, which GitHub closed automatically when its head branch was renamed.
93 lines
2.1 KiB
TypeScript
93 lines
2.1 KiB
TypeScript
import type { Instances } from "@webstudio-is/sdk";
|
|
import { idAttribute, selectorIdAttribute } from "@webstudio-is/react-sdk";
|
|
|
|
export const createCanvasElement = ({
|
|
selector,
|
|
rect,
|
|
parent = document.body,
|
|
}: {
|
|
selector: string;
|
|
rect?: { left: number; top: number; width: number; height: number };
|
|
parent?: HTMLElement;
|
|
}) => {
|
|
const element = document.createElement("div");
|
|
const [id] = selector.split(",");
|
|
element.setAttribute(idAttribute, id);
|
|
element.setAttribute(selectorIdAttribute, selector);
|
|
if (rect !== undefined) {
|
|
element.getBoundingClientRect = () =>
|
|
DOMRect.fromRect({
|
|
x: rect.left,
|
|
y: rect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
});
|
|
}
|
|
parent.appendChild(element);
|
|
return element;
|
|
};
|
|
|
|
export const createBoundTextInstances = (): Instances =>
|
|
new Map([
|
|
[
|
|
"bound-text",
|
|
{
|
|
type: "instance",
|
|
id: "bound-text",
|
|
component: "ws:element",
|
|
tag: "span",
|
|
children: [{ type: "expression", value: "value" }],
|
|
},
|
|
],
|
|
]);
|
|
|
|
export const createMixedBoundTextInstances = (): Instances =>
|
|
new Map([
|
|
[
|
|
"separator",
|
|
{
|
|
type: "instance",
|
|
id: "separator",
|
|
component: "ws:element",
|
|
tag: "span",
|
|
children: [{ type: "text", value: " · " }],
|
|
},
|
|
],
|
|
[
|
|
"reading-time",
|
|
{
|
|
type: "instance",
|
|
id: "reading-time",
|
|
component: "ws:element",
|
|
tag: "span",
|
|
children: [
|
|
{ type: "text", value: " · " },
|
|
{ type: "expression", value: 'readTime ?? ""' },
|
|
],
|
|
},
|
|
],
|
|
[
|
|
"paragraph",
|
|
{
|
|
type: "instance",
|
|
id: "paragraph",
|
|
component: "ws:element",
|
|
tag: "p",
|
|
children: [
|
|
{ type: "text", value: "" },
|
|
{ type: "id", value: "separator" },
|
|
{ type: "id", value: "reading-time" },
|
|
],
|
|
},
|
|
],
|
|
[
|
|
"body",
|
|
{
|
|
type: "instance",
|
|
id: "body",
|
|
component: "Body",
|
|
children: [{ type: "id", value: "paragraph" }],
|
|
},
|
|
],
|
|
]);
|