From 58e46071f1934c02f2a19fd9fb7a696838acc787 Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Tue, 6 Aug 2024 08:15:28 +0300 Subject: [PATCH 01/12] feat(devtools): init layout editor --- .../compiler/devtools/client/ui/Devtools.vue | 11 ++- .../client/ui/components/AppToolbar.vue | 39 ++++++++++ .../devtools/client/ui/components/AppTree.vue | 2 +- .../client/ui/components/base/Toolbar.vue | 64 ++++++++++++++++ .../ui/components/toolbar/ComponentSelect.vue | 65 ++++++++++++++++ .../toolbar/ComponentSelectItem.vue | 18 +++++ .../ui/components/toolbar/FlexEditor.vue | 76 +++++++++++++++++++ .../ui/components/toolbar/LayoutEditor.vue | 62 +++++++++++++++ .../components/toolbar/components/Button.vue | 17 +++++ .../toolbar/components/ButtonGroup.vue | 21 +++++ .../toolbar/components/ButtonToggle.vue | 25 ++++++ .../toolbar/components/DateInput.vue | 13 ++++ .../components/toolbar/components/Input.vue | 14 ++++ .../toolbar/components/TimeInput.vue | 13 ++++ .../toolbar/components/_Template.vue | 14 ++++ .../client/ui/components/toolbar/types.ts | 12 +++ .../ui/composables/useAppTree/useAppTree.ts | 5 +- .../useAppTree/useSelectedAppTreeItem.ts | 2 +- .../composables/useComponent/useComponent.ts | 24 ++++++ .../useComponent/useComponentCode.ts | 43 ++++++++++- packages/compiler/devtools/client/ui/index.ts | 7 +- 21 files changed, 536 insertions(+), 11 deletions(-) create mode 100644 packages/compiler/devtools/client/ui/components/AppToolbar.vue create mode 100644 packages/compiler/devtools/client/ui/components/base/Toolbar.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/ComponentSelectItem.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/FlexEditor.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/LayoutEditor.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/components/Button.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/components/ButtonGroup.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/components/ButtonToggle.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/components/DateInput.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/components/Input.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/components/TimeInput.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/components/_Template.vue create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/types.ts diff --git a/packages/compiler/devtools/client/ui/Devtools.vue b/packages/compiler/devtools/client/ui/Devtools.vue index 6590647e5a..57411e7219 100644 --- a/packages/compiler/devtools/client/ui/Devtools.vue +++ b/packages/compiler/devtools/client/ui/Devtools.vue @@ -8,9 +8,14 @@ @mousedown="onMouseDown" @mouseup="onMouseUp" /> - - + + + +
+ +
+
@@ -32,10 +37,12 @@ + + + + diff --git a/packages/compiler/devtools/client/ui/components/AppTree.vue b/packages/compiler/devtools/client/ui/components/AppTree.vue index 05b5c7b0da..ccd17b4573 100644 --- a/packages/compiler/devtools/client/ui/components/AppTree.vue +++ b/packages/compiler/devtools/client/ui/components/AppTree.vue @@ -4,7 +4,7 @@ import AppTreeItemComponent from './AppTreeItem.vue' import { ref, computed } from 'vue' - const appTree = useAppTree() + const { appTree } = useAppTree() const filter = ref('') diff --git a/packages/compiler/devtools/client/ui/components/base/Toolbar.vue b/packages/compiler/devtools/client/ui/components/base/Toolbar.vue new file mode 100644 index 0000000000..5f170d996a --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/base/Toolbar.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue new file mode 100644 index 0000000000..7cf05591e6 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelectItem.vue b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelectItem.vue new file mode 100644 index 0000000000..5268bd8fd8 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelectItem.vue @@ -0,0 +1,18 @@ + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/FlexEditor.vue b/packages/compiler/devtools/client/ui/components/toolbar/FlexEditor.vue new file mode 100644 index 0000000000..41013b81fe --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/FlexEditor.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/LayoutEditor.vue b/packages/compiler/devtools/client/ui/components/toolbar/LayoutEditor.vue new file mode 100644 index 0000000000..6f452b55ec --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/LayoutEditor.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/Button.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/Button.vue new file mode 100644 index 0000000000..61644690f0 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/components/Button.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonGroup.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonGroup.vue new file mode 100644 index 0000000000..a5f008bf88 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonGroup.vue @@ -0,0 +1,21 @@ + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonToggle.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonToggle.vue new file mode 100644 index 0000000000..d5d1fbde9e --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/components/ButtonToggle.vue @@ -0,0 +1,25 @@ + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/DateInput.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/DateInput.vue new file mode 100644 index 0000000000..86eb14ce0a --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/components/DateInput.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/Input.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/Input.vue new file mode 100644 index 0000000000..5a3d5c8e8d --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/components/Input.vue @@ -0,0 +1,14 @@ + + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/TimeInput.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/TimeInput.vue new file mode 100644 index 0000000000..a0d410eaf6 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/components/TimeInput.vue @@ -0,0 +1,13 @@ + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/components/_Template.vue b/packages/compiler/devtools/client/ui/components/toolbar/components/_Template.vue new file mode 100644 index 0000000000..6868687a43 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/components/_Template.vue @@ -0,0 +1,14 @@ + + + + diff --git a/packages/compiler/devtools/client/ui/components/toolbar/types.ts b/packages/compiler/devtools/client/ui/components/toolbar/types.ts new file mode 100644 index 0000000000..9308de8ca3 --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/toolbar/types.ts @@ -0,0 +1,12 @@ +export type HumanState = { + direction: 'horizontal' | 'vertical', + horizontal: 'left' | 'center' | 'right', + vertical: 'top' | 'center' | 'bottom', +} + +export type FlexState = { + display: 'flex', + 'flex-direction': 'row' | 'column', + 'justify-content': 'flex-start' | 'center' | 'flex-end', + 'align-items': 'flex-start' | 'center' | 'flex-end', +} diff --git a/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts b/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts index 2e3121963d..aa9c851f43 100644 --- a/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts +++ b/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts @@ -218,5 +218,8 @@ export const useAppTree = () => { } }) - return appTree + return { + appTree, + refresh + } } diff --git a/packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts b/packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts index 9ec5a0becd..bd482f0c6c 100644 --- a/packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts +++ b/packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts @@ -3,7 +3,7 @@ import { AppTreeItemComponent, AppTreeItem, useAppTree } from './useAppTree' const selectedAppTreeItem = ref(null) as Ref -const tree = useAppTree() +const { appTree: tree } = useAppTree() const walk = (tree: AppTreeItem[], search: string | HTMLElement): AppTreeItem | null => { for (const item of tree) { diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts index d2e27ce6db..82abbb4922 100644 --- a/packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts +++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts @@ -130,6 +130,28 @@ export const useComponent = defineGlobal(() => { }) ?? [] }) + const style = computed({ + get() { + if (!code.attributes.value?.style) { return {} } + + return code.attributes.value.style.split(';').reduce((acc, style) => { + const [key, value] = style.split(':') + if (key && value) { + acc[key.trim()] = value.trim() + } + return acc + }, {} as Record) + }, + + set(newStyle: Record) { + const style = Object.entries(newStyle).map(([key, value]) => `${key}: ${value}`).join('; ') + code.updateAttribute('style', style) + if (source.value) { + saveSource(source.value) + } + } + }) + return { isParsed, name, @@ -141,8 +163,10 @@ export const useComponent = defineGlobal(() => { refreshSource, openInVSCode, updateAttribute: code.updateAttribute, + appendChild: code.appendChild, props, slots, selectAppTreeItem, + style, } }) diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts index 7207fc0b54..ae79bc393c 100644 --- a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts +++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts @@ -1,5 +1,5 @@ import { type Ref, computed, type VNode } from 'vue'; -import { HTMLRootNode, parseSource } from '../../../parser/parseSource'; +import { HTMLContentNode, HTMLRootNode, parseSource } from '../../../parser/parseSource'; import { useComponentMeta } from './useComponentMeta' import { printSource } from '../../../parser/printSource'; @@ -162,8 +162,6 @@ export const useComponentCode = (source: Ref, vNode: Ref, vNode: Ref { + if (!ast.value) { + throw new Error('Unable to append child: no AST available') + } + + if (ast.value.children.length !== 1) { + throw new Error('Unable to append child: multi-node') + } + + const element = ast.value.children[0] + + const newRoot: HTMLRootNode = { + type: 'root', + children: [] + } + + if (element.type === 'content') { + throw new Error('Unable to append child: content node can not have children') + } + + const newChildren = [ + ...element.children, + { + type: 'content', + text: code, + parent: element, + } satisfies HTMLContentNode + ] + + newRoot.children.push({ + ...element, + children: newChildren, + parent: newRoot, + }) + + source.value = printSource(newRoot) + } + return { meta, ast, @@ -228,5 +264,6 @@ export const useComponentCode = (source: Ref, vNode: Ref ({ document.body.appendChild(appRoot) createApp(App) - .use(createVuesticEssential({ + .use(createVuestic({ config: { colors: { variables: { outlinePrimary: '#00b4d8', outlineSecondary: '#90e0ef', outlinePrimaryBackground: '#00b4d811', - outlineSecondaryBackground: '#90e0ef01' + outlineSecondaryBackground: '#90e0ef01', + backgroundToolbar: '#252422', } }, } From d43a8dec5a26b60c069e27cbd9c3230fc0375110 Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Tue, 6 Aug 2024 17:41:40 +0300 Subject: [PATCH 02/12] raw --- .../devtools/client/ui/components/base/CodeView.vue | 1 + packages/compiler/playground/src/pages/TestPage.vue | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/compiler/devtools/client/ui/components/base/CodeView.vue b/packages/compiler/devtools/client/ui/components/base/CodeView.vue index db0c7638eb..12b2076a10 100644 --- a/packages/compiler/devtools/client/ui/components/base/CodeView.vue +++ b/packages/compiler/devtools/client/ui/components/base/CodeView.vue @@ -94,6 +94,7 @@ max-width: 600px; min-width: 300px; --padding: 16px; + overflow: auto; &__view { height: 100%; diff --git a/packages/compiler/playground/src/pages/TestPage.vue b/packages/compiler/playground/src/pages/TestPage.vue index 83c3fb0b64..56dac9b387 100644 --- a/packages/compiler/playground/src/pages/TestPage.vue +++ b/packages/compiler/playground/src/pages/TestPage.vue @@ -16,7 +16,7 @@ const onLoginClick = () => {

Login

-
+
{ label="Password" type="password" /> + + Button + +
- + Test Test 2 + + Button +
Date: Thu, 8 Aug 2024 04:24:53 +0300 Subject: [PATCH 03/12] fix: update component path when code updated --- packages/compiler/devtools/shared/slug.ts | 6 ------ packages/compiler/playground/src/pages/TestPage.vue | 4 ---- 2 files changed, 10 deletions(-) diff --git a/packages/compiler/devtools/shared/slug.ts b/packages/compiler/devtools/shared/slug.ts index 0f0cf323fc..2468890333 100644 --- a/packages/compiler/devtools/shared/slug.ts +++ b/packages/compiler/devtools/shared/slug.ts @@ -5,12 +5,6 @@ type MinifiedPath = `${typeof PREFIX}:${string}` | string const knownPaths = new Map() export const minifyPath = (path: Path) => { - for (const [p, minifiedPath] of knownPaths.entries()) { - if (p === path) { - return minifiedPath - } - } - const minified = `${PREFIX}-${knownPaths.size}` as const knownPaths.set(minified, path) diff --git a/packages/compiler/playground/src/pages/TestPage.vue b/packages/compiler/playground/src/pages/TestPage.vue index 56dac9b387..aa2decbf90 100644 --- a/packages/compiler/playground/src/pages/TestPage.vue +++ b/packages/compiler/playground/src/pages/TestPage.vue @@ -26,10 +26,6 @@ const onLoginClick = () => { label="Password" type="password" /> - - Button - -
From 4a77ee4e67ed662dcb645a28a122f97b258c4459 Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Thu, 8 Aug 2024 05:39:04 +0300 Subject: [PATCH 04/12] feat: add delete node toolbar button --- .../client/ui/components/AppToolbar.vue | 9 +++++++ .../ui/components/toolbar/ComponentSelect.vue | 8 +++++- .../toolbar/components/BasicDiv.vue | 27 +++++++++++++++++++ .../ui/composables/useAppTree/useAppTree.ts | 5 ++++ .../client/ui/composables/useComponent/api.ts | 8 +++++- .../composables/useComponent/useComponent.ts | 8 +++++- .../useComponent/useComponentSource.ts | 9 ++++++- packages/compiler/devtools/plugin/compiler.ts | 1 + packages/compiler/devtools/server/file.ts | 19 +++++++++++++ .../devtools/server/server-middleware.ts | 9 +++++-- 10 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/devtools/client/ui/components/toolbar/components/BasicDiv.vue diff --git a/packages/compiler/devtools/client/ui/components/AppToolbar.vue b/packages/compiler/devtools/client/ui/components/AppToolbar.vue index 5b08881acb..67a28746c2 100644 --- a/packages/compiler/devtools/client/ui/components/AppToolbar.vue +++ b/packages/compiler/devtools/client/ui/components/AppToolbar.vue @@ -2,6 +2,13 @@ import { VaCard, VaButton, VaDropdown, VaDropdownContent } from 'vuestic-ui'; import LayoutEditor from './toolbar/LayoutEditor.vue'; import ComponentSelect from './toolbar/ComponentSelect.vue'; + import { useComponent } from '../composables/useComponent'; + + const { deleteComponent } = useComponent() + + const removeComponent = async () => { + await deleteComponent() + } diff --git a/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue index 7cf05591e6..8d7fd5b987 100644 --- a/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue +++ b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue @@ -7,6 +7,7 @@ import ButtonToggle from './components/ButtonToggle.vue'; import DateInput from './components/DateInput.vue'; import Input from './components/Input.vue'; import TimeInput from './components/TimeInput.vue'; +import BasicDiv from './components/BasicDiv.vue'; const { appendChild, saveSource, source } = useComponent() const { refresh } = useAppTree() @@ -16,7 +17,6 @@ const emit = defineEmits(['componentAdded']) const addComponent = async (code: string) => { appendChild(code.trim()); await saveSource(source.value!) - emit('componentAdded') setTimeout(() => { refresh() }, 300) @@ -26,6 +26,12 @@ const addComponent = async (code: string) => { diff --git a/packages/compiler/devtools/client/ui/components/component-options/options/Number.vue b/packages/compiler/devtools/client/ui/components/component-options/options/Number.vue index 181c8ea52b..ea9d3a1c13 100644 --- a/packages/compiler/devtools/client/ui/components/component-options/options/Number.vue +++ b/packages/compiler/devtools/client/ui/components/component-options/options/Number.vue @@ -1,7 +1,8 @@ diff --git a/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue index 8d7fd5b987..99589a20c2 100644 --- a/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue +++ b/packages/compiler/devtools/client/ui/components/toolbar/ComponentSelect.vue @@ -9,14 +9,14 @@ import Input from './components/Input.vue'; import TimeInput from './components/TimeInput.vue'; import BasicDiv from './components/BasicDiv.vue'; -const { appendChild, saveSource, source } = useComponent() +const { source, code } = useComponent() const { refresh } = useAppTree() const emit = defineEmits(['componentAdded']) -const addComponent = async (code: string) => { - appendChild(code.trim()); - await saveSource(source.value!) +const addComponent = async (childCode: string) => { + + await source.update(code.appendChild(childCode.trim())) setTimeout(() => { refresh() }, 300) diff --git a/packages/compiler/devtools/client/ui/components/toolbar/FlexEditor.vue b/packages/compiler/devtools/client/ui/components/toolbar/FlexEditor.vue index 41013b81fe..79c588e4ae 100644 --- a/packages/compiler/devtools/client/ui/components/toolbar/FlexEditor.vue +++ b/packages/compiler/devtools/client/ui/components/toolbar/FlexEditor.vue @@ -1,7 +1,7 @@ diff --git a/packages/compiler/devtools/client/ui/composables/useAppTree/index.ts b/packages/compiler/devtools/client/ui/composables/useAppTree/index.ts index 931fa3e1df..b2c654b13d 100644 --- a/packages/compiler/devtools/client/ui/composables/useAppTree/index.ts +++ b/packages/compiler/devtools/client/ui/composables/useAppTree/index.ts @@ -1,2 +1 @@ export * from './useAppTree' -export * from './useSelectedAppTreeItem' diff --git a/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts b/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts index 9e008a9157..c21b140885 100644 --- a/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts +++ b/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts @@ -234,7 +234,7 @@ const appTree = ref([]) export const _appTree = appTree export const useAppTree = () => { - const { selectedAppTreeItem } = useSelectedAppTreeItem() + const { selectedAppTreeItem, sameNodeItems, selectAppTreeItem } = useSelectedAppTreeItem() const refresh = async () => { const selectedNodeElement = selectedAppTreeItem.value?.el @@ -270,6 +270,9 @@ export const useAppTree = () => { return { appTree, - refresh + refresh, + selectAppTreeItem, + selectedAppTreeItem, + sameNodeItems, } } diff --git a/packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts b/packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts index a68b65b6a4..d4cb636934 100644 --- a/packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts +++ b/packages/compiler/devtools/client/ui/composables/useAppTree/useSelectedAppTreeItem.ts @@ -3,7 +3,6 @@ import { AppTreeItemComponent, AppTreeItem, _appTree, walkTree } from './useAppT const selectedAppTreeItem = ref(null) as Ref - export const useSelectedAppTreeItem = () => { return { selectedAppTreeItem, diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts index 1582c82f43..4a5b17f9aa 100644 --- a/packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts +++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponent.ts @@ -1,59 +1,15 @@ -import { computed, PropType } from 'vue'; +import { computed } from 'vue'; import { useComponentCode } from "./useComponentCode" import { useComponentSource } from "./useComponentSource" import { defineGlobal } from '../base/defineGlobal' -import { useSelectedAppTreeItem } from '../useAppTree'; - -export type ComponentAttribute = { - name: string, - readonly currentValue: any, - codeValue: string | null | undefined, - codeAttribute?: { - name: string, - value: string | null, - isDynamic: boolean, - isVModel: boolean, - isEvent: boolean, - }, - updateAttribute: (name: string, value: string | null) => void, -} - -export type ComponentProp = { - meta: { - default?: any, - type?: PropType, - } -} & ComponentAttribute - -const findPropFromAttributes = (attributes: Record, propName: string) => { - const possibleNames = [ - propName, - `:${propName}`, - `v-bind:${propName}`, - `@${propName}`, - `v-model:${propName}`, - ] - - for (const name of possibleNames) { - if (name in attributes) { - return { - name: name, - value: attributes[name], - isDynamic: name.startsWith(':') || name.startsWith('v-bind'), - isVModel: name.startsWith('v-model'), - isEvent: name.startsWith('@'), - } - } - } - - return null -} +import { useAppTree } from '../useAppTree'; +import { useComponentOptions } from './useComponentOptions'; export const useComponent = defineGlobal(() => { const { selectedAppTreeItem, selectAppTreeItem - } = useSelectedAppTreeItem() + } = useAppTree() const vNode = computed(() => { return selectedAppTreeItem.value?.vnode ?? null @@ -67,113 +23,21 @@ export const useComponent = defineGlobal(() => { return selectedAppTreeItem.value?.ids[0] }) - const { source, saveSource, openInVSCode, refreshSource, isSourceLoading, removeFromSource, fileName } = useComponentSource(uid) - const code = useComponentCode(source, vNode) - const name = computed(() => { return selectedAppTreeItem.value?.name }) - const isParsed = computed(() => { - return code.attributes.value !== null && code.slots.value !== null && !isSourceLoading.value && source.value !== null - }) - - const props = computed(() => { - if (!code.attributes.value) { return {} } - - const props = {} as Record - - for (const name in code.meta.value.props) { - const propMeta = code.meta.value.props?.[name as string]! - - const attributeFromCode = findPropFromAttributes(code.attributes.value!, name) - - props[name] = { - name: name, - meta: propMeta, - get currentValue() { - return vNode.value?.props?.[name] - }, - get codeValue() { - return attributeFromCode?.value - }, - set codeValue(newCodeValue: string | null | undefined) { - code.updateAttribute(name, newCodeValue) - if (source.value) { - saveSource(source.value) - } - }, - codeAttribute: attributeFromCode ?? undefined, - updateAttribute: code.updateAttribute, - } - } - - return props - }) - - const slots = computed(() => { - return code.slots.value?.map((slot) => { - let timeout: ReturnType - - return { - name: slot.name, - get codeValue() { - return slot.text - }, - set codeValue(newCodeValue: string) { - clearTimeout(timeout) - timeout = setTimeout(() => { - code.updateSlot(slot.name, newCodeValue) - }, 300) - }, - } - }) ?? [] - }) - - const style = computed({ - get() { - if (!code.attributes.value?.style) { return {} } - - return code.attributes.value.style.split(';').reduce((acc, style) => { - const [key, value] = style.split(':') - if (key && value) { - acc[key.trim()] = value.trim() - } - return acc - }, {} as Record) - }, - - set(newStyle: Record) { - const style = Object.entries(newStyle).map(([key, value]) => `${key}: ${value}`).join('; ') - code.updateAttribute('style', style) - if (source.value) { - saveSource(source.value) - } - } - }) - - const deleteComponent = async () => { - await removeFromSource() - selectAppTreeItem(null) - } + const source = useComponentSource(uid) + const code = useComponentCode(source, vNode) + const options = useComponentOptions(code, source, vNode) return { - isParsed, + uid, name, - fileName, - source, - vNode, element, - uid, - saveSource, - refreshSource, - openInVSCode, - updateAttribute: code.updateAttribute, - appendChild: code.appendChild, - deleteComponent, - props, - slots, - selectAppTreeItem, - style, + source, + code, + options, + setComponent: selectAppTreeItem } }) diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts index ae79bc393c..aee4cf1b98 100644 --- a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts +++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts @@ -1,7 +1,8 @@ -import { type Ref, computed, type VNode } from 'vue'; +import { type Ref, computed, type VNode, type ComputedRef } from 'vue'; import { HTMLContentNode, HTMLRootNode, parseSource } from '../../../parser/parseSource'; import { useComponentMeta } from './useComponentMeta' import { printSource } from '../../../parser/printSource'; +import { ComponentSource } from './useComponentSource' const transformPropName = (name: string, type: 'attr' | 'bind' | 'v-model' | 'event') => { switch (type) { @@ -27,7 +28,7 @@ const extractSlotName = (keys: string[]) => { } } -export const useComponentCode = (source: Ref, vNode: Ref) => { +export const useComponentCode = (source: ComponentSource, vNode: Ref) => { const ast = computed(() => { if (!source.value) return null @@ -129,7 +130,7 @@ export const useComponentCode = (source: Ref, vNode: Ref, vNode: Ref { @@ -179,8 +180,7 @@ export const useComponentCode = (source: Ref, vNode: Ref { @@ -216,7 +216,7 @@ export const useComponentCode = (source: Ref, vNode: Ref { @@ -254,12 +254,17 @@ export const useComponentCode = (source: Ref, vNode: Ref { + return attributes.value !== null && slots.value !== null && !source.isLoading && source.value !== null + }) + return { + isParsed, meta, - ast, + // ast, attributes, slots, updateAttribute, @@ -267,3 +272,5 @@ export const useComponentCode = (source: Ref, vNode: Ref diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentOptions.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentOptions.ts new file mode 100644 index 0000000000..72c613cf7e --- /dev/null +++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentOptions.ts @@ -0,0 +1,127 @@ +import { computed, type PropType, type ComputedRef, VNode } from 'vue'; +import type { useComponentCode } from './useComponentCode' +import type { useComponentSource } from './useComponentSource' + +type Code = ReturnType +type Source = ReturnType + +export type ComponentAttribute = { + name: string, + readonly currentValue: any, + codeValue: string | null | undefined, + codeAttribute?: { + name: string, + value: string | null, + isDynamic: boolean, + isVModel: boolean, + isEvent: boolean, + }, + updateAttribute: Code['updateAttribute'], +} + +export type ComponentProp = { + meta: { + default?: any, + type?: PropType, + } +} & ComponentAttribute + +const findPropFromAttributes = (attributes: Record, propName: string) => { + const possibleNames = [ + propName, + `:${propName}`, + `v-bind:${propName}`, + `@${propName}`, + `v-model:${propName}`, + ] + + for (const name of possibleNames) { + if (name in attributes) { + return { + name: name, + value: attributes[name], + isDynamic: name.startsWith(':') || name.startsWith('v-bind'), + isVModel: name.startsWith('v-model'), + isEvent: name.startsWith('@'), + } + } + } + + return null +} + +export const useComponentOptions = (code: Code, source: Source, vNode: ComputedRef) => { + const props = computed(() => { + if (!code.attributes.value) { return {} } + + const props = {} as Record + + for (const name in code.meta.value.props) { + const propMeta = code.meta.value.props?.[name as string]! + + const attributeFromCode = findPropFromAttributes(code.attributes.value!, name) + + props[name] = { + name: name, + meta: propMeta, + get currentValue() { + return vNode.value?.props?.[name] + }, + get codeValue() { + return attributeFromCode?.value + }, + set codeValue(newCodeValue: string | null | undefined) { + source.update(code.updateAttribute(name, newCodeValue)) + }, + codeAttribute: attributeFromCode ?? undefined, + updateAttribute: code.updateAttribute, + } + } + + return props + }) + + const slots = computed(() => { + return code.slots.value?.map((slot) => { + let timeout: ReturnType + + return { + name: slot.name, + get codeValue() { + return slot.text + }, + set codeValue(newCodeValue: string) { + clearTimeout(timeout) + timeout = setTimeout(() => { + source.update(code.updateSlot(slot.name, newCodeValue)) + }, 300) + }, + } + }) ?? [] + }) + + const style = computed({ + get() { + if (!code.attributes.value?.style) { return {} } + + return code.attributes.value.style.split(';').reduce((acc, style) => { + const [key, value] = style.split(':') + if (key && value) { + acc[key.trim()] = value.trim() + } + return acc + }, {} as Record) + }, + + set(newStyle: Record) { + const style = Object.entries(newStyle).map(([key, value]) => `${key}: ${value}`).join('; ') + source.update(code.updateAttribute('style', style)) + } + }) + + return { + props, + slots, + style, + } +} diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentSource.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentSource.ts index 72f2a9a448..e1670add6a 100644 --- a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentSource.ts +++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentSource.ts @@ -1,16 +1,11 @@ -import { ref, watch, Ref } from 'vue'; -import { getNodeSource, getVSCodePath, setNodeSource, deleteNodeSource, getFileRelativePath } from './api'; +import { ref, watch, computed, Ref } from 'vue'; +import { getNodeSource, setNodeSource, deleteNodeSource, getVSCodePath, getFileRelativePath } from './api'; import { useAsyncComputed } from '../base/useAsyncComputed'; export const useComponentSource = (uid: Ref) => { /** @notice Source is async and may not be available until loaded */ const source = ref(null) const isSourceLoading = ref(false) - const fileName = useAsyncComputed(async () => { - if (!uid.value) { return null } - - return await (await getFileRelativePath(uid.value)).text() - }, '') const resetSource = () => { source.value = null @@ -40,8 +35,10 @@ export const useComponentSource = (uid: Ref) => { } const removeFromSource = async () => { + // TODO: Handle history here if (!uid.value) { throw new Error('Can not delete source: no q available') } + source.value = '' await deleteNodeSource(uid.value) } @@ -58,13 +55,22 @@ export const useComponentSource = (uid: Ref) => { fetch(`/__open-in-editor?file=${path}`) } + const fileName = useAsyncComputed(async () => { + if (!uid.value) { return null } + + return await (await getFileRelativePath(uid.value)).text() + }, '') + return { + content: source, + get value () { return source.value }, + get isLoading () { return isSourceLoading.value }, fileName, - source, - isSourceLoading, - refreshSource: loadSource, - saveSource, - removeFromSource, + refresh: loadSource, + update: saveSource, + remove: removeFromSource, openInVSCode, } } + +export type ComponentSource = ReturnType diff --git a/packages/compiler/devtools/client/ui/composables/useHoveredElement.ts b/packages/compiler/devtools/client/ui/composables/useHoveredElement.ts index ac46bd4912..4d405f802f 100644 --- a/packages/compiler/devtools/client/ui/composables/useHoveredElement.ts +++ b/packages/compiler/devtools/client/ui/composables/useHoveredElement.ts @@ -1,9 +1,9 @@ import { onBeforeMount, onMounted, ref, watch } from "vue" import { PREFIX } from "../../../shared/CONST" -import { useSelectedAppTreeItem } from "./useAppTree" +import { useAppTree } from "./useAppTree" export const useHoveredElement = () => { - const { selectedAppTreeItem } = useSelectedAppTreeItem() + const { selectedAppTreeItem } = useAppTree() const hoveredElement = ref(null) diff --git a/packages/compiler/devtools/server/file.ts b/packages/compiler/devtools/server/file.ts index 6dada36853..5919c584ce 100644 --- a/packages/compiler/devtools/server/file.ts +++ b/packages/compiler/devtools/server/file.ts @@ -79,13 +79,23 @@ export const deleteComponentSource = async (path: string, start: number, end: nu if (intent === 0) { await writeFile(path, fileSource.slice(0, start) + fileSource.slice(end)); - return; + return { + path, + start, + end: start, + } } const fileSourceStart = fileSource.slice(0, start - intent - '\n'.length); const fileSourceEnd = fileSource.slice(end); await writeFile(path, fileSourceStart + fileSourceEnd); + + return { + path, + start: start - intent - '\n'.length, + end: start - intent - '\n'.length, + } } export const getComponentLineAndCol = async (path: string, start: number) => { diff --git a/packages/compiler/devtools/server/server-middleware.ts b/packages/compiler/devtools/server/server-middleware.ts index 69ba7fdd27..f9387cafe8 100644 --- a/packages/compiler/devtools/server/server-middleware.ts +++ b/packages/compiler/devtools/server/server-middleware.ts @@ -47,7 +47,12 @@ export const devtoolsServerMiddleware = (): Connect.NextHandleFunction => { } if (req.method === 'DELETE' && req.url.startsWith(`${API_PREFIX}/node-source`)) { - await deleteComponentSource(path, start, end); + const newPath = await deleteComponentSource(path, start, end); + + replacePath(minified, stringifyFileQuery(newPath.path, newPath.start, newPath.end)); + + res.writeHead(200) + res.end(); return } From b053e7cdc642096432fca8cfc03bae1cd75af13c Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Mon, 19 Aug 2024 20:46:19 +0300 Subject: [PATCH 08/12] raw --- packages/compiler/devtools/client/ui/Devtools.vue | 15 +++++++++++++++ .../client/ui/components/base/Toolbar.vue | 1 + .../playground/src/pages/TestButtonBase.vue | 5 ++++- .../useInputMask/useInputMask.stories.ts | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/compiler/devtools/client/ui/Devtools.vue b/packages/compiler/devtools/client/ui/Devtools.vue index 90667258b1..0720cbc520 100644 --- a/packages/compiler/devtools/client/ui/Devtools.vue +++ b/packages/compiler/devtools/client/ui/Devtools.vue @@ -9,6 +9,20 @@ + + + + + + + + + + + + @@ -40,6 +54,7 @@ import ComponentView from './components/ComponentView.vue' import DraggableWindow from './components/base/DraggableWindow.vue' import AppTree from './components/AppTree.vue' import AppToolbar from './components/AppToolbar.vue' +import Toolbar from './components/base/Toolbar.vue' import { VaCard, useToast, useColors, VaScrollContainer } from 'vuestic-ui' diff --git a/packages/compiler/devtools/client/ui/components/base/Toolbar.vue b/packages/compiler/devtools/client/ui/components/base/Toolbar.vue index 5f170d996a..99544a825d 100644 --- a/packages/compiler/devtools/client/ui/components/base/Toolbar.vue +++ b/packages/compiler/devtools/client/ui/components/base/Toolbar.vue @@ -59,6 +59,7 @@ transform: translateY(-100%); z-index: 1; pointer-events: all; + height: max-content; } } diff --git a/packages/compiler/playground/src/pages/TestButtonBase.vue b/packages/compiler/playground/src/pages/TestButtonBase.vue index fff17c2a7f..0b71143119 100644 --- a/packages/compiler/playground/src/pages/TestButtonBase.vue +++ b/packages/compiler/playground/src/pages/TestButtonBase.vue @@ -7,7 +7,10 @@ diff --git a/packages/ui/src/composables/useInputMask/useInputMask.stories.ts b/packages/ui/src/composables/useInputMask/useInputMask.stories.ts index 54e210f9a4..23d4810f1d 100644 --- a/packages/ui/src/composables/useInputMask/useInputMask.stories.ts +++ b/packages/ui/src/composables/useInputMask/useInputMask.stories.ts @@ -104,7 +104,7 @@ export const WithOptionalGroup = defineStory({ const value = ref('') const input = ref() - const { masked, unmasked } = useInputMask(createMaskFromRegex(/(\+(\d{1,3}) )?\(\d{2,3}\) (\d){3}-\d\d-\d\d/), input) + const { masked, unmasked } = useInputMask(createMaskFromRegex(/(\+\d{1,3} )?\(\d{2,3}\) (\d){3}-\d\d-\d\d/), input) return { value, input, masked, unmasked } }, From 97edd0dd6add08d1cc6d4f8f2c7f718e1ecdbaef Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Mon, 26 Aug 2024 10:11:06 +0300 Subject: [PATCH 09/12] fix(devtools): handle source file correctly --- .../compiler/devtools/client/ui/Devtools.vue | 5 +++-- .../component-options/ComponentSource.vue | 17 +++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/compiler/devtools/client/ui/Devtools.vue b/packages/compiler/devtools/client/ui/Devtools.vue index 0720cbc520..264398f751 100644 --- a/packages/compiler/devtools/client/ui/Devtools.vue +++ b/packages/compiler/devtools/client/ui/Devtools.vue @@ -9,7 +9,8 @@ - + + diff --git a/packages/compiler/devtools/client/ui/components/component-options/ComponentSource.vue b/packages/compiler/devtools/client/ui/components/component-options/ComponentSource.vue index 2481a2cf98..537068f6ab 100644 --- a/packages/compiler/devtools/client/ui/components/component-options/ComponentSource.vue +++ b/packages/compiler/devtools/client/ui/components/component-options/ComponentSource.vue @@ -1,17 +1,22 @@ diff --git a/packages/compiler/devtools/client/ui/components/component-options/ComponentFile.vue b/packages/compiler/devtools/client/ui/components/component-options/ComponentFile.vue new file mode 100644 index 0000000000..d0e568d6ab --- /dev/null +++ b/packages/compiler/devtools/client/ui/components/component-options/ComponentFile.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts b/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts index c21b140885..68a3f54a74 100644 --- a/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts +++ b/packages/compiler/devtools/client/ui/composables/useAppTree/useAppTree.ts @@ -200,14 +200,16 @@ const getAppTree = async () => { } -export const walkTree = (search: string | Element, tree: AppTreeItem[] = appTree.value): AppTreeItem | null => { +export const walkTree = (search: AppTreeItem | HTMLElement | string, tree: AppTreeItem[] = appTree.value): AppTreeItem | null => { for (const item of tree) { if ('text' in item) { continue } - if (typeof search === 'string' && item.ids.includes(search)) { - return item + if (typeof search === 'string') { + if (item.ids.includes(search)) { + return item + } } else if (search instanceof HTMLElement) { if (item.el?.isEqualNode(search)) { return item @@ -216,6 +218,16 @@ export const walkTree = (search: string | Element, tree: AppTreeItem[] = appTree if (item.repeatedElements?.some((el) => el.isEqualNode(search))) { return item } + } else if ('name' in search) { + const searchEl = search.el + const searchName = search.name + + const isSameNode = item.el?.isEqualNode(searchEl) || item.repeatedElements?.some((el) => el.isEqualNode(el)) + const isSameName = item.name === searchName + + if (isSameName && isSameNode) { + return item + } } const child = walkTree(search, item.children) @@ -237,8 +249,7 @@ export const useAppTree = () => { const { selectedAppTreeItem, sameNodeItems, selectAppTreeItem } = useSelectedAppTreeItem() const refresh = async () => { - const selectedNodeElement = selectedAppTreeItem.value?.el - + const oldSelectedAppTreeItem = selectedAppTreeItem.value const tree = await getAppTree() if (!Array.isArray(tree)) { @@ -248,8 +259,8 @@ export const useAppTree = () => { } // Keep node selected when app tree is refreshed - if (selectedNodeElement) { - const selectedNode = walkTree(selectedNodeElement) + if (oldSelectedAppTreeItem) { + const selectedNode = walkTree(oldSelectedAppTreeItem) if (selectedNode && 'el' in selectedNode) { selectedAppTreeItem.value = selectedNode From 8d800879ddb20d89e54ce7052cdc8a68ac36d6e7 Mon Sep 17 00:00:00 2001 From: Maksim Nedoshev Date: Mon, 26 Aug 2024 10:54:54 +0300 Subject: [PATCH 12/12] fix(devtools): remove possible dup attributes caused by v-bind and static assignment --- .../useComponent/useComponentCode.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts index aee4cf1b98..a56c5c1d22 100644 --- a/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts +++ b/packages/compiler/devtools/client/ui/composables/useComponent/useComponentCode.ts @@ -28,6 +28,23 @@ const extractSlotName = (keys: string[]) => { } } +/** Mutates attrs */ +const removeDuplicatedAttributes = (attrs: Record) => { + Object + .keys(attrs) + .forEach((attributeName, i, keys) => { + const attributeNameNormalized = attributeName.replace(/^:|^v-model:|^v-bind:/, '') + + if (attributeNameNormalized === attributeName) { return } + + if (keys.includes(attributeNameNormalized)) { + delete attrs[attributeName] + } + }) + + return attrs +} + export const useComponentCode = (source: ComponentSource, vNode: Ref) => { const ast = computed(() => { if (!source.value) return null @@ -126,7 +143,7 @@ export const useComponentCode = (source: ComponentSource, vNode: Ref