Editor.jsについての備忘録2 Block Toolの作成

今回はブロックツールの作成のメモです。(ほぼ翻訳)

Editor.jsは最低限の機能で構成されていて、好きなものを取り込めるようになっています。それを作るためのAPIも用意されているとのこと。今回はブロック要素ツールの作り方についてまとめていきます。

1. 準備

3つのファイルを用意する。(例ではsimple-imgというブロックを作る)

simple-image.js
simple-image.css
example.html

example.htmlで動作テストする。

<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"></script>
<script src="simple-image.js"></script>
<link href="simple-image.css" rel="stylesheet"/>

<div id="editorjs"></div>

<script>
   const editor = new EditorJS({
     autofocus: true,
     tools: {
       image: SimpleImage
     }
   });
</script>

そこでsimple-image.js, simple-image.cssを読み込んでおく。

2. RenderとSaveとアイコン

Editor.jsでブロックツールを作るには最低限 RenderとSaveメソッドを作る必要がある。それに加えてツールboxのアイコン。

Render:ツールボックスから選択したときに追加されるブロック図
Save:ブロックの保存形式

少なくともそれさえ作って、simple-image.jsに組み込めば動く。

class名にしたSimpleImageがhtmlで指定するtoolと結びつく感じ。

class SimpleImage {
 static get toolbox() {
   return {
     title: 'Image',
     icon: '<svg width="17" height="15" viewBox="0 0 336 276" xmlns="http://www.w3.org/2000/svg"><path d="M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-66 39v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z"/></svg>'
   };
 }

 render(){
   return document.createElement('input');
 }

 save(blockContent){
   return {
     url: blockContent.value
   }
 }
}

document.createElementでインプットboxを作って、そこの値をurl: blockContent.valueで取り出す感じ。

結構簡単に作れるみたいですね。

3. 保存データの利用

JSONで返ってきた値をHTMLに変換する仕組みを作ります。jsファイルにconstructorメソッドを追加。あとrenderでclass付与をしていきます。

constructor({data}){
   this.data = data;
 }

 render(){
   const wrapper = document.createElement('div');
   const input = document.createElement('input');

   wrapper.classList.add('simple-image');
   wrapper.appendChild(input);

   input.placeholder = 'Paste an image URL...';
   input.value = this.data && this.data.url ? this.data.url : '';

   return wrapper;
 }

div要素を新たに作ってそれに入れ込んでる。class付与もしている。


なお?は 条件演算子と呼ばれるもので以下の処理を簡単に書くことができるみたい。へー、便利。

条件式 ? Trueの処理 : Falseの処理

あとはcssに付与したクラスのスタイルを定めればOK

.simple-image {
   padding: 20px 0;
}

.simple-image input {
   width: 100%;
   padding: 10px;
   border: 1px solid #e4e4e4;
   border-radius: 3px;
   outline: none;
   font-size: 14px;
}

ちゃんとclassも付与できるのですね。すごいよく考えられています。

4. 値の検証

今のままだと入力ミスした場合なんかでも保存されるので、それを防ぐために値を満たしているか検証することもできます。

validate(savedData){
   if (!savedData.url.trim()){
     return false;
   }

   return true;
 }

validateメソッドを設定して、trueかfalseでスキップできるようになります。

5. Viewの変更

視覚効果を変えます。まずはwrapperにアクセスできるようにリターンを設定。あとaddEventListenerを追加。

render(){
   this.wrapper = document.createElement('div');
   const input = document.createElement('input');

   this.wrapper.classList.add('simple-image');
   this.wrapper.appendChild(input);

   input.placeholder = 'Paste an image URL...';
   input.value = this.data && this.data.url ? this.data.url : '';

   input.addEventListener('paste', (event) => {
     this._createImage(event.clipboardData.getData('text'));
   });

   return this.wrapper;
 }

貼り付けられたときにテキストを取得する部分の追加。

  _createImage(url){
   const image = document.createElement('img');
   const caption = document.createElement('input');

   image.src = url;
   caption.placeholder = 'Caption...';

   this.wrapper.innerHTML = '';
   this.wrapper.appendChild(image);
   this.wrapper.appendChild(caption);
 }

そしたらWrapperのところにイメージとキャプションを追加する。

Captionを増やしたのでSaveのところにCaptionを追加する。

save(blockContent){
   const image = blockContent.querySelector('img');
   const caption = blockContent.querySelector('input');

   return {
     url: image.src,
     caption: caption.value
   }
 }

保存したDataがある場合のための処理も加える。renderの部分に

  if (this.data && this.data.url){
    this._createImage(this.data.url, this.data.caption);
    return this.wrapper;
  }

 _createImage(url, captionText){
   const image = document.createElement('img');
   const caption = document.createElement('input');

   image.src = url;
   caption.placeholder = 'Caption...';
  caption.value = captionText || '';

   this.wrapper.innerHTML = '';
   this.wrapper.appendChild(image);
   this.wrapper.appendChild(caption);
 }

こんな感じで処理を追加していく。

6. インラインツールバーの設定

ブロックの中で使用できるツールバーを設定していく。

_createImage(url, captionText){
 const image = document.createElement('img');
 const caption = document.createElement('div');

 image.src = url;
 caption.contentEditable = true;
 caption.innerHTML = captionText || '';

 this.wrapper.innerHTML = '';
 this.wrapper.appendChild(image);
 this.wrapper.appendChild(caption);
}

save(blockContent){
 const image = blockContent.querySelector('img');
 const caption = blockContent.querySelector('[contenteditable]');

 return {
   url: image.src,
   caption: caption.innerHTML || ''
 }
}

インラインツールバーが利用できるようにdiv要素を追加。

ツールバーはeditorjsを作るときに設定する。

<!-- HTML code of example -->
<script>
const editor = new EditorJS({
 autofocus: true,
 tools: {
   image: {
     class: SimpleImage,
     inlineToolbar: true
   }
 },
 // ... data
});

// ... saving button handler
</script>

ここのinlineToolbar: true。

限定する場合はinlineToolbar: ['link']のようにする。

なお、初期値ではlink, bold, italic.が使えるようになっている。

7. ブロックメニューの設定

ブロックに適用できるメニューを設定する。

constructor({data}){
     this.data = {
    url: data.url || '',
    caption: data.caption || '',
    withBorder: data.withBorder !== undefined ? data.withBorder : false,
    withBackground: data.withBackground !== undefined ? data.withBackground : false,
    stretched: data.stretched !== undefined ? data.stretched : false,
  };
   this.wrapper = undefined;
   this.settings = [
     {
       name: 'withBorder',
       icon: `<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M15.8 10.592v2.043h2.35v2.138H15.8v2.232h-2.25v-2.232h-2.4v-2.138h2.4v-2.28h2.25v.237h1.15-1.15zM1.9 8.455v-3.42c0-1.154.985-2.09 2.2-2.09h4.2v2.137H4.15v3.373H1.9zm0 2.137h2.25v3.325H8.3v2.138H4.1c-1.215 0-2.2-.936-2.2-2.09v-3.373zm15.05-2.137H14.7V5.082h-4.15V2.945h4.2c1.215 0 2.2.936 2.2 2.09v3.42z"/></svg>`
     },
     {
       name: 'stretched',
       icon: `<svg width="17" height="10" viewBox="0 0 17 10" xmlns="http://www.w3.org/2000/svg"><path d="M13.568 5.925H4.056l1.703 1.703a1.125 1.125 0 0 1-1.59 1.591L.962 6.014A1.069 1.069 0 0 1 .588 4.26L4.38.469a1.069 1.069 0 0 1 1.512 1.511L4.084 3.787h9.606l-1.85-1.85a1.069 1.069 0 1 1 1.512-1.51l3.792 3.791a1.069 1.069 0 0 1-.475 1.788L13.514 9.16a1.125 1.125 0 0 1-1.59-1.591l1.644-1.644z"/></svg>`
     },
     {
       name: 'withBackground',
       icon: `<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10.043 8.265l3.183-3.183h-2.924L4.75 10.636v2.923l4.15-4.15v2.351l-2.158 2.159H8.9v2.137H4.7c-1.215 0-2.2-.936-2.2-2.09v-8.93c0-1.154.985-2.09 2.2-2.09h10.663l.033-.033.034.034c1.178.04 2.12.96 2.12 2.089v3.23H15.3V5.359l-2.906 2.906h-2.35zM7.951 5.082H4.75v3.201l3.201-3.2zm5.099 7.078v3.04h4.15v-3.04h-4.15zm-1.1-2.137h6.35c.635 0 1.15.489 1.15 1.092v5.13c0 .603-.515 1.092-1.15 1.092h-6.35c-.635 0-1.15-.489-1.15-1.092v-5.13c0-.603.515-1.092 1.15-1.092z"/></svg>`
     }
   ];
 }

  renderSettings(){
   const wrapper = document.createElement('div');

   this.settings.forEach( tune => {
     let button = document.createElement('div');

     button.classList.add('cdx-settings-button');
     button.innerHTML = tune.icon;
     wrapper.appendChild(button);
   });

   return wrapper;
 }

save(blockContent){
   const image = blockContent.querySelector('img');
   const caption = blockContent.querySelector('input');

  return Object.assign(this.data, {
     url: image.src,
     caption: caption.value
  
  });
 }

_toggleTune(tune) {
   this.data[tune] = !this.data[tune];
 }

renderSettingsメソッドを使って実現する。
constructorで名前とアイコンを設定。そうするとブロック要素を選択した時に設定したアイコンが表示される。

表示を変えるためにはトグルでclassを変化させてcssを変える感じ。

  _toggleTune(tune) {
   this.data[tune] = !this.data[tune];
   this._acceptTuneView();
 }

_acceptTuneView() {
   this.settings.forEach( tune => {
     this.wrapper.classList.toggle(tune.name, !!this.data[tune.name]);
   });
 }
.simple-image.withBorder img {
   border: 1px solid #e8e8eb;
}

.simple-image.withBackground {
   background: #eff2f5;
   padding: 10px;
}

.simple-image.withBackground img {
   display: block;
   max-width: 60%;
   margin: 0 auto 15px;
}

8. APIへのアクセス

editor.jsのapiにアクセスするにはapiパラメータを使用

constructor({data, api}){
   this.api = api;
   // ...
 }
_acceptTuneView() {
 this.settings.forEach( tune => {
   this.wrapper.classList.toggle(tune.name, !!this.data[tune.name]);

   if (tune.name === 'stretched') {
     this.api.blocks.stretchBlock(this.api.blocks.getCurrentBlockIndex(), !!this.data.stretched);
   }
 });
}

この辺りよくわかってない。

9. サニタイズ

HTMLコードを含めることができるのでそれを無害化するための処理も含まれている。

sanitizer.clean()で保存時に有効なフィールドを設定できる。

const sanitizerConfig = {
    b: true, 
    a: {
      href: true
    },
    i: true
  };

まずsaveのところで有効にできるフィールドを設定する。

caption: this.api.sanitizer.clean(caption.innerHTML || '', sanitizerConfig)

でsave時に他の要素を除去する。

これで余計な要素が入り込まないようになる。

あとは自動で設定するやり方もある

static get sanitize(){
   return {
     url: false, // disallow HTML
     caption: {} // only tags from Inline Toolbar 
   }


こんな感じでカスタムできるとのこと。まだあんまり実感わかないけど簡単に試してみようと思う。



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