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

Last updated at Posted at 2020-07-18

はじめに

Excelファイルをメールでやり取りしている作業をWeb化しようという試みをしている。
Excelファイルの内容自体はマスタデータに備考欄を付いた一覧表になっているので、Web化する上でグリッドライブラリーの「Handsontable」を使用する。
問題なのは備考欄の部分である。備考欄には太文字と赤文字と取消線が使用されており、 Handsontable にはRichTextエディター入力が標準で用意されてされていない、しかしHTML表示はサポートしているので実現するにはHTMLタグを使えばいいわけだが、一般の人にHTMLタグを強要するには無理である。
Handsontable にRichTextエディター入力が実現できればいいなとネットで検索したところ、下記サイトを見つけた。
https://jsfiddle.net/isiddhant/uy7y7wss/7/

Handsontable のセル上でEnterキーを押すかダブルクリックすればWYSIWYGエディター(CKEditor4)が起動して、太文字や取消線の入力が出来て反映される。まさに欲しかった機能で、これを参考に改良していけばいい。

改良方針

  • 脱JQuery
  • ウィンドウの表示位置の修正と移動の実現
  • Pタグと改行コードの除去
  • ツールバーを必要最小限にする

昨今の流れとして脱JQueryにしています。
ウィンドウの表示位置は画面中央にしてドラッグで移動できるように修正しました。
エディタ入力後のセルを見ると余分な隙間が出来ます。これは Pタグで囲まれてセットされてしまうのが原因なので Pタグを除外します。また、改行させた場合に brタグ だけでなく改行コードも一緒にセットされてしまうので改行コードを除外します。
今回ツールバーに必要なのは、アンドゥとリドゥと太字と取消とテキストカラーがあればいいです。それを実現するためにCKEditor4のスタンダード版からフルセット版に変更しました。
CKEditorのツールバーをカスタマイズする下記サイトを利用しました。
https://ckeditor.com/latest/samples/toolbarconfigurator/index.html#basic

重なり順序の変更

CKEditor をマウスで移動させると下図のように HandsonTable の選択枠やヘッダの下に表示されてしまいます。
CKEditor4zIndex.png

これを解消したくて調査しました。

対象 z-index
99
ヘッダ行 100
ヘッダ列 101
ヘッダ角 102

z-index を103以上にすれば CKEditorが最上位になります。今回は110にしておきます。

cke_myid.style.zIndex = 110;

初回の表示位置

必ずではないのですが、初回表示が指定位置に変更されない上に値がセットされない状態が発生しました。
そのため、Open時にsetTimeout関数で括ることで対応しました。
現象が発生した場合、初期位置に一瞬表示されるものの指定位置に移動します。

Handsontable用の複数選択ドロップダウンのプラグインのソースリストを参考にしました。
handsontable-chosen-editor

表示位置

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

// 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 または Alt(option) + Enter になります。
※通常のテキストでの改行は、Alt(option) + Enter のみです。

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

CodePen

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

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

ソースコード

<!DOCTYPE html>
<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">
</head>
<body>
<div id="hot"></div>
<script src="https://cdn.ckeditor.com/4.11.4/full/ckeditor.js"></script>
<script src="https://cdn.jsdelivr.net/npm/handsontable@6.2.2/dist/handsontable.full.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', 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>',
    },
  ];

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

  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());

  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 isMoveWindow = false;

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

  const RichTextEditor = Handsontable.editors.TextEditor.prototype.extend();

  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;

    CKEDITOR.replace(this.TEXTAREA.id, {
      on: {
        'instanceReady': function(ev) {
        },
        'loaded': function(ev) {
        },
        'key': function(event) {
          if (event.data.keyCode == CKEDITOR.ALT + 13) {
            this.execCommand('shiftEnter');
          } else if (event.data.keyCode == 13) {
            event.cancel();
            that.finishEditing(false, false);
          } else if (event.data.keyCode == 27) {
            event.cancel();
            that.finishEditing(true, false);
          }
        },
      },
      // ツールバーグループのカスタマイズ
      toolbarGroups: [
        {name: 'document', groups: ['mode', 'document', 'doctools']},
        {name: 'clipboard', groups: ['clipboard', 'undo']},
        {name: 'editing', groups: ['find', 'selection', 'spellchecker', 'editing']},
        {name: 'forms', groups: ['forms']},
        {name: 'basicstyles', groups: ['basicstyles', 'cleanup']},
        {name: 'paragraph', groups: ['list', 'indent', 'blocks', 'align', 'bidi', 'paragraph']},
        {name: 'links', groups: ['links']},
        {name: 'insert', groups: ['insert']},
        {name: 'styles', groups: ['styles']},
        {name: 'colors', groups: ['colors']},
        {name: 'tools', groups: ['tools']},
        {name: 'others', groups: ['others']},
        {name: 'about', groups: ['about']},
      ],
      removeButtons: 'Source,Save,Templates,NewPage,Preview,Print,PasteFromWord,PasteText,' +
        'Find,Replace,Cut,Copy,Paste,SelectAll,Scayt,Form,Checkbox,Radio,TextField,' +
        'Textarea,Select,ImageButton,HiddenField,Subscript,Superscript,CopyFormatting,' +
        'RemoveFormat,NumberedList,BulletedList,Outdent,Indent,Blockquote,CreateDiv,' +
        'JustifyLeft,JustifyCenter,JustifyRight,JustifyBlock,BidiLtr,BidiRtl,' +
        'Language,Anchor,Unlink,Link,Image,Flash,Table,HorizontalRule,Smiley,' +
        'SpecialChar,PageBreak,Iframe,Styles,Format,Font,FontSize,' +
        'BGColor,Maximize,About,ShowBlocks,Italic,Underline,Button',
      width: '455px',
      colorButton_colors: 'F00,00F,0F0,0FF,FF0,F0F',
    });

    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);
    }
    const editor = CKEDITOR.instances[that.TEXTAREA.id];

    setTimeout(function() {
      cke_myid = document.getElementById('cke_myid');
      cke_myid.style.position = 'absolute';
      cke_myid.style.zIndex = 103;
      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.getElementById('cke_1_top');

      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;
          editor.focus();
        }
      };

      editor.focus();
      editor.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 = CKEDITOR.instances[this.TEXTAREA.id].getData();
    val = val.replace(/\r?\n/g, '');
    val = val.replace(/^<P>/i, '');
    val = val.replace(/<\/P>$/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>

注意

Handsontable は、MITライセンスの6.2.2を利用しています。 Ver 7.0以降はPRO版で有償となります。
CKEditor4は、商用利用については有償となります。CKEditor4のサポートは2023年までとなっています。

最後に

当初はCKEditor4ではなく、CKEditor5や他のWYSIWYGエディター(MITライセンス)に挑戦しようとしたのですが、仕組みを理解せずにやろうとして訳がわからなくなってしまい断念、先ずは地道にCKEditor4からやることにしました。
仕組みを理解したので、他のエディターにした記事を後で書きます。

今回、WYSIWYG(ウィジウィグ)エディターを知る機会が出来て良かったです。

0
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
0
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?