Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Editable Block #4468

Merged
merged 9 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add selections
  • Loading branch information
istarkov committed Nov 27, 2024
commit ec5e57f313d50b5525603e6dd562a64b1fc78e04
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
import { useStore } from "@nanostores/react";
import type {
AnyComponent,
WebstudioComponentSystemProps,
import {
selectorIdAttribute,
type AnyComponent,
type WebstudioComponentSystemProps,
} from "@webstudio-is/react-sdk";
import * as React from "react";
import { $isContentMode } from "~/shared/nano-states";

import { $isDesignMode, $selectedInstanceSelector } from "~/shared/nano-states";
import type { InstanceSelector } from "~/shared/tree-utils";

export const EditableBlockTemplate = React.forwardRef<
HTMLDivElement,
WebstudioComponentSystemProps
>((props, ref) => {
const isContentMode = useStore($isContentMode);
WebstudioComponentSystemProps & { children: React.ReactNode }
>(({ children, ...props }, ref) => {
const isDesignMode = useStore($isDesignMode);
const selectedInstanceSelector = useStore($selectedInstanceSelector);
const templateInstanceStringSelector = props[selectorIdAttribute];

if (isContentMode) {
if (!isDesignMode) {
return <></>;
}

return <div style={{ display: "contents" }} ref={ref} {...props} />;
if (selectedInstanceSelector === undefined) {
return <></>;
}

const selectedSelector = selectedInstanceSelector.join(",");

// Exclude all selected ancestors and self
if (templateInstanceStringSelector.endsWith(selectedSelector)) {
return <div style={{ display: "contents" }} ref={ref} {...props} />;
}

const visibleChildren = React.Children.toArray(children)
.filter((child) => React.isValidElement(child))
.filter((child) => {
const { instanceSelector } = child.props as {
instanceSelector: InstanceSelector;
};

return selectedSelector.endsWith(instanceSelector.join(","));
});

return (
<div style={{ display: "contents" }} ref={ref} {...props}>
{visibleChildren}
</div>
);
}) as AnyComponent;
74 changes: 69 additions & 5 deletions apps/builder/app/canvas/features/build-mode/editable-block.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,88 @@
import type {
AnyComponent,
WebstudioComponentSystemProps,
import { useStore } from "@nanostores/react";
import {
editableBlockTemplateComponent,
idAttribute,
selectorIdAttribute,
type AnyComponent,
type WebstudioComponentSystemProps,
} from "@webstudio-is/react-sdk";

import * as React from "react";
import { $instances, $selectedInstanceSelector } from "~/shared/nano-states";

export const EditableBlock = React.forwardRef<
HTMLDivElement,
{ children: React.ReactNode } & WebstudioComponentSystemProps
>(({ children, ...props }, ref) => {
const instances = useStore($instances);
const instanceId = props[idAttribute];
const instance = instances.get(instanceId);
const selectedInstanceSelector = useStore($selectedInstanceSelector);

if (instance === undefined) {
return <div>Editable Block instance is undefined</div>;
}

const templateInstanceId = instance.children.find(
(child) =>
child.type === "id" &&
instances.get(child.value)?.component === editableBlockTemplateComponent
)?.value;

if (templateInstanceId === undefined) {
return <div>Editable Block template instance not found</div>;
}

const templateInstance = instances.get(templateInstanceId);
if (templateInstance === undefined) {
return <div>Editable Block template instance is undefined</div>;
}

const templateChildrenIds = templateInstance.children
.filter((child) => child.type === "id")
.map((child) => child.value);

if (templateChildrenIds === undefined) {
return <div>Editable block template children not found</div>;
}

const childArray = React.Children.toArray(children).filter((child) =>
React.isValidElement(child)
);

const editableBlockStyle =
childArray.length === 1 ? {} : { display: "contents" };
if (selectedInstanceSelector !== undefined) {
const selectedSelector = selectedInstanceSelector.join(",");
// If any template child is selected then render only template
const stringSelector = props[selectorIdAttribute];

const isTemplateChildSelected = templateChildrenIds.some((childId) => {
const childSelector = `${childId},${templateInstanceId},${stringSelector}`;

if (selectedSelector.endsWith(childSelector)) {
return true;
}
});

if (isTemplateChildSelected) {
return (
<div style={{ display: "contents" }} ref={ref} {...props}>
{childArray.filter((child) => {
const { instanceSelector } = child.props;

return instanceSelector[0] === templateInstanceId;
})}
</div>
);
}
}

const hasContent = childArray.length > 1;
const editableBlockStyle = hasContent ? { display: "contents" } : {};

return (
<div ref={ref} style={editableBlockStyle} {...props}>
{childArray}
{hasContent ? null : <div>Editable block you can edit</div>}
</div>
);
}) as AnyComponent;
13 changes: 13 additions & 0 deletions apps/builder/app/canvas/instance-hovering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
$editableBlockChildOutline,
$hoveredInstanceSelector,
$instances,
$selectedInstanceSelector,
$textEditingInstanceSelector,
findEditableBlockChildSelector,
} from "~/shared/nano-states";
Expand Down Expand Up @@ -204,9 +205,21 @@ export const subscribeInstanceHovering = ({
}
);

// selected instance selection can change hovered instance outlines (example EditableBlock/Template/Child)
const usubscribeSelectedInstanceSelector =
$selectedInstanceSelector.subscribe(() => {
const instanceSelector = $hoveredInstanceSelector.get();
if (instanceSelector) {
updateHoveredRect(instanceSelector);
} else {
$hoveredInstanceOutline.set(undefined);
}
});

signal.addEventListener("abort", () => {
unsubscribeScrollState();
clearTimeout(mouseOutTimeoutId);
unsubscribeHoveredInstanceId();
usubscribeSelectedInstanceSelector();
});
};
56 changes: 40 additions & 16 deletions apps/builder/app/shared/dom-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ const sumRects = (first: Rect, second: Rect) => {
};
};

export const getAllElementsBoundingBox = (elements: Element[]): DOMRect => {
export const getAllElementsBoundingBox = (
elements: Element[],
depth: number = 0
): DOMRect => {
const rects: Rect[] = [];

if (elements.length === 0) {
Expand All @@ -151,30 +154,51 @@ export const getAllElementsBoundingBox = (elements: Element[]): DOMRect => {

if (element.children.length === 0) {
const textNode = element.firstChild;
if (textNode?.nodeType !== Node.TEXT_NODE) {
continue;
}

// Create a range object
const range = document.createRange();
// Set the range to encompass the text node
range.selectNodeContents(textNode);
// Get the bounding rectangle
const rect = range.getBoundingClientRect();
if (textNode?.nodeType === Node.TEXT_NODE) {
// Create a range object
const range = document.createRange();
// Set the range to encompass the text node
range.selectNodeContents(textNode);
// Get the bounding rectangle
const rect = range.getBoundingClientRect();

if (rect.width !== 0 || rect.height !== 0) {
rects.push(rect);
range.detach();
continue;
}
range.detach();
}
}

if (rect.width !== 0 || rect.height !== 0) {
rects.push(rect);
if (element.children.length > 0) {
const childRect = getAllElementsBoundingBox(
[...element.children],
depth + 1
);
if (childRect.width !== 0 || childRect.height !== 0) {
const { top, right, bottom, left } = childRect;
rects.push({ top, right, bottom, left });
continue;
}
}

range.detach();
if (depth > 0) {
continue;
}

// We here, let's try ancestor size
const parentElement = element.parentElement;
if (parentElement === null) {
continue;
}
const parentRect = getAllElementsBoundingBox([parentElement]);

const childRect = getAllElementsBoundingBox([...element.children]);
if (childRect.width !== 0 || childRect.height !== 0) {
const { top, right, bottom, left } = childRect;
if (parentRect.width !== 0 || parentRect.height !== 0) {
const { top, right, bottom, left } = parentRect;
rects.push({ top, right, bottom, left });
continue;
}
}

Expand Down