技術・業務

【Select2】半角文字の項目を楽に検索したい!

こんにちは。システムデザイン開発の社員Kです。

選択項目を検索できるJavaScriptプラグイン「Select2」をご利用の方も多いかと思いますが、便利な反面、「半角文字で登録している項目が検索で引っかからない!」というご経験はありませんか?

今回は、「半角文字の項目を全角文字で検索できるようにする方法」について解説していきます。
こちらを読んでいただければ、「Select2」をさらに便利に利用できるようになるかと思いますので、ぜひご参考にしてみてください。

半角文字で登録している項目が検索で引っかからない!

Select2には選択項目の検索機能が付いており、この機能によって選択項目が大量にある場合でも目的のものを瞬時に探すことができる便利なプラグインです。

弊社が開発したWebシステムにも組み込むことがあり、利用したお客様からは、目的の項目が見つけやすく便利だと好評でしたが、「半角文字で登録している項目が検索で引っかからない!」という声が寄せられました。

このように、半角項目が出てきません。


Select2標準の検索処理は日本語を想定して作られているわけではないので、文字が完全一致していないと候補として出てきません。この検索処理をカスタマイズし、半角文字の項目を全角文字で検索できるよう改修を行ったので、その対応内容を今回はご紹介したいと思います。

前準備

まずは入力フォームを用意します。必要なプラグインはCDNで導入しました。
Select2はjQueryを使用するので、jQueryも追加してあげます(Select2より前で読み込んでください)。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>

        <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css" rel="stylesheet">
    </head>
    <body>
        <select class="select2" style="width: 500px">
            <option value="1">アイウエオ</option>
            <option value="2">アイウエオ</option>
            <option value="3">アイウエオ</option>
            <option value="4">abc123</option>
            <option value="5">abc123</option>
        </select>
    </body>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.full.min.js"></script>
    <script src="./custom-select2.js"></script>
</html>

カスタム用のjsファイル(custom-select2.jsとしました)を作成し、読み込むようにします。
このファイルにカスタマイズした検索関数定義やセレクトボックス要素へのSelect2適用処理を書いていきます。

基本的な動きは変更したくなかったため、元の関数をコピーし、必要な部分だけ変えることにしました。関数は以下のソースからコピーしました。
https://github.com/select2/select2/blob/master/src/js/select2/defaults.js

function matcher(params, data) {
    // Always return the object if there is nothing to compare
    if (params.term == null || params.term.trim() === "") {
        return data;
    }

    // Do a recursive check for options with children
    if (data.children && data.children.length > 0) {
        // Clone the data object if there are children
        // This is required as we modify the object to remove any non-matches
        var match = $.extend(true, {}, data);

        // Check each child of the option
        for (var c = data.children.length - 1; c >= 0; c--) {
            var child = data.children[c];

            var matches = matcher(params, child);

            // If there wasn't a match, remove the object in the array
            if (matches == null) {
                match.children.splice(c, 1);
            }
        }

        // If any children matched, return the new object
        if (match.children.length > 0) {
            return match;
        }

        // If there were no matching children, check just the plain object
        return matcher(params, match);
    }

    var original = stripDiacritics(data.text).toUpperCase();
    var term = stripDiacritics(params.term).toUpperCase();

    // Check if the text contains the term
    if (original.indexOf(term) > -1) {
        return data;
    }

    // If it doesn't contain the term, don't return anything
    return null;
}

関数名を「matcher」から変更します。今回は「customMatcher」としました。
再帰的に呼び出している箇所も変更します。

function customMatcher(params, data) {
    // Always return the object if there is nothing to compare
    if (params.term == null || params.term.trim() === "") {
        return data;
    }

    // Do a recursive check for options with children
    if (data.children && data.children.length > 0) {
        // Clone the data object if there are children
        // This is required as we modify the object to remove any non-matches
        var match = $.extend(true, {}, data);

        // Check each child of the option
        for (var c = data.children.length - 1; c >= 0; c--) {
            var child = data.children[c];

            var matches = customMatcher(params, child);

            // If there wasn't a match, remove the object in the array
            if (matches == null) {
                match.children.splice(c, 1);
            }
        }

        // If any children matched, return the new object
        if (match.children.length > 0) {
            return match;
        }

        // If there were no matching children, check just the plain object
        return customMatcher(params, match);
    }

    var original = stripDiacritics(data.text).toUpperCase();
    var term = stripDiacritics(params.term).toUpperCase();

    // Check if the text contains the term
    if (original.indexOf(term) > -1) {
        return data;
    }

    // If it doesn't contain the term, don't return anything
    return null;
}

このままでは処理で使用しているstripDiacritics関数が無いため、動きません。
外部からは呼び出せないようなので、こちらもコピーしてファイル内で定義します。

function customMatcher(params, data) {
    // Always return the object if there is nothing to compare
    if (params.term == null || params.term.trim() === "") {
        return data;
    }

    // Do a recursive check for options with children
    if (data.children && data.children.length > 0) {
        // Clone the data object if there are children
        // This is required as we modify the object to remove any non-matches
        var match = $.extend(true, {}, data);

        // Check each child of the option
        for (var c = data.children.length - 1; c >= 0; c--) {
            var child = data.children[c];

            var matches = customMatcher(params, child);

            // If there wasn't a match, remove the object in the array
            if (matches == null) {
                match.children.splice(c, 1);
            }
        }

        // If any children matched, return the new object
        if (match.children.length > 0) {
            return match;
        }

        // If there were no matching children, check just the plain object
        return customMatcher(params, match);
    }

    var original = stripDiacritics(data.text).toUpperCase();
    var term = stripDiacritics(params.term).toUpperCase();

    // Check if the text contains the term
    if (original.indexOf(term) > -1) {
        return data;
    }

    // If it doesn't contain the term, don't return anything
    return null;
}

// 発音記号除去
function stripDiacritics(text) {
    // Used 'uni range + named function' from http://jsperf.com/diacritics/18
    function match(a) {
        return DIACRITICS[a] || a;
    }

    return text.replace(/[^\u0000-\u007E]/g, match);
}

このstripDiacritics関数は、名前の通り発音記号を取り除く関数で、例えば「ă」を「a」に変換してくれるようです。この関数内で使用しているDIACRITICSがその対応表のようになっているみたいです。このデータは呼び出せるようなので関数内に定義し、動くようにします。

function customMatcher(params, data) {
    // Always return the object if there is nothing to compare
    if (params.term == null || params.term.trim() === "") {
        return data;
    }

    // Do a recursive check for options with children
    if (data.children && data.children.length > 0) {
        // Clone the data object if there are children
        // This is required as we modify the object to remove any non-matches
        var match = $.extend(true, {}, data);

        // Check each child of the option
        for (var c = data.children.length - 1; c >= 0; c--) {
            var child = data.children[c];

            var matches = customMatcher(params, child);

            // If there wasn't a match, remove the object in the array
            if (matches == null) {
                match.children.splice(c, 1);
            }
        }

        // If any children matched, return the new object
        if (match.children.length > 0) {
            return match;
        }

        // If there were no matching children, check just the plain object
        return customMatcher(params, match);
    }

    var original = stripDiacritics(data.text).toUpperCase();
    var term = stripDiacritics(params.term).toUpperCase();

    // Check if the text contains the term
    if (original.indexOf(term) > -1) {
        return data;
    }

    // If it doesn't contain the term, don't return anything
    return null;
}

// 発音記号除去
function stripDiacritics(text) {
    // Used 'uni range + named function' from http://jsperf.com/diacritics/18
    function match(a) {
        const DIACRITICS = $.fn.select2.amd.require('select2/diacritics');
        return DIACRITICS[a] || a;
    }

    return text.replace(/[^\u0000-\u007E]/g, match);
}

最後に、カスタマイズした関数を使用するように設定、セレクトボックスにSelect2を適用する処理を書けば、前準備は完了です。自分で定義した関数が検索に使用されるようになります。

$(function() {
    // デフォルト使用matcher変更
    $.fn.select2.defaults.defaults.matcher = customMatcher;

    // セレクトボックスにselect2を適用
    $(".select2").select2();
});

function customMatcher(params, data) {
    // Always return the object if there is nothing to compare
    if (params.term == null || params.term.trim() === "") {
        return data;
    }

    // Do a recursive check for options with children
    if (data.children && data.children.length > 0) {
        // Clone the data object if there are children
        // This is required as we modify the object to remove any non-matches
        var match = $.extend(true, {}, data);

        // Check each child of the option
        for (var c = data.children.length - 1; c >= 0; c--) {
            var child = data.children[c];

            var matches = customMatcher(params, child);

            // If there wasn't a match, remove the object in the array
            if (matches == null) {
                match.children.splice(c, 1);
            }
        }

        // If any children matched, return the new object
        if (match.children.length > 0) {
            return match;
        }

        // If there were no matching children, check just the plain object
        return customMatcher(params, match);
    }

    var original = stripDiacritics(data.text).toUpperCase();
    var term = stripDiacritics(params.term).toUpperCase();

    // Check if the text contains the term
    if (original.indexOf(term) > -1) {
        return data;
    }

    // If it doesn't contain the term, don't return anything
    return null;
}

// 発音記号除去
function stripDiacritics(text) {
    // Used 'uni range + named function' from http://jsperf.com/diacritics/18
    function match(a) {
        const DIACRITICS = $.fn.select2.amd.require('select2/diacritics');
        return DIACRITICS[a] || a;
    }

    return text.replace(/[^\u0000-\u007E]/g, match);
}

動くことが確認できたら、本題に入ります。

半角文字の項目を全角文字で検索できるようにする

ここで処理を見てみましょう。以下はmatcher関数のコードです。

    var original = stripDiacritics(data.text).toUpperCase();
    var term = stripDiacritics(params.term).toUpperCase();

    // Check if the text contains the term
    if (original.indexOf(term) > -1) {
        return data;
    }

    // If it doesn't contain the term, don't return anything
    return null;

検索欄に入力された文字列はparams.termに、各項目のテキストはdata.textに格納されています。

返り値に着目すると、検索欄に入力された文字と各項目のテキストを比較し、入力文字が含まれていればdataが、含まれていなければnullが返されています。
この判定を行っているif文に変換した文字の比較処理を追加してあげればよさそうです。

比較処理は、検索文字列と項目テキストの両方を半角に揃えて行うことにしました。
こうすることで、検索文字列が全角、項目テキストが半角のパターンだけでなく、その逆パターンや半角全角が混在する文字も引っかかるようになります。

全角から半角に変換する関数を定義します。
全角→半角の変換方法については、下記サイトのものを利用させていただきました。
https://qiita.com/spm84/items/4ea8c53ac3aafcd4d66c

$(function() {
    // デフォルト使用matcher変更
    $.fn.select2.defaults.defaults.matcher = customMatcher;

    // セレクトボックスにselect2を適用
    $(".select2").select2();
});

function customMatcher(params, data) {
    // Always return the object if there is nothing to compare
    if (params.term == null || params.term.trim() === "") {
        return data;
    }

    // Do a recursive check for options with children
    if (data.children && data.children.length > 0) {
        // Clone the data object if there are children
        // This is required as we modify the object to remove any non-matches
        var match = $.extend(true, {}, data);

        // Check each child of the option
        for (var c = data.children.length - 1; c >= 0; c--) {
            var child = data.children[c];

            var matches = customMatcher(params, child);

            // If there wasn't a match, remove the object in the array
            if (matches == null) {
                match.children.splice(c, 1);
            }
        }

        // If any children matched, return the new object
        if (match.children.length > 0) {
            return match;
        }

        // If there were no matching children, check just the plain object
        return customMatcher(params, match);
    }

    var original = stripDiacritics(data.text).toUpperCase();
    var term = stripDiacritics(params.term).toUpperCase();

    // Check if the text contains the term
    if (original.indexOf(term) > -1) {
        return data;
    }

    // If it doesn't contain the term, don't return anything
    return null;
}

// 発音記号除去
function stripDiacritics(text) {
    // Used 'uni range + named function' from http://jsperf.com/diacritics/18
    function match(a) {
        const DIACRITICS = $.fn.select2.amd.require('select2/diacritics');
        return DIACRITICS[a] || a;
    }

    return text.replace(/[^\u0000-\u007E]/g, match);
}

// 全角→半角変換
function convertZenkakuToHankaku(original) {
    // カナ変換対応表(半角に変換する用)
    const kanaMap = {
        "ガ": "ガ", "ギ": "ギ", "グ": "グ", "ゲ": "ゲ", "ゴ": "ゴ",
        "ザ": "ザ", "ジ": "ジ", "ズ": "ズ", "ゼ": "ゼ", "ゾ": "ゾ",
        "ダ": "ダ", "ヂ": "ヂ", "ヅ": "ヅ", "デ": "デ", "ド": "ド",
        "バ": "バ", "ビ": "ビ", "ブ": "ブ", "ベ": "ベ", "ボ": "ボ",
        "パ": "パ", "ピ": "ピ", "プ": "プ", "ペ": "ペ", "ポ": "ポ",
        "ヴ": "ヴ", "ヷ": "ヷ", "ヺ": "ヺ",
        "ア": "ア", "イ": "イ", "ウ": "ウ", "エ": "エ", "オ": "オ",
        "カ": "カ", "キ": "キ", "ク": "ク", "ケ": "ケ", "コ": "コ",
        "サ": "サ", "シ": "シ", "ス": "ス", "セ": "セ", "ソ": "ソ",
        "タ": "タ", "チ": "チ", "ツ": "ツ", "テ": "テ", "ト": "ト",
        "ナ": "ナ", "ニ": "ニ", "ヌ": "ヌ", "ネ": "ネ", "ノ": "ノ",
        "ハ": "ハ", "ヒ": "ヒ", "フ": "フ", "ヘ": "ヘ", "ホ": "ホ",
        "マ": "マ", "ミ": "ミ", "ム": "ム", "メ": "メ", "モ": "モ",
        "ヤ": "ヤ", "ユ": "ユ", "ヨ": "ヨ",
        "ラ": "ラ", "リ": "リ", "ル": "ル", "レ": "レ", "ロ": "ロ",
        "ワ": "ワ", "ヲ": "ヲ", "ン": "ン",
        "ァ": "ァ", "ィ": "ィ", "ゥ": "ゥ", "ェ": "ェ", "ォ": "ォ",
        "ッ": "ッ", "ャ": "ャ", "ュ": "ュ", "ョ": "ョ",
        "。": "。", "、": "、", "ー": "ー", "「": "「", "」": "」", "・": "・"
    };

    // 正規表現を作成
    const reg = new RegExp('(' + Object.keys(kanaMap).join('|') + ')', 'g');

    // 全角を半角に変換
    var hankaku = original
        .replace(/[A-Za-z0-9]/g, function (s) {
            return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
        })
        .replace(reg, function (match) {
            return kanaMap[match];
        })
        .replace(/゛/g, '゙')
        .replace(/゜/g, '゚');

    return hankaku;
}

続いて、customMatcher関数に検索文字列と項目テキストの半角変換処理を追加し、dataを返すif文の条件に半角変換後の文字列比較を追加します。

    var original = stripDiacritics(data.text).toUpperCase();
    var term = stripDiacritics(params.term).toUpperCase();

    // 全角→半角 変換
    var hankaku = convertZenkakuToHankaku(original);
    var term_hankaku = convertZenkakuToHankaku(term);

    // Check if the text contains the term
    if (original.indexOf(term) > -1 || hankaku.indexOf(term_hankaku) > -1) {
        return data;
    }

    // If it doesn't contain the term, don't return anything
    return null;

最終的に完成したコードがこちらです。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>

        <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css" rel="stylesheet">
    </head>
    <body>
        <select class="select2" style="width: 500px">
            <option value="1">アイウエオ</option>
            <option value="2">アイウエオ</option>
            <option value="3">アイウエオ</option>
            <option value="4">abc123</option>
            <option value="5">abc123</option>
        </select>
    </body>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.full.min.js"></script>
    <script src="./custom-select2.js"></script>
</html>
$(function() {
    // デフォルト使用matcher変更
    $.fn.select2.defaults.defaults.matcher = customMatcher;

    // セレクトボックス select2化
    $(".select2").select2();
});


function customMatcher(params, data) {
    // Always return the object if there is nothing to compare
    if (params.term == null || params.term.trim() === "") {
        return data;
    }

    // Do a recursive check for options with children
    if (data.children && data.children.length > 0) {
        // Clone the data object if there are children
        // This is required as we modify the object to remove any non-matches
        var match = $.extend(true, {}, data);

        // Check each child of the option
        for (var c = data.children.length - 1; c >= 0; c--) {
            var child = data.children[c];

            var matches = customMatcher(params, child);

            // If there wasn't a match, remove the object in the array
            if (matches == null) {
                match.children.splice(c, 1);
            }
        }

        // If any children matched, return the new object
        if (match.children.length > 0) {
            return match;
        }

        // If there were no matching children, check just the plain object
        return customMatcher(params, match);
    }

    var original = stripDiacritics(data.text).toUpperCase();
    var term = stripDiacritics(params.term).toUpperCase();

    // 全角→半角 変換
    var hankaku = convertZenkakuToHankaku(original);
    var term_hankaku = convertZenkakuToHankaku(term);

    // Check if the text contains the term
    if (original.indexOf(term) > -1 || hankaku.indexOf(term_hankaku) > -1) {
        return data;
    }

    // If it doesn't contain the term, don't return anything
    return null;
}

// 発音記号除去
function stripDiacritics(text) {
    // Used 'uni range + named function' from http://jsperf.com/diacritics/18
    function match(a) {
        const DIACRITICS = $.fn.select2.amd.require('select2/diacritics');
        return DIACRITICS[a] || a;
    }

    return text.replace(/[^\u0000-\u007E]/g, match);
}

// 全角→半角変換
function convertZenkakuToHankaku(original) {
    // カナ変換対応表(半角に変換する用)
    const kanaMap = {
        "ガ": "ガ", "ギ": "ギ", "グ": "グ", "ゲ": "ゲ", "ゴ": "ゴ",
        "ザ": "ザ", "ジ": "ジ", "ズ": "ズ", "ゼ": "ゼ", "ゾ": "ゾ",
        "ダ": "ダ", "ヂ": "ヂ", "ヅ": "ヅ", "デ": "デ", "ド": "ド",
        "バ": "バ", "ビ": "ビ", "ブ": "ブ", "ベ": "ベ", "ボ": "ボ",
        "パ": "パ", "ピ": "ピ", "プ": "プ", "ペ": "ペ", "ポ": "ポ",
        "ヴ": "ヴ", "ヷ": "ヷ", "ヺ": "ヺ",
        "ア": "ア", "イ": "イ", "ウ": "ウ", "エ": "エ", "オ": "オ",
        "カ": "カ", "キ": "キ", "ク": "ク", "ケ": "ケ", "コ": "コ",
        "サ": "サ", "シ": "シ", "ス": "ス", "セ": "セ", "ソ": "ソ",
        "タ": "タ", "チ": "チ", "ツ": "ツ", "テ": "テ", "ト": "ト",
        "ナ": "ナ", "ニ": "ニ", "ヌ": "ヌ", "ネ": "ネ", "ノ": "ノ",
        "ハ": "ハ", "ヒ": "ヒ", "フ": "フ", "ヘ": "ヘ", "ホ": "ホ",
        "マ": "マ", "ミ": "ミ", "ム": "ム", "メ": "メ", "モ": "モ",
        "ヤ": "ヤ", "ユ": "ユ", "ヨ": "ヨ",
        "ラ": "ラ", "リ": "リ", "ル": "ル", "レ": "レ", "ロ": "ロ",
        "ワ": "ワ", "ヲ": "ヲ", "ン": "ン",
        "ァ": "ァ", "ィ": "ィ", "ゥ": "ゥ", "ェ": "ェ", "ォ": "ォ",
        "ッ": "ッ", "ャ": "ャ", "ュ": "ュ", "ョ": "ョ",
        "。": "。", "、": "、", "ー": "ー", "「": "「", "」": "」", "・": "・"
    };

    // 正規表現を作成
    const reg = new RegExp('(' + Object.keys(kanaMap).join('|') + ')', 'g');

    // 全角を半角に変換
    var hankaku = original
        .replace(/[A-Za-z0-9]/g, function (s) {
            return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
        })
        .replace(reg, function (match) {
            return kanaMap[match];
        })
        .replace(/゛/g, '゙')
        .replace(/゜/g, '゚');

    return hankaku;
}

実際に動かしてみると…
半角文字の項目が出てくるようになりました!

まとめ

今回はSelect2の検索用関数をカスタマイズして、半角文字の項目を全角文字で検索できるようにする方法をご紹介しました。

この対応を応用すれば、例えばカタカナ項目をひらがなで検索したり、「㈱」を「株式会社」で検索できるようにするといったことも可能になると思います。ぜひ活用してみてください。


システムデザイン開発は、北海道の地で35年以上の歴史があります。企業向けのシステム設計~開発・構築~保守運用までワンストップサービスを提供するシステム開発会社です。豊富な開発実績と高い技術力を強みとして、北海道から全国へ幅広い分野・業種へトータルにサポートいたします。

システムの導入やご検討、お困りごとがありましたら、お気軽にご相談・お問合せください。

SDDの受託システムとは?

お問い合わせはこちら

タイトルとURLをコピーしました