Skip to content

Commit

Permalink
feat: focus next item when the user finished to edit style value (#71)
Browse files Browse the repository at this point in the history
* feat: focus next item when the user finished to edit style value

* chore: fix typos

* refacotr: use more suitable name
  • Loading branch information
ktsn committed Nov 24, 2018
1 parent 2f33c62 commit 9709305
Show file tree
Hide file tree
Showing 6 changed files with 289 additions and 45 deletions.
26 changes: 16 additions & 10 deletions src/view/components/StyleDeclaration.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
<div class="style-declaration">
<span class="style-declaration-prop">
<StyleValue
:auto-focus="autoFocusProp"
:auto-focus="autoFocus === 'prop'"
:value="prop"
class="style-declaration-prop-text"
@input-start="$emit('input-start')"
@input-start="$emit('input-start:prop')"
@input="inputProp"
@input-end="finishInputProp"
/>
</span>
<span class="style-declaration-value">
<StyleValue
:auto-focus="autoFocus === 'value'"
:value="value"
@input-start="$emit('input-start')"
@input-start="$emit('input-start:value')"
@input="inputValue"
@input-end="finishInputValue"
/>
Expand All @@ -37,13 +38,18 @@ export default Vue.extend({
type: String,
required: true
},
value: {
type: String,
required: true
},
autoFocusProp: {
type: Boolean,
default: false
autoFocus: {
type: String,
default: null,
validator(value: string) {
return value === 'prop' || value === 'value'
}
}
},
Expand All @@ -56,8 +62,8 @@ export default Vue.extend({
this.$emit('update:value', value)
},
finishInputProp(rawProp: string): void {
this.$emit('input-end')
finishInputProp(rawProp: string, meta: { reason: string }): void {
this.$emit('input-end:prop', meta)
const prop = rawProp.trim()
if (!prop) {
Expand All @@ -67,8 +73,8 @@ export default Vue.extend({
}
},
finishInputValue(rawValue: string): void {
this.$emit('input-end')
finishInputValue(rawValue: string, meta: { reason: string }): void {
this.$emit('input-end:value', meta)
const value = rawValue.trim()
if (!value) {
Expand Down
94 changes: 70 additions & 24 deletions src/view/components/StyleInformation.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<template>
<ul class="style-information">
<li
v-for="rule in rules"
v-for="(rule, ruleIndex) in rules"
:key="rule.path.join('.')"
class="rule"
@click="onClickRule(rule)"
@click="onClickRule(rule, ruleIndex)"
>
<p class="selector-list">
<span
Expand All @@ -19,19 +19,21 @@
@click.stop
>
<li
v-for="d in rule.children"
:key="d.path.join('.')"
v-for="(decl, declIndex) in rule.children"
:key="decl.path.join('.')"
class="declaration"
>
<StyleDeclaration
:prop="d.prop"
:value="d.value"
:auto-focus-prop="autoFocusOnNextRender"
@update:prop="updateDeclarationProp(d.path, arguments[0])"
@update:value="updateDeclarationValue(d.path, arguments[0])"
@remove="removeDeclaration(d.path)"
@input-start="onStartStyleInput"
@input-end="onEndStyleInput"
:prop="decl.prop"
:value="decl.value"
:auto-focus="shouldFocusFor(ruleIndex, declIndex)"
@update:prop="updateDeclarationProp(decl.path, arguments[0])"
@update:value="updateDeclarationValue(decl.path, arguments[0])"
@remove="removeDeclaration(decl.path)"
@input-start:prop="onStartStyleInput"
@input-start:value="onStartStyleInput"
@input-end:prop="onEndStyleInput(ruleIndex, declIndex, 'prop', arguments[0])"
@input-end:value="onEndStyleInput(ruleIndex, declIndex, 'value', arguments[0])"
/>
</li>
</ul>
Expand Down Expand Up @@ -60,19 +62,13 @@ export default Vue.extend({
data() {
return {
autoFocusOnNextRender: false,
autoFocusTarget: undefined as
| { rule: number; declaration: number; type: 'prop' | 'value' }
| undefined,
endingInput: false
}
},
watch: {
rules(): void {
this.$nextTick(() => {
this.autoFocusOnNextRender = false
})
}
},
created() {
const vm: any = this
vm.endingInputTimer = null
Expand All @@ -97,13 +93,19 @@ export default Vue.extend({
this.$emit('remove-declaration', { path })
},
onClickRule(rule: STRuleForPrint): void {
onClickRule(rule: STRuleForPrint, index: number): void {
if (this.endingInput) return
this.$emit('add-declaration', {
path: rule.path.concat(rule.children.length)
})
this.autoFocusOnNextRender = true
// Focus on the new declaration prop
this.autoFocusTarget = {
rule: index,
declaration: rule.children.length,
type: 'prop'
}
},
/*
Expand All @@ -118,16 +120,60 @@ export default Vue.extend({
clearTimeout(vm.endingInputTimer)
this.endingInput = true
this.autoFocusTarget = undefined
},
onEndStyleInput(): void {
onEndStyleInput(
rule: number,
decl: number,
type: string,
meta: { reason: string }
): void {
// If the user end the input by pressing enter or tab key,
// focus on the next form.
if (meta.reason === 'enter' || meta.reason === 'tab') {
this.focusOnNextForm(rule, decl, type)
}
const delayToEndEdit = 200
const vm: any = this
clearTimeout(vm.endingInputTimer)
vm.endingInputTimer = setTimeout(() => {
this.endingInput = false
}, delayToEndEdit)
},
focusOnNextForm(rule: number, decl: number, type: string): void {
if (type === 'prop') {
this.autoFocusTarget = {
rule,
declaration: decl,
type: 'value'
}
return
}
if (type === 'value') {
this.autoFocusTarget = {
rule,
declaration: decl + 1,
type: 'prop'
}
// If it is the last item, add a new rule
const targetRule = this.rules[rule]
if (decl + 1 === targetRule.children.length) {
this.$emit('add-declaration', {
path: targetRule.path.concat(targetRule.children.length)
})
}
}
},
shouldFocusFor(rule: number, decl: number): 'prop' | 'value' | null {
const f = this.autoFocusTarget
return f && f.rule === rule && f.declaration === decl ? f.type : null
}
}
})
Expand Down
25 changes: 16 additions & 9 deletions src/view/components/StyleValue.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
contenteditable="true"
@input="input"
@keydown="onKeyDown"
@blur="endEdit"
@blur="endEdit($event, 'blur')"
/>
</template>

Expand Down Expand Up @@ -51,12 +51,15 @@ export default Vue.extend({
if (input && newValue !== input.textContent) {
input.textContent = newValue
}
}
},
},
mounted() {
if (this.autoFocus) {
this.startEdit()
autoFocus: {
handler(value: boolean): void {
if (value) {
this.startEdit()
}
},
immediate: true
}
},
Expand All @@ -73,12 +76,12 @@ export default Vue.extend({
})
},
endEdit(event: Event): void {
endEdit(event: Event, reason: 'blur' | 'enter' | 'tab'): void {
if (this.editing) {
this.editing = false
const el = event.currentTarget as HTMLDivElement
this.$emit('input-end', el.textContent)
this.$emit('input-end', el.textContent, { reason })
}
},
Expand Down Expand Up @@ -139,7 +142,11 @@ export default Vue.extend({
switch (event.key) {
case 'Enter':
event.preventDefault()
this.endEdit(event)
this.endEdit(event, 'enter')
break
case 'Tab':
event.preventDefault()
this.endEdit(event, 'tab')
break
case 'Up':
case 'ArrowUp':
Expand Down
119 changes: 119 additions & 0 deletions tests/view/StyleInformation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { shallowMount, Wrapper } from '@vue/test-utils'
import StyleInformation from '@/view/components/StyleInformation.vue'
import { STRuleForPrint } from '@/parser/style/types'

describe('StyleInformation', () => {
describe('moving focus', () => {
const StyleDeclaration = {
name: 'StyleDeclaration',
props: ['prop', 'value', 'autoFocus'],
render(this: any, h: Function) {
return h('div', {
attrs: {
styleDeclarationStub: true,
prop: this.prop,
value: this.value,
autoFocus: this.autoFocus
}
})
}
}

const rules: STRuleForPrint[] = [
{
path: [0],
selectors: ['a'],
children: [
{
path: [0, 0],
prop: 'color',
value: 'red'
},
{
path: [0, 1],
prop: 'font-size',
value: '22px'
}
]
}
]

const create = () => {
return shallowMount(StyleInformation, {
propsData: {
rules
},
stubs: {
StyleDeclaration
}
})
}

const toDeclarationHtml = (wrapper: Wrapper<any>) => {
return wrapper
.findAll(StyleDeclaration)
.wrappers.map(w => w.html())
.join('\n')
}

it('does not move focus if editing is ended by blur', () => {
const wrapper = create()
wrapper
.findAll(StyleDeclaration)
.at(0)
.vm.$emit('input-end:prop', { reason: 'blur' })

expect(toDeclarationHtml(wrapper)).toMatchSnapshot()
})

it('moves from prop to value', () => {
const wrapper = create()
wrapper
.findAll(StyleDeclaration)
.at(0)
.vm.$emit('input-end:prop', { reason: 'enter' })

expect(toDeclarationHtml(wrapper)).toMatchSnapshot()
})

it('moves from value to next prop value', () => {
const wrapper = create()
wrapper
.findAll(StyleDeclaration)
.at(0)
.vm.$emit('input-end:value', { reason: 'enter' })

expect(toDeclarationHtml(wrapper)).toMatchSnapshot()
})

it('adds a new declaration and moves focus to it', () => {
const wrapper = create()
wrapper
.findAll(StyleDeclaration)
.at(1)
.vm.$emit('input-end:value', { reason: 'enter' })

expect(wrapper.emitted('add-declaration')[0][0]).toEqual({
path: [0, 2]
})

wrapper.setProps({
rules: [
{
...rules[0],
children: [
...rules[0].children,
{
path: [0, 2],
prop: 'property',
value: 'value'
}
]
}
]
})

expect(toDeclarationHtml(wrapper)).toMatchSnapshot()
})
})
})

0 comments on commit 9709305

Please sign in to comment.