Skip to content

Commit d850805

Browse files
committed
feat: add "Convert To Absolute Position" command
1 parent 81d6452 commit d850805

File tree

4 files changed

+122
-5
lines changed

4 files changed

+122
-5
lines changed

apps/builder/app/builder/features/ai/apply-operations.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,18 @@ const applyStylesByOp = (operation: operations.editStylesWsOperation) => {
156156
});
157157
}
158158

159-
for (const embedStyleDecl of newStyles) {
159+
for (const { state, property, value } of newStyles) {
160160
const styleDecl = {
161-
...embedStyleDecl,
162161
breakpointId: baseBreakpoint?.id,
163162
styleSourceId,
163+
state,
164+
property,
164165
};
165-
styles.set(getStyleDeclKey(styleDecl), styleDecl);
166+
if (value !== null) {
167+
styles.set(getStyleDeclKey(styleDecl), { ...styleDecl, value });
168+
} else {
169+
styles.delete(getStyleDeclKey(styleDecl));
170+
}
166171
}
167172
}
168173
}

apps/builder/app/builder/shared/commands.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
$styles,
1818
$styleSourceSelections,
1919
$selectedBreakpoint,
20+
$selectedInstanceSelector,
2021
} from "~/shared/nano-states";
2122
import {
2223
$breakpointsMenuView,
@@ -56,6 +57,10 @@ import { findAvailableVariables } from "~/shared/data-variables";
5657
import { atom } from "nanostores";
5758
import type { StyleValue } from "@webstudio-is/css-engine";
5859
import { createBatchUpdate } from "../features/style-panel/shared/use-style-data";
60+
import { applyOperations } from "../features/ai/apply-operations";
61+
import { getInstanceStyleDecl } from "../features/style-panel/shared/model";
62+
import { getElementByInstanceSelector } from "~/shared/dom-utils";
63+
import type { operations } from "@webstudio-is/ai";
5964

6065
export const $styleSourceInputElement = atom<HTMLInputElement | undefined>();
6166

@@ -226,6 +231,103 @@ export const convertPositionToProperty = (
226231
batch.publish();
227232
};
228233

234+
export const convertToAbsolutePosition = () => {
235+
const selectedInstance = $selectedInstance.get();
236+
const selectedInstanceSelector = $selectedInstanceSelector.get();
237+
238+
if (!selectedInstance || !selectedInstanceSelector) {
239+
return;
240+
}
241+
242+
const position = getInstanceStyleDecl("position", selectedInstanceSelector);
243+
if (
244+
position.usedValue.type === "keyword" &&
245+
position.usedValue.value === "absolute"
246+
) {
247+
return;
248+
}
249+
250+
// Ensure parent instance isn't using `position: static`
251+
const selectedInstancePath = $selectedInstancePath.get()!;
252+
const parent = selectedInstancePath[1];
253+
254+
let parentRect: DOMRect | undefined;
255+
let isParentStatic = false;
256+
257+
if (parent) {
258+
const parentElement = getElementByInstanceSelector(parent.instanceSelector);
259+
parentRect = parentElement?.getBoundingClientRect();
260+
261+
// Change parent position to relative if it's static
262+
const parentPosition = getInstanceStyleDecl(
263+
"position",
264+
parent.instanceSelector
265+
);
266+
isParentStatic =
267+
parentPosition.usedValue.type === "keyword" &&
268+
parentPosition.usedValue.value === "static";
269+
}
270+
271+
const instanceElement = getElementByInstanceSelector(
272+
selectedInstanceSelector
273+
);
274+
275+
if (!instanceElement) {
276+
return;
277+
}
278+
279+
const operations: operations.WsOperations = [];
280+
281+
if (isParentStatic) {
282+
operations.push({
283+
operation: "applyStyles",
284+
instanceIds: [parent.instance.id],
285+
styles: [
286+
{
287+
property: "position",
288+
value: { type: "keyword", value: "relative" },
289+
},
290+
],
291+
});
292+
}
293+
294+
const rect = instanceElement.getBoundingClientRect();
295+
296+
operations.push({
297+
operation: "applyStyles",
298+
instanceIds: [selectedInstance.id],
299+
styles: [
300+
{
301+
property: "position",
302+
value: { type: "keyword", value: "absolute" },
303+
},
304+
{
305+
property: "top",
306+
value: {
307+
type: "unit",
308+
value: rect.top - (parentRect?.top ?? 0),
309+
unit: "px",
310+
},
311+
},
312+
{
313+
property: "left",
314+
value: {
315+
type: "unit",
316+
value: rect.left - (parentRect?.left ?? 0),
317+
unit: "px",
318+
},
319+
},
320+
// Unset margin properties.
321+
{ property: "marginTop", value: null },
322+
{ property: "marginLeft", value: null },
323+
{ property: "marginRight", value: null },
324+
{ property: "marginBottom", value: null },
325+
],
326+
});
327+
328+
applyOperations(operations);
329+
};
330+
229331
export const wrapIn = (component: string) => {
230332
const instancePath = $selectedInstancePath.get();
231333
// global root or body are selected
@@ -564,6 +666,10 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({
564666
name: "convertPositionToMargin",
565667
handler: () => convertPositionToProperty("margin"),
566668
},
669+
{
670+
name: "convertToAbsolutePosition",
671+
handler: convertToAbsolutePosition,
672+
},
567673

568674
{
569675
name: "deleteInstanceBuilder",

apps/builder/app/shared/dom-utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,14 @@ export const getInstanceSelectorFromElement = (element: Element) => {
2727
export const getElementByInstanceSelector = (
2828
instanceSelector: InstanceSelector | Readonly<InstanceSelector>
2929
) => {
30+
const context =
31+
document.location.pathname !== "/canvas"
32+
? (document.querySelector('iframe[src="/canvas"]') as HTMLIFrameElement)
33+
?.contentDocument
34+
: document;
35+
3036
return (
31-
document.querySelector<HTMLElement>(
37+
context?.querySelector<HTMLElement>(
3238
`[${selectorIdAttribute}="${instanceSelector.join(",")}"]`
3339
) ?? undefined
3440
);

packages/sdk/src/schema/embed-template.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const EmbedTemplateStyleDeclRaw = z.object({
8080
// State selector, e.g. :hover
8181
state: z.optional(z.string()),
8282
property: z.string(),
83-
value: StyleValue,
83+
value: StyleValue.or(z.null()),
8484
});
8585

8686
export type EmbedTemplateStyleDecl = Simplify<

0 commit comments

Comments
 (0)