TinyMCE + ReactでNotionライクなスラッシュコマンド(ショートカット)使えるようにする

category
date
Jan 27, 2023
slug
tinymce-react-slash-shortcut
status
Published
summary
TinyMCE + Reactでオートコンプリート機能を使って、Notionライクなスラッシュコマンド(ショートカット)使えるようにしてみた話
type
Post

TinyMCE + Reactでのsetup関数

setup関数は以下のように設定します。
<Editor
  onInit={(evt, editor) => (editorRef.current = editor)}
  initialValue=""
  init={{
    height: 500,
    menubar: false,
    plugins: [
      'advlist autolink lists link image charmap print preview anchor',
      'searchreplace visualblocks code fullscreen',
      'insertdatetime media table paste code help wordcount'
    ],
    toolbar:
      'undo redo | formatselect | ' +
      'bold italic backcolor | alignleft aligncenter ' +
      'alignright alignjustify | bullist numlist outdent indent | ' +
      'removeformat | help',
    content_style:
      'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
    setup: (editor) => {
      editor.ui.registry.addButton('myButton', {
        text: 'My button',
        onAction: () => {
          alert('Button clicked!');
        }
      });
    }
  }}
/>
 

Autocompleter APIを使用する

オートコンプリートを設定するために、TyneMCEのAutocompleter API を使用します。
setup: (editor) => {
  const insertActions = [
    {
      text: 'Heading 1',
      icon: 'h1',
      action: function () {
        editor.execCommand(
          'mceInsertContent',
          false,
          '<h1></h1>'
        );
        editor.selection.select(editor.selection.getNode());
      }
    },
    {
      type: 'separator'
    },
    {
      text: 'Bulleted list',
      icon: 'unordered-list',
      action: function () {
        editor.execCommand('InsertUnorderedList', false);
      }
    },
	];
	
	// Autocompleter API
	editor.ui.registry.addAutocompleter(
	  'Menubar-item-variable',
	  {
	    ch: '/',
	    minChars: 0 /** Zero value means that the menu appears as soon as you type the "/" */,
	    columns: 1,
	    fetch: (pattern) => {
	      const matchedActions = insertActions.filter(
	        function (action) {
	          return (
	            action.type === 'separator' ||
	            action.text
	              .toLowerCase()
	              .indexOf(pattern.toLowerCase()) !== -1
	          );
	        }
	      );
	
	      return new Promise((resolve) => {
	        var results = matchedActions.map(function (
	          action
	        ) {
	          return {
	            meta: action,
	            text: action.text,
	            icon: action.icon,
	            value: action.text,
	            type: action.type
	          };
	        });
	        resolve(results);
	      });
	    },
	    onAction: (autocompleteApi, rng, action, meta) => {
	      editor.selection.setRng(rng);
	      // Some actions don't delete the "slash", so we delete all the slash
	      // command content before performing the action
	      editor.execCommand('Delete');
	      meta.action();
	      autocompleteApi.hide();
	    }
	  }
	);
}

© Titch 2022 - 2024