Editor.jsについての備忘録3 インラインツールの作成

Editor.jsのインラインツールのカスタマイズ方法について備忘録としてまとめる。

インラインの取り込み

外部のインラインツールを取り込むのは、通常のブロックツールを取り込むときと同じ。(事前にscriptのsrcを読み込んでおく。)

const editor = new EditorJS({
       
   holder: 'editor',
   tools: {
       paragraph: { 
         class: Paragraph, 
         inlineToolbar: true 
       },
       p: { 
         class: CustParagraph, 
         inlineToolbar: true 
       } ,
       Marker: { //マーカーツールを読み込む。
         class: MarkerTool,
         shortcut: 'CMD+SHIFT+M',
       }
   }
});

キャプチャ

こんな感じにインポートできる。

インラインツールの作成

チュートリアルを参考に作成。


isInline、render, surround, checkstateが必要。例はマーカーツール。

class MarkerTool {

   static get isInline() {
       return true;
   }

   constructor() {
       this.button = null;
       this.state = false;
   }

   render() {
       this.button = document.createElement('button');
       this.button.type = 'button';
       this.button.textContent = 'M';

       return this.button;
   }

   surround(range) {
       if (this.state) {
           // If highlights is already applied, do nothing for now
           return;
       }

       const selectedText = range.extractContents();

       // Create MARK element
       const mark = document.createElement('MARK');

       // Append to the MARK element selected TextNode
       mark.appendChild(selectedText);

       // Insert new element
       range.insertNode(mark);
   }

  
   checkState(selection) {
       const text = selection.anchorNode;

       if (!text) {
           return;
       }

       const anchorElement = text instanceof Element ? text : text.parentElement;
     
       this.state = !!anchorElement.closest('MARK');
   }
}

isInlineでインラインツールかどうかを確認する。renderでボタンの外観処理、surroundで選択範囲の処理、checkStateで状態確認を行う。

出力に含めるためにはsanitaizeで通すものを指定する。

static get sanitize() {
       return {
           mark: {
               class: 'cdx-marker'
           }
       };
   }

sanitizeで通すタグを指定する。これを指定しないと保存時に反映されない。


基本的にはこれだけでも実装可能だが、取り消し処理やパラメータの設定などを追加すると使いやすくなる。特に取り消し処理は必須。

パラメータの設定は、選択するとinputが出てくるようにしてその値を読みだして実装する感じ。

renderActions() {
   this.colorPicker = document.createElement('input');
   this.colorPicker.type = 'color';
   this.colorPicker.value = '#f5f1cc';
   this.colorPicker.hidden = true;

   return this.colorPicker;
}

showActions(mark) {
   this.colorPicker.value = mark.style.backgroundColor || '#f5f1cc';

   this.colorPicker.onchange = () => {
       mark.style.backgroundColor = this.colorPicker.value;
   };
   this.colorPicker.hidden = false;
}

hideActions() {
   this.colorPicker.onchange = null;
   this.colorPicker.hidden = true;
}

checkState() {
   const mark = this.api.selection.findParentTag(this.tag);

   this.state = !!mark;

   if (this.state) {
       this.showActions(mark);
   } else {
       this.hideActions();
   }
}

完成形

class MarkerTool {

 static get isInline() {
   return true;
 }

 get state() {
   return this._state;
 }

 set state(state) {
   this._state = state;

   this.button.classList.toggle(this.api.styles.inlineToolButtonActive, state);
 }

 constructor({api}) {
   this.api = api;
   this.button = null;
   this._state = false;

   this.tag = 'MARK';
   this.class = 'cdx-marker';
 }

 render() {
   this.button = document.createElement('button');
   this.button.type = 'button';
   this.button.innerHTML = '<svg width="20" height="18"><path d="M10.458 12.04l2.919 1.686-.781 1.417-.984-.03-.974 1.687H8.674l1.49-2.583-.508-.775.802-1.401zm.546-.952l3.624-6.327a1.597 1.597 0 0 1 2.182-.59 1.632 1.632 0 0 1 .615 2.201l-3.519 6.391-2.902-1.675zm-7.73 3.467h3.465a1.123 1.123 0 1 1 0 2.247H3.273a1.123 1.123 0 1 1 0-2.247z"/></svg>';
   this.button.classList.add(this.api.styles.inlineToolButton);

   return this.button;
 }

 surround(range) {
   if (this.state) {
     this.unwrap(range);
     return;
   }

   this.wrap(range);
 }

 wrap(range) {
   const selectedText = range.extractContents();
   const mark = document.createElement(this.tag);

   mark.classList.add(this.class);
   mark.appendChild(selectedText);
   range.insertNode(mark);

   this.api.selection.expandToTag(mark);
 }

 unwrap(range) {
   const mark = this.api.selection.findParentTag(this.tag, this.class);
   const text = range.extractContents();

   mark.remove();

   range.insertNode(text);
 }


 checkState() {
   const mark = this.api.selection.findParentTag(this.tag);

   this.state = !!mark;
 
   if (this.state) {
     this.showActions(mark);
   } else {
     this.hideActions();
   }
 }

 renderActions() {
   this.colorPicker = document.createElement('input');
   this.colorPicker.type = 'color';
   this.colorPicker.value = '#f5f1cc';
   this.colorPicker.hidden = true;

   return this.colorPicker;
 }

 showActions(mark) {
   const {backgroundColor} = mark.style;
   this.colorPicker.value = backgroundColor ? this.convertToHex(backgroundColor) : '#f5f1cc';

   this.colorPicker.onchange = () => {
     mark.style.backgroundColor = this.colorPicker.value;
   };
   this.colorPicker.hidden = false;
 }

 hideActions() {
   this.colorPicker.onchange = null;
   this.colorPicker.hidden = true;
 }

 convertToHex(color) {
   const rgb = color.match(/(\d+)/g);

   let hexr = parseInt(rgb[0]).toString(16);
   let hexg = parseInt(rgb[1]).toString(16);
   let hexb = parseInt(rgb[2]).toString(16);

   hexr = hexr.length === 1 ? '0' + hexr : hexr;
   hexg = hexg.length === 1 ? '0' + hexg : hexg;
   hexb = hexb.length === 1 ? '0' + hexb : hexb;

   return '#' + hexr + hexg + hexb;
 }
static get sanitize() {
       return {
           mark: {
               class: 'cdx-marker'
           }
       };
   }
}

これでマーカーツールの出来上がり。

キャプチャ

パラメータをいくつか設定するともっと柔軟に対応できそう。

カスタムBlockを作るよりは楽かもしれない。

良ければサポートお願いします。サポート費用はサーバー維持などの開発費に使わせていただきます。