Skip to content

Commit

Permalink
feat: support updating style declaration in preview (#6)
Browse files Browse the repository at this point in the history
* feat: add style value component with basic features

* fix: end style value editing when pressed enter

* chore: add helper function to find declaration

* chore: add style declaration modifier

* feat: support updating style declaration in preview

* chore: more proper contenteditable value handling

* chore: naive client side declaration updating

* chore: select entire text content when starting declaration editing

* feat: allow to edit declaration prop

* refactor: move parsing logic of !important to store module

* refactor: remove unused type

* test: add more test case for StyleValue
  • Loading branch information
ktsn committed Mar 14, 2018
1 parent 74e4158 commit 50e56d3
Show file tree
Hide file tree
Showing 20 changed files with 600 additions and 23 deletions.
19 changes: 18 additions & 1 deletion src/message/bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
Modifiers,
insertToTemplate,
insertComponentScript,
modify
modify,
updateDeclaration
} from '../parser/modifier'
import { mapValues } from '../utils'

Expand Down Expand Up @@ -136,4 +137,20 @@ export function observeServerEvents(
// TODO: change this notification more clean and optimized way
bus.emit('initProject', mapValues(vueFiles, vueFileToPayload))
})

bus.on('updateDeclaration', ({ uri, declaration }) => {
const { code, styles } = vueFiles[uri]
const updated = modify(code, [updateDeclaration(styles, declaration)])

bus.emit('updateEditor', {
uri,
code: updated
})

// TODO: move mutation to outside of this logic
vueFiles[uri] = parseVueFile(updated, uri)

// TODO: change this notification more clean and optimized way
bus.emit('initProject', mapValues(vueFiles, vueFileToPayload))
})
}
6 changes: 5 additions & 1 deletion src/message/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RuleForPrint } from '../parser/style/types'
import { RuleForPrint, DeclarationUpdater } from '../parser/style/types'
import { VueFilePayload } from '../parser/vue-file'

export interface Events {
Expand Down Expand Up @@ -26,6 +26,10 @@ export interface Events {
code: string
}
removeDocument: string
updateDeclaration: {
uri: string
declaration: DeclarationUpdater
}
}

export interface Commands {
Expand Down
18 changes: 17 additions & 1 deletion src/parser/modifier.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import assert from 'assert'
import * as t from '@babel/types'
import { flatten } from '../utils'
import { flatten, clone } from '../utils'
import { Template, TextNode, ElementChild, Element } from './template/types'
import { getNode } from './template/manipulate'
import { findComponentOptions, findProperty } from './script/manipulate'
import { DeclarationUpdater, Style } from './style/types'
import { getDeclaration } from './style/manipulate'
import { genDeclaration } from './style/codegen'

export type Modifiers = (Modifier | Modifier[])[]

Expand Down Expand Up @@ -295,3 +298,16 @@ function inferScriptIndent(code: string, node: t.Node): string {
return ''
}
}

export function updateDeclaration(
styles: Style[],
decl: DeclarationUpdater
): Modifier[] {
const target = getDeclaration(styles, decl.path)

if (!target) {
return [empty]
}

return replace(target, genDeclaration(clone(target, decl)))
}
2 changes: 1 addition & 1 deletion src/parser/style/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ function genPseudoClass(node: t.PseudoClass): string {
}
}

function genDeclaration(decl: t.Declaration): string {
export function genDeclaration(decl: t.Declaration): string {
let buf = decl.prop + ': ' + decl.value

if (decl.important) {
Expand Down
22 changes: 22 additions & 0 deletions src/parser/style/manipulate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,25 @@ export function addScope(node: t.Style, scope: string): t.Style {
})
})
}

export function getDeclaration(
styles: t.Style[],
path: number[]
): t.Declaration | undefined {
const res = path.reduce<any | undefined>(
(acc, i) => {
if (!acc) return

if (acc.children) {
return acc.children[i]
} else if (acc.body) {
return acc.body[i]
} else if (acc.declarations) {
return acc.declarations[i]
}
},
{ children: styles }
)

return res && res.type === 'Declaration' ? res : undefined
}
12 changes: 9 additions & 3 deletions src/parser/style/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import { genSelector } from './codegen'
import * as t from './types'
import { takeWhile, dropWhile } from '../../utils'

export function transformStyle(root: postcss.Root, code: string): t.Style {
export function transformStyle(
root: postcss.Root,
code: string,
index: number
): t.Style {
if (!root.nodes) {
return {
path: [index],
body: [],
range: [-1, -1]
}
Expand All @@ -16,9 +21,9 @@ export function transformStyle(root: postcss.Root, code: string): t.Style {
.map((node, i) => {
switch (node.type) {
case 'atrule':
return transformAtRule(node, [i], code)
return transformAtRule(node, [index, i], code)
case 'rule':
return transformRule(node, [i], code)
return transformRule(node, [index, i], code)
default:
return undefined
}
Expand All @@ -28,6 +33,7 @@ export function transformStyle(root: postcss.Root, code: string): t.Style {
})

return {
path: [index],
body,
range: toRange(root.source, code)
}
Expand Down
12 changes: 11 additions & 1 deletion src/parser/style/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Range } from '../modifier'

export interface Style extends Range {
path: [number]
body: (AtRule | Rule)[]
}

Expand Down Expand Up @@ -81,5 +82,14 @@ export interface DeclarationForPrint {
path: number[]
prop: string
value: string
important: boolean
}

/**
* Used to update ast
*/
export interface DeclarationUpdater {
path: number[]
prop?: string
value?: string
important?: boolean
}
4 changes: 2 additions & 2 deletions src/parser/vue-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export function parseVueFile(code: string, uri: string): VueFile {
return resolved.toString()
})

const styleAsts = styles.map(s => {
return transformStyle(postcssParse(s.content), s.content)
const styleAsts = styles.map((s, i) => {
return transformStyle(postcssParse(s.content), s.content, i)
})

return {
Expand Down
10 changes: 8 additions & 2 deletions src/payload.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { VueFilePayload } from './parser/vue-file'
import { RuleForPrint } from './parser/style/types'
import { RuleForPrint, DeclarationUpdater } from './parser/style/types'

export type ServerPayload = InitProject | ChangeDocument | MatchRules
export type ClientPayload = SelectNode | AddNode
export type ClientPayload = SelectNode | AddNode | UpdateDeclaration

export interface InitProject {
type: 'InitProject'
Expand Down Expand Up @@ -31,3 +31,9 @@ export interface AddNode {
nodeUri: string
path: number[]
}

export interface UpdateDeclaration {
type: 'UpdateDeclaration'
uri: string
declaration: DeclarationUpdater
}
2 changes: 2 additions & 0 deletions src/server/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export function wsEventObserver(
return emit('selectNode', payload)
case 'AddNode':
return emit('addNode', payload)
case 'UpdateDeclaration':
return emit('updateDeclaration', payload)
default:
throw new Error(
'Unexpected client payload: ' + (payload as any).type
Expand Down
9 changes: 7 additions & 2 deletions src/view/components/PageMain.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
<div v-else class="style-information">
<p class="style-information-title">Node Styles</p>
<div class="style-information-list">
<StyleInformation v-if="matchedRules.length > 0" :rules="matchedRules" />
<StyleInformation
v-if="matchedRules.length > 0"
:rules="matchedRules"
@update-declaration="updateDeclaration"
/>
<p class="not-found" v-else>Not found</p>
</div>
</div>
Expand Down Expand Up @@ -94,7 +98,8 @@ export default Vue.extend({
'endDragging',
'setDraggingPlace',
'select',
'applyDraggingElement'
'applyDraggingElement',
'updateDeclaration'
])
})
</script>
Expand Down
33 changes: 30 additions & 3 deletions src/view/components/StyleInformation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@

<ul class="declaration-list">
<li class="declaration" v-for="d in rule.declarations" :key="d.path.join('.')">
<span class="declaration-prop"><span class="declaration-prop-text">{{ d.prop }}</span></span>
<span class="declaration-value">{{ d.value }}</span>
<span v-if="d.important" class="declaration-important">!important</span>
<span class="declaration-prop"><StyleValue
class="declaration-prop-text"
:value="d.prop"
@input="inputStyleProp(d.path, arguments[0])"
/></span>
<StyleValue
:value="d.value"
@input="inputStyleValue(d.path, arguments[0])"
/>
</li>
</ul>
</li>
Expand All @@ -18,16 +24,37 @@

<script lang="ts">
import Vue from 'vue'
import StyleValue from './StyleValue.vue'
import { RuleForPrint } from '@/parser/style/types'
export default Vue.extend({
name: 'StyleInformation',
components: {
StyleValue
},
props: {
rules: {
type: Array as () => RuleForPrint[],
required: true
}
},
methods: {
inputStyleProp(path: number[], prop: string): void {
this.$emit('update-declaration', {
path,
prop: prop.trim()
})
},
inputStyleValue(path: number[], value: string): void {
this.$emit('update-declaration', {
path,
value: value.trim()
})
}
}
})
</script>
Expand Down
88 changes: 88 additions & 0 deletions src/view/components/StyleValue.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<template>
<button
v-if="!editing"
class="style-value"
@click="startEdit"
@focus="startEdit"
>{{ value }}</button>
<div
v-else
class="style-value editing"
contenteditable="true"
ref="input"
@input="input"
@keypress.enter="endEdit"
@blur="endEdit"
></div>
</template>

<script lang="ts">
import Vue from 'vue'
import { selectNodeContents } from '@/view/editing'
export default Vue.extend({
name: 'StyleValue',
props: {
value: {
type: String,
required: true
}
},
data() {
return {
editing: false
}
},
watch: {
value(newValue: string): void {
const input = this.$refs.input as HTMLDivElement | undefined
if (input && newValue !== input.textContent) {
input.textContent = newValue
}
}
},
methods: {
startEdit(): void {
this.editing = true
this.$nextTick(() => {
const input = this.$refs.input as HTMLDivElement | undefined
if (input) {
input.textContent = this.value
selectNodeContents(input)
}
})
},
endEdit(): void {
this.editing = false
},
input(event: Event): void {
const el = event.target as HTMLDivElement
this.$emit('input', el.textContent)
}
}
})
</script>

<style lang="scss" scoped>
.style-value {
display: inline;
padding: 0;
border-width: 0;
background: none;
font-family: inherit;
font-size: inherit;
}
.style-value.editing {
margin: -1px;
border: 1px solid #aaa;
background-color: #fff;
outline: none;
}
</style>
10 changes: 10 additions & 0 deletions src/view/editing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function selectNodeContents(el: Node): void {
// Skip if selection does not exist (in case of testing environment)
if (!('getSelection' in window)) return

const selection = window.getSelection()
const range = new Range()
range.selectNodeContents(el)
selection.removeAllRanges()
selection.addRange(range)
}

0 comments on commit 50e56d3

Please sign in to comment.