Skip to content

Commit

Permalink
feat(editor): update correct breakpoint value
Browse files Browse the repository at this point in the history
  • Loading branch information
liady committed Dec 13, 2024
1 parent c0c4752 commit e01fe30
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { Config } from 'tailwindcss/types/config'
import { type StyleMediaSizeModifier, type StyleModifier } from '../../canvas-types'
import { isStyleInfoKey, type StyleMediaSizeModifier, type StyleModifier } from '../../canvas-types'
import type { ScreenSize } from '../../responsive-types'
import { extractScreenSizeFromCss } from '../../responsive-utils'
import { mapDropNulls } from '../../../../core/shared/array-utils'
import { getTailwindClassMapping, TailwindPropertyMapping } from '../tailwind-style-plugin'
import { parseTailwindPropertyFactory } from '../tailwind-style-plugin'
import type { StylePluginContext } from '../style-plugins'

export const TAILWIND_DEFAULT_SCREENS = {
sm: '640px',
Expand Down Expand Up @@ -73,3 +76,38 @@ export function getModifiers(
})
.filter((m): m is StyleMediaSizeModifier => m != null)
}

export function getPropertiesToAppliedModifiersMap(
currentClassNameAttribute: string,
propertyNames: string[],
config: Config | null,
context: StylePluginContext,
): Record<string, StyleModifier[]> {
const parseTailwindProperty = parseTailwindPropertyFactory(config, context)
const classMapping = getTailwindClassMapping(currentClassNameAttribute.split(' '), config)
return propertyNames.reduce((acc, propertyName) => {
if (!isStyleInfoKey(propertyName)) {
return acc
}
const parsedProperty = parseTailwindProperty(
classMapping[TailwindPropertyMapping[propertyName]],
propertyName,
)
if (parsedProperty?.type == 'property' && parsedProperty.currentVariant.modifiers != null) {
return {
...acc,
[propertyName]: parsedProperty.currentVariant.modifiers,
}
} else {
return acc
}
}, {} as Record<string, StyleModifier[]>)
}

export function getTailwindVariantFromAppliedModifier(
appliedModifier: StyleMediaSizeModifier | null,
): string | null {
return appliedModifier?.modifierOrigin?.type === 'tailwind'
? appliedModifier.modifierOrigin.variant
: null
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import type { ElementPath } from 'utopia-shared/src/types'
import { emptyComments } from 'utopia-shared/src/types'
import { mapDropNulls } from '../../../../core/shared/array-utils'
import { jsExpressionValue } from '../../../../core/shared/element-template'
import type { PropertiesToUpdate } from '../../../../core/tailwind/tailwind-class-list-utils'
import type {
PropertiesToRemove,
PropertiesToUpdate,
} from '../../../../core/tailwind/tailwind-class-list-utils'
import {
getParsedClassList,
removeClasses,
Expand All @@ -17,6 +20,8 @@ import type { EditorStateWithPatch } from '../../commands/utils/property-utils'
import { applyValuesAtPath } from '../../commands/utils/property-utils'
import * as PP from '../../../../core/shared/property-path'
import type { Config } from 'tailwindcss/types/config'
import type { StylePluginContext } from '../style-plugins'
import { getPropertiesToAppliedModifiersMap } from './tailwind-responsive-utils'

export type ClassListUpdate =
| { type: 'add'; property: string; value: string }
Expand All @@ -37,27 +42,54 @@ export const runUpdateClassList = (
element: ElementPath,
classNameUpdates: ClassListUpdate[],
config: Config | null,
context: StylePluginContext,
): EditorStateWithPatch => {
const currentClassNameAttribute =
getClassNameAttribute(getElementFromProjectContents(element, editorState.projectContents))
?.value ?? ''

// this will map every property to the modifiers that are currently affecting it
const propertyToAppliedModifiersMap = getPropertiesToAppliedModifiersMap(
currentClassNameAttribute,
classNameUpdates.map((update) => update.property),
config,
context,
)

const parsedClassList = getParsedClassList(currentClassNameAttribute, config)

const propertiesToRemove = mapDropNulls(
(update) => (update.type !== 'remove' ? null : update.property),
classNameUpdates,
const propertiesToRemove: PropertiesToRemove = classNameUpdates.reduce(
(acc: PropertiesToRemove, val) =>
val.type === 'remove'
? {
...acc,
[val.property]: {
modifiers: propertyToAppliedModifiersMap[val.property] ?? [],
},
}
: acc,
{},
)

const propertiesToUpdate: PropertiesToUpdate = classNameUpdates.reduce(
(acc: { [property: string]: string }, val) =>
val.type === 'remove' ? acc : { ...acc, [val.property]: val.value },
(acc: PropertiesToUpdate, val) =>
val.type === 'remove'
? acc
: {
...acc,
[val.property]: {
newValue: val.value,
modifiers: propertyToAppliedModifiersMap[val.property] ?? [],
},
},
{},
)

const updatedClassList = [
removeClasses(propertiesToRemove),
updateExistingClasses(propertiesToUpdate),
// currently we're not adding new breakpoint styles (but only editing current ones),
// so we don't need to pass the propertyToAppliedModifiersMap here
addNewClasses(propertiesToUpdate),
].reduce((classList, fn) => fn(classList), parsedClassList)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ export const TailwindPlugin = (config: Config | null): StylePlugin => ({
elementPath,
[...propsToDelete, ...propsToSet],
config,
{ sceneSize: getContainingSceneSize(elementPath, editorState.jsxMetadata) },
)
},
})
Expand Down
24 changes: 16 additions & 8 deletions editor/src/core/tailwind/tailwind-class-list-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ describe('tailwind class list utils', () => {
describe('removing classes', () => {
it('can remove property', () => {
const classList = getParsedClassList('p-4 m-2 text-white w-4 flex flex-row', null)
const updatedClassList = removeClasses(['padding', 'textColor'])(classList)
const updatedClassList = removeClasses({
padding: { modifiers: [] },
textColor: { modifiers: [] },
})(classList)
expect(getClassListFromParsedClassList(updatedClassList, null)).toMatchInlineSnapshot(
`"m-2 w-4 flex flex-row"`,
)
Expand All @@ -151,7 +154,10 @@ describe('tailwind class list utils', () => {
'p-4 m-2 text-white hover:text-red-100 w-4 flex flex-row',
null,
)
const updatedClassList = removeClasses(['padding', 'textColor'])(classList)
const updatedClassList = removeClasses({
padding: { modifiers: [] },
textColor: { modifiers: [] },
})(classList)
expect(getClassListFromParsedClassList(updatedClassList, null)).toMatchInlineSnapshot(
`"m-2 hover:text-red-100 w-4 flex flex-row"`,
)
Expand All @@ -162,16 +168,18 @@ describe('tailwind class list utils', () => {
it('can update class in class list', () => {
const classList = getParsedClassList('p-4 m-2 text-white w-4 flex flex-row', null)
const updatedClassList = updateExistingClasses({
flexDirection: 'column',
width: '23px',
flexDirection: { newValue: 'column', modifiers: [] },
width: { newValue: '23px', modifiers: [] },
})(classList)
expect(getClassListFromParsedClassList(updatedClassList, null)).toMatchInlineSnapshot(
`"p-4 m-2 text-white w-[23px] flex flex-col"`,
)
})
it('does not remove property with selector', () => {
const classList = getParsedClassList('p-4 hover:p-6 m-2 text-white w-4 flex flex-row', null)
const updatedClassList = updateExistingClasses({ padding: '8rem' })(classList)
const updatedClassList = updateExistingClasses({
padding: { newValue: '8rem', modifiers: [] },
})(classList)
expect(getClassListFromParsedClassList(updatedClassList, null)).toMatchInlineSnapshot(
`"p-32 hover:p-6 m-2 text-white w-4 flex flex-row"`,
)
Expand All @@ -182,9 +190,9 @@ describe('tailwind class list utils', () => {
it('can add new class to class list', () => {
const classList = getParsedClassList('p-4 m-2 text-white w-4 flex flex-row', null)
const updatedClassList = addNewClasses({
backgroundColor: 'white',
justifyContent: 'space-between',
positionLeft: '-20px',
backgroundColor: { newValue: 'white', modifiers: [] },
justifyContent: { newValue: 'space-between', modifiers: [] },
positionLeft: { newValue: '-20px', modifiers: [] },
})(classList)
expect(getClassListFromParsedClassList(updatedClassList, null)).toMatchInlineSnapshot(
`"p-4 m-2 text-white w-4 flex flex-row bg-white justify-between -left-[20px]"`,
Expand Down
84 changes: 70 additions & 14 deletions editor/src/core/tailwind/tailwind-class-list-utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as TailwindClassParser from '@xengine/tailwindcss-class-parser'
import type { Config } from 'tailwindcss/types/config'
import { mapDropNulls } from '../shared/array-utils'
import type { StyleMediaSizeModifier, StyleModifier } from '../../components/canvas/canvas-types'

export type ParsedTailwindClass = {
property: string
value: string
variants: unknown[]
variants: { type: string; value: string }[]
negative: boolean
} & Record<string, unknown>

Expand Down Expand Up @@ -49,7 +50,13 @@ export type ClassListTransform = (
) => TailwindClassParserResult[]

export interface PropertiesToUpdate {
[property: string]: string
[property: string]: { newValue: string; modifiers: StyleModifier[] }
}

export interface PropertiesToRemove {
[property: string]: {
modifiers: StyleModifier[]
}
}

export const addNewClasses =
Expand All @@ -60,12 +67,12 @@ export const addNewClasses =
)

const newClasses: TailwindClassParserResult[] = mapDropNulls(
([prop, value]) =>
([prop, update]) =>
existingProperties.has(prop)
? null
: {
type: 'parsed',
ast: { property: prop, value: value, variants: [], negative: false },
ast: { property: prop, value: update.newValue, variants: [], negative: false },
},
Object.entries(propertiesToAdd),
)
Expand All @@ -78,30 +85,79 @@ export const updateExistingClasses =
(propertiesToUpdate: PropertiesToUpdate): ClassListTransform =>
(parsedClassList: TailwindClassParserResult[]) => {
const classListWithUpdatedClasses: TailwindClassParserResult[] = parsedClassList.map((cls) => {
if (cls.type !== 'parsed' || cls.ast.variants.length > 0) {
return cls
}
const updatedProperty = propertiesToUpdate[cls.ast.property]
if (updatedProperty == null) {
if (!shouldUpdateClass(cls, propertiesToUpdate)) {
return cls
}
const propertyToUpdate = propertiesToUpdate[cls.ast.property]
return {
type: 'parsed',
ast: { property: cls.ast.property, value: updatedProperty, variants: [], negative: false },
ast: {
property: cls.ast.property,
value: propertyToUpdate.newValue,
variants: cls.ast.variants,
negative: false,
},
}
})
return classListWithUpdatedClasses
}

export const removeClasses =
(propertiesToRemove: string[]): ClassListTransform =>
(propertiesToRemove: PropertiesToRemove): ClassListTransform =>
(parsedClassList: TailwindClassParserResult[]) => {
const propertiesToRemoveSet = new Set(propertiesToRemove)
const classListWithRemovedClasses = parsedClassList.filter((cls) => {
if (cls.type !== 'parsed' || cls.ast.variants.length > 0) {
if (!shouldUpdateClass(cls, propertiesToRemove)) {
return cls
}
return !propertiesToRemoveSet.has(cls.ast.property)
return propertiesToRemove[cls.ast.property] != null
})
return classListWithRemovedClasses
}

function getTailwindSizeVariant(modifiers: StyleModifier[]): string | null {
const mediaModifier = modifiers.find((m): m is StyleMediaSizeModifier => m.type === 'media-size')
if (mediaModifier == null) {
return null
}
if (mediaModifier.modifierOrigin?.type !== 'tailwind') {
return null
}
return mediaModifier.modifierOrigin.variant
}

function shouldUpdateClass(
cls: TailwindClassParserResult,
propertiesToUpdate: PropertiesToUpdate | PropertiesToRemove,
): cls is TailwindClassParserResult & { type: 'parsed' } {
if (cls.type !== 'parsed') {
return false
}
const propertyToUpdate = propertiesToUpdate[cls.ast.property]
if (propertyToUpdate == null) {
// this property is not in the list
return false
}
const sizeVariantToUpdate = getTailwindSizeVariant(propertyToUpdate.modifiers)
if (
sizeVariantToUpdate == null &&
cls.ast.variants.filter((v) => v.type === 'media').length > 0
) {
// we need to update the default property value but this class has size variants
return false
}
if (
sizeVariantToUpdate != null &&
!variantsHasMediaSizeVariant(cls.ast.variants, sizeVariantToUpdate)
) {
// we need to update a specific size variant but this class doesn't have it
return false
}
return true
}

function variantsHasMediaSizeVariant(
variants: { type: string; value: string }[],
sizeVariant: string,
): boolean {
return variants.some((v) => v.type === 'media' && v.value === sizeVariant)
}

0 comments on commit e01fe30

Please sign in to comment.