From a0009b35aac4136f0d00e280325e59bfffffe1d3 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Tue, 15 Oct 2024 17:05:48 +0200 Subject: [PATCH 01/10] wip --- .../strategies/set-padding-strategy.tsx | 23 +++-- editor/src/components/canvas/canvas-types.ts | 9 +- .../select-mode/padding-resize-control.tsx | 27 ++++-- .../select-mode/subdued-padding-control.tsx | 17 +++- .../src/components/canvas/padding-utils.tsx | 46 +++------ .../canvas/plugins/inline-style-plugin.ts | 14 ++- .../plugins/tailwind-style-plugin.spec.ts | 95 +++++++++++++++++++ .../canvas/plugins/tailwind-style-plugin.ts | 25 +++++ 8 files changed, 204 insertions(+), 52 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.tsx index bb69d4fc2a7c..4dacd8c409b7 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.tsx @@ -10,7 +10,7 @@ import { optionalMap } from '../../../../core/shared/optional-utils' import type { ElementPath } from '../../../../core/shared/project-file-types' import { assertNever } from '../../../../core/shared/utils' import { stylePropPathMappingFn } from '../../../inspector/common/property-path-hooks' -import type { EdgePiece } from '../../canvas-types' +import type { EdgePiece, StyleInfo } from '../../canvas-types' import { CSSCursor, isHorizontalEdgePiece, oppositeEdgePiece } from '../../canvas-types' import { deleteProperties } from '../../commands/delete-properties-command' import { setCursorCommand } from '../../commands/set-cursor-command' @@ -32,7 +32,7 @@ import { paddingPropForEdge, paddingToPaddingString, printCssNumberWithDefaultUnit, - simplePaddingFromMetadata, + simplePaddingFromStyleInfo, } from '../../padding-utils' import type { CanvasStrategyFactory } from '../canvas-strategies' import { onlyFitWhenDraggingThisControl } from '../canvas-strategies' @@ -118,6 +118,7 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti canvasState.startingMetadata, canvasState.startingElementPathTree, selectedElements[0], + canvasState.styleInfoReader(selectedElements[0]), ) ) { return null @@ -184,7 +185,11 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti const edgePiece = interactionSession.activeControl.edgePiece const drag = interactionSession.interactionData.drag ?? canvasVector({ x: 0, y: 0 }) - const padding = simplePaddingFromMetadata(canvasState.startingMetadata, selectedElement) + const padding = simplePaddingFromStyleInfo( + canvasState.startingMetadata, + selectedElement, + canvasState.styleInfoReader(selectedElement), + ) const paddingPropInteractedWith = paddingPropForEdge(edgePiece) const currentPadding = padding[paddingPropInteractedWith]?.renderedValuePx ?? 0 const rawDelta = deltaFromEdge(drag, edgePiece) @@ -351,6 +356,7 @@ function supportsPaddingControls( metadata: ElementInstanceMetadataMap, pathTrees: ElementPathTrees, path: ElementPath, + styleInfo: StyleInfo | null, ): boolean { const element = MetadataUtils.findElementByElementPath(metadata, path) if (element == null) { @@ -365,7 +371,7 @@ function supportsPaddingControls( return false } - const padding = simplePaddingFromMetadata(metadata, path) + const padding = simplePaddingFromStyleInfo(metadata, path, styleInfo) const { top, right, bottom, left } = element.specialSizeMeasurements.padding const elementHasNonzeroPaddingFromMeasurements = [top, right, bottom, left].some( (s) => s != null && s > 0, @@ -430,9 +436,10 @@ function paddingValueIndicatorProps( const edgePiece = interactionSession.activeControl.edgePiece - const padding = simplePaddingFromMetadata( + const padding = simplePaddingFromStyleInfo( canvasState.startingMetadata, filteredSelectedElements[0], + canvasState.styleInfoReader(selectedElement), ) const currentPadding = padding[paddingPropForEdge(edgePiece)] ?? unitlessCSSNumberWithRenderedValue(0) @@ -558,7 +565,11 @@ function calculateAdjustDelta( const edgePiece = interactionSession.activeControl.edgePiece const drag = interactionSession.interactionData.drag ?? canvasVector({ x: 0, y: 0 }) - const padding = simplePaddingFromMetadata(canvasState.startingMetadata, selectedElement) + const padding = simplePaddingFromStyleInfo( + canvasState.startingMetadata, + selectedElement, + canvasState.styleInfoReader(selectedElement), + ) const paddingPropInteractedWith = paddingPropForEdge(edgePiece) const currentPadding = padding[paddingPropForEdge(edgePiece)]?.renderedValuePx ?? 0 const rawDelta = deltaFromEdge(drag, edgePiece) diff --git a/editor/src/components/canvas/canvas-types.ts b/editor/src/components/canvas/canvas-types.ts index 8477786c2cab..4c1cca0757a6 100644 --- a/editor/src/components/canvas/canvas-types.ts +++ b/editor/src/components/canvas/canvas-types.ts @@ -26,7 +26,7 @@ import type { import { InteractionSession } from './canvas-strategies/interaction-state' import type { CanvasStrategyId } from './canvas-strategies/canvas-strategy-types' import type { MouseButtonsPressed } from '../../utils/mouse' -import type { CSSNumber, FlexDirection } from '../inspector/common/css-utils' +import type { CSSNumber, CSSPadding, FlexDirection } from '../inspector/common/css-utils' export const CanvasContainerID = 'canvas-container' @@ -550,8 +550,15 @@ export const withPropertyTag = (value: T): WithPropertyTag => ({ export type FlexGapInfo = WithPropertyTag export type FlexDirectionInfo = WithPropertyTag +export type PaddingInfo = WithPropertyTag +export type PaddingSideInfo = WithPropertyTag export interface StyleInfo { gap: FlexGapInfo | null flexDirection: FlexDirectionInfo | null + padding: PaddingInfo | null + paddingTop: PaddingSideInfo | null + paddingRight: PaddingSideInfo | null + paddingBottom: PaddingSideInfo | null + paddingLeft: PaddingSideInfo | null } diff --git a/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx b/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx index 0951eb5597ac..975a18439953 100644 --- a/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/padding-resize-control.tsx @@ -34,7 +34,7 @@ import { paddingAdjustMode, paddingFromSpecialSizeMeasurements, PaddingIndictorOffset, - simplePaddingFromMetadata, + simplePaddingFromStyleInfo, } from '../../padding-utils' import { useBoundingBox } from '../bounding-box-hooks' import { CanvasOffsetWrapper } from '../canvas-offset-wrapper' @@ -43,6 +43,7 @@ import type { CSSNumberWithRenderedValue } from './controls-common' import { CanvasLabel, fallbackEmptyValue, PillHandle, useHoverWithDelay } from './controls-common' import { MetadataUtils } from '../../../../core/model/element-metadata-utils' import { mapDropNulls } from '../../../../core/shared/array-utils' +import { getActivePlugin } from '../../plugins/style-plugins' export const paddingControlTestId = (edge: EdgePiece): string => `padding-control-${edge}` export const paddingControlHandleTestId = (edge: EdgePiece): string => @@ -358,12 +359,24 @@ export const PaddingResizeControl = controlForStrategyMemoized((props: PaddingCo } }, [hoveredViews, selectedElements]) - const currentPadding = React.useMemo(() => { - return combinePaddings( - paddingFromSpecialSizeMeasurements(elementMetadata, selectedElements[0]), - simplePaddingFromMetadata(elementMetadata, selectedElements[0]), - ) - }, [elementMetadata, selectedElements]) + const currentPadding = useEditorState( + Substores.fullStore, + (store) => { + return combinePaddings( + paddingFromSpecialSizeMeasurements(store.editor.jsxMetadata, selectedElements[0]), + simplePaddingFromStyleInfo( + elementMetadata, + selectedElements[0], + getActivePlugin(store.editor).styleInfoFactory({ + metadata: store.editor.jsxMetadata, + projectContents: store.editor.projectContents, + elementPathTree: store.editor.elementPathTree, + })(selectedElements[0]), + ), + ) + }, + 'PaddingResizeControl currentPadding', + ) const shownByParent = selectedElementHovered || anyControlHovered diff --git a/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx b/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx index d46615b6ad39..d3a04c20465b 100644 --- a/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx +++ b/editor/src/components/canvas/controls/select-mode/subdued-padding-control.tsx @@ -2,9 +2,10 @@ import React from 'react' import { useColorTheme } from '../../../../uuiui' import { Substores, useEditorState, useRefEditorState } from '../../../editor/store/store-hook' import type { EdgePiece } from '../../canvas-types' -import { paddingPropForEdge, simplePaddingFromMetadata } from '../../padding-utils' +import { paddingPropForEdge, simplePaddingFromStyleInfo } from '../../padding-utils' import { useBoundingBox } from '../bounding-box-hooks' import { CanvasOffsetWrapper } from '../canvas-offset-wrapper' +import { getActivePlugin } from '../../plugins/style-plugins' export interface SubduedPaddingControlProps { side: EdgePiece @@ -25,9 +26,21 @@ export const SubduedPaddingControl = React.memo((pro const isVerticalPadding = !isHorizontalPadding const paddingKey = paddingPropForEdge(side) + const styleInfoRef = useRefEditorState((store) => + getActivePlugin(store.editor).styleInfoFactory({ + metadata: store.editor.jsxMetadata, + projectContents: store.editor.projectContents, + elementPathTree: store.editor.elementPathTree, + })(targets[0]), + ) + // TODO Multiselect const sideRef = useBoundingBox(targets, (ref, boundingBox) => { - const padding = simplePaddingFromMetadata(elementMetadata.current, targets[0]) + const padding = simplePaddingFromStyleInfo( + elementMetadata.current, + targets[0], + styleInfoRef.current, + ) const paddingValue = padding[paddingKey]?.renderedValuePx ?? 0 const { x, y, width, height } = boundingBox diff --git a/editor/src/components/canvas/padding-utils.tsx b/editor/src/components/canvas/padding-utils.tsx index 7f2247aeeaed..1bf1e006e8d8 100644 --- a/editor/src/components/canvas/padding-utils.tsx +++ b/editor/src/components/canvas/padding-utils.tsx @@ -2,7 +2,10 @@ import { styleStringInArray } from '../../utils/common-constants' import { getLayoutProperty } from '../../core/layout/getLayoutProperty' import { MetadataUtils } from '../../core/model/element-metadata-utils' import { defaultEither, isLeft, right } from '../../core/shared/either' -import type { ElementInstanceMetadataMap } from '../../core/shared/element-template' +import type { + ElementInstanceMetadata, + ElementInstanceMetadataMap, +} from '../../core/shared/element-template' import { isJSXElement } from '../../core/shared/element-template' import type { CanvasVector, Size } from '../../core/shared/math-utils' import { numberIsZero, roundTo, zeroRectIfNullOrInfinity } from '../../core/shared/math-utils' @@ -11,7 +14,7 @@ import type { ElementPath } from '../../core/shared/project-file-types' import { assertNever } from '../../core/shared/utils' import type { CSSNumber, CSSNumberUnit, CSSPadding } from '../inspector/common/css-utils' import { printCSSNumber } from '../inspector/common/css-utils' -import type { EdgePiece } from './canvas-types' +import type { EdgePiece, StyleInfo } from './canvas-types' import type { AdjustPrecision, CSSNumberWithRenderedValue, @@ -58,12 +61,11 @@ export function paddingFromSpecialSizeMeasurements( return paddingMappedMeasurements } -export function simplePaddingFromMetadata( +export function simplePaddingFromStyleInfo( metadata: ElementInstanceMetadataMap, elementPath: ElementPath, + styleInfo: StyleInfo | null, ): CSSPaddingMappedValues { - const element = MetadataUtils.findElementByElementPath(metadata, elementPath) - const defaults: CSSPaddingMappedValues = { paddingTop: undefined, paddingRight: undefined, @@ -71,39 +73,15 @@ export function simplePaddingFromMetadata( paddingLeft: undefined, } - if (element == null || isLeft(element.element) || !isJSXElement(element.element.value)) { - return { - paddingTop: undefined, - paddingRight: undefined, - paddingBottom: undefined, - paddingLeft: undefined, - } - } - const paddingNumbers = paddingFromSpecialSizeMeasurements(metadata, elementPath) - const padding: CSSPadding | undefined = defaultEither( - undefined, - getLayoutProperty('padding', right(element.element.value.props), styleStringInArray), - ) + const padding: CSSPadding | undefined = styleInfo?.padding?.value const paddingLonghands: CSSPaddingMappedValues = { - paddingTop: defaultEither( - undefined, - getLayoutProperty('paddingTop', right(element.element.value.props), styleStringInArray), - ), - paddingBottom: defaultEither( - undefined, - getLayoutProperty('paddingBottom', right(element.element.value.props), styleStringInArray), - ), - paddingLeft: defaultEither( - undefined, - getLayoutProperty('paddingLeft', right(element.element.value.props), styleStringInArray), - ), - paddingRight: defaultEither( - undefined, - getLayoutProperty('paddingRight', right(element.element.value.props), styleStringInArray), - ), + paddingTop: styleInfo?.paddingTop?.value, + paddingBottom: styleInfo?.paddingBottom?.value, + paddingLeft: styleInfo?.paddingLeft?.value, + paddingRight: styleInfo?.paddingRight?.value, } const make = (prop: CSSPaddingKey): CSSNumberWithRenderedValue | undefined => { diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 11bc7dfa873b..19106971606b 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -31,10 +31,20 @@ export const InlineStylePlugin: StylePlugin = { const gap = getPropertyFromInstance('gap', instance.element.value) const flexDirection = getPropertyFromInstance('flexDirection', instance.element.value) + const padding = getPropertyFromInstance('padding', instance.element.value) + const paddingTop = getPropertyFromInstance('paddingTop', instance.element.value) + const paddingBottom = getPropertyFromInstance('paddingBottom', instance.element.value) + const paddingLeft = getPropertyFromInstance('paddingLeft', instance.element.value) + const paddingRight = getPropertyFromInstance('paddingRight', instance.element.value) return { - gap: gap, - flexDirection: flexDirection, + gap, + flexDirection, + padding, + paddingTop, + paddingBottom, + paddingLeft, + paddingRight, } }, normalizeFromInlineStyle: (editor) => editor, diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts index c4490e97d2a0..f72621f26058 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts @@ -79,6 +79,101 @@ describe('tailwind style plugin', () => { style: { backgroundColor: 'blue', display: 'flex', height: 100, left: 2, top: 2, width: 100 }, }) }) + it('can normalize padding (shorthand) from inline style', async () => { + const editor = await renderTestEditorWithModel( + Project({ + top: 2, + left: 2, + width: 100, + height: 100, + backgroundColor: 'blue', + display: 'flex', + flexDirection: 'row', + padding: '12px', + }), + 'await-first-dom-report', + ) + const target = EP.fromString('sb/scene/div') + const normalizedEditor = TailwindPlugin(null).normalizeFromInlineStyle( + editor.getEditorState().editor, + [target], + ) + + const normalizedElement = getJSXElementFromProjectContents( + target, + normalizedEditor.projectContents, + )! + + expect(formatJSXAttributes(normalizedElement.props)).toEqual({ + className: 'flex-row p-[12px]', + 'data-uid': 'div', + style: { backgroundColor: 'blue', display: 'flex', height: 100, left: 2, top: 2, width: 100 }, + }) + }) + it('can normalize padding (longhand) from inline style', async () => { + const editor = await renderTestEditorWithModel( + Project({ + top: 2, + left: 2, + width: 100, + height: 100, + backgroundColor: 'blue', + paddingTop: '1px', + paddingRight: '2px', + paddingBottom: '3px', + paddingLeft: '4px', + }), + 'await-first-dom-report', + ) + const target = EP.fromString('sb/scene/div') + const normalizedEditor = TailwindPlugin(null).normalizeFromInlineStyle( + editor.getEditorState().editor, + [target], + ) + + const normalizedElement = getJSXElementFromProjectContents( + target, + normalizedEditor.projectContents, + )! + + expect(formatJSXAttributes(normalizedElement.props)).toEqual({ + className: 'pt-px pr-[2px] pb-[3px] pl-[4px]', + 'data-uid': 'div', + style: { backgroundColor: 'blue', height: 100, left: 2, top: 2, width: 100 }, + }) + }) + it('can normalize horizontal padding and missing vertical padding', async () => { + const editor = await renderTestEditorWithModel( + Project({ + top: 2, + left: 2, + width: 100, + height: 100, + backgroundColor: 'blue', + paddingRight: '12px', + paddingLeft: '12px', + paddingTop: '12px', + }), + 'await-first-dom-report', + ) + const target = EP.fromString('sb/scene/div') + const normalizedEditor = TailwindPlugin(null).normalizeFromInlineStyle( + editor.getEditorState().editor, + [target], + ) + + const normalizedElement = getJSXElementFromProjectContents( + target, + normalizedEditor.projectContents, + )! + + expect(formatJSXAttributes(normalizedElement.props)).toEqual({ + className: 'pr-[12px] pl-[12px] pt-[12px]', + 'data-uid': 'div', + style: { backgroundColor: 'blue', height: 100, left: 2, top: 2, width: 100 }, + }) + }) + it('can normalize inline style with custom Tailwind config', async () => { const editor = await renderTestEditorWithModel( Project({ diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 23e2e7413a9c..5315c9095065 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -30,6 +30,11 @@ function parseTailwindProperty(value: unknown, parse: Parser): WithPropert const TailwindPropertyMapping = { gap: 'gap', flexDirection: 'flexDirection', + padding: 'padding', + paddingTop: 'paddingTop', + paddingRight: 'paddingRight', + paddingBottom: 'paddingBottom', + paddingLeft: 'paddingLeft', } as const function isSupportedTailwindProperty(prop: unknown): prop is keyof typeof TailwindPropertyMapping { @@ -80,6 +85,26 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ mapping[TailwindPropertyMapping.flexDirection], cssParsers.flexDirection, ), + padding: parseTailwindProperty( + mapping[TailwindPropertyMapping.padding], + cssParsers.padding, + ), + paddingTop: parseTailwindProperty( + mapping[TailwindPropertyMapping.paddingTop], + cssParsers.paddingTop, + ), + paddingRight: parseTailwindProperty( + mapping[TailwindPropertyMapping.paddingRight], + cssParsers.paddingRight, + ), + paddingBottom: parseTailwindProperty( + mapping[TailwindPropertyMapping.paddingBottom], + cssParsers.paddingBottom, + ), + paddingLeft: parseTailwindProperty( + mapping[TailwindPropertyMapping.paddingLeft], + cssParsers.paddingLeft, + ), } }, normalizeFromInlineStyle: (editorState, elementsToNormalize) => { From 904b33b493cccad6e6e2e7a232f93f72e88e7d06 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 17 Oct 2024 16:19:02 +0200 Subject: [PATCH 02/10] placehoolder test --- .../set-padding-strategy.spec.browser2.tsx | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx index 286cbe95f566..114e330843d6 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx @@ -1,7 +1,14 @@ import { assertNever } from '../../../../core/shared/utils' +import { TailwindConfigPath } from '../../../../core/tailwind/tailwind-config' +import { createModifiedProject } from '../../../../sample-projects/sample-project-utils.test-utils' import type { Modifiers } from '../../../../utils/modifiers' -import { cmdModifier, shiftModifier } from '../../../../utils/modifiers' -import { expectSingleUndo2Saves, wait } from '../../../../utils/utils.test-utils' +import { cmdModifier } from '../../../../utils/modifiers' +import { + expectSingleUndo2Saves, + setFeatureForBrowserTestsUseInDescribeBlockOnly, + wait, +} from '../../../../utils/utils.test-utils' +import { StoryboardFilePath } from '../../../editor/store/editor-state' import { cssNumber } from '../../../inspector/common/css-utils' import type { EdgePiece } from '../../canvas-types' import { isHorizontalEdgePiece } from '../../canvas-types' @@ -39,6 +46,7 @@ import { getPrintedUiJsCode, makeTestProjectCodeWithSnippet, renderTestEditorWithCode, + renderTestEditorWithModel, } from '../../ui-jsx.test-utils' import { PaddingTearThreshold, SetPaddingStrategyName } from './set-padding-strategy' @@ -745,6 +753,61 @@ describe('Padding resize strategy', () => { }) }) }) + + describe('Tailwind', () => { + describe('tailwind', () => { + setFeatureForBrowserTestsUseInDescribeBlockOnly('Tailwind', true) + const Project = createModifiedProject({ + [StoryboardFilePath]: ` + import React from 'react' + import { Scene, Storyboard } from 'utopia-api' + export var storyboard = ( + + +
+
+
+
+ + + ) + + `, + [TailwindConfigPath]: ` + const TailwindConfig = { } + export default TailwindConfig + `, + 'app.css': ` + @tailwind base; + @tailwind components; + @tailwind utilities;`, + }) + + it('can set tailwind padding', async () => { + // TODO + const editor = await renderTestEditorWithModel(Project, 'await-first-dom-report') + // await selectComponentsForTest(editor, [EP.fromString('sb/scene/div')]) + // await doGapResize(editor, canvasPoint({ x: 10, y: 0 })) + const div = editor.renderedDOM.getByTestId('mydiv') + expect(div.className).toEqual('top-10 left-10 absolute flex flex-row gap-16') + }) + }) + }) }) async function testAdjustIndividualPaddingValue(edge: EdgePiece, precision: AdjustPrecision) { From c08119eb1c5c791f81e55019af53c2a9a6d3055e Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 17 Oct 2024 16:20:30 +0200 Subject: [PATCH 03/10] padding default values --- editor/src/components/editor/store/dispatch.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/editor/src/components/editor/store/dispatch.tsx b/editor/src/components/editor/store/dispatch.tsx index f014fcbd20d6..534cdb2f90b2 100644 --- a/editor/src/components/editor/store/dispatch.tsx +++ b/editor/src/components/editor/store/dispatch.tsx @@ -1126,6 +1126,11 @@ function addNormalizationDataFromActions( const PropertyDefaultValues: Record = { gap: '0px', + padding: '0px', + paddingTop: '0px', + paddingRight: '0px', + paddingBottom: '0px', + paddingLeft: '0px', } function patchRemovedProperties( From ed1e2823913e381eaa81257fe7d239bf4f2cf296 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 17 Oct 2024 16:22:39 +0200 Subject: [PATCH 04/10] update tailwind style plugin tests --- .../components/canvas/plugins/tailwind-style-plugin.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts index 7be679d98009..26ee456668fa 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.spec.ts @@ -98,6 +98,7 @@ describe('tailwind style plugin', () => { const normalizedEditor = TailwindPlugin(null).normalizeFromInlineStyle( editor.getEditorState().editor, [target], + [], ) const normalizedElement = getJSXElementFromProjectContents( @@ -130,6 +131,7 @@ describe('tailwind style plugin', () => { const normalizedEditor = TailwindPlugin(null).normalizeFromInlineStyle( editor.getEditorState().editor, [target], + [], ) const normalizedElement = getJSXElementFromProjectContents( @@ -161,6 +163,7 @@ describe('tailwind style plugin', () => { const normalizedEditor = TailwindPlugin(null).normalizeFromInlineStyle( editor.getEditorState().editor, [target], + [], ) const normalizedElement = getJSXElementFromProjectContents( From cb2a4c97d925436cc1be3855b838aa068724c62e Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Thu, 17 Oct 2024 17:19:35 +0200 Subject: [PATCH 05/10] wip --- .../components/editor/actions/action-utils.ts | 137 ++++++++++++++++-- 1 file changed, 124 insertions(+), 13 deletions(-) diff --git a/editor/src/components/editor/actions/action-utils.ts b/editor/src/components/editor/actions/action-utils.ts index 5f00da49b4c8..befcc8688cde 100644 --- a/editor/src/components/editor/actions/action-utils.ts +++ b/editor/src/components/editor/actions/action-utils.ts @@ -3,6 +3,8 @@ import { mapDropNulls, safeIndex } from '../../../core/shared/array-utils' import type { CanvasCommand } from '../../canvas/commands/commands' import type { EditorAction } from '../action-types' import { isFromVSCodeAction } from './actions-from-vscode' +import * as EP from '../../../core/shared/element-path' +import * as PP from '../../../core/shared/property-path' export function isTransientAction(action: EditorAction): boolean { switch (action.action) { @@ -402,25 +404,134 @@ export interface PropertiesWithElementPath { properties: PropertyPath[] } -export function getPropertiesToRemoveFromCommands( - commands: CanvasCommand[], -): PropertiesWithElementPath[] { - return mapDropNulls((command) => { +type PropertyUpdateDelta = + | { + type: 'delete' + prop: string + element: ElementPath + } + | { + type: 'set' + prop: string + value: string | number + element: ElementPath + } + +interface SimpleInlineStyle { + [elementPathString: string]: Record +} + +function ensureEntryExists( + style: Record, + elementPath: ElementPath, + defaultValue: T, +): void { + const elementPathString = EP.toString(elementPath) + if (!(elementPathString in style)) { + style[elementPathString] = defaultValue + } +} + +function applyDeltaToSimpleInlineStyle( + style: SimpleInlineStyle, + delta: PropertyUpdateDelta, +): SimpleInlineStyle { + ensureEntryExists(style, delta.element, {}) + switch (delta.type) { + case 'delete': + delete style[EP.toString(delta.element)][delta.prop] + break + case 'set': + style[EP.toString(delta.element)][delta.prop] = delta.value + break + } + return style +} + +function interpretPropertyUpdates(updates: PropertyUpdateDelta[]): SimpleInlineStyle { + return updates.reduce(applyDeltaToSimpleInlineStyle, {}) +} + +function makeDeleteDelta(element: ElementPath, prop: PropertyPath): PropertyUpdateDelta | null { + const [maybeStyle, maybeProp] = prop.propertyElements + if (maybeStyle !== 'style' || maybeProp == null || typeof maybeProp !== 'string') { + return null + } + return { + type: 'delete', + element: element, + prop: maybeProp, + } +} +function makeSetDelta( + element: ElementPath, + prop: PropertyPath, + value: string | number, +): PropertyUpdateDelta | null { + const [maybeStyle, maybeProp] = prop.propertyElements + if (maybeStyle !== 'style' || maybeProp == null || typeof maybeProp !== 'string') { + return null + } + return { + type: 'set', + element: element, + prop: maybeProp, + value: value, + } +} + +function propertyUpdateDeltaFromCanvasCommand(commands: CanvasCommand[]): PropertyUpdateDelta[] { + return commands.flatMap((command) => { switch (command.type) { case 'DELETE_PROPERTIES': - return { elementPath: command.element, properties: command.properties } + return command.properties.flatMap((p) => makeDeleteDelta(command.element, p) ?? []) + case 'SET_PROPERTY': + return makeSetDelta(command.element, command.property, command.value) ?? [] case 'UPDATE_BULK_PROPERTIES': - return { - elementPath: command.element, - properties: mapDropNulls( - (p) => (p.type === 'DELETE' ? p.path : null), - command.properties, + return [ + ...command.properties.flatMap((p) => + p.type === 'SET' ? makeDeleteDelta(command.element, p.path) ?? [] : [], + ), + ...command.properties.flatMap((p) => + p.type === 'SET' ? makeSetDelta(command.element, p.path, p.value) ?? [] : [], ), - } + ] default: - return null + return [] } - }, commands) + }) +} + +interface ElementPathAndProp { + elementPath: ElementPath + prop: string +} + +export function getPropertiesToRemoveFromCommands( + commands: CanvasCommand[], +): PropertiesWithElementPath[] { + const deltas = propertyUpdateDeltaFromCanvasCommand(commands) + const simpleStyle = interpretPropertyUpdates(deltas) + + const allRemoves: ElementPathAndProp[] = mapDropNulls( + (delta) => (delta.type === 'set' ? null : { elementPath: delta.element, prop: delta.prop }), + deltas, + ) + + const propsToRemove = allRemoves.filter( + (remove) => simpleStyle[EP.toString(remove.elementPath)][remove.prop] == null, // The prop hasn't been readded by a subsequent set + ) + + const result: Record = {} + for (const remove of propsToRemove) { + ensureEntryExists(result, remove.elementPath, []) + result[EP.toString(remove.elementPath)].push(remove.prop) + } + + return Object.entries(result).map(([elementPathString, props]) => ({ + elementPath: EP.fromString(elementPathString), + properties: props.map((p) => PP.create('style', p)), + })) } export function getPropertiesToRemoveFromActions( From 16660b5b74425c51ff96d1c3557d1c30713cd55a Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Fri, 18 Oct 2024 16:27:12 +0200 Subject: [PATCH 06/10] fix value serialization issue --- editor/src/components/canvas/plugins/tailwind-style-plugin.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts index 9ce39bdabc05..7d4391154a48 100644 --- a/editor/src/components/canvas/plugins/tailwind-style-plugin.ts +++ b/editor/src/components/canvas/plugins/tailwind-style-plugin.ts @@ -114,6 +114,8 @@ function getPropertyCleanupCommands(propertiesToRemove: PropertiesWithElementPat }, propertiesToRemove) } +const underscoresToSpaces = (s: string | undefined) => s?.replace(/[-_]/g, ' ') + export const TailwindPlugin = (config: Config | null): StylePlugin => ({ name: 'Tailwind', styleInfoFactory: @@ -136,7 +138,7 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({ cssParsers.flexDirection, ), padding: parseTailwindProperty( - mapping[TailwindPropertyMapping.padding], + underscoresToSpaces(mapping[TailwindPropertyMapping.padding]), cssParsers.padding, ), paddingTop: parseTailwindProperty( From d68fbc0122af6d9ed9678b0df9b3276a458dc096 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Fri, 18 Oct 2024 16:55:23 +0200 Subject: [PATCH 07/10] c'est horrible --- .../components/editor/actions/action-utils.ts | 111 +++++++++++++++++- .../editor/store/dispatch-strategies.tsx | 12 +- .../src/components/editor/store/dispatch.tsx | 41 ++----- 3 files changed, 126 insertions(+), 38 deletions(-) diff --git a/editor/src/components/editor/actions/action-utils.ts b/editor/src/components/editor/actions/action-utils.ts index befcc8688cde..c93696662db7 100644 --- a/editor/src/components/editor/actions/action-utils.ts +++ b/editor/src/components/editor/actions/action-utils.ts @@ -1,5 +1,5 @@ import type { ElementPath, PropertyPath } from 'utopia-shared/src/types' -import { mapDropNulls, safeIndex } from '../../../core/shared/array-utils' +import { mapDropNulls, safeIndex, uniq } from '../../../core/shared/array-utils' import type { CanvasCommand } from '../../canvas/commands/commands' import type { EditorAction } from '../action-types' import { isFromVSCodeAction } from './actions-from-vscode' @@ -507,10 +507,7 @@ interface ElementPathAndProp { prop: string } -export function getPropertiesToRemoveFromCommands( - commands: CanvasCommand[], -): PropertiesWithElementPath[] { - const deltas = propertyUpdateDeltaFromCanvasCommand(commands) +function getPropertiesToRemoveFromDeltas(deltas: PropertyUpdateDelta[]): Record { const simpleStyle = interpretPropertyUpdates(deltas) const allRemoves: ElementPathAndProp[] = mapDropNulls( @@ -527,8 +524,110 @@ export function getPropertiesToRemoveFromCommands( ensureEntryExists(result, remove.elementPath, []) result[EP.toString(remove.elementPath)].push(remove.prop) } + return result +} + +const PaddingLonghands = ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'] + +interface PropertyPatchValues { + gap?: '0px' + paddingTop?: '0px' + paddingRight?: '0px' + paddingBottom?: '0px' + paddingLeft?: '0px' +} + +function setPropertyValue( + propertyPatchValues: PropertyPatchValues, + prop: string, +): PropertyPatchValues { + switch (prop) { + case 'gap': + propertyPatchValues.gap = '0px' + break + case 'paddingTop': + propertyPatchValues.paddingTop = '0px' + break + case 'paddingRight': + propertyPatchValues.paddingRight = '0px' + break + case 'paddingBottom': + propertyPatchValues.paddingBottom = '0px' + break + case 'paddingLeft': + propertyPatchValues.paddingLeft = '0px' + break + default: + break + } + + return propertyPatchValues +} + +export interface PropertiesToPatchWithDefaults { + [elementPathString: string]: PropertyPatchValues +} + +function substituteShorthandProperties( + propsToSetByElement: Record, + elementPathString: string, + prop: string, +): string[] { + const paddingShorthandSet = propsToSetByElement[elementPathString]?.includes('padding') + + if (prop === 'padding') { + return PaddingLonghands.filter((p) => !propsToSetByElement[elementPathString]?.includes(p)) + } + + if (PaddingLonghands.includes(prop)) { + if (paddingShorthandSet) { + return [] + } + } + return [prop] +} + +export function getPropertiesToPatchFromCommands( + commands: CanvasCommand[], +): PropertiesToPatchWithDefaults { + const deltas = propertyUpdateDeltaFromCanvasCommand(commands) + const propsToRemove = getPropertiesToRemoveFromDeltas(deltas) + + const propsToSetByElement: Record = {} + for (const delta of deltas) { + if (delta.type === 'set') { + ensureEntryExists(propsToSetByElement, delta.element, []) + propsToSetByElement[EP.toString(delta.element)].push(delta.prop) + } + } + + const withPropsSubstituted = Object.entries(propsToRemove).map( + ([elementPathString, props]): [string, string[]] => [ + elementPathString, + uniq( + props.flatMap((prop) => + substituteShorthandProperties(propsToSetByElement, elementPathString, prop), + ), + ), + ], + ) + + const result: PropertiesToPatchWithDefaults = {} + for (const [elementPathString, props] of withPropsSubstituted) { + ensureEntryExists(result, EP.fromString(elementPathString), {}) + result[elementPathString] = props.reduce(setPropertyValue, {}) + } + + return result +} + +export function getPropertiesToRemoveFromCommands( + commands: CanvasCommand[], +): PropertiesWithElementPath[] { + const deltas = propertyUpdateDeltaFromCanvasCommand(commands) + const propsToRemove = getPropertiesToRemoveFromDeltas(deltas) - return Object.entries(result).map(([elementPathString, props]) => ({ + return Object.entries(propsToRemove).map(([elementPathString, props]) => ({ elementPath: EP.fromString(elementPathString), properties: props.map((p) => PP.create('style', p)), })) diff --git a/editor/src/components/editor/store/dispatch-strategies.tsx b/editor/src/components/editor/store/dispatch-strategies.tsx index 1f153bc0c37c..076d8866e617 100644 --- a/editor/src/components/editor/store/dispatch-strategies.tsx +++ b/editor/src/components/editor/store/dispatch-strategies.tsx @@ -30,8 +30,12 @@ import type { StartPostActionSession, } from '../action-types' import { SelectComponents } from '../action-types' -import type { PropertiesWithElementPath } from '../actions/action-utils' +import type { + PropertiesToPatchWithDefaults, + PropertiesWithElementPath, +} from '../actions/action-utils' import { + getPropertiesToPatchFromCommands, getPropertiesToRemoveFromCommands, isClearInteractionSession, isCreateOrUpdateInteractionSession, @@ -63,10 +67,10 @@ import type { ElementPath } from 'utopia-shared/src/types' export interface PropertiesToPatch { type: 'properties-to-patch' - propertiesToPatch: PropertiesWithElementPath[] + propertiesToPatch: PropertiesToPatchWithDefaults } -export const propertiesToPatch = (props: PropertiesWithElementPath[]): PropertiesToPatch => ({ +export const propertiesToPatch = (props: PropertiesToPatchWithDefaults): PropertiesToPatch => ({ type: 'properties-to-patch', propertiesToPatch: props, }) @@ -704,7 +708,7 @@ function handleUpdate( ), newStrategyState: newStrategyState, postProcessingData: propertiesToPatch( - getPropertiesToRemoveFromCommands(strategyResult.commands), + getPropertiesToPatchFromCommands(strategyResult.commands), ), } } else { diff --git a/editor/src/components/editor/store/dispatch.tsx b/editor/src/components/editor/store/dispatch.tsx index 534cdb2f90b2..6f76b498bfbd 100644 --- a/editor/src/components/editor/store/dispatch.tsx +++ b/editor/src/components/editor/store/dispatch.tsx @@ -7,7 +7,7 @@ import type { CanvasAction } from '../../canvas/canvas-types' import type { LocalNavigatorAction } from '../../navigator/actions' import type { EditorAction, EditorDispatch, UpdateMetadataInEditorState } from '../action-types' import { isLoggedIn } from '../action-types' -import type { PropertiesWithElementPath } from '../actions/action-utils' +import type { PropertiesToPatchWithDefaults } from '../actions/action-utils' import { isTransientAction, isUndoOrRedo, @@ -68,6 +68,7 @@ import { handleStrategies, normalizationData, updatePostActionState } from './di import type { MetaCanvasStrategy } from '../../canvas/canvas-strategies/canvas-strategies' import { RegisteredCanvasStrategies } from '../../canvas/canvas-strategies/canvas-strategies' import { arrayOfPathsEqual, removePathsWithDeadUIDs } from '../../../core/shared/element-path' +import * as EP from '../../../core/shared/element-path' import { notice } from '../../../components/common/notice' import { getUidMappings, getAllUniqueUidsFromMapping } from '../../../core/model/get-uid-mappings' import { updateSimpleLocks } from '../../../core/shared/element-locking' @@ -96,6 +97,7 @@ import { getActivePlugin } from '../../canvas/plugins/style-plugins' import { mapDropNulls } from '../../../core/shared/array-utils' import { propertyToSet, updateBulkProperties } from '../../canvas/commands/set-property-command' import { foldAndApplyCommandsSimple } from '../../canvas/commands/commands' +import * as PP from '../../../core/shared/property-path' type DispatchResultFields = { nothingChanged: boolean @@ -1124,40 +1126,23 @@ function addNormalizationDataFromActions( ) } -const PropertyDefaultValues: Record = { - gap: '0px', - padding: '0px', - paddingTop: '0px', - paddingRight: '0px', - paddingBottom: '0px', - paddingLeft: '0px', -} - function patchRemovedProperties( editor: EditorState, - propertiesToPatch: PropertiesWithElementPath[], + propertiesToPatch: PropertiesToPatchWithDefaults, ): EditorState { const propertiesToSetCommands = mapDropNulls( - ({ elementPath, properties }) => - getJSXElementFromProjectContents(elementPath, editor.projectContents) == null + ([elementPathString, properties]) => + getJSXElementFromProjectContents(EP.fromString(elementPathString), editor.projectContents) == + null ? null : updateBulkProperties( 'always', - elementPath, - mapDropNulls((property) => { - const [maybeStyle, maybeCSSProp] = property.propertyElements - if (maybeStyle !== 'style' || maybeCSSProp == null) { - return null - } - - const defaultValue = PropertyDefaultValues[maybeCSSProp] - if (defaultValue == null) { - return null - } - return propertyToSet(property, defaultValue) - }, properties), + EP.fromString(elementPathString), + Object.entries(properties).map(([property, value]) => { + return propertyToSet(PP.create('style', property), value) + }), ), - propertiesToPatch, + Object.entries(propertiesToPatch), ) return foldAndApplyCommandsSimple(editor, propertiesToSetCommands) } @@ -1167,7 +1152,7 @@ function runRemovedPropertyPatchingAndNormalization( postProcessingData: PostProcessingData, ): EditorState { if (postProcessingData.type === 'properties-to-patch') { - return postProcessingData.propertiesToPatch.length === 0 + return Object.keys(postProcessingData.propertiesToPatch).length === 0 ? editorState : patchRemovedProperties(editorState, postProcessingData.propertiesToPatch) } From 00743e2fceefa1ee2aa4125c23f82d3c8ffe1cfc Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 21 Oct 2024 10:38:24 +0200 Subject: [PATCH 08/10] tests --- .../set-padding-strategy.spec.browser2.tsx | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx index 114e330843d6..cb5264482d25 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx @@ -1,3 +1,4 @@ +import * as EP from '../../../../core/shared/element-path' import { assertNever } from '../../../../core/shared/utils' import { TailwindConfigPath } from '../../../../core/tailwind/tailwind-config' import { createModifiedProject } from '../../../../sample-projects/sample-project-utils.test-utils' @@ -5,6 +6,7 @@ import type { Modifiers } from '../../../../utils/modifiers' import { cmdModifier } from '../../../../utils/modifiers' import { expectSingleUndo2Saves, + selectComponentsForTest, setFeatureForBrowserTestsUseInDescribeBlockOnly, wait, } from '../../../../utils/utils.test-utils' @@ -755,9 +757,10 @@ describe('Padding resize strategy', () => { }) describe('Tailwind', () => { - describe('tailwind', () => { - setFeatureForBrowserTestsUseInDescribeBlockOnly('Tailwind', true) - const Project = createModifiedProject({ + setFeatureForBrowserTestsUseInDescribeBlockOnly('Tailwind', true) + + const TailwindProject = (classes: string) => + createModifiedProject({ [StoryboardFilePath]: ` import React from 'react' import { Scene, Storyboard } from 'utopia-api' @@ -776,9 +779,9 @@ describe('Padding resize strategy', () => { }} >
@@ -798,14 +801,40 @@ describe('Padding resize strategy', () => { @tailwind utilities;`, }) - it('can set tailwind padding', async () => { - // TODO - const editor = await renderTestEditorWithModel(Project, 'await-first-dom-report') - // await selectComponentsForTest(editor, [EP.fromString('sb/scene/div')]) - // await doGapResize(editor, canvasPoint({ x: 10, y: 0 })) - const div = editor.renderedDOM.getByTestId('mydiv') - expect(div.className).toEqual('top-10 left-10 absolute flex flex-row gap-16') - }) + it('can set tailwind padding', async () => { + const editor = await renderTestEditorWithModel( + TailwindProject('p-12'), + 'await-first-dom-report', + ) + await selectComponentsForTest(editor, [EP.fromString('sb/scene/mydiv')]) + await testPaddingResizeForEdge(editor, 50, 'top', 'precise') + await editor.getDispatchFollowUpActionsFinished() + const div = editor.renderedDOM.getByTestId('mydiv') + expect(div.className).toEqual('top-10 left-10 absolute flex flex-row p-[6rem_3rem_3rem_3rem]') + }) + + it('can remove tailwind padding', async () => { + const editor = await renderTestEditorWithModel( + TailwindProject('p-4'), + 'await-first-dom-report', + ) + await selectComponentsForTest(editor, [EP.fromString('sb/scene/mydiv')]) + await testPaddingResizeForEdge(editor, -150, 'top', 'precise') + await editor.getDispatchFollowUpActionsFinished() + const div = editor.renderedDOM.getByTestId('mydiv') + expect(div.className).toEqual('top-10 left-10 absolute flex flex-row pb-4 pl-4 pr-4') + }) + + it('can set tailwind padding longhand', async () => { + const editor = await renderTestEditorWithModel( + TailwindProject('pt-12'), + 'await-first-dom-report', + ) + await selectComponentsForTest(editor, [EP.fromString('sb/scene/mydiv')]) + await testPaddingResizeForEdge(editor, 50, 'top', 'precise') + await editor.getDispatchFollowUpActionsFinished() + const div = editor.renderedDOM.getByTestId('mydiv') + expect(div.className).toEqual('top-10 left-10 absolute flex flex-row pt-12') }) }) }) From c4933758b8b333861dbb08f9164d8996df842dd2 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 21 Oct 2024 11:02:13 +0200 Subject: [PATCH 09/10] fix typo in test --- .../strategies/set-padding-strategy.spec.browser2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx index cb5264482d25..d5d791ff98f6 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.spec.browser2.tsx @@ -834,7 +834,7 @@ describe('Padding resize strategy', () => { await testPaddingResizeForEdge(editor, 50, 'top', 'precise') await editor.getDispatchFollowUpActionsFinished() const div = editor.renderedDOM.getByTestId('mydiv') - expect(div.className).toEqual('top-10 left-10 absolute flex flex-row pt-12') + expect(div.className).toEqual('top-10 left-10 absolute flex flex-row pt-24') }) }) }) From 445fa14286c83f01b7793ca6f4506abd7dd81950 Mon Sep 17 00:00:00 2001 From: Berci Kormendy Date: Mon, 21 Oct 2024 17:03:11 +0200 Subject: [PATCH 10/10] wip - granular plugins --- .../canvas/plugins/inline-style-plugin.ts | 34 +++++- .../canvas/plugins/style-plugins.ts | 16 ++- .../src/components/editor/store/dispatch.tsx | 100 +----------------- 3 files changed, 41 insertions(+), 109 deletions(-) diff --git a/editor/src/components/canvas/plugins/inline-style-plugin.ts b/editor/src/components/canvas/plugins/inline-style-plugin.ts index 19106971606b..7640e5692444 100644 --- a/editor/src/components/canvas/plugins/inline-style-plugin.ts +++ b/editor/src/components/canvas/plugins/inline-style-plugin.ts @@ -1,12 +1,21 @@ import { getLayoutProperty } from '../../../core/layout/getLayoutProperty' import type { StyleLayoutProp } from '../../../core/layout/layout-helpers-new' import { MetadataUtils } from '../../../core/model/element-metadata-utils' +import { mapDropNulls } from '../../../core/shared/array-utils' import { defaultEither, isLeft, mapEither, right } from '../../../core/shared/either' import type { JSXElement } from '../../../core/shared/element-template' -import { isJSXElement } from '../../../core/shared/element-template' +import { + emptyComments, + isJSXElement, + jsExpressionValue, +} from '../../../core/shared/element-template' +import * as PP from '../../../core/shared/property-path' import { styleStringInArray } from '../../../utils/common-constants' +import type { EditorState } from '../../editor/store/editor-state' import type { ParsedCSSProperties } from '../../inspector/common/css-utils' import { withPropertyTag, type WithPropertyTag } from '../canvas-types' +import { applyValuesAtPath } from '../commands/adjust-number-command' +import { deleteValuesAtPath } from '../commands/delete-properties-command' import type { StylePlugin } from './style-plugins' function getPropertyFromInstance

( @@ -47,5 +56,26 @@ export const InlineStylePlugin: StylePlugin = { paddingRight, } }, - normalizeFromInlineStyle: (editor) => editor, + updateStyles: (editorState, elementPath, updates) => { + const propsToDelete = mapDropNulls( + (update) => (update.type === 'delete' ? PP.create('style', update.property) : null), + updates, + ) + + const propsToSet = mapDropNulls( + (update) => + update.type === 'set' + ? { + path: PP.create('style', update.property), + value: jsExpressionValue(update.value, emptyComments), + } + : null, + updates, + ) + + return [ + (e: EditorState) => deleteValuesAtPath(e, elementPath, propsToDelete), + (e: EditorState) => applyValuesAtPath(e, elementPath, propsToSet), + ].reduce((state, fn) => fn(state).editorStateWithChanges, editorState) + }, } diff --git a/editor/src/components/canvas/plugins/style-plugins.ts b/editor/src/components/canvas/plugins/style-plugins.ts index b4740be5ee80..474b5f56cd72 100644 --- a/editor/src/components/canvas/plugins/style-plugins.ts +++ b/editor/src/components/canvas/plugins/style-plugins.ts @@ -7,24 +7,22 @@ import { getTailwindConfigCached, isTailwindEnabled, } from '../../../core/tailwind/tailwind-compilation' -import type { PropertiesWithElementPath } from '../../editor/actions/action-utils' import { isFeatureEnabled } from '../../../utils/feature-switches' +export type StyleUpdate = + | { type: 'set'; property: string; value: string } + | { type: 'delete'; property: string } + export interface StylePlugin { name: string styleInfoFactory: StyleInfoFactory - normalizeFromInlineStyle: ( + updateStyles: ( editorState: EditorState, - elementsToNormalize: ElementPath[], - propertiesToRemove: PropertiesWithElementPath[], + elementPath: ElementPath, + updates: StyleUpdate[], ) => EditorState } -export const Plugins = { - InlineStyle: InlineStylePlugin, - Tailwind: TailwindPlugin, -} as const - export function getActivePlugin(editorState: EditorState): StylePlugin { if (isFeatureEnabled('Tailwind') && isTailwindEnabled()) { return TailwindPlugin(getTailwindConfigCached(editorState)) diff --git a/editor/src/components/editor/store/dispatch.tsx b/editor/src/components/editor/store/dispatch.tsx index 6f76b498bfbd..3af1cb5de1ff 100644 --- a/editor/src/components/editor/store/dispatch.tsx +++ b/editor/src/components/editor/store/dispatch.tsx @@ -1018,42 +1018,9 @@ function editorDispatchInner( storedState.patchedDerived, ) - const elementsToNormalizeFromActions = getElementsToNormalizeFromActions(dispatchedActions) - const propertiesToRemoveFromActions = getPropertiesToRemoveFromActions(dispatchedActions) - - const elementsToNormalize = [ - ...elementsToNormalizeFromActions, - ...(postProcessingDataFromStrategies?.type === 'normalization-data' - ? postProcessingDataFromStrategies.elementsToNormalize - : []), - ] - - const propertiesToRemove = [ - ...propertiesToRemoveFromActions, - ...(postProcessingDataFromStrategies?.type === 'normalization-data' - ? postProcessingDataFromStrategies.propertiesToRemove - : []), - ] - - const unpatchedEditor = runRemovedPropertyPatchingAndNormalization( - unpatchedEditorStateFromStrategies, - normalizationData(elementsToNormalize, propertiesToRemove), - ) - - const patchedEditor = - postProcessingDataFromStrategies === null - ? patchedEditorStateFromStrategies - : runRemovedPropertyPatchingAndNormalization( - patchedEditorStateFromStrategies, - addNormalizationDataFromActions( - postProcessingDataFromStrategies, - normalizationData(elementsToNormalizeFromActions, propertiesToRemoveFromActions), - ), - ) - return { - unpatchedEditor: unpatchedEditor, - patchedEditor: patchedEditor, + unpatchedEditor: unpatchedEditorStateFromStrategies, + patchedEditor: patchedEditorStateFromStrategies, unpatchedDerived: frozenDerivedState, patchedDerived: patchedDerivedState, strategyState: newStrategyState, @@ -1111,66 +1078,3 @@ function resetUnpatchedEditorTransientFields(editor: EditorState): EditorState { }, } } - -function addNormalizationDataFromActions( - data: PostProcessingData, - fromActions: NormalizationData, -): PostProcessingData { - if (data.type === 'properties-to-patch') { - return data - } - - return normalizationData( - [...data.elementsToNormalize, ...fromActions.elementsToNormalize], - [...data.propertiesToRemove, ...fromActions.propertiesToRemove], - ) -} - -function patchRemovedProperties( - editor: EditorState, - propertiesToPatch: PropertiesToPatchWithDefaults, -): EditorState { - const propertiesToSetCommands = mapDropNulls( - ([elementPathString, properties]) => - getJSXElementFromProjectContents(EP.fromString(elementPathString), editor.projectContents) == - null - ? null - : updateBulkProperties( - 'always', - EP.fromString(elementPathString), - Object.entries(properties).map(([property, value]) => { - return propertyToSet(PP.create('style', property), value) - }), - ), - Object.entries(propertiesToPatch), - ) - return foldAndApplyCommandsSimple(editor, propertiesToSetCommands) -} - -function runRemovedPropertyPatchingAndNormalization( - editorState: EditorState, - postProcessingData: PostProcessingData, -): EditorState { - if (postProcessingData.type === 'properties-to-patch') { - return Object.keys(postProcessingData.propertiesToPatch).length === 0 - ? editorState - : patchRemovedProperties(editorState, postProcessingData.propertiesToPatch) - } - - if (postProcessingData.type === 'normalization-data') { - if ( - postProcessingData.elementsToNormalize.length === 0 && - postProcessingData.propertiesToRemove.length === 0 - ) { - return editorState - } - - return getActivePlugin(editorState).normalizeFromInlineStyle( - editorState, - postProcessingData.elementsToNormalize, - postProcessingData.propertiesToRemove, - ) - } - - assertNever(postProcessingData) -}