megutech

自身の備忘録として主にWEBサーバー周りの技術について投稿しています。

JavaScriptで日本語入力の文字数制限を行いたい

テキストエリアの入力文字数制限をしたいとき、change eventを拾って入力値をチェックするだけだと、変換を伴う文字入力の際に期待した動きにならなった。

環境

jsなら何でもいいんですが、今回私はreactを使ったのでreact versionを記載します。

Service Version
react 18.2.0

上手く動かなかったコード

import React, { memo, useCallback, useState, type ReactElement } from 'react';

const TEXT_LEN = 5;

function Hoge(): ReactElement {
  const [text, setText] = useState<string>('');

  const handleChangeText = useCallback((e: React.ChangeEvent<HTMLInputElement>): void => {
    const text = e.target.value;
    setText((prevText: string): string => text.substring(0, TEXT_LEN));
  }, []);

  return (
    <input
      name="text"
      type="text"
      value={text}
      onChange={handleChangeText} />
  );
};

export default memo(Hoge);

環境によって挙動が違う。 他のOSやブラウザ、バージョンによっても違うかもしれないが、いずれにしても思った挙動にならないので調査は以下のみ。

OS Version ブラウザ 挙動
Windows 10 Chrome テキスト編集システムを使って指定文字数以上を入力すると、既に入力されている文字を消しながら入力される。
Mac ventura 13.2.1 入力可能文字数内のひらがなのみ入力され、変換を確定しても無視される。
iOS 16.3.1 Safari 入力可能文字数内のひらがなのみ入力され、変換を確定しても無視される。
Android 12 Chrome 変換途中でも文字数が肥えた時点で文字入力が確定され、入力できない。

原因

IME等のようなテキスト編集システムが原因。

対応

composition{start|update|end}でテキスト編集システムの編集セッションイベントが取れる。
これを使えばどうにかできるのではと考え、onCompositionEndで入力を確定させる対応を行ったが、onChangeでの文字入力も同時に行わないと思った通りの挙動にならなかった。

最終的には以下の形に落ち着いた。

import React, { memo, useCallback, useRef, useState, type ReactElement } from 'react';

const TEXT_LEN = 5;

function Hoge(): ReactElement {
  const [text, setText] = useState<string>('');
  const isCompositionStart = useRef<boolean>(false);

  const commitStr = useCallback(() => {
    setText((prevText: string): string => prevText.substring(0, TEXT_LEN));
  }, []);

  const handleCompositionStart = useCallback((): void => { isCompositionStart.current = true; }, []);

  const handleCompositionEnd = useCallback((): void => {
    isCompositionStart.current = false;
    commitStr();
  }, []);

  const handleChangeText = useCallback((e: React.ChangeEvent<HTMLInputElement>): void => {
    setText(e.target.value);
    if (!isCompositionStart.current) {
      commitStr();
    }
  }, [isCompositionStart.current]);

  return (
    <input
      name="text"
      type="text"
      value={text}
      onCompositionStart={handleCompositionStart}
      onCompositionEnd={handleCompositionEnd}
      onChange={handleChangeText} />
  );
};

export default memo(Hoge);

文字入力自体はいかなる状態でも受け入れる。 この対応をしないと変換前の文字で確定したり、意図した挙動とならない。

しかし文字数制限は行いたいので、文字入力後にcommitStrで文字数制限を行う。 ただしテキスト変換システムを使っている場合は、変換確定前に文字数を削ると変換がキャンセルされたりと意図しない挙動となる。

そこでonCompositionStartでテキスト変換中であるかを確認し、もし変換中であれば文字数確定をスキップする。

しかしこのままではテキスト変換システムを使った入力ではテキスト文字数が制限されない。 そのためisCompositionEndで文字数確定を実施する。 また、変換が完了しているため、テキスト変換中フラグを下げる。

所感

文字数制限について調べると、maxlengthを使ったりjsを使ったりという手法は色々出てくるが、IME等での変換が上手くいかない件に関して無視されがちだったので、備忘録をかね残しておくこととする。

もうちょいいい感じにできそうな気がするが、まあ動くしもういいかな。