在基本功能都會運作之後,加入tiptap當作WYSIWYG編輯器

TLDR;

原本的文字框可以,打字,他就是很大的textarea tag,所以在全部的功能理論上會動之後就加入

  • 找text editor
  • 嫁接tiptop進入
  • tiptap & react

text editor

編輯器是可以自己寫的,但是因為太造輪子了,所以先參考了一些其他人寫的記事本用哪咧,當時有先找最熟悉的hackMD開發的開源CodiMD,還有Stackoverflow自己開發的Stacks-editor,另外直接搜尋後有找到更多例如: froalaquiljsckeditor。當時選擇TipTap純粹是最早看到有人使用在React的專案,另外CodiMD太複雜,Stackseditor是後續替換的我不太確定怎麼接到React上面,最後就用TipTap。

TipTap

官網 install-react

TipTap是包在Prosemirror的WYSIWYG editor,除了他接再React上面很方便之外,我也是第一次用沒什麼歷史好介紹的。

TipTap的本體很簡單,他就是一個contenteditable的方塊,沒有任何東西,如果需要多加的任何功能,就要載入extension,畫面中的按鈕等等也都要自己寫,算是比較麻煩的點,不過官網有滿多種類的examples可以參考。TipTap提供了一個方便的extension Starter-kit,包了最基本的功能,但互動的按鈕等等就要在自己寫。

值得一提的是,它現在是v2,有些舊的文件是v1,格式差距滿大的。還有開發者用的是vue.js和typescript,如果不是用這些可能需要適應一下。

Installation

文件說明的一樣簡單,不過先npm i需要的packages,然後將TipTap export成一個component

// from doc
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

const Tiptap = () => {
  const editor = useEditor({
    extensions: [
      StarterKit,
    ],
    content: '<p>Hello World!</p>',
  })

  return (
    <EditorContent editor={editor} />
  )
}

export default Tiptap

之後就將Tiptap當作普通的Component在React的專案中使用,StarterKit包含了最基本的打字、簡單的Markdown例如:#當作heading,codeblock等功能,

Working with React

回傳資料也滿簡單的,editor可以選擇回傳純文字或是JSON,吃資料的時候同樣也可以選擇純文字或是JSON,JSON可以儲存文字的大小顏色等等,所以完整的儲存現在的文件就是用JSON。

const text = editor.getText()
const content = editor.getJSON()

值得注意的是editor可以設定onUpdate,並不需要手動在component下面新增。如下

let value = ""

const Tiptap = () => {
  const editor = useEditor({
    extensions: [
      StarterKit,
    ],
    content: props.content,
    onUpdate(){
      // get the text
      value = this.getText()
    },
    ...
  })

  return (
    <EditorContent editor={editor} />
  )
}

Extensions

參考:

不過要有更多的控制,例如插入字串等等,可以用editor

editor.focus()
editor.insertText("hello")

那這些操作通常會包在Extension裡面,Extension是Tiptap設定為可以加入editor之下的,例如預設加入的StartKit就是包含了Text,Bold等基本的extension,讓你可以進行打字(對這個是extension,沒有Text就不能打字)粗體、斜體等等的操作。

如果你想要按下f1時把所在的那行文字變成黃色的,或是想要可以插入youtube影片在裡面播放,都可以寫成Extension。通常會自訂extension,將加入

Keyboard shortcuts

網址:

Tiptap可以handle你在按下某些按鍵的時候,要如何動作,但他是使用Prosemirror的API,所以也是利用Extension的方式,在裡面加上addKeyboardShortcuts()。下面是我寫的,將Tab當作對象處理,防止從輸入中的editor切換到下一個元素。

const CustomExtension = Extension.create({
  addKeyboardShortcuts(){
    return{
      'Tab': () =>{
        // if it is listItem pass and let the listItem extension handle rest
        if(this.editor.can().sinkListItem('listItem')){
                return false;
        }
        // else insert \t and return true to prevent any futher action triggered
        this.editor
            .chain()
            .insertContent('\t')
            .focus()
            .run()
        return true;
      }
    }
  }
})

CodeBlockLowlight

A code languae highlighting extension, thought I would be as easy as before but not.

The extension is duplicated with the StarterKit extension

// configure the starerkit, remove the duplicated CodeBlock extension
...
extensions: [{
  StarterKit.configure({
    CodeBlock: false,
})
...

The documentation didn’t explain, but need to configure to run the highlight function

CodeBlockLowlight.configure({
            lowlight,
})

What’s lowlight?

// I suppose this is from the extension, don't know how it works
// This is part of highlight.js 
import lowlight from 'lowlight'

Manual set the CSS. The document did give an example, so I just copied it.

因為我在原先的設計上是不會用到任何外部的資源(網路上),所以在權限上設定了很寬鬆的限制

// opening links in an external browser (e.g. chrome) instead of using electron
// from https://stackoverflow.com/questions/32402327/how-can-i-force-external-links-from-browser-window-to-open-in-a-default-browser
win.webContents.on('new-window', function(e, url){
    e.preventDefault();
    require('electron').shell.openExternal(url);
})