BeginnerEngineerBlog
中の人
中の人

【Quill.js】カスタムアイコン追加してカスタム要素を挿入する

公開: 2023-03-04 02:03
更新: 2024-05-17 23:48
2025
quill react html カスタマイズ 1.3.7
Quill.jsでカスタムアイコンをツールバーに追加して要素を挿入するやり方わかったので紹介します


~~~~~
Is it dead?

Yes



こんにちは!

このissueのやりとりが好きです中の人です。

Quill.jsはこのBeginnerEngineerBlogでも利用している神リッチテキストエディタライブラリですが、冒頭のissueの通りサポートが切れたライブラリです(多分)

20240517追記
quill.jsのversion2.0がリリースされてたでおいぃぃぃぃ!!
https://quilljs.com/

で、quillはツールバーにカスタムアイコン追加したり、フォーマットをオリジナルにしたりとカスタマイズできるってのは知ってはいたのですが、実際どうやるかよくわからなかったのと面倒臭いと避けていたのですが、最近になってなんとなくやり方がわかったので、自分のメモとして紹介したいと思います!

ちなみに冒頭のissueのようにサポートが切れた(多分)ライブラリなので、今からリッチテキストエディタなにか使おうと考えている方はいろいろと他のライブラリを探すことをおすすめします!(結構バグがあります)
なぜ私はこのライブラリを選択したかと言うと、単純に一目惚れして他を探さなかったからです笑
(Editor.jsとかDraft.jsとかすごい良さそうな感じです。あと忘れてしまったのですが、QuillをForkして開発したライブラリも確かあった気がします(もしかしたら前述のライブラリがそうかも。。忘れた)。気になった方は調べてみてください。)

変更履歴
20230320
reactのsizeのselectの箇所にwarningが発生していたので修正しました
20230827
reactでQuillのimportが抜けていたので追加しました

仕様


ということでどんなカスタマイズするかですが、


こんな感じでカスタムアイコン追加してカスタム要素を挿入したいと思います


HTMLとJS


ということで単純にHTMLとjsを使ったやり方ですが、順を追ってやっていきます

20230827追記
githubに公開しました。デモはこちら

HTMLファイルを適当に作成


まずhtmlファイルを適当に作成します

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>quillテスト</title>
        <!-- 👇 Quill.jsと必要なcssファイルをcdnでインポート -->
        <script src="https://cdn.quilljs.com/1.3.7/quill.js"></script>
        <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
    </head>
    <body>
        <style>
            /* 👇 見やすいように中央寄せに整える */
            #app-container {
                margin: 0 auto;
                width: 50%;
                margin-top: 20vh;
            }
        </style>
        <div id="app-container">
            html
            <!-- 👇 quillを適用する要素の上にこんな感じで要素を組むとツールバーになる -->
            <!-- 詳細は https://quilljs.com/docs/modules/toolbar/ -->
            <div id="toolbar">
                <span class="ql-formats">
                    <select class="ql-size">
                        <option value="small"></option>
                        <option selected></option>
                        <option value="large"></option>
                        <option value="huge"></option>
                    </select>
                </span>
                <span class="ql-formats">
                    <button class="ql-code-block"></button>
                </span>
            </div>
            <!-- 👇 quillを適用する要素 -->
            <div id="app"></div>
        </div>

        <script>
            // 👇 quillを作成する      👇 quillの対象になる要素のid
            const quill = new Quill('#app', {
                // 👇 snow -> 入力モード bubble -> 参照(?)モード
                theme: 'snow',
                modules: {
                    toolbar: {
                                 // 👇 quillのツールバーはこいつですよid
                        container: '#toolbar',
                    }
                },
                // 👇 quillで適用する装飾のホワイトリスト(よくわからない方はありなしでツールバーのボタンをぽちぽちするのだ!)
                formats: [
                    'size', 'code-block',
                ]
            });
        </script>
    </body>
</html>


(ドキュメントと色々と調べてください🙇)

この時点で


こんな感じでquillによってテキストエディタが作成されます


アイコン追加


そしたら次にカスタムアイコンを追加します

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>quillテスト</title>
        <script src="https://cdn.quilljs.com/1.3.7/quill.js"></script>
        <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
        <!-- 👇 font-awesomeインポート -->
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css">
    </head>
    <body>
        <style>
            #app-container {
                margin: 0 auto;
                width: 50%;
                margin-top: 20vh;
            }
        </style>
        <div id="app-container">
            html
            <div id="toolbar">
                <span class="ql-formats">
                    <select class="ql-size">
                        <option value="small"></option>
                        <option selected></option>
                        <option value="large"></option>
                        <option value="huge"></option>
                    </select>
                </span>
                <span class="ql-formats">
                    <button class="ql-code-block"></button>
                </span>
                <!-- 👇 button追加 -->
                <span class="ql-formats">
                                   <!-- 👇 class名の命名規則はすぐ下の"クリック時の挙動を定義する"のcode-block内のコメントアウトを読むのだ! -->
                    <button class="ql-addDivBlot">
                        <!-- 👇 font-awesomeじゃなくても良い(気になる方は調べてみるのだ!) -->
                        <i class="fa-sharp fa-solid fa-square-check"></i>
                    </button>
                </span>
            </div>
            <div id="app"></div>
        </div>


        <script>
            const quill = new Quill('#app', {
                theme: 'snow',
                modules: {
                    toolbar: {
                        container: '#toolbar',
                    }
                },
                formats: [
                    'size', 'code-block',
                ]
            });
        </script>
    </body>
</html>



この時点で


良い感じにアイコンが追加されました


クリック時の挙動を定義する


次に追加したアイコンをクリックした時の挙動を制御します

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>quillテスト</title>
        <script src="https://cdn.quilljs.com/1.3.7/quill.js"></script>
        <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css">
    </head>
    <body>
        <style>
            #app-container {
                margin: 0 auto;
                width: 50%;
                margin-top: 20vh;
            }
        </style>
        <div id="app-container">
            html
            <div id="toolbar">
                <span class="ql-formats">
                    <select class="ql-size">
                        <option value="small"></option>
                        <option selected></option>
                        <option value="large"></option>
                        <option value="huge"></option>
                    </select>
                </span>
                <span class="ql-formats">
                    <button class="ql-code-block"></button>
                </span>
                <span class="ql-formats">
                    <button class="ql-addDivBlot">
                        <i class="fa-sharp fa-solid fa-square-check"></i>
                    </button>
                </span>
            </div>
            <div id="app"></div>
        </div>

        <script>
            const quill = new Quill('#app', {
                theme: 'snow',
                modules: {
                    toolbar: {
                        container: '#toolbar',
                        // 👇 handlersにql-"クラス名"に対応するfunction名を登録する
                        handlers: {
                            'addDivBlot': funcAddDivBlot,
                        }
                    }
                },
                formats: [
                    'size', 'code-block',
                ]
            });

            // 👇 handlersに登録したfunctionを定義する
            function funcAddDivBlot () {
                // 👇 一旦デバッグ用でこんな感じにしておく
                console.log('hello beginner engineer!');
            }
        </script>
    </body>
</html>

この時点の動きは


こんな感じでクリックしたら反応するfunctionが定義されました


カスタム要素を定義して挿入


個人的にここがわかりにくく混乱して全裸で外を走り回ろうとした箇所ですが

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>quillテスト</title>
        <script src="https://cdn.quilljs.com/1.3.7/quill.js"></script>
        <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css">
    </head>
    <body>
        <style>
            #app-container {
                margin: 0 auto;
                width: 50%;
                margin-top: 20vh;
            }
        </style>
        <div id="app-container">
            html
            <div id="toolbar">
                <span class="ql-formats">
                    <select class="ql-size">
                        <option value="small"></option>
                        <option selected></option>
                        <option value="large"></option>
                        <option value="huge"></option>
                    </select>
                </span>
                <span class="ql-formats">
                    <button class="ql-code-block"></button>
                </span>
                <span class="ql-formats">
                    <button class="ql-addDivBlot">
                        <i class="fa-sharp fa-solid fa-square-check"></i>
                    </button>
                </span>
            </div>
            <div id="app"></div>
        </div>


        <script>
            // 👇 BlockEmbedなるものをQuillから取得(これ以外にもimportできますが、どこを参照してimportして良いのか未だによくわかってないのでネットサーフィンを駆使して探し出すのだ!)
            const BlockEmbed = Quill.import('blots/block/embed');
            // 👇 insertしたい要素のclassを取得した親を継承して定義
            class DivBlot extends BlockEmbed {
                // 👇 おまじない
                static create (value) {
                    console.log(value);
                    // 👇 おまじないでnodeを取得
                    let node = super.create();
                    // 👇 nodeを返す(insertされる要素になります)
                    return node;
                }
            }
            // 👇 このblot(?)の名前は"div_blot"です(formatに登録する名前)
            DivBlot.blotName = 'div_blot';
            // 👇 このblot(?)のタグは"div"です("div" -> <div></div> "divdiv" -> <divdiv></divdiv>)
            DivBlot.tagName = 'div';
            // 👇 このblot(?)のclass名は"engineer_blog"です
            DivBlot.className = 'engineer_blog';

            // 👇 Quillにこのblotを登録します(もうよくわからん! > ヾ(*・∀・)/)
            Quill.register(DivBlot);
            const quill = new Quill('#app', {
                theme: 'snow',
                modules: {
                    toolbar: {
                        container: '#toolbar',
                        handlers: {
                            'addDivBlot': funcAddDivBlot,
                        }
                    }
                },
                formats: [
                                          // 👇 insertしたい要素名を登録
                    'size', 'code-block', 'div_blot',
                ]
            });
                        

            function funcAddDivBlot () {
                // 👇 selectionなるものをquillから取得  👇 おまじない
                const selection = quill.getSelection(true);
                // 👇 キャレットが当たっている位置を取得
                let cursor_index = selection.index;
                // 👇 embedを挿入します 👇 キャレットの位置に👇 div_blotの要素(format)を 👇 {}の引数を与えます
                quill.insertEmbed(cursor_index, 'div_blot', {text: "BeginnerEngineerBlog\nよろしくお願いします。"});
                // 👇 キャレットの位置を挿入した要素の一つ後ろに移動します
                quill.setSelection(cursor_index + 1);
            }
        </script>
    </body>
</html>

わけわからんよねぇぇぇ!!(☞三☞ ఠ ਉ ఠ))☞三☞

で、今の挙動は




良い感じにdiv要素が挿入されてますね!
また、カスタム要素を作成するclassのcreateメソッドに引数が届いているのも確認できます
ここまできたらゴールは目の前です


カスタム要素を好きにカスタマイズする


<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>quillテスト</title>
        <script src="https://cdn.quilljs.com/1.3.7/quill.js"></script>
        <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css">
    </head>
    <body>
        <style>
            #app-container {
                margin: 0 auto;
                width: 50%;
                margin-top: 20vh;
            }
        </style>
        <div id="app-container">
            html
            <div id="toolbar">
                <span class="ql-formats">
                    <select class="ql-size">
                        <option value="small"></option>
                        <option selected></option>
                        <option value="large"></option>
                        <option value="huge"></option>
                    </select>
                </span>
                <span class="ql-formats">
                    <button class="ql-code-block"></button>
                </span>
                <span class="ql-formats">
                    <button class="ql-addDivBlot">
                        <i class="fa-sharp fa-solid fa-square-check"></i>
                    </button>
                </span>
            </div>
            <div id="app"></div>
        </div>

        <script>
            const BlockEmbed = Quill.import('blots/block/embed');
            class DivBlot extends BlockEmbed {
                static create (value) {
                    let node = super.create();
                    // 👇 好きなようにカスタマイズ
                    node.style.display = 'flex';
                    let img = document.createElement('img');
                    img.src = 'https://begien.com/image/beginner_engineer_blog.png';
                    img.alt = 'begien.com';
                    img.style.width = '50%';
                    img.style.height = 'auto';
                    node.appendChild(img);
                    let span = document.createElement('span');
                    span.style.display = 'flex';
                    span.style.alignItems = 'center';
                    span.innerText = value.text;
                    node.appendChild(span);
                    return node;
                }
            }
            DivBlot.blotName = 'div_blot';
            DivBlot.tagName = 'div';
            DivBlot.className = 'engineer_blog';

            Quill.register(DivBlot);
            const quill = new Quill('#app', {
                theme: 'snow',
                modules: {
                    toolbar: {
                        container: '#toolbar',
                        handlers: {
                            'addDivBlot': funcAddDivBlot,
                        }
                    }
                },
                formats: [
                    'size', 'code-block', 'div_blot',
                ]
            });
            
            function funcAddDivBlot () {
                const selection = quill.getSelection(true);
                let cursor_index = selection.index;
                quill.insertEmbed(cursor_index, 'div_blot', {text: "BeginnerEngineerBlog\nよろしくお願いします。"});
                quill.setSelection(cursor_index + 1);
            }
        </script>
    </body>
</html>

完成しました


htmlとjsでの実装は以上になります


React


そしたら次にreactでの方法です

※ reactでQuill.jsを利用するにはreact専用のモジュールのインストールが必要になります
こちらを参考にインストールしてください https://www.npmjs.com/package/react-quill#with-webpack-or-create-react-app

基本的にhtmlと同じ感じなので見比べながらやってみてください

20230827
githubに公開しました。デモはこちら

import './App.css';
import {useState} from 'react';
// 👇 react-quillを使うためのモジュールをimport
import ReactQuill, {Quill} from 'react-quill';
import 'react-quill/dist/quill.snow.css';
// 👇 fontawesomeの導入はこちらがわかりやすいのだ! https://qiita.com/stin_dev/items/5755e14805e60718620c
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSquareCheck } from '@fortawesome/free-regular-svg-icons';

const formats = [
    'code-block',
    'size',
    'div_blot',
];

function CustomIcon () {
    return (
        <FontAwesomeIcon icon={faSquareCheck} />
    );
}

const modules = {
    toolbar: {
        container: "#toolbar",
        handlers: {
            "addDivBlot": funcAddDivBlot,
        }
    },
};

const BlockEmbed = Quill.import('blots/block/embed');
class DivBlot extends BlockEmbed {
    static create (value) {
        let node = super.create();
        node.style.display = 'flex';
        let img = document.createElement('img');
        img.src = 'https://begien.com/image/BeginnerEngineerBlogTopImage.png';
        img.alt = 'begien.com';
        img.style.width = '50%';
        img.style.height = 'auto';
        node.appendChild(img);
        let span = document.createElement('span');
        span.style.display = 'flex';
        span.style.alignItems = 'center';
        span.innerText = value.text;
        node.appendChild(span);
        return node;
    }
}

DivBlot.blotName = 'div_blot';
DivBlot.tagName = 'div';
DivBlot.className = 'beginner_engineer';

Quill.register(DivBlot, true);

function funcAddDivBlot () {
    const quill = this.quill;
    const selection = quill.getSelection(true);
    let cursor_index = selection.index;
    quill.insertEmbed(cursor_index, 'div_blot', {text: "BeginnerEngineerBlog\nよろしくお願いします。"});
    quill.setSelection(cursor_index + 1);
}

function App() {
    let app_style = {
        margin: '0 auto',
        width: '50%',
        marginTop: '20vh',
    };
    const [value, setValue] = useState('');


    return (
        <div className="App" style={app_style}>
            <div style={{textAlign: 'left'}}>react</div>
            <QuillToolbar
            />
            <ReactQuill
                theme="snow"
                value={value}
                onChange={setValue}
                modules={modules}
                formats={formats}
            />
        </div>
    );
}

function QuillToolbar (props) {
    return (
        <div id="toolbar" style={{display: 'flex'}}>
            <span className="ql-formats">
                <select className="ql-size" defaultValue="normal">
                    <option value="small"></option>
                    <option value="normal"></option>
                    <option value="large"></option>
                    <option value="huge"></option>
                </select>
            </span>
            <span className="ql-formats">
                <button className="ql-code-block"></button>
            </span>
            <span className="ql-formats">
                <button className="ql-addDivBlot">
                    <CustomIcon />
                </button>
            </span>
        </div>
    );
}

export default App;

これの実行結果は


良い感じですね!

以上で紹介を終わります!


おわりに


意外と複雑でちょっとずつデバッグしながらやったらできました

今回はカスタム要素の挿入の紹介でしたが、他にも選択中のテキストのカスタマイズだったり色々とできます

VueやAngularなどほぼ触ったことないのでこれらを利用する場合は同じようにやれば多分できると思います

Quillユーザーは参考にしてください!

また何度も言いますが、冒頭でも言ったようにサポート切れたであろうライブラリなので、これから利用を考えている方はよくよく他のライブラリを探してよく考えてから利用することをおすすめします!

20240517追記
冒頭に述べましたがversion2.0がリリースされました!🎉
今の所このサイトではversion2.0にアップデートしようと思ったのですがcode-blockが表示されずアップデートは諦めています(´・ω・`)
これからquill.jsを利用しようかと思っている方は2.0の利用をおすすめします!

ではでは
0
0
0
0
通信エラーが発生しました。
似たような記事