ブログ

読んで思い出す。忘れるために書く

キーボード操作で <input/> を"インデント"させる

前回に続き、機能を作る

gouf.hatenablog.com

IndentInput.vue, Editor.vue 2つのコンポーネントを作る

まとめ

  • Vue.js 上で使える CSS の scoped はコンポーネントの中でスタイル設定が閉じててわかり易しい
  • CSS の変数を使えば変更や微調整するときに確認箇所を少なくできていい

わからなかったこと

  • 本記事のように動きを持たせたい場合に於ける、 Vue.js 上でのデータの管理方法
  • Vue.js がデータの拘束方法を提供してくれているものの、複雑なことがしたくなったときにどうしても data-xxxx に値を渡すなど "Vue.js から逃げたくなってしまう"、のはいいのか、あるいはその対処法

「動いてるからいいじゃん」...? え いや そんなバカな... (´・ω・`)

ゴール

  • [Ctrl] + [←], [Ctrl] + [→] で入力欄を "インデント" する (CSS クラスを動的に切り替える)

f:id:innocent-zero:20180403003907g:plain

<template/> を記述する

Editor.vue:

<template>
  <div>
    <h1>{{ counter }}</h1>
    <h2>{{ indentLevelList }}</h2>
    <div class="flex-container">
      <div class="row">
        <div
          @keyup.ctrl.backspace="decrementCount"
          @keyup.ctrl.enter="incrementCount"
          @keyup.ctrl.uparrow="moveUpFocus"
          @keyup.ctrl.downarrow="moveDownFocus"
          @keyup.ctrl.rightarrow="incrementIndentLevel"
          @keyup.ctrl.leftarrow="decrementIndentLevel"
        >
          <indent-input
            v-for="i in counter"
            :data-index="i"
            :key="i"
            ref="input"
            :level="indentLevelList[i - 1]"
          ></indent-input>
        </div>
      </div>
    </div>
  </div>
</template>

IndentInput.vue:

<template>
  <input
    ref="input"
    :class="indentClass"
    type="text">
</template>

<script/> を記述する

Editor.vue:

<script>
import IndentInput from '@/components/IndentInput'

export default {
  name: 'Editor',
  // FIXME: 新しい <input/> を挿入したときに入力済みの文字のインデックス位置がズレている
  // FIXME: <input/> の削除で 削除対象の位置がズレている
  // TODO: インデックスと併せて入力された文字の内容を保持するデータ構造に変更する
  data () {
    return {
      counter: 1,
      indentLevelList: [0]
    }
  },
  methods: {
    moveUpFocus: function (event) {
      event.target.previousSibling.focus()
    },
    moveDownFocus: function (event) {
      event.target.nextSibling.focus()
    },
    incrementIndentLevel: function (event) {
      this.indentLevelList[event.target.getAttribute('data-index') - 1]++

      this.$forceUpdate()
    },
    decrementIndentLevel: function (event) {
      this.indentLevelList[event.target.getAttribute('data-index') - 1]--

      this.$forceUpdate()
    },
    decrementCount: function (event) {
      this.counter--

      let startIndex = event.target.getAttribute('data-index') - 1
      let deleteionSize = 1
      this.indentLevelList.splice(startIndex, deleteionSize)

      this.moveUpFocus(event)
    },
    incrementCount: function (event) {
      this.counter++

      let dataIndex = event.target.getAttribute('data-index')
      let startIndex = parseInt(dataIndex) - 1
      let deleteionSize = 0
      let level = this.indentLevelList[parseInt(dataIndex) - 1]

      this.indentLevelList.splice(startIndex + 1, deleteionSize, level)

      let self = this
      this.$nextTick(() => {
        self.moveDownFocus(event)
      })
    }
  },
  components: {
    'indent-input': IndentInput
  }
}
</script>

IndentInput.vue:

<script>
export default {
  name: 'IndentInput',
  data () {
    return {
      indentClass: 'indent-0'
    }
  },
  props: {
    'level': {
      type: Number,
      default: 0
    }
  },
  mounted () {
    if (this.$refs.input.getAttribute('data-level') !== null) {
      this.indentClass = ''.concat('indent-', this.$refs.input.getAttribute('data-level'))
      this.$refs.input.removeAttribute('data-level')
    }

    this.updateIndentLevel()

    this.$refs.input.focus()
  },
  updated () {
    this.updateIndentLevel()
  },
  watch: {
    level: function (value) {
      this.updateIndentLevel()
      this.$forceUpdate()
    }
  },
  methods: {
    updateIndentLevel: function () {
      if (this.level != null) {
        this.indentClass = ''.concat('indent-', this.level)
      }
    }
  }
}
</script>

<style/> を記述する

Editor.vue:

<style scoped>
html, body {
  height: 100%;
}
body {
  margin: 0;
}
.flex-container {
  height: 100%;
  padding: 0;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.row {
  width: auto;
}
.flex-item {
  padding: 5px;
  width: auto;
  height: 20px;
  margin: 10px;
  line-height: 20px;
  color: white;
  font-weight: bold;
  font-size: 2em;
  text-align: center;
}
.control {
  display: flex;
}
</style>

IndentInput.vue:

<style scoped>
input {
  display: block;
  --indent-margin: 30px;
  --indent-prefix: 15px;
}
.indent-1 {
  margin-left: var(--indent-margin);
}
.indent-2 {
  margin-left: calc(var(--indent-margin) + (var(--indent-prefix) * 2))
}
.indent-3 {
  margin-left: calc(var(--indent-margin) + (var(--indent-prefix) * 3))
}
.indent-4 {
  margin-left: calc(var(--indent-margin) + (var(--indent-prefix) * 4))
}
</style>

Link