2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Handsontable】RichTextエディターによる入力(CKEditor5)

Last updated at Posted at 2020-07-19

はじめに

前記事「【Handsontable】RichTextエディターによる入力(CKEditor4)」では、RichTextエディターとして「CKEditor4」を用いていましたが、現在は「CKEditor5」がリリースしていますので、CKEditor5 に対応してみました。

CKEditor4とCKEditor5の違い

機能的な内容は下記記事を参照してください。
What is different about CKEditor 5 compared to CKEditor 4?

ツールバーのカスタマイズ

今回のHandsontableのRichTextエディターとしては、CKEditor4がツールバーのフル状態から必要なものを除外していけたのですが、CKEditor5では標準ツールバーに取消線やテキストカラーは存在せず、追加するには以下の手順が必要になります。
CKEditor5 adding-a-plugin-to-a-build

  1. ckeditor5-editor-classic のリポジトリを clone してくる
  2. 追加したいプラグインをインストール
  3. ClassicEditor.builtinPlugins に 2. のプラグインを追加
  4. build する

今回は一般に公開する上ではツールバーは標準のままとするため、アンドゥとリドゥとボールドのみとしました。取消線とテキストカラーは除外しています。

戻り値の編集

CKEditor4版では、改行コードと先頭のPタグと末尾のPタグを除外しています。

CKEditor4版
RichTextEditor.prototype.getValue = function () {
  var val = CKEDITOR.instances[this.TEXTAREA.id].getData();
  val = val.replace(/\r?\n/g,"");
        val = val.replace(/^<P>/i, "");
        val = val.replace(/<\/P>$/i, "");
  return val;
};

CKEditor5版では、改行コードとPタグとnbspを除外しています。なんか、Pタグとか無駄にセットされてしまうんですよね。

CKEditor5版
RichTextEditor.prototype.getValue = function () {
  var val = myEditor.getData();
  val = val.replace(/\r?\n/g,"");
        val = val.replace(/&nbsp;/ig, "");
        val = val.replace(/<P>/ig, "");
        val = val.replace(/<\/P>/ig, "");
  return val;
};

セル移動の無効化

CKEditor4版では発生しなかったのですが、セルのEnterキーで起動させた場合に矢印キーを押すと画面が閉じてセル側が移動してしまいます。またBSキーを押すと画面が閉じてセルの中身が消えてしまいます。
しかし、セルをダブルクリックで起動した場合は矢印キーでキャレットを移動させることができるし、BSキーで文字を消すことができます。

この対処方法が分からなく原点の使い方を見直したところ、「矢印キーの入力によるセル移動を無効にする」の記載を見つけて、なんとか対処することができました。
Handsontable 使い方メモ1(基本)

Handsontable.hooks.add('beforeKeyDown', function (e) {
  var editor = this.getActiveEditor();

  if (editor.cellProperties.editor != 'RichText') return;

  if (editor.isOpened() && (isArrowKey(e.keyCode) || e.keyCode == 13 || e.keyCode == 8)) {
    e.stopImmediatePropagation();
  }
});

function isArrowKey(keyCode) {
  return Handsontable.helper.isKey(keyCode, 'ARROW_UP|ARROW_DOWN|ARROW_LEFT|ARROW_RIGHT');
}

表示位置

現在は画面中央に表示位置を指定していますが、ドロップボックスのリストのようにセルの直下にするようにも出来ます。
ただし、セルの位置によって表示位置を制御するようなことまではしてません。

// cke_myid.style.top = (document.documentElement.clientHeight - cke_myid.clientHeight) / 2 + 'px';
// cke_myid.style.left = (document.documentElement.clientWidth - cke_myid.clientWidth) / 2 + 'px';
cke_myid.style.top = that.TD.offsetTop + that.TD.offsetHeight + 'px';
cke_myid.style.left = that.TD.offsetLeft + 'px';

デモ

セル上でEnterキーを押すかダブルクリックすればRichTextエディターが起動します。
改行は、Shift + Enter になります。

デモの作成には「ScreenToGif」を使用しています。
CKEditor5.gif

CodePen

QiitaではCodePenが埋め込み可能なので貼っておきます。
jsdo.it の移行先として CodePen を使ってみる

See the Pen Rich text Editor(CKEditor5) with handsontable by やじゅ (@yaju-the-encoder) on CodePen.

ソースコード

<html>
<head>
<meta charset="UTF-8">
<title>RichText with Handsontable</title>
<link href="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.full.min.css" rel="stylesheet" >
<style>
.ck-editor__editable {
    min-height: 200px;
}
.ck.ck-editor {
    min-width: 400px;
}
</style>
</head>
<body>
<script src="https://cdn.ckeditor.com/ckeditor5/20.0.0/classic/ckeditor.js"></script>
<script src="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.full.min.js"></script>
<div id="hot"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
  let hot;

  const woData = [
    {
      0: 100,
      1: '通常テキスト',
      2: 'あいうえお',
      3: 'あいうえお<br><strong>かきくけこ</strong>',
    },
  ];

  const customColumns =
  [
    {data: 0, type: 'numeric'},
    {data: 1, type: 'text'},
    {data: 2, renderer: RichTextRenderer, editor: 'RichText'},
    {data: 3, renderer: RichTextRenderer, editor: 'RichText'},
  ];
  const customWidth = [100, 200, 300, 300];

  container = document.getElementById('hot'),
  hot = new Handsontable(container, {
    data: woData,
    colWidths: customWidth,
    colHeaders: ['numeric', 'text', 'RichText', 'RichText'],
    rowHeaders: true,
    columns: customColumns,
    manualColumnResize: true,
    manualRowResize: true,
    contextMenu: true,
    enterMoves: {row: 0, col: 1},
    outsideClickDeselects: false,
    afterOnCellMouseDown: function(sender, e) {
      if (e.row === -1) {
        this.getInstance().deselectCell();
      }
    },
  });
  hot.alter('insert_row', hot.countRows());

  Handsontable.hooks.add('beforeKeyDown', function(e) {
    const editor = this.getActiveEditor();

    if (editor.cellProperties.editor != 'RichText') return;

    if (editor.isOpened() && (isArrowKey(e.keyCode) ||
      e.keyCode == 13 || e.keyCode == 8)) {
      e.stopImmediatePropagation();
    }
  });

  function isArrowKey(keyCode) {
    return Handsontable.helper.isKey(keyCode, 'ARROW_UP|ARROW_DOWN|ARROW_LEFT|ARROW_RIGHT');
  }

  function RichTextRenderer(instance, td, row, col, prop, value, cellProperties) {
    Handsontable.renderers.HtmlRenderer.apply(this, arguments);
    td.id = 'id' + row + col;
  }
});

(function(Handsontable) {
  'use strict';

  let offsetLeft;
  let offsetTop;
  let cke_myid;
  let myEditor;
  let isMoveWindow = false;
  const RichTextEditor = Handsontable.editors.TextEditor.prototype.extend();

  const onMouseMove = function(event) {
    cke_myid.style.top = event.clientY - offsetTop + 'px';
    cke_myid.style.left = event.clientX - offsetLeft + 'px';
    isMoveWindow = true;
  };

  RichTextEditor.prototype.prepare = function(row, col, prop, td, originalValue, cellProperties) {
    Handsontable.editors.TextEditor.prototype.prepare.apply(this, arguments);
  };

  RichTextEditor.prototype.createElements = function() {
    this.$body = document.getElementsByTagName('body');

    this.TEXTAREA = document.createElement('input');
    this.TEXTAREA.setAttribute('type', 'text');
    this.TEXTAREA.setAttribute('id', 'myid');
    this.$textarea = document.getElementById('myid');


    this.textareaStyle = this.TEXTAREA.style;
    this.textareaStyle.width = 0;
    this.textareaStyle.height = 0;

    this.TEXTAREA_PARENT = document.createElement('DIV');

    this.textareaParentStyle = this.TEXTAREA_PARENT.style;
    this.textareaParentStyle.top = 0;
    this.textareaParentStyle.left = 0;

    this.TEXTAREA_PARENT.appendChild(this.TEXTAREA);

    this.instance.rootElement.appendChild(this.TEXTAREA_PARENT);

    const that = this;

    ClassicEditor.create(this.TEXTAREA, {
      toolbar: ['undo', 'redo', 'bold'],
    })
        .then( (editor) => {
          myEditor = editor;
          editor.editing.view.document.on( 'keydown', ( event, data ) => {
            if ( data.keyCode == 13 && data.altKey ) {
              document.execCommand('shiftEnter');
            } else if (data.keyCode == 13 && !data.shiftKey) {
              data.stopPropagation();
              data.preventDefault();
              event.stop();
              that.finishEditing(false, false);
            } else if (data.keyCode == 27) {
              event.stop();
              data.preventDefault();
              that.finishEditing(true, false);
            }
          });
        } )
        .catch( (error) => {
          console.error( error );
        } );

    this.instance._registerTimeout(setTimeout(function() {
      that.refreshDimensions();
    }, 0));
  };

  RichTextEditor.prototype.open = function(keyboardEvent) {
    this.refreshDimensions();

    const that = this;

    if (keyboardEvent) {
      this.setValue(this.TD.innerHTML);
    }

    setTimeout(function() {
      cke_myid = document.querySelectorAll('[role="application"]')[0];
      cke_myid.style.position = 'absolute';
      cke_myid.style.zIndex = 110;
      cke_myid.style.top = (document.documentElement.clientHeight - cke_myid.clientHeight) / 2 + 'px';
      cke_myid.style.left = (document.documentElement.clientWidth - cke_myid.clientWidth) / 2 + 'px';
      // cke_myid.style.top = that.TD.offsetTop + that.TD.offsetHeight + 'px';
      // cke_myid.style.left = that.TD.offsetLeft + 'px';

      const toolbar = document.querySelectorAll('[role="toolbar"]')[0];

      toolbar.onmousedown = function(event) {
        offsetLeft = event.clientX - parseInt(cke_myid.style.left);
        offsetTop = event.clientY - parseInt(cke_myid.style.top);
        document.addEventListener('mousemove', onMouseMove);
      };
      toolbar.onmouseup = function(event) {
        document.removeEventListener('mousemove', onMouseMove);
        if (isMoveWindow) {
          isMoveWindow = false;
          myEditor.editing.view.focus();
        }
      };

      myEditor.editing.view.focus();
      myEditor.setData(that.TEXTAREA.value);
    }, 1);
  };

  RichTextEditor.prototype.init = function() {
    Handsontable.editors.TextEditor.prototype.init.apply(this, arguments);
  };

  RichTextEditor.prototype.close = function() {
    this.instance.listen();
    this.textareaStyle.display = 'none';
    Handsontable.editors.TextEditor.prototype.close.apply(this, arguments);
  };

  RichTextEditor.prototype.val = function(value) {
    if (typeof value == 'undefined') {
      return this.$textarea.val();
    } else {
      this.$textarea.val(value);
    }
  };

  RichTextEditor.prototype.getValue = function() {
    let val = myEditor.getData();
    val = val.replace(/\r?\n/g, '');
    val = val.replace(/&nbsp;/ig, '');
    val = val.replace(/<P>/ig, '');
    val = val.replace(/<\/P>/ig, '');
    return val;
  };

  RichTextEditor.prototype.beginEditing = function(initialValue) {
    const onBeginEditing = this.instance.getSettings().onBeginEditing;
    if (onBeginEditing && onBeginEditing() === false) {
      return;
    }

    Handsontable.editors.TextEditor.prototype.beginEditing.apply(this, arguments);
  };

  RichTextEditor.prototype.finishEditing = function(isCancelled, ctrlDown) {
    return Handsontable.editors.TextEditor.prototype.finishEditing.apply(this, arguments);
  };

  Handsontable.editors.RichTextEditor = RichTextEditor;
  Handsontable.editors.registerEditor('RichText', RichTextEditor);
})(Handsontable);
</script>
</body>
</html>

注意

Handsontable は、MITライセンスの6.2.2を利用しています。 Ver 7.0以降はPRO版で有償となります。
CKEditor5は、商用利用については有償となります。

最後に

CKEditorはとてもいいのですが、やはり商用利用については有償なのが気になりますね。
MITライセンスのWYSIWYGエディターが幾つかあるので、今回の目的に適したものを次回挑戦してみます。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?