Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 14 additions & 0 deletions frontend/src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,20 @@ class Block implements BlockOptions {
}
styleObj[style] = value;
}
setActiveStyle(style: styleProperty, value: StyleValue) {
this.setStyle(this.getActiveStyleProperty(style), value);
}
getActiveStyleValue(style: styleProperty) {
const activeStateStyle = this.getActiveStyleProperty(style);
const activeValue = activeStateStyle === style ? undefined : this.getStyle(activeStateStyle);
return activeValue === null || activeValue === "" || activeValue === undefined
? this.getStyle(style)
: activeValue;
}
getActiveStyleProperty(style: styleProperty) {
const activeState = this.activeState?.split(":")[0];
return (activeState ? `${activeState}:${style}` : style) as styleProperty;
}
setAttribute(attribute: string, value: string | undefined) {
this.attributes[attribute] = value;
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/builder.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
declare type StyleValue = string | number | boolean | null | undefined;

declare type styleProperty = keyof CSSProperties | `__${string}`;
declare type styleProperty = keyof CSSProperties | `${string}:${keyof CSSProperties}` | `__${string}`;

declare interface BlockStyleMap {
[key: styleProperty]: StyleValue;
Expand Down
21 changes: 14 additions & 7 deletions frontend/src/components/BlockEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import type Block from "@/block";
import useBuilderStore from "@/stores/builderStore";
import useCanvasStore from "@/stores/canvasStore";
import blockController from "@/utils/blockController";
import { addPxToNumber } from "@/utils/helpers";
import { addPxToNumber, getNumberFromPx } from "@/utils/helpers";
import { isReorderable, startBlockReorder } from "@/utils/useBlockReorder";
import { Ref, computed, inject, nextTick, onMounted, ref, watch, watchEffect } from "vue";
import setGuides from "../utils/guidesTracker";
Expand Down Expand Up @@ -100,6 +100,7 @@ const transforming = computed(() => resizing.value || rotating.value);
const guides = setGuides(props.target, canvasProps);
const moving = ref(false);
const preventCLick = ref(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 hasStyleValue is copy-pasted into BorderRadiusHandler.vue, BoxResizer.vue, and useSpacingHandler.ts; extract it to a shared util.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

const hasStyleValue = (value: StyleValue) => value !== null && value !== undefined && value !== "";

const showPaddingHandler = computed(() => {
return (
Expand Down Expand Up @@ -296,8 +297,14 @@ const handleMove = (ev: MouseEvent) => {
const target = ev.target as HTMLElement;
const startX = ev.clientX;
const startY = ev.clientY;
const startLeft = (props.target as HTMLElement).offsetLeft || 0;
const startTop = (props.target as HTMLElement).offsetTop || 0;
const activeLeft = props.block.getActiveStyleValue("left");
const activeTop = props.block.getActiveStyleValue("top");
const startLeft = hasStyleValue(activeLeft)
? getNumberFromPx(activeLeft)
: (props.target as HTMLElement).offsetLeft || 0;
const startTop = hasStyleValue(activeTop)
? getNumberFromPx(activeTop)
: (props.target as HTMLElement).offsetTop || 0;
Comment on lines +300 to +307

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 If the active state's left/top is a percentage (e.g. "50%"), getNumberFromPx returns 0 (it only strips "px"), so the drag starts from 0 and overwrites the % value with 100px-style pixels. BoxResizer handles this with a blockStartWidth?.includes("%") guard; the same logic is needed here — or simply keep offsetLeft/offsetTop as the start since the state style is already applied to the DOM.

Suggested change
const activeLeft = props.block.getActiveStyleValue("left");
const activeTop = props.block.getActiveStyleValue("top");
const startLeft = hasStyleValue(activeLeft)
? getNumberFromPx(activeLeft)
: (props.target as HTMLElement).offsetLeft || 0;
const startTop = hasStyleValue(activeTop)
? getNumberFromPx(activeTop)
: (props.target as HTMLElement).offsetTop || 0;
const startLeft = (props.target as HTMLElement).offsetLeft || 0;
const startTop = (props.target as HTMLElement).offsetTop || 0;


moving.value = true;
guides.showX();
Expand All @@ -314,15 +321,15 @@ const handleMove = (ev: MouseEvent) => {
const movementY = (mouseMoveEvent.clientY - startY) / scale;
let finalLeft = startLeft + movementX;
let finalTop = startTop + movementY;
props.block.setStyle("left", addPxToNumber(finalLeft));
props.block.setStyle("top", addPxToNumber(finalTop));
props.block.setActiveStyle("left", addPxToNumber(finalLeft));
props.block.setActiveStyle("top", addPxToNumber(finalTop));
await nextTick();
const { leftOffset, rightOffset } = guides.getPositionOffset();
if (leftOffset !== 0) {
props.block.setStyle("left", addPxToNumber(finalLeft + leftOffset));
props.block.setActiveStyle("left", addPxToNumber(finalLeft + leftOffset));
}
if (rightOffset !== 0) {
props.block.setStyle("left", addPxToNumber(finalLeft + rightOffset));
props.block.setActiveStyle("left", addPxToNumber(finalLeft + rightOffset));
}

mouseMoveEvent.preventDefault();
Expand Down
19 changes: 13 additions & 6 deletions frontend/src/components/BorderRadiusHandler.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ const props = defineProps<{
target: HTMLElement | SVGElement;
}>();

const borderRadius = ref(parseInt(props.target.style.borderRadius, 10) || 0);
const hasStyleValue = (value: StyleValue) => value !== null && value !== undefined && value !== "";

const getCurrentRadius = () => {
const radius = props.targetBlock.getActiveStyleValue("borderRadius");
if (hasStyleValue(radius)) return getNumberFromPx(radius);
return parseInt(props.target.style.borderRadius, 10) || 0;
};

const borderRadius = ref(getCurrentRadius());
const updating = ref(false);
const canvasProps = inject("canvasProps") as CanvasProps;
const handler = ref() as Ref<HTMLElement>;
Expand Down Expand Up @@ -73,6 +81,7 @@ const handleRounded = (ev: MouseEvent) => {
const startY = ev.clientY;
let lastX = startX;
let lastY = startY;
borderRadius.value = getCurrentRadius();
updating.value = true;

const handleDimensions = handler.value.getBoundingClientRect();
Expand All @@ -88,21 +97,19 @@ const handleRounded = (ev: MouseEvent) => {
MIN_POSITION.left = -(handleDimensions.width / 2);
}

const radius = Math.round(
Math.max(0, Math.min(getNumberFromPx(props.target.style.borderRadius) + movement, maxRadius.value)),
);
const radius = Math.round(Math.max(0, Math.min(borderRadius.value + movement, maxRadius.value)));

borderRadius.value = radius;
setHandlerPosition(radius);
props.targetBlock.setStyle("borderRadius", `${radius}px`);
props.targetBlock.setActiveStyle("borderRadius", `${radius}px`);

lastX = mouseMoveEvent.clientX;
lastY = mouseMoveEvent.clientY;
};

const mouseup = (mouseUpEvent: MouseEvent) => {
mouseUpEvent.preventDefault();
if (getNumberFromPx(props.targetBlock.getStyle("borderRadius")) < 10) {
if (getNumberFromPx(props.targetBlock.getActiveStyleValue("borderRadius")) < 10) {
handlerTop.value = MIN_POSITION.top;
handlerLeft.value = MIN_POSITION.left;
}
Expand Down
30 changes: 17 additions & 13 deletions frontend/src/components/BoxResizer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const cursorPosition = ref({ x: 0, y: 0 });
let guides = null as unknown as ReturnType<typeof guidesTracker>;

const canvasProps = inject("canvasProps") as CanvasProps;
const hasStyleValue = (value: StyleValue) => value !== null && value !== undefined && value !== "";

onMounted(() => {
guides = guidesTracker(props.target as HTMLElement, canvasProps);
Expand Down Expand Up @@ -123,18 +124,21 @@ watch(resizing, () => {
});

const targetWidth = computed(() => {
props.targetBlock.getStyle("width"); // to trigger reactivity
props.targetBlock.getActiveStyleValue("width"); // to trigger reactivity
return Math.round(getNumberFromPx(getComputedStyle(props.target).getPropertyValue("width")));
});

const targetHeight = computed(() => {
props.targetBlock.getStyle("height"); // to trigger reactivity
props.targetBlock.getActiveStyleValue("height"); // to trigger reactivity
return Math.round(getNumberFromPx(getComputedStyle(props.target).getPropertyValue("height")));
});

const fontSize = computed(() => {
props.targetBlock.getStyle("fontSize"); // to trigger reactivity
return Math.round(getNumberFromPx(getComputedStyle(props.target).getPropertyValue("font-size")));
const activeFontSize = props.targetBlock.getActiveStyleValue("fontSize");
const fontSize = hasStyleValue(activeFontSize)
? getNumberFromPx(activeFontSize)
: getNumberFromPx(getComputedStyle(props.target).getPropertyValue("font-size"));
return Math.round(fontSize);
});

// For the left/top side, the opposite edge is kept visually fixed by shifting top/left the same
Expand All @@ -148,8 +152,8 @@ const handleResize = (ev: MouseEvent, { horizontal, vertical }: ResizeDirection)
const startTop = target.offsetTop;
const startLeft = target.offsetLeft;
const ownRotation = parseFloat(getComputedStyle(target).rotate) || 0;
const blockStartWidth = props.targetBlock.getStyle("width") as string;
const blockStartHeight = props.targetBlock.getStyle("height") as string;
const blockStartWidth = props.targetBlock.getActiveStyleValue("width") as string;
const blockStartHeight = props.targetBlock.getActiveStyleValue("height") as string;
const startFontSize = fontSize.value || 0;
const canReposition = props.targetBlock.isMovable();

Expand Down Expand Up @@ -198,8 +202,8 @@ const handleResize = (ev: MouseEvent, { horizontal, vertical }: ResizeDirection)
{ horizontal, vertical },
ownRotation,
);
props.targetBlock.setStyle("left", `${Math.round(startLeft + positionDelta.x)}px`);
props.targetBlock.setStyle("top", `${Math.round(startTop + positionDelta.y)}px`);
props.targetBlock.setActiveStyle("left", `${Math.round(startLeft + positionDelta.x)}px`);
props.targetBlock.setActiveStyle("top", `${Math.round(startTop + positionDelta.y)}px`);
}
},
onEnd: () => {
Expand Down Expand Up @@ -229,9 +233,9 @@ const setWidth = (movementX: number, startWidth: number, blockStartWidth: string
const movementPercent = (movementX / parentWidth) * 100;
const startWidthPercent = (startWidth / parentWidth) * 100;
const finalWidthPercent = Math.abs(Math.round(startWidthPercent + movementPercent));
props.targetBlock.setStyle("width", `${finalWidthPercent}%`);
props.targetBlock.setActiveStyle("width", `${finalWidthPercent}%`);
} else {
props.targetBlock.setStyle("width", `${finalWidth}px`);
props.targetBlock.setActiveStyle("width", `${finalWidth}px`);
}
};

Expand All @@ -242,14 +246,14 @@ const setHeight = (movementY: number, startHeight: number, blockStartHeight: str
const movementPercent = (movementY / parentHeight) * 100;
const startHeightPercent = (startHeight / parentHeight) * 100;
const finalHeightPercent = Math.abs(Math.round(startHeightPercent + movementPercent));
props.targetBlock.setStyle("height", `${finalHeightPercent}%`);
props.targetBlock.setActiveStyle("height", `${finalHeightPercent}%`);
} else {
props.targetBlock.setStyle("height", `${finalHeight}px`);
props.targetBlock.setActiveStyle("height", `${finalHeight}px`);
}
};

const setFontSize = (movement: number, startFontSize: number) => {
const fontSize = clamp(Math.round(startFontSize + 0.5 * movement), 10, 300);
props.targetBlock.setStyle("fontSize", `${fontSize}px`);
props.targetBlock.setActiveStyle("fontSize", `${fontSize}px`);
};
</script>
9 changes: 4 additions & 5 deletions frontend/src/components/BuilderBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -349,16 +349,14 @@ const styles = computed(() => {
...dynamicStyles,
} as BlockStyleMap;

if (props.block.activeState) {
const [state, property] = props.block.activeState.split(":");
if (!props.preview && isSelected.value && props.block.activeState) {
const [state] = props.block.activeState.split(":");

if (canvasStore.activeCanvas?.activeBreakpoint === props.breakpoint) {
const stateStyles = props.block.getStateStyles(state, props.breakpoint);
if (stateStyles) {
Object.keys(stateStyles).forEach((key) => {
if (key === property) {
styleMap[key] = stateStyles[key];
}
styleMap[key] = stateStyles[key];
});
}
}
Expand Down Expand Up @@ -620,6 +618,7 @@ if (!props.preview) {
}
} else {
isSelected.value = false;
props.block.activeState = null;
}
},
{
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/components/Controls/BasePropertyControl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
:placeholder="placeholderValue"
:dynamicValueKey="dynamicValue?.key"
componentClass="w-full"
@focusin="clearActiveVariant"
@mousedown="clearActiveVariant"
@update:modelValue="updateValue"
@keydown="handleKeyDown"
@openDynamicModal="showDynamicValueModal = true"
Expand All @@ -71,6 +73,8 @@
:placeholder="placeholderValue"
:enableSlider="enableSlider"
:isLast="index === visibleVariants.length - 1"
@focusin="setActiveVariant(variant.property)"
@mousedown="setActiveVariant(variant.property)"
@update:modelValue="(v: any) => updateVariantValue(variant.name, v)"
@keydown="(e: KeyboardEvent) => handleKeyDown(e, variant.name)"
@labelMousedown="(e: MouseEvent) => handleSliderMouseDown(e, variant.name)"
Expand Down Expand Up @@ -238,6 +242,19 @@ const clearVariant = (variantName: string) => props.setVariantValue?.(variantNam

const showDynamicValueModal = ref(false);

const setActiveVariant = (property: string) => {
if (props.controlType !== "style") return;
blockController.getSelectedBlocks().forEach((block) => {
block.activeState = property;
});
};

const clearActiveVariant = () => {
blockController.getSelectedBlocks().forEach((block) => {
block.activeState = null;
});
};

const dropdownOptions = computed(() => {
const options = [];
if (props.variants?.length) {
Expand All @@ -249,6 +266,7 @@ const dropdownOptions = computed(() => {
onClick: () => {
if (props.setVariantValue) {
props.setVariantValue(variant.name, rawModelValue.value as string);
setActiveVariant(variant.property);
}
},
})),
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/RotationHandler.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const handleRotate = (ev: MouseEvent, baseAngle: number) => {
const centerX = bounds.left + bounds.width / 2;
const centerY = bounds.top + bounds.height / 2;
let previousPointerAngle = Math.atan2(ev.clientY - centerY, ev.clientX - centerX) * (180 / Math.PI);
let rotation = parseFloat(String(props.targetBlock.getStyle("rotate") || 0)) || 0;
let rotation = parseFloat(String(props.targetBlock.getActiveStyleValue("rotate") || 0)) || 0;
// ancestors don't rotate mid-drag, so their contribution can be captured once up front
const ancestorRotation = getElementRotation((props.target as Element).parentElement);
const pauseId = canvasStore.activeCanvas?.history?.pause();
Expand Down Expand Up @@ -85,7 +85,7 @@ const handleRotate = (ev: MouseEvent, baseAngle: number) => {
? Math.round(rotation / 15) * 15
: Math.round(rotation);
currentRotation.value = finalRotation;
props.targetBlock.setStyle("rotate", `${finalRotation}deg`);
props.targetBlock.setActiveStyle("rotate", `${finalRotation}deg`);
// the cursor SVG only needs rebuilding when the rounded/snapped angle actually
// changes - most mousemove ticks land on the same value, especially while snapping
if (finalRotation !== lastCursorAngle) {
Expand Down
17 changes: 9 additions & 8 deletions frontend/src/composables/useSpacingHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const sides = {
const verticalSides = [Position.Top, Position.Bottom];
const horizontalSides = [Position.Left, Position.Right];
const allSides = [...verticalSides, ...horizontalSides];
const hasStyleValue = (value: StyleValue) => value !== null && value !== undefined && value !== "";

// Shared state and drag behaviour for the Margin and Padding handlers. The per-side
// positioning and value display differ and stay in each component.
Expand All @@ -42,13 +43,12 @@ export function useSpacingHandler(getTargetBlock: () => Block, getBreakpoint: ()
const updating = ref(false);

const blockStyles = computed(() => {
const block = getTargetBlock();
const breakpoint = getBreakpoint();
let styles = { ...getTargetBlock().baseStyles };
if (breakpoint === "mobile" || breakpoint === "tablet") {
styles = { ...styles, ...getTargetBlock().mobileStyles };
}
if (breakpoint === "tablet") {
styles = { ...styles, ...getTargetBlock().tabletStyles };
const styles = { ...block.getStyles(breakpoint) };
if (block.activeState) {
const [state] = block.activeState.split(":");
Object.assign(styles, block.getStateStyles(state, breakpoint));
}
return styles;
});
Expand All @@ -71,7 +71,7 @@ export function useSpacingHandler(getTargetBlock: () => Block, getBreakpoint: ()
`${property}${sides[side].styleSuffix}` as styleProperty;

const setSpacing = (property: SpacingProperty, side: Position, value: number) =>
getTargetBlock().setStyle(styleKey(property, side), `${value}px`);
getTargetBlock().setActiveStyle(styleKey(property, side), `${value}px`);

// Shift spreads the value to all four sides, alt to both sides of the dragged axis.
const sidesToUpdate = (event: MouseEvent, side: Position) => {
Expand All @@ -88,7 +88,8 @@ export function useSpacingHandler(getTargetBlock: () => Block, getBreakpoint: ()
const { axis, outward } = sides[side];
// the handles sit on the block's edge, so dragging outward grows a margin but shrinks a padding
const sign = property === "margin" ? outward : -outward;
const startValue = getNumberFromPx(blockStyles.value[styleKey(property, side)] as string) || fallback;
const currentValue = blockStyles.value[styleKey(property, side)];
const startValue = hasStyleValue(currentValue) ? getNumberFromPx(currentValue) : fallback;
const startPoint = { x: event.clientX, y: event.clientY };

event.preventDefault();
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/utils/rotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function toLocalDelta(dx: number, dy: number, rotationDeg: number) {

// Uses the reactive block style so computed cursors update with style-panel edits.
function getTotalRotation(target: Element, targetBlock: Block): number {
const ownRotation = parseFloat(String(targetBlock.getStyle("rotate") || 0)) || 0;
const ownRotation = parseFloat(String(targetBlock.getActiveStyleValue("rotate") || 0)) || 0;
return ownRotation + getElementRotation(target.parentElement);
}

Expand Down
Loading