1
4

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エディターによる入力(Summernote)

Last updated at Posted at 2020-08-03

はじめに

Handsontable のRichTextエディターとしてこれまで「CKEditor」を用いてきました。

しかし、CKEditorはとてもいいのですが、やはり商用利用については有償なのが気になります。
そこで今回、MITライセンスまたはBSDライセンス(BSD-3-Clause)のWYSIWYGエディターの幾つかある中で、アンドゥ・リドゥ、太字、取消線、色が標準で揃っている「Summernote」を選択しました。
Summernoteは、Bootstrapの超シンプルなWYSIWYGエディターというキャッチフレーズですが今回はより軽量にしたいため、Bootstrapを除外した「summernote-lite」を使用しました。
本当はJQueryも除外したかったんですが現状は必須となっています。脱JQueryが現在主流となっているので、新しいSummernoteの改良に期待します。

使用環境

  • Summernote lite ver 0.8.18
  • jquery 3.4.1
  • handsontable 6.2.2

CKEditor版との違い

プログラムの作りとしては、CKEditor5版に近いです。
縦幅は小さくして、ツールバーは標準である程度揃っているので必要なものだけにカスタマイズしています。
文字色はデフォルトで赤色にしています。

width: 400,
height: 100,
toolbar: [
  ['misk',  ['undo', 'redo']],
  ['style', ['bold', 'strikethrough']],
  ['color', ['color']]
],
colorButton: {
  foreColor: 'red',
  backColor: 'transparent'
}

戻り値の編集

先頭と末尾に BRタグが付くようなので除外するようにしました。P タグはこれまで同様に全て除外しています。

RichTextEditor.prototype.getValue = function () {
  var val = $('#myid').summernote('code');
  val = val.replace(/<P>/ig, "");
  val = val.replace(/<\/P>/ig, "");
  val = val.replace(/^<BR>/i, "");
  val = val.replace(/<BR>$/i, "");
  return val;
};

背景色

エディターの背景色が透明になっていたので、CSSにて白色に指定しています。

.note-editable { 
  background-color: white !important;
}

表示位置

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

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

デモ

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

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

CodePen

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

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

ソースコード

<html>
<head>
<meta charset="UTF-8">
<title>RichText with Handsontable</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.full.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.css">
<style>
.note-editable { 
  background-color: white !important;
}
</style>
</head>
<body>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.full.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-lite.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/lang/summernote-ja-JP.js"></script>

<div id="hot"></div>
<script>
$(function() {
  let hot;

  const woData = [
    {
      0: 100,
      1: '通常テキスト',
      2: 'あいうえお',
      3: 'あいうえお<br>かきくけこ',
    },
    {
      0: 200,
      1: 'テスト',
      2: '<strong>あいうえお</strong><br><s>かきくけこ</s>',
      3: '<span style=\'color:#ff0000\'><strong>あいうえお</strong></span><br><s><span style=\'color:#0000ff\'>かきくけこ</span></s>',
    },
  ];

  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 editor;
  let isOpen = false;
  let isMoveWindow = false;
  const RichTextEditor = Handsontable.editors.TextEditor.prototype.extend();

  const onMouseMove = function(event) {
    editor.style.top = event.clientY - offsetTop + 'px';
    editor.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.TEXTAREA = document.createElement('input');
    this.TEXTAREA.setAttribute('type', 'text');
    this.TEXTAREA.setAttribute('id', '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;

    $('#myid').summernote({
      width: 400,
      height: 100,
      toolbar: [
        ['misk', ['undo', 'redo']],
        ['style', ['bold', 'strikethrough']],
        ['color', ['color']],
      ],
      colorButton: {
        foreColor: 'red',
        backColor: 'transparent',
      },
      callbacks: {
        onKeydown: function(event) {
          if (!isOpen) return;

          if (event.keyCode == 13 && !event.shiftKey) {
            event.stopImmediatePropagation();
            that.finishEditing(false, false);
          } else if (event.keyCode == 27) {
            event.stopImmediatePropagation();
            that.finishEditing(true, false);
          }
        },
      },
    });

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

  RichTextEditor.prototype.open = function(keyboardEvent) {
    isOpen = true;

    this.refreshDimensions();

    const that = this;

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

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

      toolbar.onmousedown = function(event) {
        offsetLeft = event.clientX - parseInt(editor.style.left);
        offsetTop = event.clientY - parseInt(editor.style.top);
        document.addEventListener('mousemove', onMouseMove);
      };
      toolbar.onmouseup = function(event) {
        document.removeEventListener('mousemove', onMouseMove);
        if (isMoveWindow) {
          isMoveWindow = false;
          $('#myid').summernote('focus');
        }
      };

      $('#myid').summernote('focus');
      $('#myid').summernote('code', that.TEXTAREA.value);
    }, 1);
  };

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

  RichTextEditor.prototype.close = function() {
    isOpen = false;
    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 = $('#myid').summernote('code');
    val = val.replace(/<P>/ig, '');
    val = val.replace(/<\/P>/ig, '');
    val = val.replace(/^<BR>/i, '');
    val = val.replace(/<BR>$/i, '');
    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>

最後に

Summernote版を作成していて、おかしな動作をしているところを CKEditor版でも確認したら同じ現象になっていたので修正して反映していたりします。修正箇所は追記して CKEditor版側の記事に記載してあります。

BSDライセンス(BSD-3-Clause)のWYSIWYGエディターで人気なものに「Quill」があり、IEは非推奨なのですが、次回はこれに挑戦してみます。

1
4
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
1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?