mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-27 23:27:09 +09:00
Ref https://github.com/webstudio-is/webstudio/issues/3632 Here changed insertion logic to wrap rich text with span element instead of Text component when drop into it. This is the last place which creates legacy components. Refactored tests with jsx to make them more readable.
266 lines
7.3 KiB
TypeScript
266 lines
7.3 KiB
TypeScript
import { nanoid } from "nanoid";
|
|
import { shallowEqual } from "shallow-equal";
|
|
import type {
|
|
Instance,
|
|
Instances,
|
|
Props,
|
|
StyleDecl,
|
|
Styles,
|
|
StyleSource,
|
|
StyleSourceSelection,
|
|
WsComponentMeta,
|
|
} from "@webstudio-is/sdk";
|
|
import { collectionComponent, elementComponent } from "@webstudio-is/sdk";
|
|
import { isRichTextTree } from "./content-model";
|
|
|
|
// slots can have multiple parents so instance should be addressed
|
|
// with full rendered path to avoid double selections with slots
|
|
// and support deletion of slot child from specific parent
|
|
// selector starts with target instance and ends with root
|
|
export type InstanceSelector = Instance["id"][];
|
|
|
|
export const areInstanceSelectorsEqual = (
|
|
left?: InstanceSelector,
|
|
right?: InstanceSelector
|
|
) => {
|
|
if (left === undefined || right === undefined) {
|
|
return false;
|
|
}
|
|
return left.join(",") === right.join(",");
|
|
};
|
|
|
|
export const isDescendantOrSelf = (
|
|
descendant: InstanceSelector,
|
|
self: InstanceSelector
|
|
) => {
|
|
if (self.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
if (descendant.length < self.length) {
|
|
return false;
|
|
}
|
|
|
|
const endSlice = descendant.slice(-self.length);
|
|
|
|
return shallowEqual(endSlice, self);
|
|
};
|
|
|
|
export type DroppableTarget = {
|
|
parentSelector: InstanceSelector;
|
|
position: number | "end";
|
|
};
|
|
|
|
const getCollectionDropTarget = (
|
|
instances: Instances,
|
|
dropTarget: DroppableTarget
|
|
) => {
|
|
const [parentId, grandparentId] = dropTarget.parentSelector;
|
|
const parent = instances.get(parentId);
|
|
const grandparent = instances.get(grandparentId);
|
|
if (parent === undefined && grandparent?.component === collectionComponent) {
|
|
return {
|
|
parentSelector: dropTarget.parentSelector.slice(1),
|
|
position: dropTarget.position,
|
|
};
|
|
}
|
|
};
|
|
|
|
export const getInstanceOrCreateFragmentIfNecessary = (
|
|
instances: Instances,
|
|
dropTarget: DroppableTarget
|
|
) => {
|
|
const [parentId] = dropTarget.parentSelector;
|
|
const instance = instances.get(parentId);
|
|
if (instance === undefined) {
|
|
return;
|
|
}
|
|
// slot should accept only single child
|
|
// otherwise multiple slots will have to maintain own children
|
|
// here all slot children are wrapped with fragment instance
|
|
if (instance.component === "Slot") {
|
|
if (instance.children.length === 0) {
|
|
const id = nanoid();
|
|
const fragment: Instance = {
|
|
type: "instance",
|
|
id,
|
|
component: "Fragment",
|
|
children: [],
|
|
};
|
|
instances.set(id, fragment);
|
|
instance.children.push({ type: "id", value: id });
|
|
return {
|
|
parentSelector: [fragment.id, ...dropTarget.parentSelector],
|
|
position: dropTarget.position,
|
|
};
|
|
}
|
|
// first slot child is always fragment
|
|
if (instance.children[0].type === "id") {
|
|
const fragmentId = instance.children[0].value;
|
|
return {
|
|
parentSelector: [fragmentId, ...dropTarget.parentSelector],
|
|
position: dropTarget.position,
|
|
};
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* Navigator tree and canvas dnd do not have text representation
|
|
* and position does not consider it and include only instances.
|
|
* This function adjust the position to consider text children
|
|
*/
|
|
const adjustChildrenPosition = (
|
|
children: Instance["children"],
|
|
position: number
|
|
) => {
|
|
let newPosition = 0;
|
|
let idPosition = 0;
|
|
for (let index = 0; index < children.length; index += 1) {
|
|
newPosition = index;
|
|
if (idPosition === position) {
|
|
return newPosition;
|
|
}
|
|
const child = children[index];
|
|
if (child.type === "id") {
|
|
idPosition += 1;
|
|
}
|
|
}
|
|
// the index after last item
|
|
return newPosition + 1;
|
|
};
|
|
|
|
/**
|
|
* Wrap children before and after drop target with spans
|
|
* to preserve lexical specific components while allowing
|
|
* to insert into editable components
|
|
*/
|
|
export const wrapEditableChildrenAroundDropTargetMutable = (
|
|
instances: Instances,
|
|
props: Props,
|
|
metas: Map<string, WsComponentMeta>,
|
|
dropTarget: DroppableTarget
|
|
) => {
|
|
const [parentId] = dropTarget.parentSelector;
|
|
const parentInstance = instances.get(parentId);
|
|
if (parentInstance === undefined || parentInstance.children.length === 0) {
|
|
return;
|
|
}
|
|
// wrap only containers with text and rich text childre
|
|
const isParentRichText = isRichTextTree({
|
|
instances,
|
|
props,
|
|
metas,
|
|
instanceId: parentId,
|
|
});
|
|
if (!isParentRichText) {
|
|
return;
|
|
}
|
|
const position =
|
|
dropTarget.position === "end"
|
|
? parentInstance.children.length
|
|
: adjustChildrenPosition(parentInstance.children, dropTarget.position);
|
|
|
|
const newChildren: Instance["children"] = [];
|
|
let newPosition = 0;
|
|
// create left span when not at the beginning
|
|
if (position !== 0) {
|
|
const leftSpan: Instance = {
|
|
id: nanoid(),
|
|
type: "instance",
|
|
component: elementComponent,
|
|
tag: "span",
|
|
children: parentInstance.children.slice(0, position),
|
|
};
|
|
newChildren.push({ type: "id", value: leftSpan.id });
|
|
instances.set(leftSpan.id, leftSpan);
|
|
newPosition = 1;
|
|
}
|
|
// create right span when not in the end
|
|
if (position < parentInstance.children.length) {
|
|
const rightSpan: Instance = {
|
|
id: nanoid(),
|
|
type: "instance",
|
|
component: elementComponent,
|
|
tag: "span",
|
|
children: parentInstance.children.slice(position),
|
|
};
|
|
newChildren.push({ type: "id", value: rightSpan.id });
|
|
instances.set(rightSpan.id, rightSpan);
|
|
}
|
|
parentInstance.children = newChildren;
|
|
return {
|
|
parentSelector: dropTarget.parentSelector,
|
|
position: newPosition,
|
|
};
|
|
};
|
|
|
|
export const getReparentDropTargetMutable = (
|
|
instances: Instances,
|
|
props: Props,
|
|
metas: Map<string, WsComponentMeta>,
|
|
dropTarget: DroppableTarget
|
|
): undefined | DroppableTarget => {
|
|
dropTarget = getCollectionDropTarget(instances, dropTarget) ?? dropTarget;
|
|
dropTarget =
|
|
getInstanceOrCreateFragmentIfNecessary(instances, dropTarget) ?? dropTarget;
|
|
dropTarget =
|
|
wrapEditableChildrenAroundDropTargetMutable(
|
|
instances,
|
|
props,
|
|
metas,
|
|
dropTarget
|
|
) ?? dropTarget;
|
|
return dropTarget;
|
|
};
|
|
|
|
export const cloneStyles = (
|
|
styles: Styles,
|
|
clonedStyleSourceIds: Map<Instance["id"], Instance["id"]>
|
|
) => {
|
|
const clonedStyles: StyleDecl[] = [];
|
|
for (const styleDecl of styles.values()) {
|
|
const styleSourceId = clonedStyleSourceIds.get(styleDecl.styleSourceId);
|
|
if (styleSourceId === undefined) {
|
|
continue;
|
|
}
|
|
clonedStyles.push({
|
|
...styleDecl,
|
|
styleSourceId,
|
|
});
|
|
}
|
|
return clonedStyles;
|
|
};
|
|
|
|
export const findLocalStyleSourcesWithinInstances = (
|
|
styleSources: IterableIterator<StyleSource> | StyleSource[],
|
|
styleSourceSelections:
|
|
| IterableIterator<StyleSourceSelection>
|
|
| StyleSourceSelection[],
|
|
instanceIds: Set<Instance["id"]>
|
|
) => {
|
|
const localStyleSourceIds = new Set<StyleSource["id"]>();
|
|
for (const styleSource of styleSources) {
|
|
if (styleSource.type === "local") {
|
|
localStyleSourceIds.add(styleSource.id);
|
|
}
|
|
}
|
|
|
|
const subtreeLocalStyleSourceIds = new Set<StyleSource["id"]>();
|
|
for (const { instanceId, values } of styleSourceSelections) {
|
|
// skip selections outside of subtree
|
|
if (instanceIds.has(instanceId) === false) {
|
|
continue;
|
|
}
|
|
// find only local style sources on selections
|
|
for (const styleSourceId of values) {
|
|
if (localStyleSourceIds.has(styleSourceId)) {
|
|
subtreeLocalStyleSourceIds.add(styleSourceId);
|
|
}
|
|
}
|
|
}
|
|
|
|
return subtreeLocalStyleSourceIds;
|
|
};
|