はじめに
各種エディターやQiitaなどでもサポートされている、TAB
もしくは Shift
+ TAB
によるインデントを上げ下げする実装をしてみました。
[JS] Tabキーによる移動を禁止してTabを入力する。
※ベース部分の実装は上記の記事を参考にさせていただきました。ありがとうございました🙇♂️
※また、今回はvueで実装してます。
サンプル
- 今回作成したコンポーネント
CommonTextArea.vue
<template>
<textarea
:id="uid"
:value="value"
:placeholder="label"
@input="$emit('input', $event.target.value)"
/>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "nuxt-property-decorator"
@Component
export default class CommonTextArea extends Vue {
/* ===============================
* props
* =============================== */
@Prop({ type: String, default: "" })
value!: string
@Prop({ type: String, default: "" })
label!: string
/* ===============================
* lifecycle
* =============================== */
mounted() {
// keyupだと移動後に処理が発火してしまうので注意。
document.addEventListener("keydown", this.detectTabKey)
this.uid = this.generateUid()
}
beforeDestroy() {
document.removeEventListener("keydown", this.detectTabKey)
}
/* ===============================
* methods
* =============================== */
/** tabキーを押下時の処理を追加 */
detectTabKey(event: KeyboardEvent) {
// TODO `value`などが使えないため`any`(適切な型ご存知な型いましたらぜひ教えてください・・!)
const currentElement = document.activeElement as any
// DOM取得できないか、異なるidのテキストエリアの場合か、textarea以外の場合は処理しない。
if (!currentElement || currentElement.id !== this.uid || currentElement.tagName !== "TEXTAREA") return
// shift + tab の場合
if (event.shiftKey && event.key === "Tab") {
const TAB = " " // 「"\t"」にするかはお好みで。
// 移動処理を無効
event.preventDefault()
const value = currentElement.value
// sPosから現在の行数を取得する
const sPos = currentElement.selectionStart
const sValue = value.slice(0, sPos)
const sValueList = sValue.split("\n")
const currentRow = sValueList.length
const currentIdx = currentRow - 1
// 現在の行の値を取得
const valueList = value.split("\n")
const currentRowValue = valueList[currentIdx]
// TABが含まれていなければ処理終了
if (!currentRowValue.startsWith(TAB)) return
// TABが含まれていたら除去して値を更新
valueList[currentIdx] = currentRowValue.replace(TAB, "")
const result = valueList.join("\n")
this.$emit("input", result) // vueのdataにも反映
currentElement.value = result
// position移動のための処理
// 対象行でカーソル位置までにTABが含まれているかで処理が変わる
const sCurrentRowValue = sValueList[currentIdx]
const isIncludeTab = sCurrentRowValue.startsWith(TAB)
const cPos = sPos - (isIncludeTab ? TAB.length : 0)
currentElement.setSelectionRange(cPos, cPos)
return
}
// tab の場合
if (event.key === "Tab") {
event.preventDefault()
const TAB = " "
const value = currentElement.value
const sPos = currentElement.selectionStart
const ePos = currentElement.selectionEnd
const result = value.slice(0, sPos) + TAB + value.slice(ePos)
const cPos = sPos + TAB.length
this.$emit("input", result) // vueのdataにも反映
currentElement.value = result
currentElement.setSelectionRange(cPos, cPos)
}
}
generateUid() {
// 他のコンポーネントと区別がつくようにuidをつくる
return Math.random().toString(32).substring(2)
}
}
- 親コンポーネント
Parent.vue
<template>
<CommonTextArea v-model="hogeValue" label="内容" />
</template>
呼び出し部分のみ抜粋して書いておりますが、親側からはv-model
を使って呼び出せるようにしてあります😊
おわりに
どなたかの参考になれば幸いです!(もっとよい書き方があればぜひアドバイスお願いします!!)