skysan's programming notebook

コーディングして思ったことなどを気ままに

フォーカスが外れた時に、編集をキャンセルするコンポーネントを作る

はじめに

  • Trelloのチェックリストは、フォーカスが外れたときに編集内容がキャンセルされる
  • 単純にinputのblurイベントをコールするのではなく、関連機能にフォーカスがある場合はキャンセルされない
    • 対象コンポーネント(textbox、OKボタン、キャンセルボタン)にフォーカスがある場合は編集内容はキャンセルされない
    • それ以外の項目に移動した場合は、キャンセルする
  • この機能を自作してみる

成果物

仕組み

  • focusin/focusoutイベントがバブリングする性質を利用
  • 対象コンポーネントの最上位要素にイベントを登録
  • フォーカスが移動するたびに、focusin/focusoutが発生
  • つまり、focusinが発生しない場合は、キャンセルとする
    • focusout -> focusinは連続して発生するため、focusinが発生しないか、少し遅らせて判定
      • focusout時にキャンセル処理を遅延実行し、focusin時に遅延実行を無効化
      • focusinが実行されなければ、そのままキャンセル処理が実行される

コンポーネント実装の抜粋

  • CSSは省略
  • Vueで実装

(上記画像は実装中の自作アプリのため、下記のコードの見た目は異なります。)

<template>
  <div @focusout="handleFocusout" @focusin="handleFocusin">
    <div v-if="!editMode">
      <label>
        <span>{{ title }}</span>
      </label>
      <button @click.left="onEditMode">Edit</button>
      <button @click.left="deleteData">X</button>
    </div>

    <div v-if="editMode">
      <input
        ref="inputtext"
        v-model="title"
        type="text"
      >
      <button @click="update">O</button>
      <button @click="cancel">X</button>
    </div>
  </div>
</template>

<script>
const defaultText = '1234567890'

export default {
  data () {
    return {
      title: defaultText,
      editMode: false,
      focusTimerId: null
    }
  },
  destroyed () {
    clearTimeout(this.focusTimerId)
  },
  methods: {
    onEditMode () {
      this.editMode = true

      this.$nextTick(() => {
        this.$refs.inputtext.focus()
      })
    },
    cancel () {
      this.editMode = false
      this.title = defaultText
      this.$emit('cancel')
    },
    update () {
      this.editMode = false
      this.$emit('update', this.title)
    },
    deleteData () {
      this.$emit('delete')
    },
    handleFocusout () {
      if (!this.editMode) {
        return
      }
      // コンポーネントからフォーカスアウト時にキャンセル
      // NOTE: focusout/focusinのバブリングを利用
      this.focusTimerId = setTimeout(() => this.cancel(), 100)
    },
    handleFocusin () {
      if (!this.editMode) {
        return
      }
      clearTimeout(this.focusTimerId)
    }
  }
}
</script>

参考

ja.javascript.info