WYSIWYG :中文名『所見即所得』,為英文 “ What You See Is What You Get ” 取字頭的縮寫,是由『菲利普·威爾遜( Flip Wilson )』所提出的一種電腦文字編輯器方面的技術,也是一種 UI design pattern
IT 邦幫忙
const Element = ({ attributes, children, element }) => {
switch (element.type) {
case 'block-quote':
return <blockquote {...attributes}>{children}</blockquote>
case 'bulleted-list':
return <ul {...attributes}>{children}</ul>
case 'heading-one':
return <h1 {...attributes}>{children}</h1>
case 'heading-two':
return <h2 {...attributes}>{children}</h2>
case 'list-item':
return <li {...attributes}>{children}</li>
case 'numbered-list':
return <ol {...attributes}>{children}</ol>
default:
return <p {...attributes}>{children}</p>
}
}
const Leaf = ({ attributes, children, leaf }) => {
if (leaf.bold) {
children = <strong>{children}</strong>
}
if (leaf.code) {
children = <code>{children}</code>
}
if (leaf.italic) {
children = <em>{children}</em>
}
if (leaf.underline) {
children = <u>{children}</u>
}
return <span {...attributes}>{children}</span>
}
import { createEditor } from 'slate';
console.log('editor-->\n', createEditor());
Data type Interface
/**
* `Text` objects represent the nodes that contain the actual text content of a
* Slate document along with any formatting properties. They are always leaf
* nodes in the document tree as they cannot contain any children.
*/
export interface BaseText {
text: string
}
export interface TextInterface {
equals: (text: Text, another: Text, options?: { loose?: boolean }) => boolean
isText: (value: any) => value is Text
isTextList: (value: any) => value is Text[]
isTextProps: (props: any) => props is Partial<Text>
matches: (text: Text, props: Partial<Text>) => boolean
decorations: (node: Text, decorations: Range[]) => Text[]
}
Location type Interface
Provided Method api
// custom-type.d.ts
export type CustomText = {
bold?: boolean
italic?: boolean
code?: boolean
text: string
}
export type EmptyText = {
text: string
}
declare module 'slate' {
interface CustomTypes {
Text: CustomText | EmptyText
}
}
Operations are the granular, low-level actions that occur while invoking Transforms.
// Operation.ts
export type BaseInsertNodeOperation = {
type: 'insert_node'
path: Path
node: Node
}
// transforms/node.ts
/**
* Insert nodes at a specific location in the Editor.
*/
insertNodes: <T extends Node>(
editor: Editor,
nodes: Node | Node[],
options?: {
at?: Location
match?: NodeMatch<T>
mode?: 'highest' | 'lowest'
hanging?: boolean
select?: boolean
voids?: boolean
}
) => void
Data-Layer
View-Layer
/**
* Extendable Custom Types Interface
*/
type ExtendableTypes =
| 'Editor'
| 'Element'
| 'Text'
| 'Selection'
| 'Range'
| 'Point'
| 'InsertNodeOperation'
| 'InsertTextOperation'
| 'MergeNodeOperation'
| 'MoveNodeOperation'
| 'RemoveNodeOperation'
| 'RemoveTextOperation'
| 'SetNodeOperation'
| 'SetSelectionOperation'
| 'SplitNodeOperation'
export interface CustomTypes {
[key: string]: unknown
}
export type ExtendedType<
K extends ExtendableTypes,
B
> = unknown extends CustomTypes[K] ? B : CustomTypes[K]
type ExtendableTypes =
| 'Editor'
| 'Element'
| 'Text'
| 'Selection'
| 'Range'
| 'Point'
| 'InsertNodeOperation'
| 'InsertTextOperation'
| 'MergeNodeOperation'
| 'MoveNodeOperation'
| 'RemoveNodeOperation'
| 'RemoveTextOperation'
| 'SetNodeOperation'
| 'SetSelectionOperation'
| 'SplitNodeOperation'
export type ExtendedType<
K extends ExtendableTypes,
B
> = ...
ExtendableTypes 事先為可以進行擴充的 types 做好限制,在 ExtendedType 吃的第一個 generic type 有限制了它的範圍必須縮限在 ExtendableTypes 之中
CustomTypes 是主要讓開發者定義 custom types 的 interface ,利用 interface 可以 extend 的特性讓開發者可以透過 declare module 等方式擴充。
declare module 'slate' {
interface CustomTypes {
Editor: CustomEditor
Element: CustomElement
Text: CustomText | EmptyText
}
}
export type ExtendedType<
K extends ExtendableTypes,
B
> = unknown extends CustomTypes[K] ? B : CustomTypes[K]
透過小範例來協助介紹
/** text.ts */
export interface BaseText {
text: string
}
export type Text = ExtendedType<'Text', BaseText>
/** custom-types.ts */
export type ExtendedType<
K extends ExtendableTypes,
B
> = unknown extends CustomTypes[K] ? B : CustomTypes[K]
/** text.ts */
export interface BaseText {
text: string
}
export type Text = ExtendedType<'Text', BaseText>
/** custom-types.ts */
export type ExtendedType<
K extends ExtendableTypes,
B
> = unknown extends CustomTypes[K] ? B : CustomTypes[K]
在關聯式資料庫的世界裡,執行資料正規化的『目的』是為了去除冗餘的資料,我們事先針對各種正規化形式定義出了不同的『 constraints 』,而資料庫的欄位結構則須符合這些『 constraints 』,例如:在第一正規化( 1NF )中,一個欄位只能存入一筆資料,而不能是複數筆,這就是其中一條 constraint 。
slate 為了應對過於彈性的 document model ,它也規範了一套內建的 constraints ,每當觸發了編輯器的資料更新時便會自動執行 Normalization ,確保資料符合 constraints 的規範
所有的 Element 節點內必須含有至少一個 Text 子節點。在進行正規化時如果遭遇到不符合此規範的 Element 節點,會加入一個空的 Text 節點進入它的子層。
將兩個相鄰且擁有完全相同的 properties 的 Text nodes 合併成同一個節點(不會刪減文字內容,只是單純做節點合併而已)。
...
Normalize 的執行方式並不是『一個 Operation 搭配一次 Normalize 』,而是『一組完整的 FLUSHING 搭配一次 Normalize 』。畢竟一次 Transform 裡有好多次的 Operations ,才剛弄乾淨馬上就被下一個 Operation 弄髒顯然不是我們想要的
slate 實際執行正規化功能的方式是透過呼叫 Transforms api 達成,因為我們無法保證每次的正規化實際上會觸發執行哪些 Transform 。因此 slate 採用的方式是在每個 Operation 裡對更新的節點進行骯髒標記的演算法,並在骯髒標記的清單完全被清除以後才結束正規化
所有會需要 Normalizing 功能的 Transform methods 裡都一定會包進這個 method 裡來實作
insertNodes<T extends Node>(
editor: Editor,
// ... args
): void {
Editor.withoutNormalizing(editor, () => {
// ... Implementation
})
},
/**
* Call a function, deferring normalization until after it completes.
*/
withoutNormalizing(editor: Editor, fn: () => void): void {
const value = Editor.isNormalizing(editor)
NORMALIZING.set(editor, false)
try {
fn()
} finally {
NORMALIZING.set(editor, value)
}
Editor.normalize(editor)
},
/**
* Check if the editor is currently normalizing after each operation.
*/
isNormalizing(editor: Editor): boolean {
const isNormalizing = NORMALIZING.get(editor)
return isNormalizing === undefined ? true : isNormalizing
},
isNormalizing 其實單純就是回傳當前 editor 的 NORMALIZING value ,代表編輯器當前是否為『完成正規化』的狀態,只是多加了一層三元判斷:如果 value === undefined 則回傳 true (因為 undefined 為初始值,編輯器的初始狀態就是已經完成正規化的狀態了)。
normalize(
editor: Editor,
options: {
force?: boolean
} = {}
): void {
// ...
if (!Editor.isNormalizing(editor)) {
return
}
// ... Implementation
}
呼叫節點正規化的主要 function
這些操作使得一組 Transform 裡頭除了最初呼叫的那次包進 withoutNormalizing 的 fn method 會推遲執行 normalize method 之外,其餘在過程中呼叫的 normalize 都會因為 isNormalizing 回傳值為 false 因而直接跳過。
DIRTY_PATHS WeakMap
// weak-map.ts
export const DIRTY_PATHS: WeakMap<Editor, Path[]> = new WeakMap()
Function call when Operation triggered
const set = new Set()
const dirtyPaths: Path[] = []
const add = (path: Path | null) => {
if (path) {
const key = path.join(',')
if (!set.has(key)) {
set.add(key)
dirtyPaths.push(path)
}
}
}
const oldDirtyPaths = DIRTY_PATHS.get(editor) || []
const newDirtyPaths = getDirtyPaths(op)
for (const path of oldDirtyPaths) {
const newPath = Path.transform(path, op)
add(newPath)
}
for (const path of newDirtyPaths) {
add(path)
}
DIRTY_PATHS.set(editor, dirtyPaths)
在每一次的 Operation 中:
const set = new Set()
const dirtyPaths: Path[] = []
const add = (path: Path | null) => {
if (path) {
const key = path.join(',')
if (!set.has(key)) {
set.add(key)
dirtyPaths.push(path)
}
}
}
const oldDirtyPaths = DIRTY_PATHS.get(editor) || []
const newDirtyPaths = getDirtyPaths(op)
for (const path of oldDirtyPaths) {
const newPath = Path.transform(path, op)
add(newPath)
}
for (const path of newDirtyPaths) {
add(path)
}
DIRTY_PATHS.set(editor, dirtyPaths)
normalize(
editor: Editor,
options: {
force?: boolean
} = {}
): void {
...
if (getDirtyPaths(editor).length === 0) {
return
}
// Main-Process
Editor.withoutNormalizing(editor, () => {
...
});
},
withoutNormalizing(editor: Editor, fn: () => void): void {
...
try {
fn()
}
...
Editor.normalize(editor)
},
// Main-Process
Editor.withoutNormalizing(editor, () => {
// ...
const max = getDirtyPaths(editor).length * 42 // HACK: better way?
let m = 0
while (getDirtyPaths(editor).length !== 0) {
if (m > max) {
throw new Error(`...`)
}
const dirtyPath = getDirtyPaths(editor).pop()!
// If the node doesn't exist in the tree, it does not need to be normalized.
if (Node.has(editor, dirtyPath)) {
const entry = Editor.node(editor, dirtyPath)
editor.normalizeNode(entry)
}
m++
}
});