├── examples ├── content.js ├── main.ts └── test.html ├── src ├── extensions │ ├── link │ │ ├── var.less │ │ └── style.less │ ├── code-block │ │ ├── var.less │ │ ├── style.less │ │ └── hljs.ts │ ├── image │ │ └── style.less │ ├── video │ │ └── style.less │ ├── horizontal │ │ └── style.less │ ├── list │ │ └── style.less │ ├── attachment │ │ ├── icon.svg │ │ └── style.less │ ├── code │ │ └── style.less │ ├── blockquote │ │ └── style.less │ ├── math │ │ └── style.less │ ├── heading │ │ └── style.less │ ├── index.ts │ ├── task │ │ └── style.less │ ├── color │ │ └── index.ts │ ├── font-size │ │ └── index.ts │ ├── table │ │ └── style.less │ ├── back-color │ │ └── index.ts │ ├── italic │ │ └── index.ts │ ├── subscript │ │ └── index.ts │ ├── superscript │ │ └── index.ts │ ├── bold │ │ └── index.ts │ ├── underline │ │ └── index.ts │ ├── font-family │ │ └── index.ts │ ├── history │ │ └── index.ts │ ├── strikethrough │ │ └── index.ts │ ├── align │ │ └── index.ts │ ├── line-height │ │ └── index.ts │ └── indent │ │ └── index.ts ├── index.ts ├── model │ ├── index.ts │ ├── Selection.ts │ └── History.ts ├── css │ ├── style.less │ └── var.less ├── view │ └── index.ts └── tools │ └── index.ts ├── docs ├── .vitepress │ ├── cache │ │ └── deps │ │ │ ├── package.json │ │ │ ├── vue.js.map │ │ │ ├── vitepress___@vueuse_core.js.map │ │ │ └── _metadata.json │ └── theme │ │ ├── index.ts │ │ └── style.less ├── public │ └── logo.png ├── guide │ ├── introduction.md │ ├── render.md │ ├── clipboard.md │ ├── dom-parse.md │ ├── quick-start.md │ ├── selection.md │ ├── install.md │ ├── format-rules.md │ ├── history.md │ └── knode.md ├── index.md ├── extensions │ ├── built-in │ │ ├── horizontal.md │ │ ├── bold.md │ │ ├── italic.md │ │ ├── subscript.md │ │ ├── underline.md │ │ ├── superscript.md │ │ ├── color.md │ │ ├── indent.md │ │ ├── strikethrough.md │ │ ├── font-size.md │ │ ├── back-color.md │ │ ├── font-family.md │ │ ├── line-height.md │ │ ├── history.md │ │ ├── align.md │ │ ├── math.md │ │ ├── task.md │ │ ├── code.md │ │ ├── blockquote.md │ │ ├── image.md │ │ ├── video.md │ │ ├── heading.md │ │ ├── link.md │ │ ├── code-block.md │ │ └── text.md │ ├── introduction.md │ └── custom-extension.md ├── apis │ ├── knode-attrs.md │ └── editor-attrs.md └── changelog.md ├── vite-env.d.ts ├── lib ├── index.d.ts ├── view │ ├── js-render │ │ ├── index.d.ts │ │ └── dom-patch.d.ts │ └── index.d.ts ├── model │ ├── index.d.ts │ ├── Selection.d.ts │ ├── config │ │ ├── format-patch.d.ts │ │ ├── event-handler.d.ts │ │ ├── format-rules.d.ts │ │ └── function.d.ts │ └── History.d.ts ├── extensions │ ├── horizontal │ │ └── index.d.ts │ ├── bold │ │ └── index.d.ts │ ├── italic │ │ └── index.d.ts │ ├── subscript │ │ └── index.d.ts │ ├── indent │ │ └── index.d.ts │ ├── underline │ │ └── index.d.ts │ ├── superscript │ │ └── index.d.ts │ ├── color │ │ └── index.d.ts │ ├── strikethrough │ │ └── index.d.ts │ ├── code-block │ │ ├── hljs.d.ts │ │ └── index.d.ts │ ├── font-size │ │ └── index.d.ts │ ├── back-color │ │ └── index.d.ts │ ├── font-family │ │ └── index.d.ts │ ├── history │ │ └── index.d.ts │ ├── line-height │ │ └── index.d.ts │ ├── align │ │ └── index.d.ts │ ├── math │ │ └── index.d.ts │ ├── task │ │ └── index.d.ts │ ├── code │ │ └── index.d.ts │ ├── blockquote │ │ └── index.d.ts │ ├── index.d.ts │ ├── heading │ │ └── index.d.ts │ ├── image │ │ └── index.d.ts │ ├── video │ │ └── index.d.ts │ ├── link │ │ └── index.d.ts │ ├── text │ │ └── index.d.ts │ ├── attachment │ │ └── index.d.ts │ ├── list │ │ └── index.d.ts │ └── table │ │ └── index.d.ts └── tools │ └── index.d.ts ├── README.md ├── .npmignore ├── tsconfig.node.json ├── .gitignore ├── index.html ├── tsconfig.json ├── LICENSE ├── vite.config.ts └── package.json /examples/content.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/extensions/link/var.less: -------------------------------------------------------------------------------- 1 | @color: #06c27c; 2 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module '*.svg' 3 | -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/so-better/kaitify-core/HEAD/docs/public/logo.png -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './model'; 2 | export * from './extensions'; 3 | export * from './view'; 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### 基于原生 JS 的富文本编辑器核心库 2 | 3 | > [@kaitify/core 官方文档](https://www.so-better.cn/docs/kaitify-core/) 4 | -------------------------------------------------------------------------------- /examples/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | createApp(App).mount('#app') 4 | -------------------------------------------------------------------------------- /src/extensions/code-block/var.less: -------------------------------------------------------------------------------- 1 | @backColor: #f6f9ff; 2 | @borderColor: #ecf0f9; 3 | @darkBackColor: #29292d; 4 | @darkBorderColor: #333338; 5 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vue.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import './style.less' 3 | 4 | export default { 5 | extends: DefaultTheme 6 | } 7 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /lib/view/js-render/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '../../model'; 2 | /** 3 | * 默认的原生js渲染编辑器视图层 4 | */ 5 | export declare const defaultUpdateView: (this: Editor, init: boolean) => void; 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | //引入基本样式 2 | import './css/var.less' 3 | import './css/style.less' 4 | //基本数据结构 5 | export * from './model' 6 | //扩展 7 | export * from './extensions' 8 | //视图渲染 9 | export * from './view' 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # 忽略目录 2 | examples/ 3 | public/ 4 | docs/ 5 | src/ 6 | 7 | # 忽略指定文件 8 | index.html 9 | vite.config.ts 10 | tsconfig.json 11 | tsconfig.node.json 12 | LICENSE 13 | package-lock.json 14 | vite-env.d.ts 15 | -------------------------------------------------------------------------------- /src/model/index.ts: -------------------------------------------------------------------------------- 1 | export type * from './config/format-patch' 2 | export type * from './config/format-rules' 3 | export * from './Editor' 4 | export * from './History' 5 | export * from './KNode' 6 | export * from './Selection' 7 | -------------------------------------------------------------------------------- /lib/model/index.d.ts: -------------------------------------------------------------------------------- 1 | export type * from './config/format-patch'; 2 | export type * from './config/format-rules'; 3 | export * from './Editor'; 4 | export * from './History'; 5 | export * from './KNode'; 6 | export * from './Selection'; 7 | -------------------------------------------------------------------------------- /src/extensions/image/style.less: -------------------------------------------------------------------------------- 1 | .kaitify img { 2 | display: inline-block; 3 | position: relative; 4 | width: auto; 5 | min-width: 20px; 6 | max-width: 100%; 7 | padding: 0 var(--kaitify-sides-between); 8 | text-indent: 0; 9 | } 10 | -------------------------------------------------------------------------------- /src/extensions/video/style.less: -------------------------------------------------------------------------------- 1 | .kaitify video { 2 | display: inline-block; 3 | position: relative; 4 | width: auto; 5 | min-width: 100px; 6 | max-width: 100%; 7 | padding: 0 var(--kaitify-sides-between); 8 | text-indent: 0; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /lib/extensions/horizontal/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 设置分隔线 6 | */ 7 | setHorizontal?: () => Promise; 8 | } 9 | } 10 | export declare const HorizontalExtension: () => Extension; 11 | -------------------------------------------------------------------------------- /src/extensions/horizontal/style.less: -------------------------------------------------------------------------------- 1 | .kaitify { 2 | hr { 3 | appearance: none; 4 | display: block; 5 | width: 100%; 6 | height: 1px; 7 | background: fade(#000, 10); 8 | border: none; 9 | margin: var(--kaitify-large-margin) 0; 10 | } 11 | 12 | &.kaitify-dark hr { 13 | background: fade(#fff, 10); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | docs/kaitify-core 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | /package-lock.json 25 | -------------------------------------------------------------------------------- /src/extensions/list/style.less: -------------------------------------------------------------------------------- 1 | .kaitify { 2 | ol, 3 | ul { 4 | margin: 0 0 var(--kaitify-large-margin) 0; 5 | padding: 0 0 0 2em; 6 | 7 | &:last-child { 8 | margin-bottom: 0 !important; 9 | } 10 | 11 | li { 12 | margin: 0 0 var(--kaitify-large-margin) 0; 13 | 14 | &:last-child { 15 | margin-bottom: 0; 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | kaitify - 基于原生JS的富文本编辑器核心库 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/extensions/attachment/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/extensions/bold/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 光标所在文本是否加粗 6 | */ 7 | isBold?: () => boolean; 8 | /** 9 | * 设置加粗 10 | */ 11 | setBold?: () => Promise; 12 | /** 13 | * 取消加粗 14 | */ 15 | unsetBold?: () => Promise; 16 | } 17 | } 18 | export declare const BoldExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /lib/extensions/italic/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 光标所在文本是否斜体 6 | */ 7 | isItalic?: () => boolean; 8 | /** 9 | * 设置斜体 10 | */ 11 | setItalic?: () => Promise; 12 | /** 13 | * 取消斜体 14 | */ 15 | unsetItalic?: () => Promise; 16 | } 17 | } 18 | export declare const ItalicExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /src/extensions/link/style.less: -------------------------------------------------------------------------------- 1 | @import url(./var.less); 2 | 3 | .kaitify { 4 | a { 5 | color: @color; 6 | text-decoration: none; 7 | pointer-events: none; 8 | 9 | &:hover { 10 | text-decoration: underline; 11 | } 12 | } 13 | 14 | //非编辑状态下 15 | &:not([contenteditable='true']) { 16 | a { 17 | pointer-events: all; 18 | transition: all 300ms; 19 | 20 | &:hover { 21 | cursor: pointer; 22 | color: fade(@color, 80); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/extensions/subscript/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 光标所在文本是否下标 6 | */ 7 | isSubscript?: () => boolean; 8 | /** 9 | * 设置下标 10 | */ 11 | setSubscript?: () => Promise; 12 | /** 13 | * 取消下标 14 | */ 15 | unsetSubscript?: () => Promise; 16 | } 17 | } 18 | export declare const SubscriptExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /lib/extensions/indent/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 是否可以使用缩进 6 | */ 7 | canUseIndent?: () => boolean; 8 | /** 9 | * 增加缩进 10 | */ 11 | setIncreaseIndent?: () => Promise; 12 | /** 13 | * 减少缩进 14 | */ 15 | setDecreaseIndent?: () => Promise; 16 | } 17 | } 18 | export declare const IndentExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /lib/extensions/underline/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 光标所在文本是否下划线 6 | */ 7 | isUnderline?: () => boolean; 8 | /** 9 | * 设置下划线 10 | */ 11 | setUnderline?: () => Promise; 12 | /** 13 | * 取消下划线 14 | */ 15 | unsetUnderline?: () => Promise; 16 | } 17 | } 18 | export declare const UnderlineExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /lib/extensions/superscript/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 光标所在文本是否上标 6 | */ 7 | isSuperscript?: () => boolean; 8 | /** 9 | * 设置上标 10 | */ 11 | setSuperscript?: () => Promise; 12 | /** 13 | * 取消上标 14 | */ 15 | unsetSuperscript?: () => Promise; 16 | } 17 | } 18 | export declare const SuperscriptExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /lib/extensions/color/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 光标所在文本的颜色是否与入参一致 6 | */ 7 | isColor?: (value: string) => boolean; 8 | /** 9 | * 设置颜色 10 | */ 11 | setColor?: (value: string) => Promise; 12 | /** 13 | * 取消颜色 14 | */ 15 | unsetColor?: (value: string) => Promise; 16 | } 17 | } 18 | export declare const ColorExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /lib/extensions/strikethrough/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 光标所在文本是否删除线 6 | */ 7 | isStrikethrough?: () => boolean; 8 | /** 9 | * 设置删除线 10 | */ 11 | setStrikethrough?: () => Promise; 12 | /** 13 | * 取消删除线 14 | */ 15 | unsetStrikethrough?: () => Promise; 16 | } 17 | } 18 | export declare const StrikethroughExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /lib/extensions/code-block/hljs.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 支持的语言列表 3 | */ 4 | export declare const HljsLanguages: readonly ["plaintext", "json", "javascript", "java", "typescript", "python", "php", "css", "less", "scss", "html", "markdown", "objectivec", "swift", "dart", "nginx", "http", "go", "ruby", "c", "cpp", "csharp", "sql", "shell", "r", "kotlin", "rust"]; 5 | /** 6 | * 语言类型 7 | */ 8 | export type HljsLanguageType = (typeof HljsLanguages)[number]; 9 | /** 10 | * 获取经过hljs处理的html元素 11 | */ 12 | export declare const getHljsHtml: (code: string, language: string) => string; 13 | -------------------------------------------------------------------------------- /lib/extensions/font-size/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 光标所在文本的字号大小是否与入参一致 6 | */ 7 | isFontSize?: (value: string) => boolean; 8 | /** 9 | * 设置字号 10 | */ 11 | setFontSize?: (value: string) => Promise; 12 | /** 13 | * 取消字号 14 | */ 15 | unsetFontSize?: (value: string) => Promise; 16 | } 17 | } 18 | export declare const FontSizeExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /lib/model/Selection.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from './KNode'; 2 | /** 3 | * 光标点位类型,node仅支持文本节点和闭合节点 4 | */ 5 | export type SelectionPointType = { 6 | node: KNode; 7 | offset: number; 8 | }; 9 | /** 10 | * 光标选区 11 | */ 12 | export declare class Selection { 13 | /** 14 | * 起点 15 | */ 16 | start?: SelectionPointType; 17 | /** 18 | * 终点 19 | */ 20 | end?: SelectionPointType; 21 | /** 22 | * 是否已经初始化设置光标位置 23 | */ 24 | focused(): boolean; 25 | /** 26 | * 光标是否折叠 27 | */ 28 | collapsed(): boolean; 29 | } 30 | -------------------------------------------------------------------------------- /docs/guide/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 简介 3 | --- 4 | 5 | # 简介 6 | 7 | ## kaitify 是什么? 8 | 9 | > kaitify(发音:/ˈkeɪtɪfaɪ/)是一款基于原生 Javascript 构建的富文本编辑器核心库 10 | 11 | - 所谓的“核心库”:即它不是我们常见的开箱即用的富文本编辑器。它只提供构建编辑器所需要的配置和 API,这意味着开发者们需要基于它去做一定的封装。虽然这样显得好像有点繁琐,但是当我们定制化需求较高时,往往会更符合要求。 12 | 13 | > 但是它并非什么都需要我们去完成,你可以理解为一个“无 UI”的富文本编辑器,即只是没有具体操作栏界面,但它内置的扩展(`Extension`)繁多,足以帮助我们快速构建一个功能强大的 L1 级别的富文本编辑器。 14 | 15 | - 所谓的“基于原生 Javascript”:它不依赖于 React、Vue3 等任何前端框架,也不依赖于任何的 UI 组件库,完全采用原生编写,方便我们去适配各种项目场景。同时它又能够良好地和 React、Vue3 等前端框架进行整合。 16 | 17 | > [!TIP] 怎么样? 18 | > 接下来去更详细地了解 `kaitify` 是怎么使用的吧~ 19 | -------------------------------------------------------------------------------- /lib/extensions/back-color/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 光标所在文本的背景颜色是否与入参一致 6 | */ 7 | isBackColor?: (value: string) => boolean; 8 | /** 9 | * 设置背景颜色 10 | */ 11 | setBackColor?: (value: string) => Promise; 12 | /** 13 | * 取消背景颜色 14 | */ 15 | unsetBackColor?: (value: string) => Promise; 16 | } 17 | } 18 | export declare const BackColorExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /lib/extensions/font-family/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 光标所在文本的字体是否与入参一致 6 | */ 7 | isFontFamily?: (value: string) => boolean; 8 | /** 9 | * 设置字体 10 | */ 11 | setFontFamily?: (value: string) => Promise; 12 | /** 13 | * 取消字体 14 | */ 15 | unsetFontFamily?: (value: string) => Promise; 16 | } 17 | } 18 | export declare const FontFamilyExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /lib/extensions/history/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 是否可以撤销 6 | */ 7 | canUndo?: () => boolean; 8 | /** 9 | * 是否可以重做 10 | */ 11 | canRedo?: () => boolean; 12 | /** 13 | * 撤销 14 | */ 15 | undo?: () => Promise; 16 | /** 17 | * 重做 18 | */ 19 | redo?: () => Promise; 20 | } 21 | } 22 | export declare const HistoryExtension: () => Extension; 23 | -------------------------------------------------------------------------------- /lib/extensions/line-height/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | declare module '../../model' { 3 | interface EditorCommandsType { 4 | /** 5 | * 光标所在的块节点是否都是符合的行高 6 | */ 7 | isLineHeight?: (value: string | number) => boolean; 8 | /** 9 | * 设置行高 10 | */ 11 | setLineHeight?: (value: string | number) => Promise; 12 | /** 13 | * 取消行高 14 | */ 15 | unsetLineHeight?: (value: string | number) => Promise; 16 | } 17 | } 18 | export declare const LineHeightExtension: () => Extension; 19 | -------------------------------------------------------------------------------- /examples/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | kaitify 7 | 8 | 9 | 10 | 11 |
12 | 13 | 21 | 22 | -------------------------------------------------------------------------------- /lib/model/config/format-patch.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../KNode'; 2 | /** 3 | * 这里的比对结果仅进行格式化处理,只需要判断节点是否变化 4 | */ 5 | /** 6 | * 节点数组比对结果类型 7 | */ 8 | export type NodePatchResultType = { 9 | /** 10 | * 新节点 11 | */ 12 | newNode: KNode | null; 13 | /** 14 | * 旧节点 15 | */ 16 | oldNode: KNode | null; 17 | }; 18 | /** 19 | * 对新旧两个节点数组进行比对 20 | */ 21 | export declare const patchNodes: (newNodes: KNode[], oldNodes: (KNode | null)[]) => NodePatchResultType[]; 22 | /** 23 | * 对新旧两个节点进行比对 24 | */ 25 | export declare const patchNode: (newNode: KNode, oldNode: KNode) => NodePatchResultType[]; 26 | -------------------------------------------------------------------------------- /src/extensions/code/style.less: -------------------------------------------------------------------------------- 1 | @import url(../code-block/var.less); 2 | 3 | .kaitify { 4 | code { 5 | display: inline-block; 6 | padding: 3px; 7 | margin: 0 var(--kaitify-sides-between); 8 | border-radius: var(--kaitify-border-radius); 9 | font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono; 10 | background: @backColor; 11 | border: 1px solid @borderColor; 12 | color: var(--kaitify-font-color); 13 | line-height: 1; 14 | text-indent: 0; 15 | } 16 | 17 | &.kaitify-dark code { 18 | background: @darkBackColor; 19 | border-color: @darkBorderColor; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/view/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Editor, KNode, KNodeMarksType, KNodeStylesType } from '../model'; 2 | /** 3 | * 渲染参数类型 4 | */ 5 | export type KNodeRenderOptionType = { 6 | key: number; 7 | tag: string; 8 | attrs: KNodeMarksType; 9 | styles: KNodeStylesType; 10 | namespace?: string; 11 | textContent?: string; 12 | children?: KNodeRenderOptionType[]; 13 | }; 14 | /** 15 | * 节点渲染成dom后在dom上生成的一个特殊标记名称,它的值是节点的key值 16 | */ 17 | export declare const NODE_MARK = "kaitify-node"; 18 | /** 19 | * 获取节点的渲染参数 20 | */ 21 | export declare const getNodeRenderOptions: (editor: Editor, node: KNode) => KNodeRenderOptionType; 22 | -------------------------------------------------------------------------------- /lib/extensions/align/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../Extension'; 2 | export type AlignValueType = 'left' | 'right' | 'center' | 'justify'; 3 | declare module '../../model' { 4 | interface EditorCommandsType { 5 | /** 6 | * 光标所在的块节点是否都是符合的对齐方式 7 | */ 8 | isAlign?: (value: AlignValueType) => boolean; 9 | /** 10 | * 设置对齐方式 11 | */ 12 | setAlign?: (value: AlignValueType) => Promise; 13 | /** 14 | * 取消对齐方式 15 | */ 16 | unsetAlign?: (value: AlignValueType) => Promise; 17 | } 18 | } 19 | export declare const AlignExtension: () => Extension; 20 | -------------------------------------------------------------------------------- /src/extensions/blockquote/style.less: -------------------------------------------------------------------------------- 1 | @color: #6b6b6b; 2 | @borderColor: #e5e5e5; 3 | @darkColor: #d2d2d2; 4 | @darkBorderColor: #4e4e4e; 5 | 6 | .kaitify { 7 | blockquote { 8 | line-height: var(--kaitify-line-height); 9 | margin: 0 0 var(--kaitify-large-margin) 0 !important; 10 | padding: var(--kaitify-small-padding) 0 var(--kaitify-small-padding) var(--kaitify-large-padding); 11 | border-left: 5px solid @borderColor; 12 | color: @color; 13 | 14 | &:last-child { 15 | margin-bottom: 0 !important; 16 | } 17 | } 18 | 19 | &.kaitify-dark blockquote { 20 | color: @darkColor; 21 | border-left-color: @darkBorderColor; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/extensions/attachment/style.less: -------------------------------------------------------------------------------- 1 | @import url(../link/var.less); 2 | 3 | .kaitify { 4 | span[kaitify-attachment] { 5 | display: inline-flex; 6 | justify-content: flex-start; 7 | flex-wrap: wrap; 8 | align-items: center; 9 | margin: 0 var(--kaitify-sides-between); 10 | color: @color; 11 | background-position: left center; 12 | background-repeat: no-repeat; 13 | background-size: 20px; 14 | padding-left: 25px; 15 | 16 | &:hover { 17 | cursor: pointer !important; 18 | } 19 | } 20 | 21 | //非编辑状态下 22 | &:not([contenteditable='true']) { 23 | span[kaitify-attachment]:hover { 24 | text-decoration: underline; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/extensions/math/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | declare module '../../model' { 4 | interface EditorCommandsType { 5 | /** 6 | * 获取光标所在的数学公式节点,如果光标不在一个数学公式节点内,返回null 7 | */ 8 | getMath?: () => KNode | null; 9 | /** 10 | * 判断光标范围内是否有数学公式节点 11 | */ 12 | hasMath?: () => boolean; 13 | /** 14 | * 插入数学公式 15 | */ 16 | setMath?: (value: string) => Promise; 17 | /** 18 | * 更新数学公式 19 | */ 20 | updateMath?: (value: string) => Promise; 21 | } 22 | } 23 | export declare const MathExtension: () => Extension; 24 | -------------------------------------------------------------------------------- /docs/guide/render.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 视图渲染 3 | --- 4 | 5 | # 视图渲染 6 | 7 | > 编辑器在构建完成时会进行一次视图渲染,使得我们可以在页面看到 kaitify 所渲染的编辑器内容,但是后续的每一次节点操作或者光标修改,都需要我们手动调用 `updateView` 方法去更新视图 8 | 9 | ## updateView 做了什么? 10 | 11 | 1. 对新旧节点数组进行比对,得出有变更的节点 12 | 2. 对有变更的节点采取一定的策略,进行格式化校验 13 | 3. 进行视图渲染,更新编辑器内的 `dom` 14 | 4. 如果编辑器内容发生了变化,触发 `onChange`,同时根据入参判断是否需要加入历史记录 15 | 5. 根据入参判断是否需要更新真实光标 16 | 17 | ## 何时调用 updateView? 18 | 19 | 当我们对节点数组进行操作,修改了节点,或者对光标进行操作,修改了光标位置等,我们通常需要调用 `updateView` 方法来更新编辑器视图 20 | 21 | 如果确定节点数组没有变化,仅仅是修改了光标信息,那么只需要调用 `updateRealSelection` 方法来渲染真实光标 22 | 23 | ## 性能问题 24 | 25 | 为了引起不必要的性能开销,尤其是当编辑器的内容过多时,我们需要合理地调用 `updateView` 方法 26 | 27 | 当你在执行一系列节点操作时,只需要在所有的操作结束后,调用一次 `updateView` 方法即可,无需多次调用 28 | 29 | 频繁地过多地调用可能会引起编辑器的卡顿、视图渲染延迟等问题 30 | -------------------------------------------------------------------------------- /src/extensions/math/style.less: -------------------------------------------------------------------------------- 1 | @backColor: #f1f2f3; 2 | @darkBackColor: #2a2a2a; 3 | 4 | .kaitify { 5 | span[kaitify-math] { 6 | display: inline-block; 7 | padding: var(--kaitify-small-padding); 8 | border-radius: var(--kaitify-border-radius); 9 | margin: 0 var(--kaitify-sides-between); 10 | transition: all 300ms; 11 | max-width: 100%; 12 | text-indent: 0; 13 | 14 | &:hover { 15 | cursor: pointer !important; 16 | background: fade(@backColor, 50); 17 | } 18 | 19 | .katex { 20 | overflow: hidden; 21 | font-size: 1.4em; 22 | } 23 | } 24 | 25 | &.kaitify-dark span[kaitify-math] { 26 | &:hover { 27 | background: fade(@darkBackColor, 50); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/model/Selection.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from './KNode' 2 | /** 3 | * 光标点位类型,node仅支持文本节点和闭合节点 4 | */ 5 | export type SelectionPointType = { 6 | node: KNode 7 | offset: number 8 | } 9 | 10 | /** 11 | * 光标选区 12 | */ 13 | export class Selection { 14 | /** 15 | * 起点 16 | */ 17 | start?: SelectionPointType 18 | /** 19 | * 终点 20 | */ 21 | end?: SelectionPointType 22 | 23 | /** 24 | * 是否已经初始化设置光标位置 25 | */ 26 | focused() { 27 | return !!this.start && !!this.end 28 | } 29 | 30 | /** 31 | * 光标是否折叠 32 | */ 33 | collapsed() { 34 | if (!this.focused()) { 35 | return false 36 | } 37 | return this.start!.node.isEqual(this.end!.node) && this.start!.offset == this.end!.offset 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/extensions/task/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | declare module '../../model' { 4 | interface EditorCommandsType { 5 | /** 6 | * 获取光标所在的待办节点,如果光标不在一个待办节点内,返回null 7 | */ 8 | getTask?: () => KNode | null; 9 | /** 10 | * 判断光标范围内是否有待办节点 11 | */ 12 | hasTask?: () => boolean; 13 | /** 14 | * 光标范围内是否都是待办节点 15 | */ 16 | allTask?: () => boolean; 17 | /** 18 | * 设置待办 19 | */ 20 | setTask?: () => Promise; 21 | /** 22 | * 取消待办 23 | */ 24 | unsetTask?: () => Promise; 25 | } 26 | } 27 | export declare const TaskExtension: () => Extension; 28 | -------------------------------------------------------------------------------- /lib/extensions/code/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | declare module '../../model' { 4 | interface EditorCommandsType { 5 | /** 6 | * 获取光标所在的行内代码,如果光标不在一个行内代码内,返回null 7 | */ 8 | getCode?: () => KNode | null; 9 | /** 10 | * 判断光标范围内是否有行内代码 11 | */ 12 | hasCode?: () => boolean; 13 | /** 14 | * 光标范围内是否都是行内代码 15 | */ 16 | allCode?: () => boolean; 17 | /** 18 | * 设置行内代码 19 | */ 20 | setCode?: () => Promise; 21 | /** 22 | * 取消行内代码 23 | */ 24 | unsetCode?: () => Promise; 25 | } 26 | } 27 | export declare const CodeExtension: () => Extension; 28 | -------------------------------------------------------------------------------- /src/extensions/heading/style.less: -------------------------------------------------------------------------------- 1 | .kaitify { 2 | h1, 3 | h2, 4 | h3, 5 | h4, 6 | h5, 7 | h6, 8 | p { 9 | line-height: var(--kaitify-line-height); 10 | margin: 0 0 var(--kaitify-large-margin) 0 !important; 11 | 12 | &:last-child { 13 | margin-bottom: 0 !important; 14 | } 15 | } 16 | 17 | //段落和标题 18 | h1 { 19 | font-size: 48px; 20 | font-weight: bold; 21 | } 22 | h2 { 23 | font-size: 36px; 24 | font-weight: bold; 25 | } 26 | h3 { 27 | font-size: 28px; 28 | font-weight: bold; 29 | } 30 | h4 { 31 | font-size: 24px; 32 | font-weight: bold; 33 | } 34 | h5 { 35 | font-size: 18px; 36 | font-weight: bold; 37 | } 38 | h6 { 39 | font-size: 16px; 40 | font-weight: bold; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/extensions/code-block/style.less: -------------------------------------------------------------------------------- 1 | @import url(./var.less); 2 | 3 | .kaitify { 4 | pre { 5 | background: @backColor; 6 | color: var(--kaitify-font-color); 7 | border: 1px solid @borderColor; 8 | border-radius: var(--kaitify-border-radius); 9 | padding: var(--kaitify-padding); 10 | line-height: var(--kaitify-line-height); 11 | font-family: Consolas, monospace, Monaco, Andale Mono, Ubuntu Mono; 12 | font-size: var(--kaitify-font-size); 13 | font-weight: normal; 14 | margin: 0 0 var(--kaitify-large-margin) 0 !important; 15 | overflow-x: auto; 16 | 17 | &:last-child { 18 | margin-bottom: 0 !important; 19 | } 20 | } 21 | 22 | &.kaitify-dark pre { 23 | background: @darkBackColor; 24 | border-color: @darkBorderColor; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/extensions/blockquote/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | declare module '../../model' { 4 | interface EditorCommandsType { 5 | /** 6 | * 获取光标所在的引用节点,如果光标不在一个引用节点内,返回null 7 | */ 8 | getBlockquote?: () => KNode | null; 9 | /** 10 | * 判断光标范围内是否有引用节点 11 | */ 12 | hasBlockquote?: () => boolean; 13 | /** 14 | * 光标范围内是否都是引用节点 15 | */ 16 | allBlockquote?: () => boolean; 17 | /** 18 | * 设置引用 19 | */ 20 | setBlockquote?: () => Promise; 21 | /** 22 | * 取消引用 23 | */ 24 | unsetBlockquote?: () => Promise; 25 | } 26 | } 27 | export declare const BlockquoteExtension: () => Extension; 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | "allowSyntheticDefaultImports": true, 9 | "types": ["node"], 10 | "baseUrl": "./", 11 | "paths": { 12 | "@/*": ["src/*"] 13 | }, 14 | 15 | /* Bundler mode */ 16 | "moduleResolution": "bundler", 17 | "allowImportingTsExtensions": true, 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "preserve", 22 | 23 | /* Linting */ 24 | "strict": true, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | "noFallthroughCasesInSwitch": true 28 | }, 29 | "include": ["src/**/*.ts", "vite-env.d.ts"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /src/extensions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Extension' 2 | export * from './text' 3 | export * from './history' 4 | export * from './image' 5 | export * from './video' 6 | export * from './bold' 7 | export * from './italic' 8 | export * from './strikethrough' 9 | export * from './underline' 10 | export * from './superscript' 11 | export * from './subscript' 12 | export * from './code' 13 | export * from './font-size' 14 | export * from './font-family' 15 | export * from './color' 16 | export * from './back-color' 17 | export * from './link' 18 | export * from './align' 19 | export * from './line-height' 20 | export * from './indent' 21 | export * from './horizontal' 22 | export * from './blockquote' 23 | export * from './heading' 24 | export * from './list' 25 | export * from './task' 26 | export * from './math' 27 | export * from './code-block' 28 | export * from './attachment' 29 | export * from './table' 30 | -------------------------------------------------------------------------------- /lib/model/config/event-handler.d.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '../Editor'; 2 | /** 3 | * 监听外部改变selection 4 | */ 5 | export declare const onSelectionChange: (this: Editor) => Promise; 6 | /** 7 | * 监听beforeinput 8 | */ 9 | export declare const onBeforeInput: (this: Editor, e: Event) => Promise; 10 | /** 11 | * 监听中文输入 12 | */ 13 | export declare const onComposition: (this: Editor, e: Event) => Promise; 14 | /** 15 | * 监听键盘事件 16 | */ 17 | export declare const onKeyboard: (this: Editor, e: Event) => void; 18 | /** 19 | * 监听编辑器获取焦点 20 | */ 21 | export declare const onFocus: (this: Editor, e: Event) => void; 22 | /** 23 | * 监听编辑器失去焦点 24 | */ 25 | export declare const onBlur: (this: Editor, e: Event) => void; 26 | /** 27 | * 监听编辑器复制 28 | */ 29 | export declare const onCopy: (this: Editor, e: Event) => void; 30 | /** 31 | * 监听编辑器剪切 32 | */ 33 | export declare const onCut: (this: Editor, e: Event) => void; 34 | -------------------------------------------------------------------------------- /lib/extensions/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './Extension'; 2 | export * from './text'; 3 | export * from './history'; 4 | export * from './image'; 5 | export * from './video'; 6 | export * from './bold'; 7 | export * from './italic'; 8 | export * from './strikethrough'; 9 | export * from './underline'; 10 | export * from './superscript'; 11 | export * from './subscript'; 12 | export * from './code'; 13 | export * from './font-size'; 14 | export * from './font-family'; 15 | export * from './color'; 16 | export * from './back-color'; 17 | export * from './link'; 18 | export * from './align'; 19 | export * from './line-height'; 20 | export * from './indent'; 21 | export * from './horizontal'; 22 | export * from './blockquote'; 23 | export * from './heading'; 24 | export * from './list'; 25 | export * from './task'; 26 | export * from './math'; 27 | export * from './code-block'; 28 | export * from './attachment'; 29 | export * from './table'; 30 | -------------------------------------------------------------------------------- /docs/guide/clipboard.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 剪切板操作 3 | --- 4 | 5 | # 剪切板操作 6 | 7 | ## 复制 8 | 9 | 编辑器自身对复制操作没有进行重写,但是你可以通过 `allowCopy` 来禁用复制功能 10 | 11 | ## 剪切 12 | 13 | 编辑器自身对剪切操作没有进行重写,主要处理了剪切时编辑器内节点的更新,你可以通过 `allowCut` 来禁用剪切功能 14 | 15 | ## 粘贴 16 | 17 | 编辑器重写了粘贴的操作,并且可以通过 `allowPaste` 来禁用粘贴功能 18 | 19 | 在允许粘贴的情况下,还可以通过 `allowPasteHtml` 来设置是否允许粘贴内容的样式 20 | 21 | - 当 `editor.priorityPasteFiles` 为 `true` 时,编辑器内会优先考虑剪切板中的文件数据,此时如果剪切板存在文件,会先进行文件粘贴处理 22 | - 当 `editor.priorityPasteFiles` 为 `false` 时,编辑器内部会优先考虑剪切板中的 `html` 数据(必须是在允许粘贴内容样式的前提下),此时哪怕剪切板有文件,但是只要同时存在 `html` 数据,也会直接处理 `html` 数据而忽略文件数据,此时会触发 `onPasteHtml` 23 | - 如果不允许携带样式的内容粘贴,则只会粘贴纯文本内容,此时会触发 `onPasteText` 24 | - 对于文件的粘贴,具体分为图片粘贴和视频粘贴、其他文件粘贴,依次触发的 `onPasteImage` `onPasteVideo` `onPasteFile` 25 | 26 | > 在使用 onPasteText、onPasteHtml、onPasteImage、onPasteVideo、onPasteFile 这些自定义粘贴事件自行处理粘贴时,不需要调用 updateView 方法来更新视图,编辑器自身会在粘贴完成后更新视图 27 | 28 | > 编辑器默认没有处理图片、视频以外的文件粘贴,你可以基于 onPasteFile 自行处理 29 | -------------------------------------------------------------------------------- /lib/extensions/heading/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | export type HeadingLevelType = 0 | 1 | 2 | 3 | 4 | 5 | 6; 4 | declare module '../../model' { 5 | interface EditorCommandsType { 6 | /** 7 | * 获取光标所在的标题,如果光标不在一个标题内,返回null 8 | */ 9 | getHeading?: (level: HeadingLevelType) => KNode | null; 10 | /** 11 | * 判断光标范围内是否有标题 12 | */ 13 | hasHeading?: (level: HeadingLevelType) => boolean; 14 | /** 15 | * 光标范围内是否都是标题 16 | */ 17 | allHeading?: (level: HeadingLevelType) => boolean; 18 | /** 19 | * 设置标题 20 | */ 21 | setHeading?: (level: HeadingLevelType) => Promise; 22 | /** 23 | * 取消标题 24 | */ 25 | unsetHeading?: (level: HeadingLevelType) => Promise; 26 | } 27 | } 28 | export declare const HeadingExtension: () => Extension; 29 | -------------------------------------------------------------------------------- /lib/model/History.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from './KNode'; 2 | import { Selection } from './Selection'; 3 | /** 4 | * 历史记录的record类型 5 | */ 6 | export type HistoryRecordType = { 7 | nodes: KNode[]; 8 | selection: Selection; 9 | }; 10 | /** 11 | * 历史记录 12 | */ 13 | export declare class History { 14 | /** 15 | * 存放历史记录的堆栈 16 | */ 17 | records: HistoryRecordType[]; 18 | /** 19 | * 存放撤销记录的堆栈 20 | */ 21 | redoRecords: HistoryRecordType[]; 22 | /** 23 | * 复制selection 24 | */ 25 | cloneSelection(newNodes: KNode[], selection: Selection): Selection; 26 | /** 27 | * 保存新的记录 28 | */ 29 | setState(nodes: KNode[], selection: Selection): void; 30 | /** 31 | * 撤销操作:返回上一个历史记录 32 | */ 33 | setUndo(): HistoryRecordType | null; 34 | /** 35 | * 重做操作:返回下一个历史记录 36 | */ 37 | setRedo(): HistoryRecordType | null; 38 | /** 39 | * 更新当前记录的编辑器的光标 40 | */ 41 | updateSelection(selection: Selection): void; 42 | } 43 | -------------------------------------------------------------------------------- /lib/extensions/image/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | /** 4 | * 插入图片方法入参类型 5 | */ 6 | export type SetImageOptionType = { 7 | src: string; 8 | alt?: string; 9 | width?: string; 10 | }; 11 | /** 12 | * 更新图片方法入参类型 13 | */ 14 | export type UpdateImageOptionType = { 15 | src?: string; 16 | alt?: string; 17 | }; 18 | declare module '../../model' { 19 | interface EditorCommandsType { 20 | /** 21 | * 获取光标所在的图片,如果光标不在一张图片内,返回null 22 | */ 23 | getImage?: () => KNode | null; 24 | /** 25 | * 判断光标范围内是否有图片 26 | */ 27 | hasImage?: () => boolean; 28 | /** 29 | * 插入图片 30 | */ 31 | setImage?: (options: SetImageOptionType) => Promise; 32 | /** 33 | * 更新图片 34 | */ 35 | updateImage?: (options: UpdateImageOptionType) => Promise; 36 | } 37 | } 38 | export declare const ImageExtension: () => Extension; 39 | -------------------------------------------------------------------------------- /lib/extensions/code-block/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | import { HljsLanguageType } from './hljs'; 4 | declare module '../../model' { 5 | interface EditorCommandsType { 6 | /** 7 | * 获取光标所在的代码块节点,如果光标不在一个代码块节点内,返回null 8 | */ 9 | getCodeBlock?: () => KNode | null; 10 | /** 11 | * 判断光标范围内是否有代码块节点 12 | */ 13 | hasCodeBlock?: () => boolean; 14 | /** 15 | * 光标范围内是否都是代码块节点 16 | */ 17 | allCodeBlock?: () => boolean; 18 | /** 19 | * 设置代码块 20 | */ 21 | setCodeBlock?: () => Promise; 22 | /** 23 | * 取消代码块 24 | */ 25 | unsetCodeBlock?: () => Promise; 26 | /** 27 | * 更新光标所在代码块的语言类型 28 | */ 29 | updateCodeBlockLanguage?: (language: HljsLanguageType) => Promise; 30 | } 31 | } 32 | export declare const CodeBlockExtension: () => Extension; 33 | export * from './hljs'; 34 | -------------------------------------------------------------------------------- /lib/extensions/video/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | /** 4 | * 插入视频方法入参类型 5 | */ 6 | export type SetVideoOptionType = { 7 | src: string; 8 | width?: string; 9 | autoplay?: boolean; 10 | }; 11 | /** 12 | * 更新视频方法入参类型 13 | */ 14 | export type UpdateVideoOptionType = { 15 | controls?: boolean; 16 | muted?: boolean; 17 | loop?: boolean; 18 | }; 19 | declare module '../../model' { 20 | interface EditorCommandsType { 21 | /** 22 | * 获取光标所在的视频,如果光标不在一个视频内,返回null 23 | */ 24 | getVideo?: () => KNode | null; 25 | /** 26 | * 判断光标范围内是否有视频 27 | */ 28 | hasVideo?: () => boolean; 29 | /** 30 | * 插入视频 31 | */ 32 | setVideo?: (options: SetVideoOptionType) => Promise; 33 | /** 34 | * 更新视频 35 | */ 36 | updateVideo?: (options: UpdateVideoOptionType) => Promise; 37 | } 38 | } 39 | export declare const VideoExtension: () => Extension; 40 | -------------------------------------------------------------------------------- /lib/extensions/link/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | /** 4 | * 插入链接方法入参类型 5 | */ 6 | export type SetLinkOptionType = { 7 | href: string; 8 | text?: string; 9 | newOpen?: boolean; 10 | }; 11 | /** 12 | * 更新链接方法入参类型 13 | */ 14 | export type UpdateLinkOptionType = { 15 | href?: string; 16 | newOpen?: boolean; 17 | }; 18 | declare module '../../model' { 19 | interface EditorCommandsType { 20 | /** 21 | * 获取光标所在的链接,如果光标不在一个链接内,返回null 22 | */ 23 | getLink?: () => KNode | null; 24 | /** 25 | * 判断光标范围内是否有链接 26 | */ 27 | hasLink?: () => boolean; 28 | /** 29 | * 设置连接 30 | */ 31 | setLink?: (options: SetLinkOptionType) => Promise; 32 | /** 33 | * 更新链接 34 | */ 35 | updateLink?: (options: UpdateLinkOptionType) => Promise; 36 | /** 37 | * 取消链接 38 | */ 39 | unsetLink?: () => Promise; 40 | } 41 | } 42 | export declare const LinkExtension: () => Extension; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 so-better 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | lastUpdated: false 3 | aside: false 4 | layout: home 5 | title: 主页 6 | 7 | hero: 8 | name: kaitify 9 | text: 基于原生JS的富文本编辑器核心库 10 | tagline: 轻松构建一个L1级别的富文本编辑器... 11 | image: 12 | src: /logo.png 13 | width: 288 14 | actions: 15 | - theme: brand 16 | text: Get Started 17 | link: /guide/introduction 18 | - theme: alt 19 | text: View on GitHub 20 | link: https://github.com/so-better/kaitify-core 21 | 22 | features: 23 | - title: '@kaitify/vue' 24 | details: 基于kaitify开发的vue富文本编辑器核心库 25 | link: https://www.so-better.cn/docs/kaitify-vue/ 26 | - title: '@kaitify/react' 27 | details: 基于kaitify开发的react富文本编辑器核心库 28 | link: https://www.so-better.cn/docs/kaitify-react/ 29 | - title: dap-util 30 | details: 一个轻量的前端JavaScript工具库,专注于JavaScript,不关心UI 31 | link: https://www.so-better.cn/docs/dap-util/ 32 | - title: animator-clip 33 | details: 一个基于 JavaScript 的 requestAnimationFrame API 封装的轻量级 JS 动画插件 34 | link: https://www.so-better.cn/docs/animator-clip/ 35 | - title: rem-fit 36 | details: 一款使用rem适配web页面的轻量级插件 37 | link: https://www.so-better.cn/docs/rem-fit/ 38 | - title: ruax 39 | details: 一个轻量级的Javascript异步数据请求库 40 | link: https://www.so-better.cn/docs/ruax/ 41 | --- 42 | -------------------------------------------------------------------------------- /src/css/style.less: -------------------------------------------------------------------------------- 1 | .kaitify { 2 | display: block; 3 | position: relative; 4 | font-size: var(--kaitify-font-size); 5 | font-family: var(--kaitify-font-family); 6 | color: var(--kaitify-font-color); 7 | -webkit-font-smoothing: antialiased; 8 | text-size-adjust: none; 9 | line-height: var(--kaitify-line-height); 10 | background: var(--kaitify-background-color); 11 | padding: var(--kaitify-padding); 12 | overflow-x: hidden; 13 | overflow-y: auto; 14 | outline: none; 15 | white-space: break-spaces; //使用该样式处理空格 16 | 17 | ::selection { 18 | background: var(--kaitify-lighter-theme); 19 | } 20 | 21 | *, 22 | *::before, 23 | *::after { 24 | box-sizing: border-box; 25 | -webkit-tap-highlight-color: transparent; 26 | outline: none; 27 | } 28 | 29 | //编辑状态下才有的样式 30 | &[contenteditable='true'] { 31 | &[kaitify-placeholder].kaitify-showplaceholder::before { 32 | content: attr(kaitify-placeholder); 33 | position: absolute; 34 | left: auto; 35 | top: auto; 36 | line-height: var(--kaitify-line-height); 37 | opacity: 0.5; 38 | cursor: text; 39 | vertical-align: middle; 40 | } 41 | 42 | [contenteditable='false']:hover { 43 | cursor: not-allowed; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/extensions/text/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNodeMarksType, KNodeStylesType } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | declare module '../../model' { 4 | interface EditorCommandsType { 5 | /** 6 | * 判断光标所在文本是否具有某个样式 7 | */ 8 | isTextStyle?: (styleName: string, styleValue?: string | number) => boolean; 9 | /** 10 | * 判断光标所在文本是否具有某个标记 11 | */ 12 | isTextMark?: (markName: string, markValue?: string | number) => boolean; 13 | /** 14 | * 设置光标所在文本样式 15 | */ 16 | setTextStyle?: (styles: KNodeStylesType, updateView?: boolean) => Promise; 17 | /** 18 | * 设置光标所在文本标记 19 | */ 20 | setTextMark?: (marks: KNodeMarksType, updateView?: boolean) => Promise; 21 | /** 22 | * 移除光标所在文本样式 23 | */ 24 | removeTextStyle?: (styleNames?: string[], updateView?: boolean) => Promise; 25 | /** 26 | * 移除光标所在文本标记 27 | */ 28 | removeTextMark?: (markNames?: string[], updateView?: boolean) => Promise; 29 | /** 30 | * 清除格式 31 | */ 32 | clearFormat?: () => Promise; 33 | } 34 | } 35 | export declare const TextExtension: () => Extension; 36 | -------------------------------------------------------------------------------- /src/css/var.less: -------------------------------------------------------------------------------- 1 | @theme: #308af3; 2 | 3 | .kaitify { 4 | //主题色 5 | --kaitify-theme: @theme; 6 | //最浅主题色,通常用于悬浮效果 7 | --kaitify-lightest-theme: fade(@theme, 10); 8 | //更浅主题色,通常用于激活效果 9 | --kaitify-lighter-theme: fade(@theme, 20); 10 | //浅主题色,用于选区颜色 11 | --kaitify-light-theme: fade(@theme, 30); 12 | //字体颜色 13 | --kaitify-font-color: #505050; 14 | //边框颜色 15 | --kaitify-border-color: #dedede; 16 | //背景色 17 | --kaitify-background-color: #fff; 18 | //行高 19 | --kaitify-line-height: 1.5; 20 | //字号 21 | --kaitify-font-size: 14px; 22 | //通用圆角大小 23 | --kaitify-border-radius: 3px; 24 | //外边距 25 | --kaitify-margin: 10px; 26 | --kaitify-small-margin: 5px; 27 | --kaitify-large-margin: 15px; 28 | //内边距 29 | --kaitify-padding: 10px; 30 | --kaitify-small-padding: 5px; 31 | --kaitify-large-padding: 20px; 32 | //节点两侧和其他节点的间距 33 | --kaitify-sides-between: 2px; 34 | //字体 35 | --kaitify-font-family: PingFang SC, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Helvetica, Hiragino KaKu Gothic Pro, Microsoft YaHei, Arial, sans-serif; 36 | } 37 | 38 | .kaitify.kaitify-dark { 39 | //字体颜色 40 | --kaitify-font-color: #f5f5f5; 41 | //边框颜色 42 | --kaitify-border-color: #3b3b3b; 43 | //背景色 44 | --kaitify-background-color: #1b1b1f; 45 | } 46 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import dts from 'vite-plugin-dts' 4 | import path from 'path' 5 | import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js' 6 | 7 | export default defineConfig({ 8 | plugins: [vue(), dts(), cssInjectedByJsPlugin({ topExecutionPriority: true })], 9 | resolve: { 10 | alias: { 11 | '@': path.resolve(__dirname, './src') 12 | } 13 | }, 14 | publicDir: 'public', 15 | build: { 16 | //打包后的目录名称 17 | outDir: 'lib', 18 | minify: 'terser', 19 | lib: { 20 | entry: path.resolve(__dirname, 'src/index.ts'), 21 | name: 'kaitify-core', 22 | fileName: format => `kaitify-core.${format}.js` 23 | }, 24 | rollupOptions: { 25 | // 确保外部化处理那些你不想打包进库的依赖 26 | external: ['vue'], 27 | output: { 28 | // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量 29 | globals: { 30 | vue: 'Vue' 31 | }, 32 | exports: 'named' 33 | } 34 | }, 35 | sourcemap: false //是否构建source map 文件 36 | }, 37 | css: { 38 | preprocessorOptions: { 39 | less: { 40 | // 使用 less 编写样式的 UI 库(如 antd)时建议加入这个设置 41 | javascriptEnabled: true 42 | } 43 | } 44 | }, 45 | server: { 46 | host: '0.0.0.0' 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /docs/guide/dom-parse.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DOM转换规则 3 | --- 4 | 5 | # DOM 转换规则 6 | 7 | 构建编辑器时,编辑器内部会将入参 `value` 提供的 `html` 内容转为 `KNode` 节点数组 8 | 9 | 除此之外,在粘贴 `html` 内容时,也会将粘贴的内容转为节点数组 10 | 11 | 二者涉及到的转换功能,离不开 `Editor` 实例的 `htmlParseNode` 方法和 `domParseNode` 方法 12 | 13 | > htmlParseNode 本质上内部调用的仍然是 domParseNode 方法,所以这里着重讲述 domParseNode 方法 14 | 15 | ## domParseNode 16 | 17 | `domParseNode` 方法内部在将 `dom` 元素转为 `KNode` 节点时,会有一个默认的处理转换过程 18 | 19 | - dom(`nodeType == 3`)会被转为文本节点 20 | - dom(`nodeType == 1`)会再次进行分类: 21 | - 元素标签在 editor.emptyRenderTags 范围内的会被置空,即编辑器不进行处理和渲染 22 | - `p` `div` `address` `article` `aside` `nav` `section` 元素会转为块节点 23 | - `span` `label`元素会被转为行内节点 24 | - `br`元素会被转为闭合节点 25 | - 其余元素不在 `editor.extraKeepTags` 内的,都会被转为默认文本标签的行内节点 26 | 27 | > 每个内置扩展都可能设置了 extraKeepTags 属性,所以编辑器内保留的元素远不止以上这些 28 | 29 | 最终,`domParseNode` 会返回给你一个 `KNode` 节点 30 | 31 | ## DOM 转换后回调 onDomParseNode 32 | 33 | 如果上述返回的节点并不完全符合你的需求,或者在预期之外,我们还提供了一个 `onDomParseNode` 方法,在 `domParseNode` 方法返回节点时再进行处理 34 | ,以确保最终返回的节点符合要求 35 | 36 | ```ts 37 | const editor = await Editor.configure({ 38 | value: '


', 39 | onDomParseNode(node) { 40 | //有data-inline的节点都转为行内节点 41 | if (node.hasMarks() && node.marks['data-inline']) { 42 | node.type = 'inline' 43 | } 44 | return node 45 | } 46 | }) 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/guide/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速上手 3 | --- 4 | 5 | # 快速上手 6 | 7 | ## 基本概念 8 | 9 | - `Editor`:kaitify 的核心类型,整个编辑器的所有操作包括任何扩展 `Extension` 都离不开这个类,我们需要它去创建一个编辑器实例`editor`,通过编辑器实例来对编辑器进行操作 10 | - `KNode`:kaitify 的节点,实际上我们可以把它叫作 `Node`,之所以取名为 `KNode`,是为了与浏览器原生的 `Node` 对象作为区分,它是编辑器内容的基础数据类型,编辑器的 `html` 内容都是由节点数组(`editor.stackNodes`)经过渲染生成的 11 | 12 | ## 构建一个编辑器 13 | 14 | ```html 15 |
16 | ``` 17 | 18 | ```ts 19 | //编辑器创建并渲染的过程是异步的 20 | //因此需要通过 `await` 来等待编辑器创建完成后获取编辑器实例 21 | const editor = await Editor.configure({ 22 | el: '#editor', 23 | value: '

hello

' 24 | }) 25 | ``` 26 | 27 | ## 创建一个段落,并加入到编辑器内 28 | 29 | ```html 30 |
31 | ``` 32 | 33 | ```ts 34 | const editor = await Editor.configure({ 35 | el: '#editor', 36 | value: '

hello

' 37 | }) 38 | const paragraph = KNode.create({ 39 | type: 'block', 40 | tag: 'p', 41 | children: [ 42 | { 43 | type: 'text', 44 | textContent: '我是一个段落' 45 | } 46 | ] 47 | }) 48 | //直接给stackNodes重新赋值,整个编辑器的内容都会被替换成这个段落 49 | editor.stackNodes = [paragraph] 50 | //更新编辑器视图 51 | editor.updateView() 52 | ``` 53 | 54 | > [!IMPORTANT] 至此,基本的编辑器已经创建完成了 55 | > 如果你还需要复杂的操作,可以通过 Editor 和 KNode 去操作编辑器,也可以使用编辑器内置的扩展功能 56 | -------------------------------------------------------------------------------- /lib/extensions/attachment/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | export type SetAttachmentOptionType = { 4 | url: string; 5 | text: string; 6 | icon?: string; 7 | }; 8 | export type UpdateAttachmentOptionType = { 9 | url?: string; 10 | text?: string; 11 | icon?: string; 12 | }; 13 | export type AttachmentExtensionPropsType = { 14 | icon: string; 15 | }; 16 | declare module '../../model' { 17 | interface EditorCommandsType { 18 | /** 19 | * 获取光标所在的附件节点,如果光标不在一个附件节点内,返回null 20 | */ 21 | getAttachment?: () => KNode | null; 22 | /** 23 | * 判断光标范围内是否有附件节点 24 | */ 25 | hasAttachment?: () => boolean; 26 | /** 27 | * 插入附件 28 | */ 29 | setAttachment?: (options: SetAttachmentOptionType) => Promise; 30 | /** 31 | * 更新附件 32 | */ 33 | updateAttachment?: (options: UpdateAttachmentOptionType) => Promise; 34 | /** 35 | * 获取附件信息 36 | */ 37 | getAttachmentInfo?: () => { 38 | url: string; 39 | text: string; 40 | icon: string; 41 | } | null; 42 | } 43 | } 44 | export declare const AttachmentExtension: (props?: AttachmentExtensionPropsType) => Extension; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kaitify/core", 3 | "version": "0.0.2-beta.8", 4 | "private": false, 5 | "type": "module", 6 | "author": "so-better", 7 | "main": "lib/kaitify-core.umd.js", 8 | "module": "lib/kaitify-core.es.js", 9 | "types": "lib/index.d.ts", 10 | "license": "MIT", 11 | "description": "基于原生JS的富文本编辑器核心库", 12 | "scripts": { 13 | "dev": "vite", 14 | "lib": "tsc && vite build", 15 | "docs:dev": "vitepress dev docs", 16 | "docs:build": "vitepress build docs" 17 | }, 18 | "dependencies": { 19 | "csstype": "^3.1.3", 20 | "dap-util": "1.6.0", 21 | "highlight.js": "^11.10.0", 22 | "interactjs": "^1.10.27", 23 | "katex": "^0.16.11", 24 | "vue": "^3.3.13" 25 | }, 26 | "devDependencies": { 27 | "@types/katex": "^0.16.7", 28 | "@types/node": "^20.11.24", 29 | "@vitejs/plugin-vue": "^5.0.4", 30 | "less": "^3.0.4", 31 | "less-loader": "^5.0.0", 32 | "terser": "^5.16.9", 33 | "typescript": "^5.6.3", 34 | "vite": "^5.4.10", 35 | "vite-plugin-css-injected-by-js": "^3.5.1", 36 | "vite-plugin-dts": "^4.3.0", 37 | "vitepress": "^1.5.0" 38 | }, 39 | "browserslist": [ 40 | "> 1%", 41 | "last 2 versions", 42 | "not dead" 43 | ], 44 | "repository": { 45 | "type": "git", 46 | "url": "https://github.com/so-better/kaitify-core" 47 | }, 48 | "publishConfig": { 49 | "access": "public", 50 | "registry": "https://registry.npmjs.org" 51 | } 52 | } -------------------------------------------------------------------------------- /docs/.vitepress/theme/style.less: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-home-hero-name-background: linear-gradient(120deg, #8b1bbf 20%, #308af3); 3 | --vp-home-hero-name-color: transparent; 4 | --vp-c-indigo-1: #1269cd; 5 | --vp-c-indigo-2: #1e79e1; 6 | --vp-c-indigo-3: #308af3; 7 | 8 | --vp-code-block-bg: #f7f8fa; 9 | 10 | h5 > .VPBadge.danger { 11 | font-family: var(--vp-font-family-mono); 12 | } 13 | 14 | &.dark { 15 | --vp-code-block-bg: #1f1f22; 16 | } 17 | } 18 | 19 | .demo-button { 20 | display: inline-flex; 21 | justify-content: center; 22 | align-items: center; 23 | padding: 2px 6px; 24 | border: 1px solid fade(#308af3, 30%); 25 | background: fade(#308af3, 10%); 26 | border-radius: 3px; 27 | font-size: 14px; 28 | color: #308af3; 29 | transition: all 300ms; 30 | 31 | &:not(:disabled):focus-visible { 32 | outline: none; 33 | } 34 | 35 | &:not(:disabled):hover { 36 | cursor: pointer; 37 | background: fade(#308af3, 20%); 38 | } 39 | 40 | &:not(:disabled):active { 41 | border: 1px solid fade(#308af3, 50%); 42 | background: fade(#308af3, 30%); 43 | } 44 | 45 | & + .demo-button { 46 | margin-left: 10px; 47 | } 48 | 49 | &:disabled { 50 | opacity: 0.6; 51 | cursor: not-allowed; 52 | } 53 | } 54 | 55 | .kaitify { 56 | border: 1px solid #ddd; 57 | border-radius: 3px; 58 | transition: border-color 500ms; 59 | 60 | &:focus { 61 | border-color: var(--kaitify-theme); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /docs/guide/selection.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Selection 3 | --- 4 | 5 | # Selection 6 | 7 | > kaitify 内部维护了一套基于浏览器 Selection API 而构建的虚拟光标数据,我们也称之为 Selection,它能够更直观地呈现出光标在节点数组中的位置 8 | 9 | ## 虚拟光标结构 10 | 11 | 编辑器创建时,会初始化一个 `Selection` 实例,可以通过编辑器实例的 `selection` 属性访问,实例具有以下属性: 12 | 13 | ##### start 14 | 15 | 虚拟光标的起点,包含 2 个属性:`node` 和 `offset`,其中 `node` 表示起点光标所在的 `KNode` 节点,只能是文本节点或者闭合节点,`offset` 表示光标在节点内的偏移值 16 | 17 | ##### end 18 | 19 | 虚拟光标的终点,包含 2 个属性:`node` 和 `offset`,其中 `node` 表示起点光标所在的 `KNode` 节点,只能是文本节点或者闭合节点,`offset` 表示光标在节点内的偏移值 20 | 21 | > 对于可以作为起点和终点的节点,通常称之为“可以设置为光标点的节点”或者“可聚焦节点” 22 | 23 | > 终点所表示的位置必须在起点位置的后面,如果通过修改 editor.selection 去刻意修改 end 导致 end 在 start 前面,可能出现难以预料的错误 24 | 25 | ## 常用方法 26 | 27 | ##### focused() 28 | 29 | 是否已经初始化设置光标位置 30 | 31 | - 类型 32 | 33 | ```ts 34 | focused(): boolean 35 | ``` 36 | 37 | - 详细信息 38 | 39 | 编辑器创建后,光标未聚焦在编辑器内时,通过编辑器实例访问 `selection` 属性,虽然可以获取到 `start` 和 `end` 属性,但是二者都是 `undefined`,表示编辑器未初始设置光标位置,此时通过该方法判断会返回 `false` 40 | 41 | - 示例 42 | 43 | ```ts 44 | const focused = editor.selection.focused() 45 | //常用来判断编辑器是否初始设置光标,从而继续下一步操作 46 | ``` 47 | 48 | ##### collapsed() 49 | 50 | 光标是否折叠 51 | 52 | - 类型 53 | 54 | ```ts 55 | collapsed(): boolean 56 | ``` 57 | 58 | - 详细信息 59 | 60 | 该方法会判断编辑器内的虚拟光标是否折叠,即光标的起点和终点是否在同一个位置,即 `node` 和 `offset` 都完全一致,如果折叠返回 `true`,否则返回 `false` 61 | -------------------------------------------------------------------------------- /src/view/index.ts: -------------------------------------------------------------------------------- 1 | import { common as DapCommon } from 'dap-util' 2 | import { Editor, KNode, KNodeMarksType, KNodeStylesType } from '@/model' 3 | 4 | /** 5 | * 渲染参数类型 6 | */ 7 | export type KNodeRenderOptionType = { 8 | key: number 9 | tag: string 10 | attrs: KNodeMarksType 11 | styles: KNodeStylesType 12 | namespace?: string 13 | textContent?: string 14 | children?: KNodeRenderOptionType[] 15 | } 16 | 17 | /** 18 | * 节点渲染成dom后在dom上生成的一个特殊标记名称,它的值是节点的key值 19 | */ 20 | export const NODE_MARK = 'kaitify-node' 21 | 22 | /** 23 | * 获取节点的渲染参数 24 | */ 25 | export const getNodeRenderOptions = (editor: Editor, node: KNode): KNodeRenderOptionType => { 26 | //文本节点 27 | if (node.isText()) { 28 | return { 29 | key: node.key, 30 | tag: editor.textRenderTag, 31 | namespace: node.namespace, 32 | attrs: node.hasMarks() ? DapCommon.clone({ ...node.marks, [NODE_MARK]: node.key }) : { [NODE_MARK]: node.key }, 33 | styles: node.hasStyles() ? DapCommon.clone(node.styles!) : {}, 34 | textContent: node.textContent 35 | } 36 | } 37 | //其他节点 38 | return { 39 | key: node.key, 40 | tag: node.tag!, 41 | namespace: node.namespace, 42 | attrs: node.hasMarks() ? DapCommon.clone({ ...node.marks, [NODE_MARK]: node.key }) : { [NODE_MARK]: node.key }, 43 | styles: node.hasStyles() ? DapCommon.clone(node.styles!) : {}, 44 | children: node.hasChildren() ? node.children!.map(item => getNodeRenderOptions(editor, item)) : [] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/model/config/format-rules.d.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from '../Editor'; 2 | import { KNode } from '../KNode'; 3 | /** 4 | * 格式化函数类型 5 | */ 6 | export type RuleFunctionType = (state: { 7 | editor: Editor; 8 | node: KNode; 9 | }) => void; 10 | /** 11 | * 针对节点自身:处理块节点的标签,部分块节点需要转为默认块节点标签 12 | */ 13 | export declare const fomratBlockTagParse: RuleFunctionType; 14 | /** 15 | * 针对子节点中的块节点: 16 | * 1. 子节点中含有块节点则该节点转为块节点; 17 | * 2. 子节点中含有块节点,则其他节点也转为块节点 18 | */ 19 | export declare const formatBlockInChildren: RuleFunctionType; 20 | /** 21 | * 针对节点自身:处理不可编辑的非块级节点:在两侧添加零宽度空白字符 & 重置不可编辑节点内的光标位置 22 | */ 23 | export declare const formatUneditableNoodes: RuleFunctionType; 24 | /** 25 | * 针对节点的子节点数组:处理子节点中的占位符,如果占位符和其他节点共存则删除占位符,如果只存在占位符则将多个占位符合并为一个(光标可能会更新) 26 | */ 27 | export declare const formatPlaceholderMerge: RuleFunctionType; 28 | /** 29 | * 针对节点自身: 30 | * 1. 统一将文本节点内的\r\n换成\n,解决Windows兼容问题 31 | * 2. 统一将文本节点内的 (\u00A0)换成普通空格 32 | * 3. 统一将文本节点内的零宽度无断空格换成零宽度空格(\uFEFF -> \u200B) 33 | * 4. 统一将文本节点内的\n后面加上零宽度空白字符 34 | */ 35 | export declare const formatLineBreakSpaceText: RuleFunctionType; 36 | /** 37 | * 针对节点自身:将文本节点内连续的零宽度空白字符合并(光标可能会更新) 38 | */ 39 | export declare const formatZeroWidthTextMerge: RuleFunctionType; 40 | /** 41 | * 针对节点的子节点数组:兄弟节点合并策略(光标可能会更新) 42 | */ 43 | export declare const formatSiblingNodesMerge: RuleFunctionType; 44 | /** 45 | * 针对节点的子节点数组:父子节点合并策略(光标可能会更新) 46 | */ 47 | export declare const formatParentNodeMerge: RuleFunctionType; 48 | -------------------------------------------------------------------------------- /src/extensions/task/style.less: -------------------------------------------------------------------------------- 1 | .kaitify { 2 | div[kaitify-task] { 3 | display: block; 4 | position: relative; 5 | margin: 0 0 var(--kaitify-large-margin) 0; 6 | padding: 0 0 0 26px; 7 | width: 100%; 8 | 9 | &:last-child { 10 | margin-bottom: 0 !important; 11 | } 12 | 13 | &::before { 14 | position: absolute; 15 | left: 0; 16 | top: 3px; 17 | content: ''; 18 | width: 15px; 19 | height: 15px; 20 | border: 1px solid var(--kaitify-theme); 21 | border-radius: var(--kaitify-border-radius); 22 | user-select: none; 23 | touch-action: none; 24 | transition: background 200ms; 25 | } 26 | 27 | &::after { 28 | content: ''; 29 | position: absolute; 30 | left: 5px; 31 | top: 4.5px; 32 | width: 0px; 33 | height: 0px; 34 | margin-top: 5px; 35 | border-style: solid; 36 | border-width: 0; 37 | transform: rotate(45deg); 38 | border-color: #fff; 39 | user-select: none; 40 | touch-action: none; 41 | transition: width 100ms ease-in-out, height 200ms ease-in-out, margin-top 200ms ease-in-out; 42 | } 43 | 44 | &[kaitify-task='done'] { 45 | text-decoration: line-through; 46 | 47 | &::before { 48 | background: var(--kaitify-theme); 49 | } 50 | 51 | &::after { 52 | margin-top: 0; 53 | width: 5px; 54 | height: 10px; 55 | border-width: 0 2px 2px 0; 56 | } 57 | } 58 | } 59 | 60 | &[contenteditable='true'] { 61 | div[kaitify-task] { 62 | &::before { 63 | cursor: pointer; 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /docs/extensions/built-in/horizontal.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: horizontal 水平线 3 | --- 4 | 5 | # horizontal 水平线 6 | 7 | 支持水平线的渲染,提供插入水平线的能力 8 | 9 | ## Commands 命令 10 | 11 | ##### setHorizontal() 12 | 13 | 在光标内插入水平线 14 | 15 | - 类型 16 | 17 | ```ts 18 | setHorizontal(): Promise 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法会向光标所在处插入一个水平线,插入完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 24 | 25 | - 示例 26 | 27 | ```ts 28 | await editor.commands.setHorizontal() 29 | ``` 30 | 31 | ## 代码示例 32 | 33 |
34 | 35 |
36 |
37 | 38 | 66 | -------------------------------------------------------------------------------- /src/extensions/color/index.ts: -------------------------------------------------------------------------------- 1 | import { KNodeStylesType } from '@/model' 2 | import { Extension } from '../Extension' 3 | 4 | declare module '../../model' { 5 | interface EditorCommandsType { 6 | /** 7 | * 光标所在文本的颜色是否与入参一致 8 | */ 9 | isColor?: (value: string) => boolean 10 | /** 11 | * 设置颜色 12 | */ 13 | setColor?: (value: string) => Promise 14 | /** 15 | * 取消颜色 16 | */ 17 | unsetColor?: (value: string) => Promise 18 | } 19 | } 20 | 21 | export const ColorExtension = () => 22 | Extension.create({ 23 | name: 'color', 24 | onPasteKeepStyles(node) { 25 | const styles: KNodeStylesType = {} 26 | if (node.isText() && node.hasStyles()) { 27 | if (node.styles!.hasOwnProperty('color')) styles.color = node.styles!.color 28 | } 29 | return styles 30 | }, 31 | addCommands() { 32 | const isColor = (value: string) => { 33 | return this.commands.isTextStyle!('color', value) 34 | } 35 | 36 | const setColor = async (value: string) => { 37 | if (isColor(value)) { 38 | return 39 | } 40 | await this.commands.setTextStyle!({ 41 | color: value 42 | }) 43 | } 44 | 45 | const unsetColor = async (value: string) => { 46 | if (!isColor(value)) { 47 | return 48 | } 49 | await this.commands.removeTextStyle!(['color']) 50 | } 51 | 52 | return { 53 | isColor, 54 | setColor, 55 | unsetColor 56 | } 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /lib/tools/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNodeMarksType, KNodeStylesType } from '../model'; 2 | /** 3 | * 用于KNode生成唯一的key 4 | */ 5 | export declare const createUniqueKey: () => number; 6 | /** 7 | * 用于编辑器生成唯一的guid 8 | */ 9 | export declare const createGuid: () => number; 10 | /** 11 | * 判断字符串是否零宽度空白字符 12 | */ 13 | export declare const isZeroWidthText: (val: string) => boolean; 14 | /** 15 | * 获取一个零宽度空白字符 16 | */ 17 | export declare const getZeroWidthText: () => string; 18 | /** 19 | * 驼峰转中划线 20 | */ 21 | export declare const camelToKebab: (val: string) => string; 22 | /** 23 | * 中划线转驼峰 24 | */ 25 | export declare const kebabToCamel: (val: string) => string; 26 | /** 27 | * 获取dom元素的属性集合 28 | */ 29 | export declare const getDomAttributes: (dom: HTMLElement) => KNodeMarksType; 30 | /** 31 | * 获取dom元素的样式集合 32 | */ 33 | export declare const getDomStyles: (dom: HTMLElement) => KNodeStylesType; 34 | /** 35 | * 初始化编辑器dom 36 | */ 37 | export declare const initEditorDom: (dom: HTMLElement | string) => HTMLElement; 38 | /** 39 | * 判断某个dom是否包含另一个dom 40 | */ 41 | export declare const isContains: (parent: Node, child: Node) => boolean; 42 | /** 43 | * 延迟指定时间 44 | */ 45 | export declare const delay: (num?: number | undefined) => Promise; 46 | /** 47 | * 删除对象的某个属性 48 | */ 49 | export declare const deleteProperty: (val: any, propertyName: string) => T; 50 | /** 51 | * 键盘Tab是否按下 52 | */ 53 | export declare const isOnlyTab: (e: KeyboardEvent) => boolean; 54 | /** 55 | * 键盘Tab和shift是否一起按下 56 | */ 57 | export declare const isTabWithShift: (e: KeyboardEvent) => boolean; 58 | -------------------------------------------------------------------------------- /lib/extensions/list/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | export type OrderedListType = 'decimal' | 'lower-alpha' | 'upper-alpha' | 'lower-roman' | 'upper-roman' | 'lower-greek' | 'cjk-ideographic'; 4 | export type UnorderListType = 'disc' | 'circle' | 'square'; 5 | export type ListOptionsType = { 6 | ordered?: boolean; 7 | listType?: OrderedListType | UnorderListType; 8 | }; 9 | declare module '../../model' { 10 | interface EditorCommandsType { 11 | /** 12 | * 获取光标所在的有序列表或者无序列表,如果光标不在一个有序列表或者无序列表内,返回null 13 | */ 14 | getList?: (options: ListOptionsType) => KNode | null; 15 | /** 16 | * 判断光标范围内是否有有序列表或者无序列表 17 | */ 18 | hasList?: (options: ListOptionsType) => boolean; 19 | /** 20 | * 判断光标范围内是否都是有序列表或者无序列表 21 | */ 22 | allList?: (options: ListOptionsType) => boolean; 23 | /** 24 | * 设置有序列表或者无序列表 25 | */ 26 | setList?: (options: ListOptionsType) => Promise; 27 | /** 28 | * 取消有序列表或者无序列表 29 | */ 30 | unsetList?: (options: ListOptionsType) => Promise; 31 | /** 32 | * 是否可以生成内嵌列表 33 | */ 34 | canCreateInnerList?: () => { 35 | node: KNode; 36 | previousNode: KNode; 37 | } | null; 38 | /** 39 | * 根据当前光标所在的li节点生成一个内嵌列表 40 | */ 41 | createInnerList?: () => Promise; 42 | } 43 | } 44 | export declare const ListExtension: () => Extension; 45 | -------------------------------------------------------------------------------- /lib/extensions/table/index.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from '../../model'; 2 | import { Extension } from '../Extension'; 3 | export type TableCellsMergeDirectionType = 'left' | 'top' | 'right' | 'bottom'; 4 | declare module '../../model' { 5 | interface EditorCommandsType { 6 | /** 7 | * 获取光标所在的表格节点,如果光标不在一个表格节点内,返回null 8 | */ 9 | getTable?: () => KNode | null; 10 | /** 11 | * 判断光标范围内是否有表格节点 12 | */ 13 | hasTable?: () => boolean; 14 | /** 15 | * 是否可以合并单元格 16 | */ 17 | canMergeTableCells?: (direction: TableCellsMergeDirectionType) => boolean; 18 | /** 19 | * 插入表格 20 | */ 21 | setTable?: ({ rows, columns }: { 22 | rows: number; 23 | columns: number; 24 | }) => Promise; 25 | /** 26 | * 取消表格 27 | */ 28 | unsetTable?: () => Promise; 29 | /** 30 | * 合并单元格 31 | */ 32 | mergeTableCell?: (direction: TableCellsMergeDirectionType) => Promise; 33 | /** 34 | * 添加行 35 | */ 36 | addTableRow?: (direction: 'top' | 'bottom') => Promise; 37 | /** 38 | * 删除行 39 | */ 40 | deleteTableRow?: () => Promise; 41 | /** 42 | * 添加列 43 | */ 44 | addTableColumn?: (direction: 'left' | 'right') => Promise; 45 | /** 46 | * 删除列 47 | */ 48 | deleteTableColumn?: () => Promise; 49 | } 50 | } 51 | export declare const TableExtension: () => Extension; 52 | -------------------------------------------------------------------------------- /src/extensions/font-size/index.ts: -------------------------------------------------------------------------------- 1 | import { KNodeStylesType } from '@/model' 2 | import { Extension } from '../Extension' 3 | 4 | declare module '../../model' { 5 | interface EditorCommandsType { 6 | /** 7 | * 光标所在文本的字号大小是否与入参一致 8 | */ 9 | isFontSize?: (value: string) => boolean 10 | /** 11 | * 设置字号 12 | */ 13 | setFontSize?: (value: string) => Promise 14 | /** 15 | * 取消字号 16 | */ 17 | unsetFontSize?: (value: string) => Promise 18 | } 19 | } 20 | 21 | export const FontSizeExtension = () => 22 | Extension.create({ 23 | name: 'fontSize', 24 | onPasteKeepStyles(node) { 25 | const styles: KNodeStylesType = {} 26 | if (node.isText() && node.hasStyles()) { 27 | if (node.styles!.hasOwnProperty('fontSize')) styles.fontSize = node.styles!.fontSize 28 | } 29 | return styles 30 | }, 31 | addCommands() { 32 | const isFontSize = (value: string) => { 33 | return this.commands.isTextStyle!('fontSize', value) 34 | } 35 | 36 | const setFontSize = async (value: string) => { 37 | if (isFontSize(value)) { 38 | return 39 | } 40 | await this.commands.setTextStyle!({ 41 | fontSize: value 42 | }) 43 | } 44 | 45 | const unsetFontSize = async (value: string) => { 46 | if (!isFontSize(value)) { 47 | return 48 | } 49 | await this.commands.removeTextStyle!(['fontSize']) 50 | } 51 | 52 | return { 53 | isFontSize, 54 | setFontSize, 55 | unsetFontSize 56 | } 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /docs/guide/install.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 安装 3 | --- 4 | 5 | # 安装 6 | 7 | ## 下载 kaitify 本地到使用 8 | 9 | - 下载地址:[kaitify](https://registry.npmmirror.com/@kaitify/core/download/@kaitify/core-0.0.2-beta.8.tgz) 10 | - 下载完成后最终解压得到一个 package 文件夹,进入 package 文件夹后,将 package 目录下的整个 lib 目录拷贝到你的项目下 11 | - 在 html 页面中引入 js 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | ```html 19 | 20 | 23 | ``` 24 | 25 | ## 通过 CDN 使用 kaitify 26 | 27 | 你可以借助 `script` 标签直接通过 CDN 来使用 `kaitify` 28 | 29 | ```html 30 | 31 | 32 | 33 | 34 | ``` 35 | 36 | ```html 37 | 38 | 41 | ``` 42 | 43 | ## 通过 npm/yarn/pnpm 安装 kaitify 44 | 45 | > 假设你已了解关于  html、css  和  javascript  的中级知识,并且对于 npm,es6,webpack 已经有了足够的了解,我们更推荐这类安装方式 46 | 47 | ::: code-group 48 | 49 | ```bash [npm] 50 | npm install @kaitify/core 51 | 52 | # 安装指定版本 53 | npm install @kaitify/core@0.0.2-beta.8 54 | ``` 55 | 56 | ```bash [yarn] 57 | yarn install @kaitify/core 58 | 59 | # 安装指定版本 60 | yarn install @kaitify/core@0.0.2-beta.8 61 | ``` 62 | 63 | ```bash [pnpm] 64 | pnpm install @kaitify/core 65 | 66 | # 安装指定版本 67 | pnpm install @kaitify/core@0.0.2-beta.8 68 | ``` 69 | 70 | ::: 71 | -------------------------------------------------------------------------------- /lib/view/js-render/dom-patch.d.ts: -------------------------------------------------------------------------------- 1 | import { KNode, KNodeMarksType, KNodeStylesType } from '../../model'; 2 | /** 3 | * 节点数组比对结果类型 4 | */ 5 | type NodePatchResultType = { 6 | /** 7 | * 差异类型:insert:插入节点;remove:移除节点;update:节点更新;replace:节点被替换;move:节点同级位置移动 8 | */ 9 | type: 'insert' | 'remove' | 'update' | 'replace' | 'move'; 10 | /** 11 | * 新节点 12 | */ 13 | newNode: KNode | null; 14 | /** 15 | * 旧节点 16 | */ 17 | oldNode: KNode | null; 18 | /** 19 | * 更新的字段 20 | */ 21 | update?: 'textContent' | 'styles' | 'marks'; 22 | }; 23 | /** 24 | * mark比对结果类型 25 | */ 26 | export type MarkPatchResultType = { 27 | /** 28 | * 新增和更新的标记 29 | */ 30 | addMarks: KNodeMarksType; 31 | /** 32 | * 移除的标记 33 | */ 34 | removeMarks: KNodeMarksType; 35 | }; 36 | /** 37 | * style比对结果类型 38 | */ 39 | export type StylePatchResultType = { 40 | /** 41 | * 新增和更新的样式 42 | */ 43 | addStyles: KNodeStylesType; 44 | /** 45 | * 移除的样式 46 | */ 47 | removeStyles: KNodeStylesType; 48 | }; 49 | /** 50 | * 获取两个节点上不相同的marks 51 | */ 52 | export declare const getDifferentMarks: (newNode: KNode, oldNode: KNode) => MarkPatchResultType; 53 | /** 54 | * 获取两个节点上不相同的styles 55 | */ 56 | export declare const getDifferentStyles: (newNode: KNode, oldNode: KNode) => StylePatchResultType; 57 | /** 58 | * 对新旧两个节点数组进行比对 59 | */ 60 | export declare const patchNodes: (newNodes: KNode[], oldNodes: (KNode | null)[]) => NodePatchResultType[]; 61 | /** 62 | * 对新旧两个节点进行比对 63 | */ 64 | export declare const patchNode: (newNode: KNode, oldNode: KNode) => NodePatchResultType[]; 65 | export {}; 66 | -------------------------------------------------------------------------------- /src/extensions/table/style.less: -------------------------------------------------------------------------------- 1 | @backColor: #f1f2f3; 2 | @darkBackColor: #2a2a2a; 3 | 4 | .kaitify { 5 | table { 6 | display: table; 7 | width: 100%; 8 | border: 1px solid var(--kaitify-border-color); 9 | border-collapse: collapse; 10 | margin: 0 0 var(--kaitify-large-margin) 0; 11 | 12 | td { 13 | border: 1px solid var(--kaitify-border-color); 14 | padding: var(--kaitify-padding); 15 | vertical-align: middle; 16 | max-width: 100%; 17 | min-width: 50px; 18 | } 19 | 20 | tr:first-child { 21 | background: @backColor; 22 | 23 | td { 24 | font-weight: bold; 25 | } 26 | } 27 | 28 | tr:nth-child(2n + 3) { 29 | background: fade(@backColor, 20); 30 | } 31 | } 32 | 33 | //非编辑状态下 34 | &:not([contenteditable='true']) { 35 | table { 36 | tr:not(:first-child) { 37 | transition: background 300ms; 38 | 39 | &:hover { 40 | background: fade(@backColor, 50); 41 | } 42 | } 43 | } 44 | } 45 | 46 | &.kaitify-dark { 47 | table { 48 | tr:first-child { 49 | background: @darkBackColor; 50 | 51 | td { 52 | font-weight: bold; 53 | } 54 | } 55 | 56 | tr:nth-child(2n + 3) { 57 | background: fade(@darkBackColor, 20); 58 | } 59 | } 60 | 61 | //非编辑状态下 62 | &:not([contenteditable='true']) { 63 | table { 64 | tr:not(:first-child) { 65 | transition: background 300ms; 66 | 67 | &:hover { 68 | background: fade(@darkBackColor, 50); 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /docs/extensions/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 什么是扩展? 3 | --- 4 | 5 | # 什么是扩展? 6 | 7 | ## 扩展的定义 8 | 9 | 扩展 `Extension` 是 `kaitify` 内部的一套特殊的机制,它将处理同一种事务的逻辑集中到了一起,通过对属性的配置,来设定编辑器的行为 10 | 11 | 扩展只是方便我们进行额外的功能开发,使得同一种功能的代码能够集中在一起,便于维护和优化 12 | 13 | 我们需要知道,即使没有使用扩展,也可以通过对构建编辑器时的入参进行配置达到我们的目的,但是我本人觉得那样不太优雅,因此提供了扩展的机制 14 | 15 | ## 扩展的属性 16 | 17 | 每一个扩展都是 `Extension` 的实例对象,它具有如下的属性: 18 | 19 | ##### name 20 | 21 | 扩展的名称,不同的扩展的 `name` 必须唯一 22 | 23 | ##### registered 24 | 25 | 扩展是否已注册到编辑器内,通过该属性我们可以知道某个扩展是否已注册 26 | 27 | ##### emptyRenderTags 28 | 29 | 需要置空的标签,同编辑器实例属性 `emptyRenderTags` 30 | 31 | ##### extraKeepTags 32 | 33 | 额外保留的标签,同编辑器实例属性 `extraKeepTags` 34 | 35 | ##### formatRules 36 | 37 | 节点数组格式化规则,同编辑器实例属性 `formatRules` 38 | 39 | ## 使用扩展提供的命令 40 | 41 | 通过 `editor.commands` 来调用扩展提供的命令 42 | 43 | ```ts 44 | //调用align扩展提供的setAlign方法 45 | editor.commands.setAlign('center') 46 | ``` 47 | 48 | ## 内置扩展 49 | 50 | kaitify 内部配置了多个扩展,我们无需进行任何配置,因为它们是默认会集成到编辑器中的,但是部分扩展是支持作配置的,此时我们需要将扩展方法引入,将配置作为方法的入参传入,最终得到一个符合要求的扩展实例 51 | 52 | ```ts 53 | import { AttachmentExtension } from '@kaitify/core' 54 | 55 | const editor = await Editor.configure({ 56 | el: '#editor', 57 | value: '', 58 | placeholder: '请输入正文...', 59 | extensions: [AttachmentExtension({ icon: 'xxx.png' })] // attachment扩展支持通过入参配置全局的icon属性 60 | }) 61 | ``` 62 | 63 | kaitify 提供的内置扩展都是以函数的形式对外使用,调用函数会获得一个对应的扩展实例,有的扩展函数支持提供入参,有的则没有入参,具体可在后续介绍具体每一个内置扩展时查看 64 | 65 | > 我们配置的扩展会覆盖原本编辑器内部配置的同名扩展,因此无需担心两个相同的扩展被注册多次 66 | -------------------------------------------------------------------------------- /src/extensions/back-color/index.ts: -------------------------------------------------------------------------------- 1 | import { KNodeStylesType } from '@/model' 2 | import { Extension } from '../Extension' 3 | 4 | declare module '../../model' { 5 | interface EditorCommandsType { 6 | /** 7 | * 光标所在文本的背景颜色是否与入参一致 8 | */ 9 | isBackColor?: (value: string) => boolean 10 | /** 11 | * 设置背景颜色 12 | */ 13 | setBackColor?: (value: string) => Promise 14 | /** 15 | * 取消背景颜色 16 | */ 17 | unsetBackColor?: (value: string) => Promise 18 | } 19 | } 20 | 21 | export const BackColorExtension = () => 22 | Extension.create({ 23 | name: 'backColor', 24 | onPasteKeepStyles(node) { 25 | const styles: KNodeStylesType = {} 26 | if (node.isText() && node.hasStyles()) { 27 | if (node.styles!.hasOwnProperty('backgroundColor')) styles.backgroundColor = node.styles!.backgroundColor 28 | } 29 | return styles 30 | }, 31 | addCommands() { 32 | const isBackColor = (value: string) => { 33 | return this.commands.isTextStyle!('backgroundColor', value) || this.commands.isTextStyle!('background', value) 34 | } 35 | 36 | const setBackColor = async (value: string) => { 37 | if (isBackColor(value)) { 38 | return 39 | } 40 | await this.commands.setTextStyle!({ 41 | backgroundColor: value 42 | }) 43 | } 44 | 45 | const unsetBackColor = async (value: string) => { 46 | if (!isBackColor(value)) { 47 | return 48 | } 49 | await this.commands.removeTextStyle!(['backgroundColor', 'background']) 50 | } 51 | 52 | return { 53 | isBackColor, 54 | setBackColor, 55 | unsetBackColor 56 | } 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": "0c5e010b", 3 | "configHash": "ed7f0315", 4 | "lockfileHash": "87e88a10", 5 | "browserHash": "3b2de185", 6 | "optimized": { 7 | "vue": { 8 | "src": "../../../../node_modules/vue/dist/vue.runtime.esm-bundler.js", 9 | "file": "vue.js", 10 | "fileHash": "cacb6850", 11 | "needsInterop": false 12 | }, 13 | "vitepress > @vue/devtools-api": { 14 | "src": "../../../../node_modules/@vue/devtools-api/dist/index.js", 15 | "file": "vitepress___@vue_devtools-api.js", 16 | "fileHash": "1f19dd5f", 17 | "needsInterop": false 18 | }, 19 | "vitepress > @vueuse/core": { 20 | "src": "../../../../node_modules/@vueuse/core/index.mjs", 21 | "file": "vitepress___@vueuse_core.js", 22 | "fileHash": "8e63c2ff", 23 | "needsInterop": false 24 | }, 25 | "vitepress > @vueuse/integrations/useFocusTrap": { 26 | "src": "../../../../node_modules/@vueuse/integrations/useFocusTrap.mjs", 27 | "file": "vitepress___@vueuse_integrations_useFocusTrap.js", 28 | "fileHash": "02172737", 29 | "needsInterop": false 30 | }, 31 | "vitepress > mark.js/src/vanilla.js": { 32 | "src": "../../../../node_modules/mark.js/src/vanilla.js", 33 | "file": "vitepress___mark__js_src_vanilla__js.js", 34 | "fileHash": "9d4c1f7e", 35 | "needsInterop": false 36 | }, 37 | "vitepress > minisearch": { 38 | "src": "../../../../node_modules/minisearch/dist/es/index.js", 39 | "file": "vitepress___minisearch.js", 40 | "fileHash": "ca0e9a7e", 41 | "needsInterop": false 42 | } 43 | }, 44 | "chunks": { 45 | "chunk-3FTTGHQH": { 46 | "file": "chunk-3FTTGHQH.js" 47 | }, 48 | "chunk-L2JNJ22P": { 49 | "file": "chunk-L2JNJ22P.js" 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /docs/apis/knode-attrs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: KNode 属性 3 | --- 4 | 5 | # KNode 属性 6 | 7 | ## 实例属性 8 | 9 | 实例属性通过创建的编辑器实例来调用 10 | 11 | ##### key 12 | 13 | 只读属性,唯一 `id` 14 | 15 | ##### type 16 | 17 | 节点类型,可取值 `block` `inline` `closed` `text`,此值可以修改,初始值由创建节点时提供的对应参数确定 18 | 19 | ##### tag 20 | 21 | 节点的渲染标签,文本节点此属性无效,此值可以修改,初始值由创建节点时提供的对应参数确定, 22 | 23 | ##### textContent 24 | 25 | 文本节点的文本值,仅文本节点支持此属性,此值可以修改,初始值由创建节点时提供的对应参数确定 26 | 27 | ##### marks 28 | 29 | 节点的标记集合,在渲染时会当做 `dom` 的 `attrs` 进行渲染,此值可以修改,初始值由创建节点时提供的对应参数确定 30 | 31 | ##### styles 32 | 33 | 节点的样式集合,在渲染时会当做 `dom` 的 `style` 属性进行渲染,此值可以修改,初始值由创建节点时提供的对应参数确定 34 | 35 | ##### locked 36 | 37 | 是否锁定节点,锁定的节点不会被编辑器格式化校验时与其他节点进行合并,此值可以修改,初始值由创建节点时提供的对应参数确定,如果没有设置则默认为 `false` 38 | 39 | - 针对块节点:在符合合并条件的情况下是否允许编辑器将其与父节点或者子节点进行合并; 40 | - 针对行内节点:在符合合并条件的情况下是否允许编辑器将其与相邻节点或者父节点或者子节点进行合并; 41 | - 针对文本节点:在符合合并的条件下是否允许编辑器将其与相邻节点或者父节点进行合并。 42 | 43 | ##### fixed 44 | 45 | 是否为固定块节点,值为 `true` 时:当光标在节点起始处或者光标在节点内只有占位符时,执行删除操作不会删除此节点,会再次创建一个占位符进行处理;当光标在节点内且节点不是代码块样式,不会进行换行 46 | 47 | 此值可以修改,初始值由创建节点时提供的对应参数确定,如果没有设置则默认为 `false` 48 | 49 | ##### nested 50 | 51 | 是否为固定格式的内嵌块节点,如 `li`、`tr`、`td` 等,此值可以修改,初始值由创建节点时提供的对应参数确定,如果没有设置则默认为 `false` 52 | 53 | ##### void 54 | 55 | 只读属性,表示是否不可见节点,意味着此类节点在编辑器内视图内无法看到,如`colgroup`、`col`等 56 | 57 | ##### namespace 58 | 59 | 渲染 `dom` 所用到的命名空间。如果此值不存在,在默认的渲染方法中使用 `document.createElement` 方法来创建 `dom` 元素;如果此值存在,在默认的渲染方法中则会使用 `document.createElementNS` 方法来创建 `dom` 元素 60 | 61 | 此值可以修改,初始值由创建节点时提供的对应参数确定 62 | 63 | ##### children 64 | 65 | 子节点数组,文本节点和闭合节点此属性无效,通过访问该属性可以获取节点的子节点,此值可以修改,初始子节点由创建节点时提供的对应参数确定 66 | 67 | ##### parent 68 | 69 | 节点的父节点,如果节点没有父节点,此值不存在 70 | -------------------------------------------------------------------------------- /src/extensions/italic/index.ts: -------------------------------------------------------------------------------- 1 | import { KNodeStylesType } from '@/model' 2 | import { splitNodeToNodes } from '@/model/config/function' 3 | import { Extension } from '../Extension' 4 | 5 | declare module '../../model' { 6 | interface EditorCommandsType { 7 | /** 8 | * 光标所在文本是否斜体 9 | */ 10 | isItalic?: () => boolean 11 | /** 12 | * 设置斜体 13 | */ 14 | setItalic?: () => Promise 15 | /** 16 | * 取消斜体 17 | */ 18 | unsetItalic?: () => Promise 19 | } 20 | } 21 | 22 | export const ItalicExtension = () => 23 | Extension.create({ 24 | name: 'italic', 25 | onPasteKeepStyles(node) { 26 | const styles: KNodeStylesType = {} 27 | if (node.isText() && node.hasStyles()) { 28 | if (node.styles!.hasOwnProperty('fontStyle')) styles.fontStyle = node.styles!.fontStyle 29 | } 30 | return styles 31 | }, 32 | extraKeepTags: ['i'], 33 | onDomParseNode(node) { 34 | if (node.isMatch({ tag: 'i' })) { 35 | node.type = 'inline' 36 | } 37 | return node 38 | }, 39 | formatRules: [ 40 | ({ editor, node }) => { 41 | if (!node.isEmpty() && node.isMatch({ tag: 'i' })) { 42 | const styles: KNodeStylesType = node.styles || {} 43 | node.styles = { 44 | ...styles, 45 | fontStyle: 'italic' 46 | } 47 | node.tag = editor.textRenderTag 48 | splitNodeToNodes.apply(editor, [node]) 49 | } 50 | } 51 | ], 52 | addCommands() { 53 | const isItalic = () => { 54 | return this.commands.isTextStyle!('fontStyle', 'italic') 55 | } 56 | 57 | const setItalic = async () => { 58 | if (isItalic()) { 59 | return 60 | } 61 | await this.commands.setTextStyle!({ 62 | fontStyle: 'italic' 63 | }) 64 | } 65 | 66 | const unsetItalic = async () => { 67 | if (!isItalic()) { 68 | return 69 | } 70 | await this.commands.removeTextStyle!(['fontStyle']) 71 | } 72 | 73 | return { 74 | isItalic, 75 | setItalic, 76 | unsetItalic 77 | } 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /docs/guide/format-rules.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 格式化规则 3 | --- 4 | 5 | # 格式化规则 6 | 7 | ## 什么是格式化规则? 8 | 9 | 格式化规则是一组函数,类型为 `RuleFunctionType`,每个函数提供唯一入参,入参包含两个属性 `editor` 和 `node`,分别表示当前编辑器的实例和当前需要进行格式化的节点。 10 | 11 | ## 什么时候会进行格式化? 12 | 13 | - 当我们构建一个编辑器时,会进行第一次格式化,此时会针对编辑器的 `stackNodes` 里的所有节点,执行每一个格式化函数,保证最终渲染编辑器视图时,所有的节点都是符合规范的 14 | 15 | > 当编辑器的内容特别多的时候,第一次渲染编辑器就会出现较慢的情况,这时候需要考虑去做足够友好的 UI 交互来解决 16 | 17 | - 当我们每次调用 `updateView` 进行编辑器的视图更新时,编辑器内部会通过 `diff` 算法计算出更新的节点,再对这些节点进行格式化,此时也会执行每一个格式化函数 18 | 19 | ## 编辑器内置的默认格式化函数 20 | 21 | ##### 规则 1 22 | 23 | 如果节点自身是块节点,并且 `tag` 的值是 `address` `article` `aside` `nav` `section` ,此时该节点会转为默认的块节点标签 24 | 25 | ##### 规则 2 26 | 27 | 如果节点的子节点数组中含有块节点,则该子节点会被转为块节点,同时该节点的其他非块级子节点也会被转为块节点 28 | 29 | ##### 规则 3 30 | 31 | 如果节点是不可编辑的,则查找使其不可编辑的目标节点,针对该目标节点,如果是非块节点,则在两侧加上零宽度空白文本节点,且保证光标始终不在不可编辑的节点内部 32 | 33 | ##### 规则 4 34 | 35 | 如果节点的子节点中存在占位符也存在其他的非空节点,则会删除占位符;如果节点的子节点中只存在占位符则将多个占位符合并为一个 36 | 37 | ##### 规则 5 38 | 39 | - 1. 统一将文本节点内的 `\r\n` 换成 `\n`,解决 `Windows` 兼容问题 40 | - 2. 统一将文本节点内的 ` `(`\u00A0`)换成普通空格 41 | - 3. 统一将文本节点内的零宽度空格换成零宽度空格(`\uFEFF -> \u200B`) 42 | - 4. 统一将文本节点内的 `\n` 后面加上零宽度空白字符 43 | 44 | ##### 规则 6 45 | 46 | 如果节点是文本节点且文本节点内容包含多个连续的零宽度空白字符,则会将文本节点内连续的零宽度空白字符合并 47 | 48 | ##### 规则 7 49 | 50 | 如果节点的子节点中存在可以合并的兄弟节点,则会将节点进行合并(文本节点的 `styles` 和 `marks` 完全一致则可以合并;行内节点的 `tag`、`styles` 和 `marks` 完全一致则可以合并) 51 | 52 | ##### 规则 8 53 | 54 | 如果节点的子节点只有一个,如果符合合并条件,则会将节点与子节点进行合并(父节点只有一个文本节点作为子节点,且父节点是行内节点、`tag` 值为默认文本标签值 `editor.textRenderTag`,则可以合并;父节点是块节点或者行内节点,且与唯一子节点的 `tag` 一致、`type` 一致,则可以进行合并) 55 | 56 | > 编辑器的每一个内置扩展都可能会新增额外的格式化规则,所以 kaitify 的内置规则远远不止这些,以上仅仅是编辑器自身内置的规则 57 | 58 | ## 自定义新的规则 59 | 60 | 如果你想要添加新的规则,可以在创建编辑器时配置属性 `formatRules` 61 | 62 | ```ts 63 | const editor = await Editor.configure({ 64 | value: '


', 65 | formatRules: [ 66 | //给每一个块节点设置背景色红色 67 | ({ node }) => { 68 | if (node.isBlock()) { 69 | if (node.hasStyles()) { 70 | node.styles.background = 'red' 71 | } else { 72 | node.styles = { 73 | background: 'red' 74 | } 75 | } 76 | } 77 | } 78 | ] 79 | }) 80 | ``` 81 | 82 | 除了通过 `formatRules` 属性来配置自定义的格式化规则,还可以通过自定义扩展的方式,来配置扩展的格式化规则,以达到我们的目的,这里暂且不提,具体可以参阅 [如何自己创建一个扩展?](/extensions/custom-extension) 83 | -------------------------------------------------------------------------------- /src/extensions/subscript/index.ts: -------------------------------------------------------------------------------- 1 | import { KNodeStylesType } from '@/model' 2 | import { splitNodeToNodes } from '@/model/config/function' 3 | import { Extension } from '../Extension' 4 | 5 | declare module '../../model' { 6 | interface EditorCommandsType { 7 | /** 8 | * 光标所在文本是否下标 9 | */ 10 | isSubscript?: () => boolean 11 | /** 12 | * 设置下标 13 | */ 14 | setSubscript?: () => Promise 15 | /** 16 | * 取消下标 17 | */ 18 | unsetSubscript?: () => Promise 19 | } 20 | } 21 | 22 | export const SubscriptExtension = () => 23 | Extension.create({ 24 | name: 'subscript', 25 | onPasteKeepStyles(node) { 26 | const styles: KNodeStylesType = {} 27 | if (node.isText() && node.hasStyles()) { 28 | if (node.styles!.hasOwnProperty('verticalAlign')) styles.verticalAlign = node.styles!.verticalAlign 29 | } 30 | return styles 31 | }, 32 | extraKeepTags: ['sub'], 33 | onDomParseNode(node) { 34 | if (node.isMatch({ tag: 'sub' })) { 35 | node.type = 'inline' 36 | } 37 | return node 38 | }, 39 | formatRules: [ 40 | ({ editor, node }) => { 41 | if (!node.isEmpty() && node.isMatch({ tag: 'sub' })) { 42 | const styles: KNodeStylesType = node.styles || {} 43 | node.styles = { 44 | ...styles, 45 | verticalAlign: 'sub' 46 | } 47 | node.tag = editor.textRenderTag 48 | splitNodeToNodes.apply(editor, [node]) 49 | } 50 | } 51 | ], 52 | addCommands() { 53 | const isSubscript = () => { 54 | return this.commands.isTextStyle!('verticalAlign', 'sub') 55 | } 56 | 57 | const setSubscript = async () => { 58 | if (isSubscript()) { 59 | return 60 | } 61 | await this.commands.setTextStyle!({ 62 | verticalAlign: 'sub' 63 | }) 64 | } 65 | 66 | const unsetSubscript = async () => { 67 | if (!isSubscript()) { 68 | return 69 | } 70 | await this.commands.removeTextStyle!(['verticalAlign']) 71 | } 72 | 73 | return { 74 | isSubscript, 75 | setSubscript, 76 | unsetSubscript 77 | } 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /src/extensions/superscript/index.ts: -------------------------------------------------------------------------------- 1 | import { KNodeStylesType } from '@/model' 2 | import { splitNodeToNodes } from '@/model/config/function' 3 | import { Extension } from '../Extension' 4 | 5 | declare module '../../model' { 6 | interface EditorCommandsType { 7 | /** 8 | * 光标所在文本是否上标 9 | */ 10 | isSuperscript?: () => boolean 11 | /** 12 | * 设置上标 13 | */ 14 | setSuperscript?: () => Promise 15 | /** 16 | * 取消上标 17 | */ 18 | unsetSuperscript?: () => Promise 19 | } 20 | } 21 | 22 | export const SuperscriptExtension = () => 23 | Extension.create({ 24 | name: 'superscript', 25 | onPasteKeepStyles(node) { 26 | const styles: KNodeStylesType = {} 27 | if (node.isText() && node.hasStyles()) { 28 | if (node.styles!.hasOwnProperty('verticalAlign')) styles.verticalAlign = node.styles!.verticalAlign 29 | } 30 | return styles 31 | }, 32 | extraKeepTags: ['sup'], 33 | onDomParseNode(node) { 34 | if (node.isMatch({ tag: 'sup' })) { 35 | node.type = 'inline' 36 | } 37 | return node 38 | }, 39 | formatRules: [ 40 | ({ editor, node }) => { 41 | if (!node.isEmpty() && node.isMatch({ tag: 'sup' })) { 42 | const styles: KNodeStylesType = node.styles || {} 43 | node.styles = { 44 | ...styles, 45 | verticalAlign: 'super' 46 | } 47 | node.tag = editor.textRenderTag 48 | splitNodeToNodes.apply(editor, [node]) 49 | } 50 | } 51 | ], 52 | addCommands() { 53 | const isSuperscript = () => { 54 | return this.commands.isTextStyle!('verticalAlign', 'super') 55 | } 56 | 57 | const setSuperscript = async () => { 58 | if (isSuperscript()) { 59 | return 60 | } 61 | await this.commands.setTextStyle!({ 62 | verticalAlign: 'super' 63 | }) 64 | } 65 | 66 | const unsetSuperscript = async () => { 67 | if (!isSuperscript()) { 68 | return 69 | } 70 | await this.commands.removeTextStyle!(['verticalAlign']) 71 | } 72 | 73 | return { 74 | isSuperscript, 75 | setSuperscript, 76 | unsetSuperscript 77 | } 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /docs/extensions/built-in/bold.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: bold 加粗 3 | --- 4 | 5 | # bold 加粗 6 | 7 | 文本加粗 8 | 9 | ## Commands 命令 10 | 11 | ##### isBold() 12 | 13 | 判断光标所在文本是否加粗 14 | 15 | - 类型 16 | 17 | ```ts 18 | isBold(): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法用来判断光标所在文本是否加粗,返回 `boolean` 值 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isBold = editor.commands.isBold() 29 | ``` 30 | 31 | ##### setBold() 32 | 33 | 光标范围内的文本进行加粗 34 | 35 | - 类型 36 | 37 | ```ts 38 | setBold(): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法会将光标范围内的文本都进行加粗,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isBold` 判断光标所在文本都是已加粗的,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setBold() 51 | ``` 52 | 53 | ##### unsetBold() 54 | 55 | 光标范围内的文本取消加粗 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetBold(): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 该方法会对光标范围内的文本取消加粗,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isBold` 判断光标所在文本不全都是加粗的,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetBold() 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 111 | -------------------------------------------------------------------------------- /docs/extensions/built-in/italic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: italic 斜体 3 | --- 4 | 5 | # italic 斜体 6 | 7 | 文本斜体 8 | 9 | ## Commands 命令 10 | 11 | ##### isItalic() 12 | 13 | 判断光标所在文本是否斜体 14 | 15 | - 类型 16 | 17 | ```ts 18 | isItalic(): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法用来判断光标所在文本是否斜体,返回 `boolean` 值 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isItalic = editor.commands.isItalic() 29 | ``` 30 | 31 | ##### setItalic() 32 | 33 | 光标范围内的文本设置斜体 34 | 35 | - 类型 36 | 37 | ```ts 38 | setItalic(): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法会将光标范围内的文本都设置为斜体,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isItalic` 判断光标所在文本都是斜体,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setItalic() 51 | ``` 52 | 53 | ##### unsetItalic() 54 | 55 | 光标范围内的文本取消斜体 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetItalic(): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 该方法会对光标范围内的文本取消斜体,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isItalic` 判断光标所在文本不全都是斜体,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetItalic() 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 111 | -------------------------------------------------------------------------------- /docs/extensions/built-in/subscript.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: subscript 下标 3 | --- 4 | 5 | # subscript 下标 6 | 7 | 文本下标 8 | 9 | ## Commands 命令 10 | 11 | ##### isSubscript() 12 | 13 | 判断光标所在文本是否下标 14 | 15 | - 类型 16 | 17 | ```ts 18 | isSubscript(): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法用来判断光标所在文本是否下标,返回 `boolean` 值 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isSubscript = editor.commands.isSubscript() 29 | ``` 30 | 31 | ##### setSubscript() 32 | 33 | 光标范围内的文本设置下标 34 | 35 | - 类型 36 | 37 | ```ts 38 | setSubscript(): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法会将光标范围内的文本都设置下标,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isSubscript` 判断光标所在文本都是已设置下标,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setSubscript() 51 | ``` 52 | 53 | ##### unsetSubscript() 54 | 55 | 光标范围内的文本取消下标 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetSubscript(): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 该方法会对光标范围内的文本取消下标,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isSubscript` 判断光标所在文本不全都是下标,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetSubscript() 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 111 | -------------------------------------------------------------------------------- /src/extensions/bold/index.ts: -------------------------------------------------------------------------------- 1 | import { KNodeStylesType } from '@/model' 2 | import { splitNodeToNodes } from '@/model/config/function' 3 | import { Extension } from '../Extension' 4 | 5 | declare module '../../model' { 6 | interface EditorCommandsType { 7 | /** 8 | * 光标所在文本是否加粗 9 | */ 10 | isBold?: () => boolean 11 | /** 12 | * 设置加粗 13 | */ 14 | setBold?: () => Promise 15 | /** 16 | * 取消加粗 17 | */ 18 | unsetBold?: () => Promise 19 | } 20 | } 21 | 22 | export const BoldExtension = () => 23 | Extension.create({ 24 | name: 'bold', 25 | extraKeepTags: ['b', 'strong'], 26 | onDomParseNode(node) { 27 | if (node.isMatch({ tag: 'b' }) || node.isMatch({ tag: 'strong' })) { 28 | node.type = 'inline' 29 | } 30 | return node 31 | }, 32 | onPasteKeepStyles(node) { 33 | const styles: KNodeStylesType = {} 34 | if (node.isText() && node.hasStyles()) { 35 | if (node.styles!.hasOwnProperty('fontWeight')) styles.fontWeight = node.styles!.fontWeight 36 | } 37 | return styles 38 | }, 39 | formatRules: [ 40 | ({ editor, node }) => { 41 | if (!node.isEmpty() && (node.isMatch({ tag: 'b' }) || node.isMatch({ tag: 'strong' }))) { 42 | const styles: KNodeStylesType = node.styles || {} 43 | node.styles = { 44 | ...styles, 45 | fontWeight: 'bold' 46 | } 47 | node.tag = editor.textRenderTag 48 | splitNodeToNodes.apply(editor, [node]) 49 | } 50 | } 51 | ], 52 | addCommands() { 53 | const isBold = () => { 54 | return this.commands.isTextStyle!('fontWeight', 'bold') || this.commands.isTextStyle!('fontWeight', 'bolder') || this.commands.isTextStyle!('fontWeight', '700') || this.commands.isTextStyle!('fontWeight', '800') || this.commands.isTextStyle!('fontWeight', '900') 55 | } 56 | 57 | const setBold = async () => { 58 | if (isBold()) { 59 | return 60 | } 61 | await this.commands.setTextStyle!({ 62 | fontWeight: 'bold' 63 | }) 64 | } 65 | 66 | const unsetBold = async () => { 67 | if (!isBold()) { 68 | return 69 | } 70 | await this.commands.removeTextStyle!(['fontWeight']) 71 | } 72 | 73 | return { 74 | isBold, 75 | setBold, 76 | unsetBold 77 | } 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /docs/extensions/built-in/underline.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: underline 下划线 3 | --- 4 | 5 | # underline 下划线 6 | 7 | 文本下划线 8 | 9 | ## Commands 命令 10 | 11 | ##### isUnderline() 12 | 13 | 判断光标所在文本是否有下划线 14 | 15 | - 类型 16 | 17 | ```ts 18 | isUnderline(): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法用来判断光标所在文本是否有下划线,返回 `boolean` 值 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isUnderline = editor.commands.isUnderline() 29 | ``` 30 | 31 | ##### setUnderline() 32 | 33 | 光标范围内的文本设置下划线 34 | 35 | - 类型 36 | 37 | ```ts 38 | setUnderline(): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法会将光标范围内的文本都设置下划线,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isUnderline` 判断光标所在文本都有下划线,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setUnderline() 51 | ``` 52 | 53 | ##### unsetUnderline() 54 | 55 | 光标范围内的文本取消下划线 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetUnderline(): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 该方法会对光标范围内的文本取消下划线,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isUnderline` 判断光标所在文本不全都有下划线,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetUnderline() 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 111 | -------------------------------------------------------------------------------- /docs/extensions/built-in/superscript.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: superscript 上标 3 | --- 4 | 5 | # superscript 上标 6 | 7 | 文本上标 8 | 9 | ## Commands 命令 10 | 11 | ##### isSuperscript() 12 | 13 | 判断光标所在文本是否上标 14 | 15 | - 类型 16 | 17 | ```ts 18 | isSuperscript(): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法用来判断光标所在文本是否上标,返回 `boolean` 值 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isSuperscript = editor.commands.isSuperscript() 29 | ``` 30 | 31 | ##### setSuperscript() 32 | 33 | 光标范围内的文本设置上标 34 | 35 | - 类型 36 | 37 | ```ts 38 | setSuperscript(): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法会将光标范围内的文本都设置上标,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isSuperscript` 判断光标所在文本都是已设置上标,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setSuperscript() 51 | ``` 52 | 53 | ##### unsetSuperscript() 54 | 55 | 光标范围内的文本取消上标 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetSuperscript(): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 该方法会对光标范围内的文本取消上标,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isSuperscript` 判断光标所在文本不全都是上标,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetSuperscript() 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 111 | -------------------------------------------------------------------------------- /src/extensions/underline/index.ts: -------------------------------------------------------------------------------- 1 | import { KNodeStylesType } from '@/model' 2 | import { splitNodeToNodes } from '@/model/config/function' 3 | import { Extension } from '../Extension' 4 | 5 | declare module '../../model' { 6 | interface EditorCommandsType { 7 | /** 8 | * 光标所在文本是否下划线 9 | */ 10 | isUnderline?: () => boolean 11 | /** 12 | * 设置下划线 13 | */ 14 | setUnderline?: () => Promise 15 | /** 16 | * 取消下划线 17 | */ 18 | unsetUnderline?: () => Promise 19 | } 20 | } 21 | 22 | export const UnderlineExtension = () => 23 | Extension.create({ 24 | name: 'underline', 25 | onPasteKeepStyles(node) { 26 | const styles: KNodeStylesType = {} 27 | if (node.isText() && node.hasStyles()) { 28 | if (node.styles!.hasOwnProperty('textDecoration')) styles.textDecoration = node.styles!.textDecoration 29 | if (node.styles!.hasOwnProperty('textDecorationLine')) styles.textDecorationLine = node.styles!.textDecorationLine 30 | } 31 | return styles 32 | }, 33 | extraKeepTags: ['u'], 34 | onDomParseNode(node) { 35 | if (node.isMatch({ tag: 'u' })) { 36 | node.type = 'inline' 37 | } 38 | return node 39 | }, 40 | formatRules: [ 41 | ({ editor, node }) => { 42 | if (!node.isEmpty() && node.isMatch({ tag: 'u' })) { 43 | const styles: KNodeStylesType = node.styles || {} 44 | node.styles = { 45 | ...styles, 46 | textDecorationLine: 'underline' 47 | } 48 | node.tag = editor.textRenderTag 49 | splitNodeToNodes.apply(editor, [node]) 50 | } 51 | } 52 | ], 53 | addCommands() { 54 | const isUnderline = () => { 55 | return this.commands.isTextStyle!('textDecoration', 'underline') || this.commands.isTextStyle!('textDecorationLine', 'underline') 56 | } 57 | 58 | const setUnderline = async () => { 59 | if (isUnderline()) { 60 | return 61 | } 62 | await this.commands.setTextStyle!({ 63 | textDecorationLine: 'underline' 64 | }) 65 | } 66 | 67 | const unsetUnderline = async () => { 68 | if (!isUnderline()) { 69 | return 70 | } 71 | await this.commands.removeTextStyle!(['textDecoration', 'textDecorationLine']) 72 | } 73 | 74 | return { 75 | isUnderline, 76 | setUnderline, 77 | unsetUnderline 78 | } 79 | } 80 | }) 81 | -------------------------------------------------------------------------------- /docs/apis/editor-attrs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Editor 属性 3 | --- 4 | 5 | # Editor 属性 6 | 7 | ## 实例属性 8 | 9 | 实例属性通过创建的编辑器实例来调用 10 | 11 | ##### guid 12 | 13 | 只读属性,唯一 `id` 14 | 15 | ##### $el 16 | 17 | 只读属性,用于获取编辑器的的 `dom` 元素 18 | 19 | ##### allowCopy 20 | 21 | 是否允许复制,此值可以修改,初始值由创建编辑器时提供的对应参数确定 22 | 23 | ##### allowPaste 24 | 25 | 是否允许粘贴,此值可以修改,初始值由创建编辑器时提供的对应参数确定 26 | 27 | ##### allowCut 28 | 29 | 是否允许剪切,此值可以修改,初始值由创建编辑器时提供的对应参数确定 30 | 31 | ##### allowPasteHtml 32 | 33 | 粘贴时是否允许携带样式,此值可以修改,初始值由创建编辑器时提供的对应参数确定 34 | 35 | ##### priorityPasteFiles 36 | 37 | 剪切板同时存在文件和 `html`/`text` 时,是否优先粘贴文件,此值可以修改,初始值由创建编辑器时提供的对应参数确定 38 | 39 | ##### textRenderTag 40 | 41 | 此为只读属性,表示编辑器内渲染文本节点的真实标签,默认值由创建编辑器时提供的对应参数确定,如果没有设置,则默认为 `span` 42 | 43 | ##### blockRenderTag 44 | 45 | 此为只读属性,表示编辑内渲染默认块级节点的真实标签,即段落标签,默认值由创建编辑器时提供的对应参数确定,如果没有设置,则默认为 `p` 46 | 47 | ##### emptyRenderTags 48 | 49 | 此为只读属性,表示编辑器内需要置空的标签,编辑器针对需要置空的标签,会转为空节点,不会渲染到视图中,目前默认置空的元素有:`meta` `link` `style` `script` `title` `base` `noscript` `template` `annotation` `input` `form` `button`,你可以在这个基础上,在创建编辑器时添加额外的需要置空的元素标签 50 | 51 | ##### extraKeepTags 52 | 53 | 此为只读属性,表示编辑器内额外保留的标签,默认保留的标签只有 `p` `div` `address` `article` `aside` `nav` `section` `span` `label` `br`,编辑器内置的许多扩展都设置了此属性,比如 `image`扩展等,可以通过输出该属性,查看编辑器额外保留了哪些标签,你可以在此基础上,在创建编辑器时添加额外需要保留的标签,需要注意的是,额外保留的标签默认都是行内节点,如果想做更多自定义的处理,需要在创建编辑器时结合属性 `onDomParseNode` 54 | 55 | ##### extensions 56 | 57 | 此为只读属性,表示编辑器已注册的扩展数组 58 | 59 | ##### formatRules 60 | 61 | 此为只读属性,表示节点数组格式化规则 62 | 63 | ##### selection 64 | 65 | 虚拟光标 `Selection` 的实例对象,一个编辑器仅有一个 `Selection` 的实例对象,具体参考[Selection](/guide/selection) 66 | 67 | ##### history 68 | 69 | 历史记录 `History` 的实例对象,一个编辑器仅有一个 `History` 的实例对象,具体参考[History](/guide/history) 70 | 71 | ##### commands 72 | 73 | 编辑器的命令集合,通过该属性可以直接调用编辑器内置扩展提供的命令方法 74 | 75 | ##### stackNodes 76 | 77 | 编辑器内的节点数组 78 | -------------------------------------------------------------------------------- /docs/extensions/built-in/color.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: color 字体颜色 3 | --- 4 | 5 | # color 字体颜色 6 | 7 | 文本字体颜色 8 | 9 | ## Commands 命令 10 | 11 | ##### isColor() 12 | 13 | 判断光标所在文本的字体颜色是否与指定值一致 14 | 15 | - 类型 16 | 17 | ```ts 18 | isColor(value: string): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 提供一个入参,类型为 `string`,用以判断光标所在文本的字体颜色是否与指定值一致 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isRed = editor.commands.isColor('#f30') 29 | ``` 30 | 31 | ##### setColor() 32 | 33 | 设置光标所在文本的字体颜色 34 | 35 | - 类型 36 | 37 | ```ts 38 | setColor(value: string): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 提供一个入参,类型为 `string`,表示设置的字体颜色,该方法会设置光标文本为该字体颜色,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isColor` 判断光标所在文本都是该字体颜色,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setColor('#f30') 51 | ``` 52 | 53 | ##### unsetColor() 54 | 55 | 取消光标所在文本的字体颜色 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetColor(value: string): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 提供一个入参,类型为 `string`,表示取消的字体颜色,该方法会取消光标文本的该字体颜色,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isColor` 判断光标所在文本不全都是该字体颜色,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetColor('#f30') 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 111 | -------------------------------------------------------------------------------- /docs/extensions/built-in/indent.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: indent 缩进 3 | --- 4 | 5 | # indent 缩进 6 | 7 | 支持缩进属性的渲染,提供增加缩进、减少缩进的能力。 8 | 9 | > [!TIP] Tips 10 | > 通过 `Tab` 按键可以增加缩进,通过 `Shift + Tab` 按键可以减少缩进,当然前提是必须允许使用缩进功能,即 `canUseIndent` 方法返回的结果为 true 时 11 | 12 | ## Commands 命令 13 | 14 | ##### canUseIndent() 15 | 16 | 是否可以使用缩进功能 17 | 18 | - 类型 19 | 20 | ```ts 21 | canUseIndent(): boolean 22 | ``` 23 | 24 | - 详细信息 25 | 26 | 该方法返回一个布尔值,根据光标位置判断此刻是否可以使用缩进功能 27 | 28 | - 示例 29 | 30 | ```ts 31 | const canUseIndent = editor.commands.canUseIndent() 32 | ``` 33 | 34 | ##### setIncreaseIndent() 35 | 36 | 增加缩进 37 | 38 | - 类型 39 | 40 | ```ts 41 | setIncreaseIndent(): Promise 42 | ``` 43 | 44 | - 详细信息 45 | 46 | 该方法会使得光标范围内的块节点增加缩进量,并且在操作完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 47 | 48 | - 示例 49 | 50 | ```ts 51 | await editor.commands.setIncreaseIndent() 52 | ``` 53 | 54 | ##### setDecreaseIndent() 55 | 56 | 减少缩进 57 | 58 | - 类型 59 | 60 | ```ts 61 | setDecreaseIndent(): Promise 62 | ``` 63 | 64 | - 详细信息 65 | 66 | 该方法会使得光标范围内的块节点减少缩进量,并且在操作完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 67 | 68 | - 示例 69 | 70 | ```ts 71 | await editor.commands.setDecreaseIndent() 72 | ``` 73 | 74 | ## 代码示例 75 | 76 |
77 | 78 | 79 |
80 |
81 | 82 | 110 | 111 | ``` 112 | 113 | ``` 114 | -------------------------------------------------------------------------------- /docs/extensions/built-in/strikethrough.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: strikethrough 删除线 3 | --- 4 | 5 | # strikethrough 删除线 6 | 7 | 文本删除线 8 | 9 | ## Commands 命令 10 | 11 | ##### isStrikethrough() 12 | 13 | 判断光标所在文本是否有删除线 14 | 15 | - 类型 16 | 17 | ```ts 18 | isStrikethrough(): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法用来判断光标所在文本是否有删除线,返回 `boolean` 值 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isStrikethrough = editor.commands.isStrikethrough() 29 | ``` 30 | 31 | ##### setStrikethrough() 32 | 33 | 光标范围内的文本设置删除线 34 | 35 | - 类型 36 | 37 | ```ts 38 | setStrikethrough(): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法会将光标范围内的文本都设置删除线,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isStrikethrough` 判断光标所在文本都是已设置删除线的,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setStrikethrough() 51 | ``` 52 | 53 | ##### unsetStrikethrough() 54 | 55 | 光标范围内的文本取消删除线 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetStrikethrough(): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 该方法会对光标范围内的文本取消删除线的设置,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isStrikethrough` 判断光标所在文本不全都是设置删除线的,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetStrikethrough() 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 111 | -------------------------------------------------------------------------------- /src/extensions/font-family/index.ts: -------------------------------------------------------------------------------- 1 | import { KNodeMarksType, KNodeStylesType } from '@/model' 2 | import { splitNodeToNodes } from '@/model/config/function' 3 | import { deleteProperty } from '@/tools' 4 | import { Extension } from '../Extension' 5 | 6 | declare module '../../model' { 7 | interface EditorCommandsType { 8 | /** 9 | * 光标所在文本的字体是否与入参一致 10 | */ 11 | isFontFamily?: (value: string) => boolean 12 | /** 13 | * 设置字体 14 | */ 15 | setFontFamily?: (value: string) => Promise 16 | /** 17 | * 取消字体 18 | */ 19 | unsetFontFamily?: (value: string) => Promise 20 | } 21 | } 22 | 23 | export const FontFamilyExtension = () => 24 | Extension.create({ 25 | name: 'fontFamily', 26 | onPasteKeepStyles(node) { 27 | const styles: KNodeStylesType = {} 28 | if (node.isText() && node.hasStyles()) { 29 | if (node.styles!.hasOwnProperty('fontFamily')) styles.fontFamily = node.styles!.fontFamily 30 | } 31 | return styles 32 | }, 33 | extraKeepTags: ['font'], 34 | onDomParseNode(node) { 35 | if (node.isMatch({ tag: 'font' })) { 36 | node.type = 'inline' 37 | } 38 | return node 39 | }, 40 | formatRules: [ 41 | ({ editor, node }) => { 42 | if (!node.isEmpty() && node.isMatch({ tag: 'font' })) { 43 | const marks: KNodeMarksType = node.marks || {} 44 | const styles: KNodeStylesType = node.styles || {} 45 | node.styles = { 46 | ...styles, 47 | fontFamily: (marks.face as string) || '' 48 | } 49 | node.marks = deleteProperty(marks, 'face') 50 | node.tag = editor.textRenderTag 51 | splitNodeToNodes.apply(editor, [node]) 52 | } 53 | } 54 | ], 55 | addCommands() { 56 | const isFontFamily = (value: string) => { 57 | return this.commands.isTextStyle!('fontFamily', value) 58 | } 59 | 60 | const setFontFamily = async (value: string) => { 61 | if (isFontFamily(value)) { 62 | return 63 | } 64 | await this.commands.setTextStyle!({ 65 | fontFamily: value 66 | }) 67 | } 68 | 69 | const unsetFontFamily = async (value: string) => { 70 | if (!isFontFamily(value)) { 71 | return 72 | } 73 | await this.commands.removeTextStyle!(['fontFamily']) 74 | } 75 | 76 | return { 77 | isFontFamily, 78 | setFontFamily, 79 | unsetFontFamily 80 | } 81 | } 82 | }) 83 | -------------------------------------------------------------------------------- /src/extensions/history/index.ts: -------------------------------------------------------------------------------- 1 | import { platform } from 'dap-util' 2 | import { Extension } from '../Extension' 3 | 4 | declare module '../../model' { 5 | interface EditorCommandsType { 6 | /** 7 | * 是否可以撤销 8 | */ 9 | canUndo?: () => boolean 10 | /** 11 | * 是否可以重做 12 | */ 13 | canRedo?: () => boolean 14 | /** 15 | * 撤销 16 | */ 17 | undo?: () => Promise 18 | /** 19 | * 重做 20 | */ 21 | redo?: () => Promise 22 | } 23 | } 24 | 25 | /** 26 | * 键盘是否执行撤销操作 27 | */ 28 | const isUndo = function (e: KeyboardEvent) { 29 | const { Mac } = platform.os() 30 | if (Mac) { 31 | return e.key.toLocaleLowerCase() == 'z' && e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey 32 | } 33 | return e.key.toLocaleLowerCase() == 'z' && e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey 34 | } 35 | 36 | /** 37 | * 键盘是否执行重做操作 38 | */ 39 | const isRedo = function (e: KeyboardEvent) { 40 | const { Mac } = platform.os() 41 | if (Mac) { 42 | return e.key.toLocaleLowerCase() == 'z' && e.metaKey && e.shiftKey && !e.altKey && !e.ctrlKey 43 | } 44 | return e.key.toLocaleLowerCase() == 'y' && e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey 45 | } 46 | 47 | export const HistoryExtension = () => 48 | Extension.create({ 49 | name: 'history', 50 | onKeydown(event) { 51 | //撤销 52 | if (isUndo(event)) { 53 | event.preventDefault() 54 | this.commands.undo?.() 55 | } 56 | //重做 57 | else if (isRedo(event)) { 58 | event.preventDefault() 59 | this.commands.redo?.() 60 | } 61 | }, 62 | addCommands() { 63 | const canUndo = () => { 64 | return this.history.records.length > 1 65 | } 66 | 67 | const canRedo = () => { 68 | return this.history.redoRecords.length > 0 69 | } 70 | 71 | const undo = async () => { 72 | const record = this.history.setUndo() 73 | if (record) { 74 | this.stackNodes = record.nodes 75 | this.selection = record.selection 76 | await this.updateView(true, true) 77 | } 78 | } 79 | 80 | const redo = async () => { 81 | const record = this.history.setRedo() 82 | if (record) { 83 | this.stackNodes = record.nodes 84 | this.selection = record.selection 85 | await this.updateView(true, true) 86 | } 87 | } 88 | 89 | return { 90 | canUndo, 91 | canRedo, 92 | redo, 93 | undo 94 | } 95 | } 96 | }) 97 | -------------------------------------------------------------------------------- /src/extensions/strikethrough/index.ts: -------------------------------------------------------------------------------- 1 | import { KNodeStylesType } from '@/model' 2 | import { splitNodeToNodes } from '@/model/config/function' 3 | import { Extension } from '../Extension' 4 | 5 | declare module '../../model' { 6 | interface EditorCommandsType { 7 | /** 8 | * 光标所在文本是否删除线 9 | */ 10 | isStrikethrough?: () => boolean 11 | /** 12 | * 设置删除线 13 | */ 14 | setStrikethrough?: () => Promise 15 | /** 16 | * 取消删除线 17 | */ 18 | unsetStrikethrough?: () => Promise 19 | } 20 | } 21 | 22 | export const StrikethroughExtension = () => 23 | Extension.create({ 24 | name: 'strikethrough', 25 | onPasteKeepStyles(node) { 26 | const styles: KNodeStylesType = {} 27 | if (node.isText() && node.hasStyles()) { 28 | if (node.styles!.hasOwnProperty('textDecoration')) styles.textDecoration = node.styles!.textDecoration 29 | if (node.styles!.hasOwnProperty('textDecorationLine')) styles.textDecorationLine = node.styles!.textDecorationLine 30 | } 31 | return styles 32 | }, 33 | extraKeepTags: ['del'], 34 | onDomParseNode(node) { 35 | if (node.isMatch({ tag: 'del' })) { 36 | node.type = 'inline' 37 | } 38 | return node 39 | }, 40 | formatRules: [ 41 | ({ editor, node }) => { 42 | if (!node.isEmpty() && node.isMatch({ tag: 'del' })) { 43 | const styles: KNodeStylesType = node.styles || {} 44 | node.styles = { 45 | ...styles, 46 | textDecorationLine: 'line-through' 47 | } 48 | node.tag = editor.textRenderTag 49 | splitNodeToNodes.apply(editor, [node]) 50 | } 51 | } 52 | ], 53 | addCommands() { 54 | const isStrikethrough = () => { 55 | return this.commands.isTextStyle!('textDecoration', 'line-through') || this.commands.isTextStyle!('textDecorationLine', 'line-through') 56 | } 57 | 58 | const setStrikethrough = async () => { 59 | if (isStrikethrough()) { 60 | return 61 | } 62 | await this.commands.setTextStyle!({ 63 | textDecorationLine: 'line-through' 64 | }) 65 | } 66 | 67 | const unsetStrikethrough = async () => { 68 | if (!isStrikethrough()) { 69 | return 70 | } 71 | await this.commands.removeTextStyle!(['textDecoration', 'textDecorationLine']) 72 | } 73 | 74 | return { 75 | isStrikethrough, 76 | setStrikethrough, 77 | unsetStrikethrough 78 | } 79 | } 80 | }) 81 | -------------------------------------------------------------------------------- /docs/extensions/built-in/font-size.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: font-size 字号 3 | --- 4 | 5 | # font-size 字号 6 | 7 | 文本字号 8 | 9 | ## Commands 命令 10 | 11 | ##### isFontSize() 12 | 13 | 判断光标所在文本字号是否与指定值一致 14 | 15 | - 类型 16 | 17 | ```ts 18 | isFontSize(value: string): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 提供一个入参,类型为 `string`,该方法用来判断光标所在文本的字号是否指定的值,返回 `boolean` 值 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isFontSize = editor.commands.isFontSize('20px') 29 | ``` 30 | 31 | ##### setFontSize() 32 | 33 | 设置光标范围内的文本的字号 34 | 35 | - 类型 36 | 37 | ```ts 38 | setFontSize(value: string): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 提供一个入参,类型为 `string`,表示设置的字号,该方法会对光标范围内的文本设置该字号,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isFontSize` 判断光标所在文本都是该字号,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setFontSize('20px') 51 | ``` 52 | 53 | ##### unsetFontSize() 54 | 55 | 取消光标范围内的文本的字号 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetFontSize(value: string): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 提供一个入参,类型为 `string`,表示取消的字号,该方法会对光标范围内的文本取消设置该字号,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isFontSize` 判断光标所在文本不都是该字号,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetFontSize('20px') 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 111 | -------------------------------------------------------------------------------- /docs/extensions/built-in/back-color.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: back-color 背景色 3 | --- 4 | 5 | # back-color 背景色 6 | 7 | 文本背景色 8 | 9 | ## Commands 命令 10 | 11 | ##### isBackColor() 12 | 13 | 判断光标所在文本的背景色是否与指定值一致 14 | 15 | - 类型 16 | 17 | ```ts 18 | isBackColor(value: string): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 提供一个入参,类型为 `string`,用以判断光标所在文本的背景色是否与指定值一致 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isRedBack = editor.commands.isBackColor('#f30') 29 | ``` 30 | 31 | ##### setBackColor() 32 | 33 | 设置光标所在文本的背景色 34 | 35 | - 类型 36 | 37 | ```ts 38 | setBackColor(value: string): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 提供一个入参,类型为 `string`,表示设置的文本背景色,该方法会设置光标文本为该背景色,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isBackColor` 判断光标所在文本都是该背景色,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setBackColor('#f30') 51 | ``` 52 | 53 | ##### unsetBackColor() 54 | 55 | 取消光标所在文本的背景色 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetBackColor(value: string): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 提供一个入参,类型为 `string`,表示取消的背景色,该方法会取消光标文本的该背景色,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isBackColor` 判断光标所在文本不全都是该背景色,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetBackColor('#f30') 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 111 | -------------------------------------------------------------------------------- /docs/extensions/built-in/font-family.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: font-family 字体 3 | --- 4 | 5 | # font-family 字体 6 | 7 | 文本字体 8 | 9 | ## Commands 命令 10 | 11 | ##### isFontFamily() 12 | 13 | 判断光标所在文本字体是否与指定值一致 14 | 15 | - 类型 16 | 17 | ```ts 18 | isFontFamily(value: string): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 提供一个入参,类型为 `string`,该方法用来判断光标所在文本的字体是否指定的值,返回 `boolean` 值 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isFontFamily = editor.commands.isFontFamily('楷体, 楷体-简') 29 | ``` 30 | 31 | ##### setFontFamily() 32 | 33 | 设置光标范围内的文本的字体 34 | 35 | - 类型 36 | 37 | ```ts 38 | setFontFamily(value: string): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 提供一个入参,类型为 `string`,表示设置的字体值,该方法会对光标范围内的文本设置该字体,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isFontFamily` 判断光标所在文本都是该字体,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setFontFamily('楷体, 楷体-简') 51 | ``` 52 | 53 | ##### unsetFontFamily() 54 | 55 | 取消光标范围内的文本的字体 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetFontFamily(value: string): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 提供一个入参,类型为 `string`,表示取消的字体值,该方法会对光标范围内的文本取消设置该字体,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isFontFamily` 判断光标所在文本不都是该字体,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetFontFamily('楷体, 楷体-简') 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 111 | -------------------------------------------------------------------------------- /docs/extensions/built-in/line-height.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: line-height 行高 3 | --- 4 | 5 | # line-height 行高 6 | 7 | 支持行高样式的渲染,提供设置行高的能力 8 | 9 | ## Commands 命令 10 | 11 | ##### isLineHeight() 12 | 13 | 判断光标所在的块节点是否都是符合的行高 14 | 15 | - 类型 16 | 17 | ```ts 18 | isLineHeight(value: string | number): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 提供一个入参,类型为 `string | number`,用以判断光标所在的块节点的行高是否都是指定的值,返回 `boolean` 值 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isLineHeight = editor.commands.isLineHeight(3) 29 | ``` 30 | 31 | ##### setLineHeight() 32 | 33 | 设置光标所在的块节点的行高 34 | 35 | - 类型 36 | 37 | ```ts 38 | setLineHeight(value: string | number): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 提供一个入参,类型为 `string | number`,该方法会设置光标所在的块节点的行高,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isLineHeight` 判断所在块节点都已经是该行高了,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setLineHeight(3) 51 | ``` 52 | 53 | ##### unsetLineHeight() 54 | 55 | 取消设置光标所在的块节点的行高 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetLineHeight(value: string | number): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 提供一个入参,类型为 `string | number`,该方法会取消光标所在的块节点的指定行高,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isLineHeight` 判断所在块节点都已经不是该行高了,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetLineHeight(3) 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 111 | -------------------------------------------------------------------------------- /docs/extensions/built-in/history.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: history 历史记录 3 | --- 4 | 5 | # history 历史记录 6 | 7 | 提供撤销、重做等能力,并且支持快捷键 8 | 9 | 撤销快捷键:`Ctrl + Z / Command + Z (Mac)` 10 | 11 | 重做快捷键:`Ctrl + Y / Command + Shift + Z (Mac)` 12 | 13 | ## Commands 命令 14 | 15 | ##### canUndo() 16 | 17 | 是否可以撤销 18 | 19 | - 类型 20 | 21 | ```ts 22 | canUndo(): boolean 23 | ``` 24 | 25 | - 详细信息 26 | 27 | 该方法用来判断当前编辑器是否可以执行撤销操作,返回 `boolean` 值 28 | 29 | - 示例 30 | 31 | ```ts 32 | const canUndo = editor.commands.canUndo() 33 | ``` 34 | 35 | ##### canRedo() 36 | 37 | 是否可以重做 38 | 39 | - 类型 40 | 41 | ```ts 42 | canRedo(): boolean 43 | ``` 44 | 45 | - 详细信息 46 | 47 | 该方法用来判断当前编辑器是否可以执行重做操作,返回 `boolean` 值 48 | 49 | - 示例 50 | 51 | ```ts 52 | const canRedo = editor.commands.canRedo() 53 | ``` 54 | 55 | ##### undo() 56 | 57 | 执行撤销操作 58 | 59 | - 类型 60 | 61 | ```ts 62 | undo(): Promise 63 | ``` 64 | 65 | - 详细信息 66 | 67 | 该方法会执行一次撤销操作,并且在操作完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.undo() 73 | ``` 74 | 75 | ##### redo() 76 | 77 | 执行重做操作 78 | 79 | - 类型 80 | 81 | ```ts 82 | redo(): Promise 83 | ``` 84 | 85 | - 详细信息 86 | 87 | 该方法会执行一次重做操作,并且在操作完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 88 | 89 | - 示例 90 | 91 | ```ts 92 | await editor.commands.redo() 93 | ``` 94 | 95 | ## 代码示例 96 | 97 |
98 | 99 | 100 |
101 |
102 | 103 | 131 | 132 | ``` 133 | 134 | ``` 135 | -------------------------------------------------------------------------------- /docs/guide/history.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: History 3 | --- 4 | 5 | # History 6 | 7 | > kaitify 内部维护了一套历史记录缓存的机制,在编辑器渲染后的每一次视图更新的操作,都会使得编辑器的内容和光标信息被编辑器内部记录并缓存,通过这样的一个机制,可以实现撤销和重做 8 | 9 | ## 历史记录结构 10 | 11 | 编辑器创建时,会初始化一个 `History` 实例,可以通过编辑器实例的 `history` 属性访问,实例具有以下属性: 12 | 13 | ##### records 14 | 15 | 存放历史记录的堆栈,数组里的每一项元素都包含 `nodes` 和 `selection` 属性,表示每一次记录的编辑器节点内容和光标信息,在初始化编辑器,该数组只存在一个记录,所以我们可以通过判断该属性的长度来判断是否可以进行撤销操作 16 | 17 | ```ts 18 | const canUndo = editor.history.records.length > 1 19 | ``` 20 | 21 | ##### redoRecords 22 | 23 | 存放撤销记录的堆栈,数组里的每一项元素都包含 `nodes` 和 `selection` 属性,表示每一次撤销记录的编辑器节点内容和光标信息,当我们没有执行过撤销时,该数组长度为 0,因此可以通过判断该属性的长度来判断是否可以进行重做操作 24 | 25 | ```ts 26 | const canRedo = editor.history.redoRecords.length > 0 27 | ``` 28 | 29 | ## 常用方法 30 | 31 | ##### setState() 32 | 33 | 保存新的记录 34 | 35 | - 类型 36 | 37 | ```ts 38 | setState(nodes: KNode[], selection: Selection): void 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 第一个入参表示需要保存的节点数组,第二个参数表示需要保存的虚拟光标信息,该方法会向 `records` 历史记录堆栈加入新的记录,同时清空撤销记录堆栈 44 | 45 | - 示例 46 | 47 | ```ts 48 | editor.history.setState(editor.stackNodes, editor.selection) 49 | ``` 50 | 51 | ##### setUndo() 52 | 53 | 撤销操作:返回上一个历史记录 54 | 55 | - 类型 56 | 57 | ```ts 58 | setUndo(): HistoryRecordType | null 59 | ``` 60 | 61 | - 详细信息 62 | 63 | 该方法会返回上一个历史记录数据,如果不可撤销,则返回 `null`,我们只需要将该历史记录数据更新到编辑器当前视图即可 64 | 65 | - 示例 66 | 67 | ```ts 68 | //取出上一个历史记录 69 | const record = editor.history.setUndo() 70 | //存在历史记录表示可以撤销 71 | if (record) { 72 | //更新到编辑器 73 | editor.stackNodes = record.nodes 74 | editor.selection = record.selection 75 | //渲染到视图,但是不加入历史记录 76 | await editor.updateView(true, true) 77 | } 78 | ``` 79 | 80 | ##### setRedo() 81 | 82 | 重做操作:返回下一个历史记录 83 | 84 | - 类型 85 | 86 | ```ts 87 | setRedo(): HistoryRecordType | null 88 | ``` 89 | 90 | - 详细信息 91 | 92 | 该方法会返回下一个历史记录数据,如果不可重做,则返回 `null`,我们只需要将该历史记录数据更新到编辑器当前视图即可 93 | 94 | - 示例 95 | 96 | ```ts 97 | //取出下一个历史记录 98 | const record = editor.history.setRedo() 99 | //可以重做 100 | if (record) { 101 | //更新到编辑器 102 | editor.stackNodes = record.nodes 103 | editor.selection = record.selection 104 | //更新到视图,但是不加入历史记录 105 | await editor.updateView(true, true) 106 | } 107 | ``` 108 | 109 | ##### updateSelection() 110 | 111 | 仅更新当前记录的光标信息 112 | 113 | - 类型 114 | 115 | ```ts 116 | updateSelection(selection: Selection): void 117 | ``` 118 | 119 | - 详细信息 120 | 121 | 提供一个入参,类型为虚拟光标 `Selection`,该方法会将光标信息更新到当前的记录中去 122 | 123 | - 示例 124 | 125 | ```ts 126 | editor.history.updateSelection(editor.selection) 127 | ``` 128 | -------------------------------------------------------------------------------- /docs/extensions/built-in/align.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: align 对齐方式 3 | --- 4 | 5 | # align 对齐方式 6 | 7 | 支持对齐方式样式的渲染,提供设置对齐方式的能力 8 | 9 | ## Commands 命令 10 | 11 | ##### isAlign() 12 | 13 | 判断光标所在的块节点是否都是符合的对齐方式 14 | 15 | - 类型 16 | 17 | ```ts 18 | isAlign(value: AlignValueType): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 提供一个入参,类型为 `AlignValueType`,可取值为 `left` `right` `center` `justify`,用以判断光标所在的块节点是否都符合指定的对齐方式,返回 `boolean` 值 24 | 25 | - 示例 26 | 27 | ```ts 28 | const isAlignCenter = editor.commands.isAlign('center') 29 | ``` 30 | 31 | ##### setAlign() 32 | 33 | 设置光标所在的块节点的对齐方式 34 | 35 | - 类型 36 | 37 | ```ts 38 | setAlign(value: AlignValueType): Promise 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 提供一个入参,类型为 `AlignValueType`,可取值为 `left` `right` `center` `justify`,该方法会设置光标所在的块节点的对齐方式,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 44 | 45 | 如果通过 `isAlign` 判断所在块节点都已经是该对齐方式了,则不会继续执行 46 | 47 | - 示例 48 | 49 | ```ts 50 | await editor.commands.setAlign('center') 51 | ``` 52 | 53 | ##### unsetAlign() 54 | 55 | 取消设置光标所在的块节点的指定对齐方式 56 | 57 | - 类型 58 | 59 | ```ts 60 | unsetAlign(value: AlignValueType): Promise 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 提供一个入参,类型为 `AlignValueType`,可取值为 `left` `right` `center` `justify`,该方法会取消光标所在的块节点的指定对齐方式,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 66 | 67 | 如果通过 `isAlign` 判断所在块节点都已经不是该对齐方式了,则不会继续执行 68 | 69 | - 示例 70 | 71 | ```ts 72 | await editor.commands.unsetAlign('center') 73 | ``` 74 | 75 | ## 代码示例 76 | 77 |
78 | 79 | 80 | 81 | 82 |
83 |
84 | 85 | 113 | -------------------------------------------------------------------------------- /docs/extensions/built-in/math.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: math 数学公式 3 | --- 4 | 5 | # math 数学公式 6 | 7 | 支持 `Latex` 数学公式的渲染,提供插入数学公式的能力 8 | 9 | ## Commands 命令 10 | 11 | ##### getMath() 12 | 13 | 获取光标所在的数学公式节点 14 | 15 | - 类型 16 | 17 | ```ts 18 | getMath(): KNode | null 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法可以获取光标所在的唯一数学公式节点,如果光标不在一个数学公式节点内,则返回 `null` 24 | 25 | - 示例 26 | 27 | ```ts 28 | const mathNode = editor.commands.getMath() 29 | ``` 30 | 31 | ##### hasMath() 32 | 33 | 判断光标范围内是否有数学公式节点 34 | 35 | - 类型 36 | 37 | ```ts 38 | hasMath(): boolean 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法用来判断光标范围内是否有数学公式节点,返回 `boolean` 值 44 | 45 | - 示例 46 | 47 | ```ts 48 | const has = editor.commands.hasMath() 49 | ``` 50 | 51 | ##### setMath() 52 | 53 | 插入数学公式 54 | 55 | - 类型 56 | 57 | ```ts 58 | setMath(value: string): Promise 59 | ``` 60 | 61 | - 详细信息 62 | 63 | 提供一个入参,类型为 `string`,表示插入的数学公式 `Latex` 语法字符串,该方法会向编辑器内插入数学公式节点,在插入完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 64 | 65 | 需要注意:当光标范围内包含其他的数学公式节点时,无法插入新的数学公式节点 66 | 67 | - 示例 68 | 69 | ```ts 70 | await editor.commands.setMath('\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}') 71 | ``` 72 | 73 | ##### updateMath() 74 | 75 | 更新数学公式 76 | 77 | - 类型 78 | 79 | ```ts 80 | updateMath(value: string): Promise 81 | ``` 82 | 83 | - 详细信息 84 | 85 | 提供一个入参,类型为 `string`,表示更新的数学公式 `Latex` 语法字符串,该方法更新当前光标所指向的唯一数学公式节点的内容,在更新完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 86 | 87 | 需要注意:当光标不在同一个数学公式节点内时,此方法无效,因为获取不到唯一的数学公式节点 88 | 89 | - 示例 90 | 91 | ```ts 92 | await editor.commands.updateMath('\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}') 93 | ``` 94 | 95 | ## 代码示例 96 | 97 |
98 | 99 | 100 |
101 |
102 | 103 | 131 | -------------------------------------------------------------------------------- /docs/extensions/built-in/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: task 待办 3 | --- 4 | 5 | # task 待办 6 | 7 | 支持待办的渲染,提供插入待办的能力 8 | 9 | ## Commands 命令 10 | 11 | ##### getTask() 12 | 13 | 获取光标所在的待办节点 14 | 15 | - 类型 16 | 17 | ```ts 18 | getTask(): KNode | null 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法可以获取光标所在的唯一待办节点,如果光标不在一个待办节点内,则返回 `null` 24 | 25 | - 示例 26 | 27 | ```ts 28 | const taskNode = editor.commands.getTask() 29 | ``` 30 | 31 | ##### hasTask() 32 | 33 | 判断光标范围内是否有待办节点 34 | 35 | - 类型 36 | 37 | ```ts 38 | hasTask(): boolean 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法用来判断光标范围内是否有待办节点,返回 `boolean` 值 44 | 45 | - 示例 46 | 47 | ```ts 48 | const hasTask = editor.commands.hasTask() 49 | ``` 50 | 51 | ##### allTask() 52 | 53 | 光标范围内是否都是待办节点 54 | 55 | - 类型 56 | 57 | ```ts 58 | allTask(): boolean 59 | ``` 60 | 61 | - 详细信息 62 | 63 | 该方法用来判断光标范围内都是待办节点,返回 `boolean` 值 64 | 65 | - 示例 66 | 67 | ```ts 68 | const allTask = editor.commands.allTask() 69 | ``` 70 | 71 | ##### setTask() 72 | 73 | 设置待办 74 | 75 | - 类型 76 | 77 | ```ts 78 | setTask(): Promise 79 | ``` 80 | 81 | - 详细信息 82 | 83 | 该方法会将光标所在的块节点都转为待办节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 84 | 85 | 如果通过 `allTask` 判断光标范围内都是待办节点,则不会继续执行 86 | 87 | - 示例 88 | 89 | ```ts 90 | await editor.commands.setTask() 91 | ``` 92 | 93 | ##### unsetTask() 94 | 95 | 取消待办 96 | 97 | - 类型 98 | 99 | ```ts 100 | unsetTask(): Promise 101 | ``` 102 | 103 | - 详细信息 104 | 105 | 该方法会将光标所在的待办节点都转为段落节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 106 | 107 | 如果通过 `allTask` 判断光标范围内不都是待办节点,则不会继续执行 108 | 109 | - 示例 110 | 111 | ```ts 112 | await editor.commands.unsetTask() 113 | ``` 114 | 115 | ## 代码示例 116 | 117 |
118 | 119 | 120 |
121 |
122 | 123 | 151 | -------------------------------------------------------------------------------- /docs/extensions/built-in/code.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: code 行内代码 3 | --- 4 | 5 | # code 行内代码 6 | 7 | 支持行内代码的渲染,提供插入行内代码的能力 8 | 9 | ## Commands 命令 10 | 11 | ##### getCode() 12 | 13 | 获取光标所在的行内代码 14 | 15 | - 类型 16 | 17 | ```ts 18 | getCode(): KNode | null 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法可以获取光标所在的唯一行内代码节点,如果光标不在一个行内代码节点内,则返回 `null` 24 | 25 | - 示例 26 | 27 | ```ts 28 | const codeNode = editor.commands.getCode() 29 | ``` 30 | 31 | ##### hasCode() 32 | 33 | 判断光标范围内是否有行内代码节点 34 | 35 | - 类型 36 | 37 | ```ts 38 | hasCode(): boolean 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法用来判断光标范围内是否有行内代码节点,返回 `boolean` 值 44 | 45 | - 示例 46 | 47 | ```ts 48 | const hasCode = editor.commands.hasCode() 49 | ``` 50 | 51 | ##### allCode() 52 | 53 | 光标范围内是否都是行内代码节点 54 | 55 | - 类型 56 | 57 | ```ts 58 | allCode(): boolean 59 | ``` 60 | 61 | - 详细信息 62 | 63 | 该方法用来判断光标范围内都是行内代码节点,返回 `boolean` 值 64 | 65 | - 示例 66 | 67 | ```ts 68 | const allCode = editor.commands.allCode() 69 | ``` 70 | 71 | ##### setCode() 72 | 73 | 设置行内代码 74 | 75 | - 类型 76 | 77 | ```ts 78 | setCode(): Promise 79 | ``` 80 | 81 | - 详细信息 82 | 83 | 该方法会在光标所在范围内设置行内代码,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 84 | 85 | 如果通过 `allCode` 判断光标范围内都是行内代码节点,则不会继续执行 86 | 87 | - 示例 88 | 89 | ```ts 90 | await editor.commands.setCode() 91 | ``` 92 | 93 | ##### unsetCode() 94 | 95 | 取消行内代码 96 | 97 | - 类型 98 | 99 | ```ts 100 | unsetCode(): Promise 101 | ``` 102 | 103 | - 详细信息 104 | 105 | 该方法会将光标所在范围内的行内代码全部取消,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 106 | 107 | 如果通过 `allCode` 判断光标范围内不都是行内代码节点,则不会继续执行 108 | 109 | - 示例 110 | 111 | ```ts 112 | await editor.commands.unsetCode() 113 | ``` 114 | 115 | ## 代码示例 116 | 117 |
118 | 119 | 120 |
121 |
122 | 123 | 151 | -------------------------------------------------------------------------------- /docs/extensions/built-in/blockquote.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: blockquote 引用 3 | --- 4 | 5 | # blockquote 引用 6 | 7 | 支持引用的渲染,提供插入引用的能力 8 | 9 | ## Commands 命令 10 | 11 | ##### getBlockquote() 12 | 13 | 获取光标所在的引用节点 14 | 15 | - 类型 16 | 17 | ```ts 18 | getBlockquote(): KNode | null 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法可以获取光标所在的唯一引用节点,如果光标不在一个引用节点内,则返回 `null` 24 | 25 | - 示例 26 | 27 | ```ts 28 | const blockquoteNode = editor.commands.getBlockquote() 29 | ``` 30 | 31 | ##### hasBlockquote() 32 | 33 | 判断光标范围内是否有引用节点 34 | 35 | - 类型 36 | 37 | ```ts 38 | hasBlockquote(): boolean 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法用来判断光标范围内是否有引用节点,返回 `boolean` 值 44 | 45 | - 示例 46 | 47 | ```ts 48 | const hasBlockquote = editor.commands.hasBlockquote() 49 | ``` 50 | 51 | ##### allBlockquote() 52 | 53 | 光标范围内是否都是引用节点 54 | 55 | - 类型 56 | 57 | ```ts 58 | allBlockquote(): boolean 59 | ``` 60 | 61 | - 详细信息 62 | 63 | 该方法用来判断光标范围内都是引用节点,返回 `boolean` 值 64 | 65 | - 示例 66 | 67 | ```ts 68 | const allBlockquote = editor.commands.allBlockquote() 69 | ``` 70 | 71 | ##### setBlockquote() 72 | 73 | 设置引用 74 | 75 | - 类型 76 | 77 | ```ts 78 | setBlockquote(): Promise 79 | ``` 80 | 81 | - 详细信息 82 | 83 | 该方法会将光标所在的块节点都转为引用节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 84 | 85 | 如果通过 `allBlockquote` 判断光标范围内都是引用节点,则不会继续执行 86 | 87 | - 示例 88 | 89 | ```ts 90 | await editor.commands.setBlockquote() 91 | ``` 92 | 93 | ##### unsetBlockquote() 94 | 95 | 取消引用 96 | 97 | - 类型 98 | 99 | ```ts 100 | unsetBlockquote(): Promise 101 | ``` 102 | 103 | - 详细信息 104 | 105 | 该方法会将光标所在的引用节点都转为段落节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 106 | 107 | 如果通过 `allBlockquote` 判断光标范围内不都是引用节点,则不会继续执行 108 | 109 | - 示例 110 | 111 | ```ts 112 | await editor.commands.unsetBlockquote() 113 | ``` 114 | 115 | ## 代码示例 116 | 117 |
118 | 119 | 120 |
121 |
122 | 123 | 151 | -------------------------------------------------------------------------------- /lib/model/config/function.d.ts: -------------------------------------------------------------------------------- 1 | import { Extension } from '../../extensions'; 2 | import { Editor } from '../Editor'; 3 | import { KNode } from '../KNode'; 4 | import { RuleFunctionType } from './format-rules'; 5 | /** 6 | * 获取选区内的可聚焦节点所在的块节点数组 7 | */ 8 | export declare const getSelectionBlockNodes: (this: Editor) => KNode[]; 9 | /** 10 | * 打散指定的节点,将其分裂成多个节点,如果子孙节点还有子节点则继续打散 11 | */ 12 | export declare const splitNodeToNodes: (this: Editor, node: KNode) => void; 13 | /** 14 | * 清空固定块节点的内容 15 | */ 16 | export declare const emptyFixedBlock: (this: Editor, node: KNode) => void; 17 | /** 18 | * 该方法目前只为delete方法内部使用:将后一个块节点与前一个块节点合并 19 | */ 20 | export declare const mergeBlock: (this: Editor, node: KNode, target: KNode) => void; 21 | /** 22 | * 判断编辑器内的指定节点是否可以进行合并操作,parent表示和父节点进行合并,prevSibling表示和前一个兄弟节点进行合并,nextSibling表示和下一个兄弟节点合并,如果可以返回合并的对象节点 23 | */ 24 | export declare const getAllowMergeNode: (this: Editor, node: KNode, type: "parent" | "prevSibling" | "nextSibling") => KNode | null; 25 | /** 26 | * 对编辑器内的某个节点执行合并操作,parent表示和父节点进行合并,prevSibling表示和前一个兄弟节点进行合并,nextSibling表示和下一个兄弟节点合并(可能会更新光标) 27 | */ 28 | export declare const applyMergeNode: (this: Editor, node: KNode, type: "parent" | "prevSibling" | "nextSibling") => void; 29 | /** 30 | * 将编辑器内的某个非块级节点转为默认块级节点 31 | */ 32 | export declare const convertToBlock: (this: Editor, node: KNode) => void; 33 | /** 34 | * 对节点数组使用指定规则进行格式化,nodes是需要格式化的节点数组,sourceNodes是格式化的节点所在的源数组 35 | */ 36 | export declare const formatNodes: (this: Editor, rule: RuleFunctionType, nodes: KNode[], sourceNodes: KNode[]) => void; 37 | /** 38 | * 注册扩展 39 | */ 40 | export declare const registerExtension: (this: Editor, extension: Extension) => void; 41 | /** 42 | * 根据真实光标更新selection,返回布尔值表示是否更新成功 43 | */ 44 | export declare const updateSelection: (this: Editor) => boolean; 45 | /** 46 | * 纠正光标位置,返回布尔值表示是否存在纠正行为 47 | */ 48 | export declare const redressSelection: (this: Editor) => boolean; 49 | /** 50 | * 初始化校验编辑器的节点数组,如果编辑器的节点数组为空或者都是空节点,则初始化创建一个只有占位符的段落 51 | */ 52 | export declare const checkNodes: (this: Editor) => void; 53 | /** 54 | * 粘贴时对节点的标记和样式的保留处理 55 | */ 56 | export declare const handlerForPasteKeepMarksAndStyles: (this: Editor, nodes: KNode[]) => void; 57 | /** 58 | * 粘贴时对文件的处理 59 | */ 60 | export declare const handlerForPasteFiles: (this: Editor, files: FileList) => Promise; 61 | /** 62 | * 处理某个节点数组,针对为空的块级节点补充占位符,目前仅用于粘贴处理 63 | */ 64 | export declare const fillPlaceholderToEmptyBlock: (this: Editor, nodes: KNode[]) => void; 65 | /** 66 | * 粘贴处理 67 | */ 68 | export declare const handlerForPasteDrop: (this: Editor, dataTransfer: DataTransfer) => Promise; 69 | /** 70 | * 将指定的非固定块节点从父节点(非固定块节点)中抽离,插入到和父节点同级的位置 71 | */ 72 | export declare const removeBlockFromParentToSameLevel: (this: Editor, node: KNode) => void; 73 | /** 74 | * 光标所在的块节点不是只有占位符,且非固定块节点,非代码块样式的块节点,在该块节点内正常换行方法 75 | */ 76 | export declare const handlerForNormalInsertParagraph: (this: Editor) => void; 77 | /** 78 | * 设置placeholder,在每次视图更新时调用此方法 79 | */ 80 | export declare const setPlaceholder: (this: Editor) => void; 81 | /** 82 | * 合并扩展数组(只能用在扩展注册之前) 83 | */ 84 | export declare const mergeExtensions: (this: Editor, from: Extension[]) => Extension[]; 85 | -------------------------------------------------------------------------------- /src/model/History.ts: -------------------------------------------------------------------------------- 1 | import { KNode } from './KNode' 2 | import { Selection } from './Selection' 3 | /** 4 | * 历史记录的record类型 5 | */ 6 | export type HistoryRecordType = { 7 | nodes: KNode[] 8 | selection: Selection 9 | } 10 | /** 11 | * 历史记录 12 | */ 13 | export class History { 14 | /** 15 | * 存放历史记录的堆栈 16 | */ 17 | records: HistoryRecordType[] = [] 18 | /** 19 | * 存放撤销记录的堆栈 20 | */ 21 | redoRecords: HistoryRecordType[] = [] 22 | 23 | /** 24 | * 复制selection 25 | */ 26 | cloneSelection(newNodes: KNode[], selection: Selection) { 27 | const newSelection = new Selection() 28 | //如果存在选区 29 | if (selection.focused()) { 30 | //查找新的节点数组中start对应的节点 31 | const startNode = KNode.searchByKey(selection.start!.node.key, newNodes) 32 | //查找新的节点数组中end对应的节点 33 | const endNode = KNode.searchByKey(selection.end!.node.key, newNodes) 34 | //如果都存在 35 | if (startNode && endNode) { 36 | newSelection.start = { 37 | node: startNode, 38 | offset: selection.start!.offset 39 | } 40 | newSelection.end = { 41 | node: endNode, 42 | offset: selection.end!.offset 43 | } 44 | } 45 | } 46 | return newSelection 47 | } 48 | 49 | /** 50 | * 保存新的记录 51 | */ 52 | setState(nodes: KNode[], selection: Selection) { 53 | const newNodes = nodes.map(item => item.fullClone()) 54 | const newSelection = this.cloneSelection(newNodes, selection) 55 | this.records.push({ 56 | nodes: newNodes, 57 | selection: newSelection 58 | }) 59 | //每次保存新状态时清空撤销记录的堆栈 60 | this.redoRecords = [] 61 | } 62 | 63 | /** 64 | * 撤销操作:返回上一个历史记录 65 | */ 66 | setUndo(): HistoryRecordType | null { 67 | //存在的历史记录大于1则表示可以进行撤销操作 68 | if (this.records.length > 1) { 69 | //取出最近的历史记录 70 | const record = this.records.pop()! 71 | //将这个历史记录加入到撤销记录数组中 72 | this.redoRecords.push(record) 73 | //再次获取历史记录数组中的最近的一个 74 | const lastRecord = this.records[this.records.length - 1] 75 | const newNodes = lastRecord.nodes.map(item => item.fullClone()) 76 | const newSelection = this.cloneSelection(newNodes, lastRecord.selection) 77 | return { 78 | nodes: newNodes, 79 | selection: newSelection 80 | } 81 | } 82 | //没有历史记录则返回null 83 | return null 84 | } 85 | 86 | /** 87 | * 重做操作:返回下一个历史记录 88 | */ 89 | setRedo(): HistoryRecordType | null { 90 | //如果存在撤销记录 91 | if (this.redoRecords.length > 0) { 92 | //取出最近的一个撤销记录 93 | const record = this.redoRecords.pop()! 94 | //将撤销记录加入历史记录中 95 | this.records.push(record) 96 | //返回取出的这个撤销记录,即最近的一个历史记录 97 | const newNodes = record.nodes.map(item => item.fullClone()) 98 | const newSelection = this.cloneSelection(newNodes, record.selection) 99 | return { 100 | nodes: newNodes, 101 | selection: newSelection 102 | } 103 | } 104 | return null 105 | } 106 | 107 | /** 108 | * 更新当前记录的编辑器的光标 109 | */ 110 | updateSelection(selection: Selection) { 111 | if (this.records.length === 0) return 112 | const record = this.records[this.records.length - 1] 113 | const newSelection = this.cloneSelection(record.nodes, selection) 114 | this.records[this.records.length - 1].selection = newSelection 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /docs/extensions/built-in/image.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: image 图片 3 | --- 4 | 5 | # image 图片 6 | 7 | 支持图片的渲染,提供插入图片的能力,并且支持拖拽图片的右侧边缘可修改大小 8 | 9 | ## Commands 命令 10 | 11 | ##### getImage() 12 | 13 | 获取光标所在的图片节点 14 | 15 | - 类型 16 | 17 | ```ts 18 | getImage(): KNode | null 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法可以获取光标所在的唯一图片节点,如果光标不在一个图片节点内,则返回 `null` 24 | 25 | - 示例 26 | 27 | ```ts 28 | const imageNode = editor.commands.getImage() 29 | ``` 30 | 31 | ##### hasImage() 32 | 33 | 判断光标范围内是否有图片节点 34 | 35 | - 类型 36 | 37 | ```ts 38 | hasImage(): boolean 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法用来判断光标范围内是否有图片节点,返回 `boolean` 值 44 | 45 | - 示例 46 | 47 | ```ts 48 | const has = editor.commands.hasImage() 49 | ``` 50 | 51 | ##### setImage() 52 | 53 | 插入图片 54 | 55 | - 类型 56 | 57 | ```ts 58 | setImage(options: SetImageOptionType): Promise 59 | ``` 60 | 61 | - 详细信息 62 | 63 | 提供一个入参,类型为 `SetImageOptionType`,包含 3 个属性: 64 | 65 | - src :图片的链接地址 66 | - alt :图片加载失败显示的值 67 | - width :图片的初始宽度 68 | 69 | 该方法会向编辑器内插入图片节点,在插入完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 70 | 71 | - 示例 72 | 73 | ```ts 74 | await editor.commands.setImage({ 75 | src: 'https://xxxxx.png', 76 | alt: '图片加载失败' 77 | }) 78 | ``` 79 | 80 | ##### updateImage() 81 | 82 | 更新图片信息 83 | 84 | - 类型 85 | 86 | ```ts 87 | updateImage(options: UpdateImageOptionType): Promise 88 | ``` 89 | 90 | - 详细信息 91 | 92 | 提供一个入参,类型为 `UpdateImageOptionType`,包含以下 2 个属性: 93 | 94 | - src :图片的链接地址,可选,不设置则不更新此属性 95 | - alt :图片加载失败显示的值,不设置或者设置为空值则移除此属性 96 | 97 | 该方法可以更新图片信息,并且在更新完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 98 | 99 | - 示例 100 | 101 | ```ts 102 | await editor.commands.updateImage({ 103 | src: 'www.baidu.com' 104 | }) 105 | ``` 106 | 107 | ## 代码示例 108 | 109 |
110 | 111 | 112 |
113 |
114 | 115 | 151 | -------------------------------------------------------------------------------- /docs/extensions/built-in/video.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: video 视频 3 | --- 4 | 5 | # video 视频 6 | 7 | 支持视频的渲染,提供插入视频的能力,并且支持拖拽视频的右侧边缘可修改大小 8 | 9 | ## Commands 命令 10 | 11 | ##### getVideo() 12 | 13 | 获取光标所在的视频节点 14 | 15 | - 类型 16 | 17 | ```ts 18 | getVideo(): KNode | null 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法可以获取光标所在的唯一视频节点,如果光标不在一个视频节点内,则返回 `null` 24 | 25 | - 示例 26 | 27 | ```ts 28 | const videoNode = editor.commands.getVideo() 29 | ``` 30 | 31 | ##### hasVideo() 32 | 33 | 判断光标范围内是否有视频节点 34 | 35 | - 类型 36 | 37 | ```ts 38 | hasVideo(): boolean 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法用来判断光标范围内是否有视频节点,返回 `boolean` 值 44 | 45 | - 示例 46 | 47 | ```ts 48 | const hasVideo = editor.commands.hasVideo() 49 | ``` 50 | 51 | ##### setVideo() 52 | 53 | 插入视频 54 | 55 | - 类型 56 | 57 | ```ts 58 | setVideo(options: SetVideoOptionType): Promise 59 | ``` 60 | 61 | - 详细信息 62 | 63 | 提供一个入参,类型为 `SetVideoOptionType`,包含 3 个属性: 64 | 65 | - src :视频的链接地址 66 | - autoplay :视频加载完成是否自动播放 67 | - width :视频的初始宽度 68 | 69 | 该方法会向编辑器内插入视频节点,在插入完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 70 | 71 | - 示例 72 | 73 | ```ts 74 | await editor.commands.setVideo({ 75 | src: 'https://xxxxx.mp4', 76 | autoplay: true 77 | }) 78 | ``` 79 | 80 | ##### updateVideo() 81 | 82 | 更新视频信息 83 | 84 | - 类型 85 | 86 | ```ts 87 | updateVideo(options: UpdateVideoOptionType): Promise 88 | ``` 89 | 90 | - 详细信息 91 | 92 | 提供一个入参,类型为 `UpdateVideoOptionType`,包含以下 3 个属性: 93 | 94 | - controls :视频是否显示控制器,不设置则不更新此属性 95 | - muted :视频是否静音,不设置则不更新此属性 96 | - loop :视频是否循环,不设置则不更新此属性 97 | 98 | 该方法可以更新视频相关的设定,并且在更新完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 99 | 100 | - 示例 101 | 102 | ```ts 103 | await editor.commands.updateVideo({ 104 | loop: true 105 | }) 106 | ``` 107 | 108 | ## 代码示例 109 | 110 |
111 | 112 | 113 |
114 |
115 | 116 | 152 | -------------------------------------------------------------------------------- /docs/extensions/built-in/heading.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: hading 标题 3 | --- 4 | 5 | # hading 标题 6 | 7 | 支持标题的渲染,提供插入标题的能力 8 | 9 | ## Commands 命令 10 | 11 | ##### getHeading() 12 | 13 | 获取光标所在的指定等级的标题节点 14 | 15 | - 类型 16 | 17 | ```ts 18 | getHeading(level: HeadingLevelType): KNode | null 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 提供一个入参,类型为 `HeadingLevelType`,取值范围是 `0,1,2,3,4,5,6`,表示标题的等级,0 代表普通段落,从 1-6 代表 h1-h6,该方法可以获取光标所在的唯一指定的标题节点,如果光标不在一个该指定节点内,则返回 `null` 24 | 25 | - 示例 26 | 27 | ```ts 28 | //获取h1,即一级标题 29 | const headingNode = editor.commands.getHeading(1) 30 | ``` 31 | 32 | ##### hasHeading() 33 | 34 | 判断光标范围内是否有指定等级的标题节点 35 | 36 | - 类型 37 | 38 | ```ts 39 | hasHeading(level: HeadingLevelType): boolean 40 | ``` 41 | 42 | - 详细信息 43 | 44 | 提供一个入参,类型为 `HeadingLevelType`,取值范围是 `0,1,2,3,4,5,6`,表示标题的等级,0 代表普通段落,从 1-6 代表 h1-h6,该方法用来判断光标范围内是否有指定等级的标题节点,返回 `boolean` 值 45 | 46 | - 示例 47 | 48 | ```ts 49 | //判断光标范围内是否有h1,即一级标题 50 | const hasHeading = editor.commands.hasHeading(1) 51 | ``` 52 | 53 | ##### allHeading() 54 | 55 | 光标范围内是否都是指定等级的标题节点 56 | 57 | - 类型 58 | 59 | ```ts 60 | allHeading(level: HeadingLevelType): boolean 61 | ``` 62 | 63 | - 详细信息 64 | 65 | 提供一个入参,类型为 `HeadingLevelType`,取值范围是 `0,1,2,3,4,5,6`,表示标题的等级,0 代表普通段落,从 1-6 代表 h1-h6,该方法用来判断光标范围内都是指定等级的标题节点,返回 `boolean` 值 66 | 67 | - 示例 68 | 69 | ```ts 70 | //判断光标范围内是否都是h1,即一级标题 71 | const allHeading = editor.commands.allHeading(1) 72 | ``` 73 | 74 | ##### setHeading() 75 | 76 | 设置标题 77 | 78 | - 类型 79 | 80 | ```ts 81 | setHeading(level: HeadingLevelType): Promise 82 | ``` 83 | 84 | - 详细信息 85 | 86 | 提供一个入参,类型为 `HeadingLevelType`,取值范围是 `0,1,2,3,4,5,6`,表示标题的等级,0 代表普通段落,从 1-6 代表 h1-h6,该方法会将光标所在的块节点都转为指定等级的标题节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 87 | 88 | 如果通过 `allHeading` 判断光标范围内都是该等级的标题节点,则不会继续执行 89 | 90 | - 示例 91 | 92 | ```ts 93 | await editor.commands.setHeading(1) 94 | ``` 95 | 96 | ##### unsetHeading() 97 | 98 | 取消标题 99 | 100 | - 类型 101 | 102 | ```ts 103 | unsetHeading(): Promise 104 | ``` 105 | 106 | - 详细信息 107 | 108 | 提供一个入参,类型为 `HeadingLevelType`,取值范围是 `0,1,2,3,4,5,6`,表示标题的等级,0 代表普通段落,从 1-6 代表 h1-h6,该方法会将光标所在的指定标题节点都转为段落节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 109 | 110 | 如果通过 `allHeading` 判断光标范围内不都是该等级的标题节点,则不会继续执行 111 | 112 | - 示例 113 | 114 | ```ts 115 | await editor.commands.unsetHeading() 116 | ``` 117 | 118 | ## 代码示例 119 | 120 |
121 | 122 | 123 |
124 |
125 | 126 | 154 | -------------------------------------------------------------------------------- /docs/extensions/built-in/link.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: link 链接 3 | --- 4 | 5 | # link 链接 6 | 7 | 支持链接的渲染,提供插入链接的能力 8 | 9 | ## Commands 命令 10 | 11 | ##### getLink() 12 | 13 | 获取光标所在的链接节点 14 | 15 | - 类型 16 | 17 | ```ts 18 | getLink(): KNode | null 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法可以获取光标所在的唯一链接节点,如果光标不在一个链接节点内,则返回 `null` 24 | 25 | - 示例 26 | 27 | ```ts 28 | const linkNode = editor.commands.getLink() 29 | ``` 30 | 31 | ##### hasLink() 32 | 33 | 判断光标范围内是否有链接节点 34 | 35 | - 类型 36 | 37 | ```ts 38 | hasLink(): boolean 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法用来判断光标范围内是否有链接节点,返回 `boolean` 值 44 | 45 | - 示例 46 | 47 | ```ts 48 | const has = editor.commands.hasLink() 49 | ``` 50 | 51 | ##### setLink() 52 | 53 | 插入链接 54 | 55 | - 类型 56 | 57 | ```ts 58 | setLink(options: SetLinkOptionType): Promise 59 | ``` 60 | 61 | - 详细信息 62 | 63 | 提供一个入参,类型为 `SetLinkOptionType`,包含 3 个属性: 64 | 65 | - href :链接的地址 66 | - text :链接的文本,如果光标选择了一段内容,则使用光标选择的内容 67 | - newOpen :链接是否新窗口打开 68 | 69 | 该方法会向编辑器内插入链接节点,在插入完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 70 | 71 | 需要注意:当光标范围内包含其他的链接节点时,无法插入新的链接节点 72 | 73 | - 示例 74 | 75 | ```ts 76 | await editor.commands.setLink({ 77 | href: 'https://www.baidu.com', 78 | text: '百度一下,你就知道' 79 | }) 80 | ``` 81 | 82 | ##### unsetLink() 83 | 84 | 取消链接 85 | 86 | - 类型 87 | 88 | ```ts 89 | unsetLink(): Promise 90 | ``` 91 | 92 | - 详细信息 93 | 94 | 该方法会取消当前光标所在的唯一链接,在取消完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 95 | 96 | - 示例 97 | 98 | ```ts 99 | await editor.commands.setLink({ 100 | href: 'https://www.baidu.com', 101 | text: '百度一下,你就知道' 102 | }) 103 | ``` 104 | 105 | ##### updateLink() 106 | 107 | 更新链接信息 108 | 109 | - 类型 110 | 111 | ```ts 112 | updateLink(options: UpdateLinkOptionType): Promise 113 | ``` 114 | 115 | - 详细信息 116 | 117 | 提供一个入参,类型为 `UpdateLinkOptionType`,包含以下 2 个属性: 118 | 119 | - href :链接的地址,可选,不设置则不更新此属性 120 | - newOpen :链接是否新窗口打开,可选,不设置则不更新此属性 121 | 122 | 该方法可以自由地更新链接的地址、文本等,并且在更新完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 123 | 124 | 需要注意:当光标不在同一个链接节点内时,此方法无效,因为获取不到唯一的链接节点 125 | 126 | - 示例 127 | 128 | ```ts 129 | await editor.commands.updateLink({ 130 | href: 'www.baidu.com' 131 | }) 132 | ``` 133 | 134 | ## 代码示例 135 | 136 |
137 | 138 | 139 |
140 |
141 | 142 | 170 | -------------------------------------------------------------------------------- /src/extensions/align/index.ts: -------------------------------------------------------------------------------- 1 | import { KNode, KNodeStylesType } from '@/model' 2 | import { deleteProperty } from '@/tools' 3 | import { Extension } from '../Extension' 4 | import { getSelectionBlockNodes } from '@/model/config/function' 5 | 6 | export type AlignValueType = 'left' | 'right' | 'center' | 'justify' 7 | 8 | declare module '../../model' { 9 | interface EditorCommandsType { 10 | /** 11 | * 光标所在的块节点是否都是符合的对齐方式 12 | */ 13 | isAlign?: (value: AlignValueType) => boolean 14 | /** 15 | * 设置对齐方式 16 | */ 17 | setAlign?: (value: AlignValueType) => Promise 18 | /** 19 | * 取消对齐方式 20 | */ 21 | unsetAlign?: (value: AlignValueType) => Promise 22 | } 23 | } 24 | 25 | /** 26 | * 删除指定块节点及以上块节点的对齐方式 27 | */ 28 | const clearAlign = (blockNode: KNode, value: AlignValueType) => { 29 | const matchNode = blockNode.getMatchNode({ styles: { textAlign: value } }) 30 | if (matchNode) { 31 | matchNode.styles = deleteProperty(matchNode.styles!, 'textAlign') 32 | clearAlign(matchNode, value) 33 | } 34 | } 35 | 36 | export const AlignExtension = () => 37 | Extension.create({ 38 | name: 'align', 39 | onPasteKeepStyles(node) { 40 | const styles: KNodeStylesType = {} 41 | if (node.isBlock() && node.hasStyles()) { 42 | if (node.styles!.hasOwnProperty('textAlign')) styles.textAlign = node.styles!.textAlign 43 | } 44 | return styles 45 | }, 46 | addCommands() { 47 | const isAlign = (value: AlignValueType) => { 48 | if (!this.selection.focused()) { 49 | return false 50 | } 51 | //起点和终点在一起 52 | if (this.selection.collapsed()) { 53 | const block = this.selection.start!.node.getBlock() 54 | return !!block.getMatchNode({ styles: { textAlign: value } }) 55 | } 56 | //起点和终点不在一起 57 | const blockNodes = getSelectionBlockNodes.apply(this) 58 | return blockNodes.every(item => { 59 | return !!item.getMatchNode({ styles: { textAlign: value } }) 60 | }) 61 | } 62 | 63 | const setAlign = async (value: AlignValueType) => { 64 | if (isAlign(value)) { 65 | return 66 | } 67 | //起点和终点在一起 68 | if (this.selection.collapsed()) { 69 | const blockNode = this.selection.start!.node.getBlock() 70 | const styles: KNodeStylesType = blockNode.hasStyles() ? blockNode.styles! : {} 71 | blockNode.styles = { 72 | ...styles, 73 | textAlign: value 74 | } 75 | } 76 | //起点和终点不在一起 77 | else { 78 | const blockNodes = getSelectionBlockNodes.apply(this) 79 | blockNodes.forEach(item => { 80 | const styles: KNodeStylesType = item.hasStyles() ? item.styles! : {} 81 | item.styles = { 82 | ...styles, 83 | textAlign: value 84 | } 85 | }) 86 | } 87 | await this.updateView() 88 | } 89 | 90 | const unsetAlign = async (value: AlignValueType) => { 91 | if (!isAlign(value)) { 92 | return 93 | } 94 | //起点和终点在一起 95 | if (this.selection.collapsed()) { 96 | const blockNode = this.selection.start!.node.getBlock() 97 | clearAlign(blockNode, value) 98 | } 99 | //起点和终点不在一起 100 | else { 101 | const blockNodes = getSelectionBlockNodes.apply(this) 102 | blockNodes.forEach(item => { 103 | clearAlign(item, value) 104 | }) 105 | } 106 | await this.updateView() 107 | } 108 | 109 | return { 110 | isAlign, 111 | setAlign, 112 | unsetAlign 113 | } 114 | } 115 | }) 116 | -------------------------------------------------------------------------------- /docs/extensions/built-in/code-block.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: code-block 代码块 3 | --- 4 | 5 | # code-block 代码块 6 | 7 | 支持代码块的渲染,提供插入代码块的能力,注:在代码块内按下 `Tab` 键会插入 2 个空格 8 | 9 | ## Commands 命令 10 | 11 | ##### getCodeBlock() 12 | 13 | 获取光标所在的代码块节点 14 | 15 | - 类型 16 | 17 | ```ts 18 | getCodeBlock(): KNode | null 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 该方法可以获取光标所在的唯一代码块节点,如果光标不在一个代码块节点内,则返回 `null` 24 | 25 | - 示例 26 | 27 | ```ts 28 | const codeBlockNode = editor.commands.getCodeBlock() 29 | ``` 30 | 31 | ##### hasCodeBlock() 32 | 33 | 判断光标范围内是否有代码块节点 34 | 35 | - 类型 36 | 37 | ```ts 38 | hasCodeBlock(): boolean 39 | ``` 40 | 41 | - 详细信息 42 | 43 | 该方法用来判断光标范围内是否有代码块节点,返回 `boolean` 值 44 | 45 | - 示例 46 | 47 | ```ts 48 | const hasCodeBlock = editor.commands.hasCodeBlock() 49 | ``` 50 | 51 | ##### allCodeBlock() 52 | 53 | 光标范围内是否都是代码块节点 54 | 55 | - 类型 56 | 57 | ```ts 58 | allCodeBlock(): boolean 59 | ``` 60 | 61 | - 详细信息 62 | 63 | 该方法用来判断光标范围内都是代码块节点,返回 `boolean` 值 64 | 65 | - 示例 66 | 67 | ```ts 68 | const allCodeBlock = editor.commands.allCodeBlock() 69 | ``` 70 | 71 | ##### setCodeBlock() 72 | 73 | 设置代码块 74 | 75 | - 类型 76 | 77 | ```ts 78 | setCodeBlock(): Promise 79 | ``` 80 | 81 | - 详细信息 82 | 83 | 该方法会将光标所在的块节点都转为代码块节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 84 | 85 | 如果通过 `allCodeBlock` 判断光标范围内都是代码块节点,则不会继续执行 86 | 87 | - 示例 88 | 89 | ```ts 90 | await editor.commands.setCodeBlock() 91 | ``` 92 | 93 | ##### unsetCodeBlock() 94 | 95 | 取消代码块 96 | 97 | - 类型 98 | 99 | ```ts 100 | unsetCodeBlock(): Promise 101 | ``` 102 | 103 | - 详细信息 104 | 105 | 该方法会将光标所在的代码块节点都转为段落节点,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 106 | 107 | 如果通过 `allCodeBlock` 判断光标范围内不都是代码块节点,则不会继续执行 108 | 109 | - 示例 110 | 111 | ```ts 112 | await editor.commands.unsetCodeBlock() 113 | ``` 114 | 115 | ##### updateCodeBlockLanguage() 116 | 117 | 更新光标所在代码块的语言类型 118 | 119 | - 类型 120 | 121 | ```ts 122 | updateCodeBlockLanguage(language?: HljsLanguageType): Promise 123 | ``` 124 | 125 | - 详细信息 126 | 127 | 提供一个入参,类型为 `HljsLanguageType`,取值范围是 `plaintext` `json` `javascript` `java` `typescript` `python` `php` `css` `less` `scss` `html` `markdown` `objectivec` `swift` `dart` `nginx` `http` `go` `ruby` `c` `cpp` `csharp` `sql` `shell` `r` `kotlin` `rust` ,表示更新的语言值,该方法会修改所在代码块的语言类型,以达到不同的高亮效果,更新完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 128 | 129 | 如果光标不是在唯一的代码块节点内,则不会执行 130 | 131 | - 示例 132 | 133 | ```ts 134 | await editor.commands.updateCodeBlockLanguage('java') 135 | ``` 136 | 137 | ## 代码示例 138 | 139 |
140 | 141 | 142 |
143 |
144 | 145 | 173 | -------------------------------------------------------------------------------- /src/extensions/line-height/index.ts: -------------------------------------------------------------------------------- 1 | import { KNode, KNodeStylesType } from '@/model' 2 | import { getSelectionBlockNodes } from '@/model/config/function' 3 | import { deleteProperty } from '@/tools' 4 | import { Extension } from '../Extension' 5 | 6 | declare module '../../model' { 7 | interface EditorCommandsType { 8 | /** 9 | * 光标所在的块节点是否都是符合的行高 10 | */ 11 | isLineHeight?: (value: string | number) => boolean 12 | /** 13 | * 设置行高 14 | */ 15 | setLineHeight?: (value: string | number) => Promise 16 | /** 17 | * 取消行高 18 | */ 19 | unsetLineHeight?: (value: string | number) => Promise 20 | } 21 | } 22 | 23 | /** 24 | * 删除指定块节点及以上块节点的行高样式 25 | */ 26 | const clearLineHeight = (blockNode: KNode, value: string | number) => { 27 | const matchNode = blockNode.getMatchNode({ styles: { lineHeight: value } }) 28 | if (matchNode) { 29 | matchNode.styles = deleteProperty(matchNode.styles!, 'lineHeight') 30 | clearLineHeight(matchNode, value) 31 | } 32 | } 33 | 34 | export const LineHeightExtension = () => 35 | Extension.create({ 36 | name: 'lineHeight', 37 | onPasteKeepStyles(node) { 38 | const styles: KNodeStylesType = {} 39 | if (node.isBlock() && node.hasStyles()) { 40 | if (node.styles!.hasOwnProperty('lineHeight')) styles.lineHeight = node.styles!.lineHeight 41 | } 42 | return styles 43 | }, 44 | addCommands() { 45 | const isLineHeight = (value: string | number) => { 46 | if (!this.selection.focused()) { 47 | return false 48 | } 49 | //起点和终点在一起 50 | if (this.selection.collapsed()) { 51 | const block = this.selection.start!.node.getBlock() 52 | return !!block.getMatchNode({ styles: { lineHeight: value } }) 53 | } 54 | //起点和终点不在一起 55 | const blockNodes = getSelectionBlockNodes.apply(this) 56 | return blockNodes.every(item => { 57 | return !!item.getMatchNode({ styles: { lineHeight: value } }) 58 | }) 59 | } 60 | 61 | const setLineHeight = async (value: string | number) => { 62 | if (isLineHeight(value)) { 63 | return 64 | } 65 | //起点和终点在一起 66 | if (this.selection.collapsed()) { 67 | const blockNode = this.selection.start!.node.getBlock() 68 | const styles: KNodeStylesType = blockNode.hasStyles() ? blockNode.styles! : {} 69 | blockNode.styles = { 70 | ...styles, 71 | lineHeight: value 72 | } 73 | } 74 | //起点和终点不在一起 75 | else { 76 | const blockNodes = getSelectionBlockNodes.apply(this) 77 | blockNodes.forEach(item => { 78 | const styles: KNodeStylesType = item.hasStyles() ? item.styles! : {} 79 | item.styles = { 80 | ...styles, 81 | lineHeight: value 82 | } 83 | }) 84 | } 85 | await this.updateView() 86 | } 87 | 88 | const unsetLineHeight = async (value: string | number) => { 89 | if (!isLineHeight(value)) { 90 | return 91 | } 92 | //起点和终点在一起 93 | if (this.selection.collapsed()) { 94 | const blockNode = this.selection.start!.node.getBlock() 95 | clearLineHeight(blockNode, value) 96 | } 97 | //起点和终点不在一起 98 | else { 99 | const blockNodes = getSelectionBlockNodes.apply(this) 100 | blockNodes.forEach(item => { 101 | clearLineHeight(item, value) 102 | }) 103 | } 104 | await this.updateView() 105 | } 106 | 107 | return { 108 | isLineHeight, 109 | setLineHeight, 110 | unsetLineHeight 111 | } 112 | } 113 | }) 114 | -------------------------------------------------------------------------------- /docs/guide/knode.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: KNode 3 | --- 4 | 5 | # KNode 6 | 7 | > KNode 是编辑器内容的唯一组成元素,可以说编辑器的任何操作都是与 KNode 有关的,也就是我们后面通常所说的节点 8 | 9 | 富文本编辑器的内容原本都是 html 元素,但在 kaitify 的内部实现中,都被转换成了一个个节点,从而构成一个节点数组,挂载在编辑器实例属性 `stackNodes` 下 10 | 11 | ## 如何创建一个节点? 12 | 13 | 通过 `KNode.create` 方法来创建一个节点 14 | 15 | ```ts 16 | const node = KNode.create({ 17 | type: 'block', 18 | tag: 'p', 19 | children: [ 20 | { 21 | type: 'text', 22 | textContent: '这是一个段落' 23 | } 24 | ] 25 | }) 26 | ``` 27 | 28 | ## 节点构建参数 KNodeCreateOptionType 29 | 30 | ##### type 31 | 32 | 节点类型,可取值 `block` `inline` `closed` `text` 33 | 34 | - `text`:文本节点,表示一段文本内容,没有 `tag` 属性,没有子节点,在视图渲染时会根据编辑器实例属性 `textRenderTag` 来渲染成对应的 dom 35 | - `closed`:闭合节点,即没有子节点的节点,如图片、视频等 36 | - `inline`:行内节点,必须有子节点,子节点可以是 `text`、`closed` 和 `inline` 类型的 37 | - `block`:块节点,编辑器的 `stackNodes` 数组里的都是块节点,块节点的子节点可以是其他节点,但是其他节点不能作为块节点的父节点 38 | 39 | ##### tag 40 | 41 | 节点渲染成 dom 的真实元素标签,如`p`、`span`等,文本节点不需要设置此参数 42 | 43 | ##### textContent 44 | 45 | 文本节点的独有属性,表示节点的文本内容,非文本节点不需要设置此参数 46 | 47 | ##### marks 48 | 49 | 节点的标记集合,渲染成 `dom` 后表示元素的属性集合,但是不包括 `style` 属性 50 | 51 | ##### styles 52 | 53 | 节点的样式集合,样式名称请使用驼峰写法,如 `backgroundColor`,`textAlign` 等 54 | 55 | ##### locked 56 | 57 | 是否锁定节点,锁定的节点不会被编辑器格式化校验时与其他节点进行合并,默认值为 `false` 58 | 59 | - 针对块节点:在符合合并条件的情况下是否允许编辑器将其与父节点或者子节点进行合并; 60 | - 针对行内节点:在符合合并条件的情况下是否允许编辑器将其与相邻节点或者父节点或者子节点进行合并; 61 | - 针对文本节点:在符合合并的条件下是否允许编辑器将其与相邻节点或者父节点进行合并。 62 | 63 | ##### fixed 64 | 65 | 是否为固定块节点,值为 `true` 时:当光标在节点起始处或者光标在节点内只有占位符时,执行删除操作不会删除此节点,会再次创建一个占位符进行处理;当光标在节点内且节点不是代码块样式,不会进行换行,默认值为 `false` 66 | 67 | ##### nested 68 | 69 | 是否为固定格式的内嵌块节点,如 `li`、`tr`、`td` 等,默认值为 `false` 70 | 71 | ##### void 72 | 73 | 是否为不可见节点,意味着此类节点在编辑器内视图内无法看到,如`colgroup`、`col`等,默认值为 `false` 74 | 75 | ##### namespace 76 | 77 | 渲染 `dom` 所用到的命名空间。如果此值不存在,在默认的渲染方法中使用 `document.createElement` 方法来创建 `dom` 元素;如果此值存在,在默认的渲染方法中则会使用 `document.createElementNS` 方法来创建 `dom` 元素 78 | 79 | ##### children 80 | 81 | 子节点构建参数数组,文本节点和闭合节点无需设置此属性 82 | 83 | ## 零宽度空白文本节点 84 | 85 | 编辑器设定了一个非常特殊的文本节点,该节点内容没有长度,在页面表现上仅仅是一个光标的占位大小,我们称之为“零宽度空白文本节点”。这类节点在很多场景下有非常特殊的用途。 86 | 87 | 那么如何创建一个零宽度空白文本节点呢? 88 | 89 | 可以通过`KNode.createZeroWidthText`方法来创建: 90 | 91 | ```ts 92 | const zeroTextNode = KNode.createZeroWidthText(options) 93 | ``` 94 | 95 | 该方法的入参是一个对象,包含以下属性: 96 | 97 | ##### marks 98 | 99 | 同创建节点入参的 `marks` 属性,表示零宽度空白文本节点的标记 100 | 101 | ##### styles 102 | 103 | 同创建节点入参的 `styles` 属性,表示零宽度空白文本节点的样式 104 | 105 | ##### namespace 106 | 107 | 同创建节点入参的 `namespace` 属性,表示零宽度空白文本节点的命名空间 108 | 109 | ##### locked 110 | 111 | 同创建节点入参的 `locked` 属性,表示零宽度空白文本节点是否锁定 112 | 113 | ## 占位符节点 114 | 115 | 编辑器中标签 `
` 被视为占位符节点,该节点只能存在于块节点的子节点中,并且与其他节点无法共存,其通常仅在块节点无内容时作占位使用 116 | 编辑器提供了一个方法来快速创建一个占位符节点: 117 | 118 | ```ts 119 | const placeholderNode = KNode.createPlaceholder() 120 | ``` 121 | 122 | 当然你也可以使用 `create` 方法来创建,只是相对繁琐: 123 | 124 | ```ts 125 | const placeholderNode = KNode.create({ 126 | type: 'closed', 127 | tag: 'br' 128 | }) 129 | ``` 130 | 131 | ## 空节点 132 | 133 | - 对于文本节点,没有文本内容则视为空节点 134 | - 对于行内节点和块节点,没有子节点则视为空节点 135 | - 对于有子节点的节点,所有的子节点都是空节点的情况下,也视为空节点 136 | 137 | 编辑器内部会自动过滤所有的空节点,因为在 `kaitify` 中,空节点被视为无意义的节点,所以我们在创建节点时,需要避免创建空节点,以防止导致开发的功能未能按照预期执行 138 | -------------------------------------------------------------------------------- /src/extensions/code-block/hljs.ts: -------------------------------------------------------------------------------- 1 | //引入核心库 2 | import hljs from 'highlight.js/lib/core' 3 | //引入语言支持 4 | import plaintext from 'highlight.js/lib/languages/plaintext' 5 | import json from 'highlight.js/lib/languages/json' 6 | import javascript from 'highlight.js/lib/languages/javascript' 7 | import java from 'highlight.js/lib/languages/java' 8 | import typescript from 'highlight.js/lib/languages/typescript' 9 | import python from 'highlight.js/lib/languages/python' 10 | import php from 'highlight.js/lib/languages/php' 11 | import css from 'highlight.js/lib/languages/css' 12 | import less from 'highlight.js/lib/languages/less' 13 | import scss from 'highlight.js/lib/languages/scss' 14 | import html from 'highlight.js/lib/languages/xml' 15 | import markdown from 'highlight.js/lib/languages/markdown' 16 | import objectivec from 'highlight.js/lib/languages/objectivec' 17 | import swift from 'highlight.js/lib/languages/swift' 18 | import dart from 'highlight.js/lib/languages/dart' 19 | import nginx from 'highlight.js/lib/languages/nginx' 20 | import go from 'highlight.js/lib/languages/go' 21 | import http from 'highlight.js/lib/languages/http' 22 | import ruby from 'highlight.js/lib/languages/ruby' 23 | import c from 'highlight.js/lib/languages/c' 24 | import cpp from 'highlight.js/lib/languages/cpp' 25 | import csharp from 'highlight.js/lib/languages/csharp' 26 | import sql from 'highlight.js/lib/languages/sql' 27 | import shell from 'highlight.js/lib/languages/shell' 28 | import r from 'highlight.js/lib/languages/r' 29 | import kotlin from 'highlight.js/lib/languages/kotlin' 30 | import rust from 'highlight.js/lib/languages/rust' 31 | //引入css样式主题 32 | import './hljs.less' 33 | //import 'highlight.js/styles/github.css' 34 | //import 'highlight.js/styles/atom-one-light.css' 35 | //import 'highlight.js/styles/lightfair.css' 36 | //import 'highlight.js/styles/color-brewer.css' 37 | 38 | //注册语言 39 | hljs.registerLanguage('plaintext', plaintext) 40 | hljs.registerLanguage('json', json) 41 | hljs.registerLanguage('javascript', javascript) 42 | hljs.registerLanguage('java', java) 43 | hljs.registerLanguage('typescript', typescript) 44 | hljs.registerLanguage('python', python) 45 | hljs.registerLanguage('php', php) 46 | hljs.registerLanguage('css', css) 47 | hljs.registerLanguage('less', less) 48 | hljs.registerLanguage('scss', scss) 49 | hljs.registerLanguage('html', html) 50 | hljs.registerLanguage('markdown', markdown) 51 | hljs.registerLanguage('objectivec', objectivec) 52 | hljs.registerLanguage('swift', swift) 53 | hljs.registerLanguage('dart', dart) 54 | hljs.registerLanguage('nginx', nginx) 55 | hljs.registerLanguage('go', go) 56 | hljs.registerLanguage('http', http) 57 | hljs.registerLanguage('ruby', ruby) 58 | hljs.registerLanguage('c', c) 59 | hljs.registerLanguage('cpp', cpp) 60 | hljs.registerLanguage('csharp', csharp) 61 | hljs.registerLanguage('sql', sql) 62 | hljs.registerLanguage('shell', shell) 63 | hljs.registerLanguage('r', r) 64 | hljs.registerLanguage('kotlin', kotlin) 65 | hljs.registerLanguage('rust', rust) 66 | 67 | /** 68 | * 支持的语言列表 69 | */ 70 | export const HljsLanguages = ['plaintext', 'json', 'javascript', 'java', 'typescript', 'python', 'php', 'css', 'less', 'scss', 'html', 'markdown', 'objectivec', 'swift', 'dart', 'nginx', 'http', 'go', 'ruby', 'c', 'cpp', 'csharp', 'sql', 'shell', 'r', 'kotlin', 'rust'] as const 71 | 72 | //全局设置 73 | hljs.configure({ 74 | cssSelector: 'pre', 75 | classPrefix: 'kaitify-hljs-', 76 | languages: [...HljsLanguages], 77 | ignoreUnescapedHTML: true 78 | }) 79 | 80 | /** 81 | * 语言类型 82 | */ 83 | export type HljsLanguageType = (typeof HljsLanguages)[number] 84 | /** 85 | * 获取经过hljs处理的html元素 86 | */ 87 | export const getHljsHtml = function (code: string, language: string) { 88 | if (language) { 89 | return hljs.highlight(code, { 90 | language: language, 91 | ignoreIllegals: true 92 | }).value 93 | } 94 | return hljs.highlightAuto(code).value 95 | } 96 | -------------------------------------------------------------------------------- /docs/extensions/custom-extension.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 如何自己创建一个扩展? 3 | --- 4 | 5 | # 如何自己创建一个扩展? 6 | 7 | ## 创建扩展 8 | 9 | 通过 `Extension.create` 方法可以创建一个扩展实例 10 | 11 | ```ts 12 | import { Editor, Extension } from '@kaitify/core' 13 | const redExtension = Extension.create({ 14 | name: 'red', 15 | formatRules: [ 16 | ({ editor, node }) => { 17 | if (node.isBlock() && node.tag === editor.blockRenderTag) { 18 | if (node.hasStyles()) { 19 | node.styles.color = 'red' 20 | } else { 21 | node.styles = { 22 | color: 'red' 23 | } 24 | } 25 | } 26 | } 27 | ] 28 | }) 29 | const editor = await Editor.configure({ 30 | value: '


', 31 | extension: [redExtension] 32 | }) 33 | ``` 34 | 35 | ## 扩展构建参数 36 | 37 | `Extension.create`方法接收一个 `ExtensionCreateOptionType` 类型的入参,该参数包含以下属性: 38 | 39 | ##### name 40 | 41 | 扩展的名称,不同的扩展的 `name` 必须唯一 42 | 43 | ##### emptyRenderTags 44 | 45 | 自定义编辑器内定义需要置空的标签数组,编辑器针对需要置空的标签,会转为空节点,不会渲染到视图中,同编辑器构建参数 `emptyRenderTags` 46 | 47 | ##### extraKeepTags 48 | 49 | 自定义编辑器内额外保留的标签,如果某个标签的元素被编辑器转为了默认的行内节点,不符合预期行为,可以通过此参数配置保留该标签,同编辑器构建参数 `extraKeepTags` 50 | 51 | ##### formatRules 52 | 53 | 自定义节点数组格式化规则,同编辑器构建参数 `formatRules` 54 | 55 | ##### onDomParseNode 56 | 57 | 自定义 `dom` 转为非文本节点的后续处理,同编辑器构建参数 `onDomParseNode` 58 | 59 | ##### onSelectionUpdate 60 | 61 | 编辑器光标发生变化时触发,同编辑器构建参数 `onSelectionUpdate` 62 | 63 | ##### onInsertParagraph 64 | 65 | 换行时触发,参数为换行操作后光标所在的块节点,同编辑器构建参数 `onInsertParagraph` 66 | 67 | ##### onDeleteComplete 68 | 69 | 完成删除时触发,同编辑器构建参数 `onDeleteComplete` 70 | 71 | ##### onKeydown 72 | 73 | 光标在编辑器内时键盘按下触发,同编辑器构建参数 `onKeydown` 74 | 75 | ##### onKeyup 76 | 77 | 光标在编辑器内时键盘松开触发,同编辑器构建参数 `onKeyup` 78 | 79 | ##### onFocus 80 | 81 | 编辑器聚焦时触发,同编辑器构建参数 `onFocus` 82 | 83 | ##### onBlur 84 | 85 | 编辑器失焦时触发,同编辑器构建参数 `onBlur` 86 | 87 | ##### onPasteKeepMarks 88 | 89 | 粘贴 `html` 时,对于节点标记保留的自定义方法,同编辑器构建参数 `onPasteKeepMarks` 90 | 91 | ##### onPasteKeepStyles 92 | 93 | 粘贴 `html` 时,对于节点样式保留的自定义方法,同编辑器构建参数 `onPasteKeepStyles` 94 | 95 | ##### onBeforeUpdateView 96 | 97 | 视图更新前回调方法,同编辑器构建参数 `onBeforeUpdateView` 98 | 99 | ##### onAfterUpdateView 100 | 101 | 视图更新后回调方法,同编辑器构建参数 `onAfterUpdateView` 102 | 103 | ##### onDetachMentBlockFromParent 104 | 105 | 在删除和换行操作中块节点从其父节点中抽离出去成为与父节点同级的节点后触发,同编辑器构建参数 `onDetachMentBlockFromParent` 106 | 107 | ##### onBeforePatchNodeToFormat 108 | 109 | 编辑器 `updateView` 执行时,通过比对新旧节点数组获取需要格式化的节点,在这些节点被格式化前,触发此方法,同编辑器构建参数 `onBeforePatchNodeToFormat` 110 | 111 | ##### addCommands 112 | 113 | 自定义扩展命令,添加的命令可以通过 `editor.commands` 来调用 114 | 115 | ## 添加自定义命令 116 | 117 | 创建扩展时通过配置 `addCommands` 来添加自定义命令 118 | 119 | ```ts 120 | import { Editor, Extension } from '@kaitify/core' 121 | const setTextExtension = Extension.create({ 122 | name: 'setText', 123 | addCommands() { 124 | return { 125 | insertText: async (val: string) => { 126 | this.insertText(val) 127 | await this.updateView() 128 | } 129 | } 130 | } 131 | }) 132 | const editor = await Editor.configure({ 133 | value: '


', 134 | extension: [setTextExtension] 135 | }) 136 | ``` 137 | 138 | ```ts 139 | //通过命令去调用该扩展提供的insertText方法,会向编辑器插入文本并且更新视图 140 | editor.commands.insertText('hello') 141 | ``` 142 | -------------------------------------------------------------------------------- /src/extensions/indent/index.ts: -------------------------------------------------------------------------------- 1 | import { KNodeStylesType } from '@/model' 2 | import { getSelectionBlockNodes } from '@/model/config/function' 3 | import { isOnlyTab, isTabWithShift } from '@/tools' 4 | import { Extension } from '../Extension' 5 | 6 | declare module '../../model' { 7 | interface EditorCommandsType { 8 | /** 9 | * 是否可以使用缩进 10 | */ 11 | canUseIndent?: () => boolean 12 | /** 13 | * 增加缩进 14 | */ 15 | setIncreaseIndent?: () => Promise 16 | /** 17 | * 减少缩进 18 | */ 19 | setDecreaseIndent?: () => Promise 20 | } 21 | } 22 | 23 | export const IndentExtension = () => 24 | Extension.create({ 25 | name: 'indent', 26 | onPasteKeepStyles(node) { 27 | const styles: KNodeStylesType = {} 28 | if (node.isBlock() && node.hasStyles()) { 29 | if (node.styles!.hasOwnProperty('textIndent')) styles.textIndent = node.styles!.textIndent 30 | } 31 | return styles 32 | }, 33 | onKeydown(event) { 34 | if (isOnlyTab(event) && this.commands.canUseIndent?.()) { 35 | event.preventDefault() 36 | this.commands.setIncreaseIndent?.() 37 | } 38 | if (isTabWithShift(event) && this.commands.canUseIndent?.()) { 39 | event.preventDefault() 40 | this.commands.setDecreaseIndent?.() 41 | } 42 | }, 43 | addCommands() { 44 | const canUseIndent = () => { 45 | //光标范围内有代码块则不能使用缩进 46 | if (this.commands.hasCodeBlock?.()) { 47 | return false 48 | } 49 | //光标范围内可以生成内嵌列表则不能使用缩进 50 | if (!!this.commands.canCreateInnerList?.()) { 51 | return false 52 | } 53 | return true 54 | } 55 | 56 | const setIncreaseIndent = async () => { 57 | //起点和终点在一起 58 | if (this.selection.collapsed()) { 59 | const blockNode = this.selection.start!.node.getBlock() 60 | const styles: KNodeStylesType = blockNode.hasStyles() ? blockNode.styles! : {} 61 | let oldVal = 0 62 | if (styles.textIndent && typeof styles.textIndent == 'string' && styles.textIndent.endsWith('em')) { 63 | oldVal = parseFloat(styles.textIndent) 64 | } 65 | blockNode.styles = { 66 | ...styles, 67 | textIndent: `${oldVal + 2}em` 68 | } 69 | } 70 | //起点和终点不在一起 71 | else { 72 | const blockNodes = getSelectionBlockNodes.apply(this) 73 | blockNodes.forEach(item => { 74 | const styles: KNodeStylesType = item.hasStyles() ? item.styles! : {} 75 | let oldVal = 0 76 | if (styles.textIndent && typeof styles.textIndent == 'string' && styles.textIndent.endsWith('em')) { 77 | oldVal = parseFloat(styles.textIndent) 78 | } 79 | item.styles = { 80 | ...styles, 81 | textIndent: `${oldVal + 2}em` 82 | } 83 | }) 84 | } 85 | await this.updateView() 86 | } 87 | 88 | const setDecreaseIndent = async () => { 89 | //起点和终点在一起 90 | if (this.selection.collapsed()) { 91 | const blockNode = this.selection.start!.node.getBlock() 92 | const styles: KNodeStylesType = blockNode.hasStyles() ? blockNode.styles! : {} 93 | let oldVal = 0 94 | if (styles.textIndent && typeof styles.textIndent == 'string' && styles.textIndent.endsWith('em')) { 95 | oldVal = parseFloat(styles.textIndent) 96 | } 97 | blockNode.styles = { 98 | ...styles, 99 | textIndent: `${oldVal - 2 > 0 ? oldVal - 2 : 0}em` 100 | } 101 | } 102 | //起点和终点不在一起 103 | else { 104 | const blockNodes = getSelectionBlockNodes.apply(this) 105 | blockNodes.forEach(item => { 106 | const styles: KNodeStylesType = item.hasStyles() ? item.styles! : {} 107 | let oldVal = 0 108 | if (styles.textIndent && typeof styles.textIndent == 'string' && styles.textIndent.endsWith('em')) { 109 | oldVal = parseFloat(styles.textIndent) 110 | } 111 | item.styles = { 112 | ...styles, 113 | textIndent: `${oldVal - 2 > 0 ? oldVal - 2 : 0}em` 114 | } 115 | }) 116 | } 117 | await this.updateView() 118 | } 119 | 120 | return { 121 | canUseIndent, 122 | setDecreaseIndent, 123 | setIncreaseIndent 124 | } 125 | } 126 | }) 127 | -------------------------------------------------------------------------------- /docs/extensions/built-in/text.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: text 文本 3 | --- 4 | 5 | # text 文本 6 | 7 | 支持文本的渲染,包括设置样式和设置标记 8 | 9 | ## Commands 命令 10 | 11 | ##### isTextStyle() 12 | 13 | 判断光标所在文本是否具有某个样式 14 | 15 | - 类型 16 | 17 | ```ts 18 | isTextStyle(styleName: string, styleValue?: string | number): boolean 19 | ``` 20 | 21 | - 详细信息 22 | 23 | 第一个入参表示样式名称,第二个入参表示样式的值,该方法用来判断光标所在文本是否具有某个样式,且样式值是否符合,返回 `boolean` 值 24 | 25 | 如果第二个参数不设置,则会判断光标所在的文本是否具有指定的样式,而不关心这个样式的值是什么 26 | 27 | - 示例 28 | 29 | ```ts 30 | const isTextStyle = editor.commands.isTextStyle('fontSize', '16px') 31 | ``` 32 | 33 | ##### isTextMark() 34 | 35 | 判断光标所在文本是否具有某个标记 36 | 37 | - 类型 38 | 39 | ```ts 40 | isTextMark(markName: string, markValue?: string | number): boolean 41 | ``` 42 | 43 | - 详细信息 44 | 45 | 第一个入参表示标记名称,第二个入参表示标记的值,该方法用来判断光标所在文本是否具有某个标记,且标记值是否符合,返回 `boolean` 值 46 | 47 | 如果第二个参数不设置,则会判断光标所在的文本是否具有指定的标记,而不关心这个标记的值是什么 48 | 49 | - 示例 50 | 51 | ```ts 52 | const isTextMark = editor.commands.isTextMark('data-span', '1') 53 | ``` 54 | 55 | ##### setTextStyle() 56 | 57 | 设置光标范围内的文本的样式 58 | 59 | - 类型 60 | 61 | ```ts 62 | setTextStyle(styles: KNodeStylesType, updateView?: boolean): Promise 63 | ``` 64 | 65 | - 详细信息 66 | 67 | 第一个入参表示设置的样式集合,第二个参数表示是否更新视图,该方法会对光标范围内的文本设置样式 68 | 69 | 如果第二个参数为 `false`,则在设置完毕后不会更新视图和光标的渲染,需要你主动 `updateView`,如果为 `true` 则会不需要主动 `updateView`,该参数默认为 `true` 70 | 71 | - 示例 72 | 73 | ```ts 74 | await editor.commands.setTextStyle({ 75 | fontSize: '20px' 76 | }) 77 | ``` 78 | 79 | ##### setTextMark() 80 | 81 | 设置光标范围内的文本的标记 82 | 83 | - 类型 84 | 85 | ```ts 86 | setTextMark(marks: KNodeMarksType, updateView?: boolean): Promise 87 | ``` 88 | 89 | - 详细信息 90 | 91 | 第一个入参表示设置的标记集合,第二个参数表示是否更新视图,该方法会对光标范围内的文本设置标记 92 | 93 | 如果第二个参数为 `false`,则在设置完毕后不会更新视图和光标的渲染,需要你主动 `updateView`,如果为 `true` 则会不需要主动 `updateView`,该参数默认为 `true` 94 | 95 | - 示例 96 | 97 | ```ts 98 | await editor.commands.setTextMark({ 99 | 'data-span': '1' 100 | }) 101 | ``` 102 | 103 | ##### removeTextStyle() 104 | 105 | 取消光标范围内的文本的样式 106 | 107 | - 类型 108 | 109 | ```ts 110 | removeTextStyle(styleNames?: string[], updateView?: boolean): Promise 111 | ``` 112 | 113 | - 详细信息 114 | 115 | 第一个入参表示要取消的样式名称数组,第二个参数表示是否更新视图,该方法会对光标范围内的文本取消指定的样式 116 | 117 | 如果第二个参数为 `false`,则在设置完毕后不会更新视图和光标的渲染,需要你主动 `updateView`,如果为 `true` 则会不需要主动 `updateView`,该参数默认为 `true` 118 | 119 | - 示例 120 | 121 | ```ts 122 | await editor.commands.removeTextStyle(['fontSize']) 123 | ``` 124 | 125 | ##### removeTextMark() 126 | 127 | 取消光标范围内的文本的标记 128 | 129 | - 类型 130 | 131 | ```ts 132 | removeTextMark(markNames?: string[], updateView?: boolean): Promise 133 | ``` 134 | 135 | - 详细信息 136 | 137 | 第一个入参表示要取消的标记名称数组,第二个参数表示是否更新视图,该方法会对光标范围内的文本取消指定的标记 138 | 139 | 如果第二个参数为 `false`,则在设置完毕后不会更新视图和光标的渲染,需要你主动 `updateView`,如果为 `true` 则会不需要主动 `updateView`,该参数默认为 `true` 140 | 141 | - 示例 142 | 143 | ```ts 144 | await editor.commands.removeTextMark(['data-span']) 145 | ``` 146 | 147 | ##### clearFormat() 148 | 149 | 清除格式 150 | 151 | - 类型 152 | 153 | ```ts 154 | clearFormat(): Promise 155 | ``` 156 | 157 | - 详细信息 158 | 159 | 该方法会将光标范围内的文本的所有样式和标记都清除,在设置完毕后会更新视图和光标的渲染,所以调用该命令你无需主动 `updateView` 160 | 161 | - 示例 162 | 163 | ```ts 164 | await editor.commands.clearFormat() 165 | ``` 166 | 167 | ## 代码示例 168 | 169 |
170 | 176 | 177 |
178 |
179 | 180 | 208 | -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | import { data as DapData, element as DapElement } from 'dap-util' 2 | import { KNodeMarksType, KNodeStylesType } from '@/model' 3 | import { NODE_MARK } from '@/view' 4 | 5 | /** 6 | * 用于KNode生成唯一的key 7 | */ 8 | export const createUniqueKey = (): number => { 9 | let key = DapData.get(window, 'kaitify-node-key') || 0 10 | key++ 11 | DapData.set(window, 'kaitify-node-key', key) 12 | return key 13 | } 14 | 15 | /** 16 | * 用于编辑器生成唯一的guid 17 | */ 18 | export const createGuid = function (): number { 19 | //获取唯一id 20 | let key = DapData.get(window, 'kaitify-guid') || 0 21 | key++ 22 | DapData.set(window, 'kaitify-guid', key) 23 | return key 24 | } 25 | 26 | /** 27 | * 判断字符串是否零宽度空白字符 28 | */ 29 | export const isZeroWidthText = (val: string) => { 30 | return /^[\u200B]+$/g.test(val) 31 | } 32 | 33 | /** 34 | * 获取一个零宽度空白字符 35 | */ 36 | export const getZeroWidthText = () => { 37 | return '\u200B' 38 | } 39 | 40 | /** 41 | * 驼峰转中划线 42 | */ 43 | export const camelToKebab = (val: string) => { 44 | return val.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() 45 | } 46 | 47 | /** 48 | * 中划线转驼峰 49 | */ 50 | export const kebabToCamel = (val: string) => { 51 | return val.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) 52 | } 53 | 54 | /** 55 | * 获取dom元素的属性集合 56 | */ 57 | export const getDomAttributes = (dom: HTMLElement) => { 58 | const attributes = Array.from(dom.attributes) 59 | const length = attributes.length 60 | const regExp = new RegExp(`(^on)|(^style$)|(^${NODE_MARK}$)`, 'g') 61 | const result: KNodeMarksType = {} 62 | for (let i = 0; i < length; i++) { 63 | const { nodeName, nodeValue } = attributes[i] 64 | //匹配事件、样式和face外的属性 65 | if (!regExp.test(nodeName)) { 66 | result[nodeName] = nodeValue ?? '' 67 | } 68 | } 69 | return result 70 | } 71 | 72 | /** 73 | * 获取dom元素的样式集合 74 | */ 75 | export const getDomStyles = (dom: HTMLElement) => { 76 | let o: KNodeStylesType = {} 77 | const styles = dom.getAttribute('style') 78 | if (styles) { 79 | let i = 0 80 | let start = 0 81 | let splitStyles = [] 82 | while (i < styles.length) { 83 | if (styles[i] == ';' && styles.substring(i + 1, i + 8) != 'base64,') { 84 | splitStyles.push(styles.substring(start, i)) 85 | start = i + 1 86 | } 87 | //到最后了,并且最后没有分号 88 | if (i == styles.length - 1 && start < i + 1) { 89 | splitStyles.push(styles.substring(start, i + 1)) 90 | } 91 | i++ 92 | } 93 | splitStyles.forEach(style => { 94 | const index = style.indexOf(':') 95 | const property = style.substring(0, index).trim() 96 | const value = style.substring(index + 1).trim() 97 | o[kebabToCamel(property)] = value 98 | }) 99 | } 100 | return o 101 | } 102 | 103 | /** 104 | * 初始化编辑器dom 105 | */ 106 | export const initEditorDom = (dom: HTMLElement | string) => { 107 | //判断是否字符串,如果是字符串按照选择器来寻找元素 108 | if (typeof dom == 'string' && dom) { 109 | dom = document.body.querySelector(dom) as HTMLElement 110 | } 111 | dom = dom as HTMLElement 112 | //如何node不是元素则抛出异常 113 | if (!DapElement.isElement(dom)) { 114 | throw new Error('You must specify a dom container to initialize the editor') 115 | } 116 | //如果已经初始化过了则抛出异常 117 | if (DapData.get(dom, 'kaitify-init')) { 118 | throw new Error('The element node has been initialized to the editor') 119 | } 120 | //添加初始化的标记 121 | DapData.set(dom, 'kaitify-init', true) 122 | return dom 123 | } 124 | 125 | /** 126 | * 判断某个dom是否包含另一个dom 127 | */ 128 | export const isContains = (parent: Node, child: Node) => { 129 | if (child.nodeType == 3) { 130 | return DapElement.isContains(parent as HTMLElement, child.parentNode as HTMLElement) 131 | } 132 | return DapElement.isContains(parent as HTMLElement, child as HTMLElement) 133 | } 134 | 135 | /** 136 | * 延迟指定时间 137 | */ 138 | export const delay = (num: number | undefined = 0) => { 139 | return new Promise(resolve => { 140 | setTimeout(() => { 141 | resolve() 142 | }, num) 143 | }) 144 | } 145 | 146 | /** 147 | * 删除对象的某个属性 148 | */ 149 | export const deleteProperty = (val: any, propertyName: string) => { 150 | const newObj: any = {} 151 | Object.keys(val).forEach(key => { 152 | if (key != propertyName) { 153 | newObj[key] = val[key] 154 | } 155 | }) 156 | return newObj as T 157 | } 158 | 159 | /** 160 | * 键盘Tab是否按下 161 | */ 162 | export const isOnlyTab = (e: KeyboardEvent) => { 163 | return e.key.toLocaleLowerCase() == 'tab' && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey 164 | } 165 | 166 | /** 167 | * 键盘Tab和shift是否一起按下 168 | */ 169 | export const isTabWithShift = (e: KeyboardEvent) => { 170 | return e.key.toLocaleLowerCase() == 'tab' && e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey 171 | } 172 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | lastUpdated: false 3 | title: 更新日志 4 | --- 5 | 6 | # 更新日志 7 | 8 | ## v0.0.2-beta.8 9 | 10 | - 优化编辑器的 `destroy` 方法,修复了原来销毁后再创建出现问题的 bug 11 | - 编辑器的主题颜色全部挂载在容器上,而不是 `document` 上,另外编辑器的主题颜色不再受全局影响 12 | - 编辑器的深色模式样式设置从属性 `[kaitify-dark]` 改为样式类 `.kaitify-dark` 13 | - 编辑器构建入参新增 `onCreate` 和 `onCreated` 参数 14 | 15 | ## v0.0.2-beta.2 16 | 17 | - 优化编辑器对样式的判断,如果是空字符串则默认无样式 18 | - 优化 `List` 内置扩展的样式 19 | 20 | ## v0.0.1 21 | 22 | - `Editor` 所有的构建参数中的函数事件都以 `on` 开头 23 | - 第一个正式版本发布 24 | 25 | ## v0.0.1-beta.37 26 | 27 | - 修复了当段落进行缩进时,行内代码、图片、视频、数学公式等样式为 `inline-block` 的元素也进行了缩进的问题 28 | 29 | ## v0.0.1-beta.36 30 | 31 | - 优化了 `Attachment` 的样式 32 | - 优化了 `Blockquote` 的样式 33 | - 优化了 `Code` 的样式 34 | - 优化了 `CodeBlock` 的样式 35 | - 优化了 `Link` 的样式 36 | - 优化了 `Math` 的样式 37 | - 优化了 `Table` 的样式 38 | - 引用节点优化:在创建时如果子节点无块节点,会在引用内创建一个段落节点 39 | 40 | ## v0.0.1-beta.35 41 | 42 | - `Attachment` `Math` 和 `Horizontal`扩展的表现优化 43 | 44 | ## v0.0.1-beta.34 45 | 46 | - `Video` `Image` `Table`等扩展设定可拖拽的边缘大小 47 | - 代码优化 48 | 49 | ## v0.0.1-beta.33 50 | 51 | - `getContent` 方法新增两个入参,分别表示是否排除 `\n` 换行符、是否排除零宽度空白字符 52 | - 编辑器格式化处理 `\n` 后面加上零宽度空白字符时,优化了光标的位置更新逻辑 53 | - 代码块换行逻辑优化:代码块内最后一行如果没有内容,此时再进行换行会在整个代码块后进行换行(之前有这个功能,但是在空格逻辑优化后功能需要优化和修改) 54 | - 修复了在代码块内执行撤销偶然导致光标丢失的 bug 55 | 56 | ## v0.0.1-beta.32 57 | 58 | - 优化 `getHTML` 方法:对于返回的 `style` 标签内的样式进行了过滤,现在只会返回与编辑器相关的样式;同时新增了 `filterCssText` 参数,用于自定义需要保留的样式 59 | - 优化行内代码的样式 60 | - 修复中文输入没有加入历史记录的 bug 61 | - 重新定义了空格的渲染逻辑,并且针对 `\n` 换行符进行了格式化处理 62 | 63 | ## v0.0.1-beta.30 64 | 65 | - 代码细节优化 66 | 67 | ## v0.0.1-beta.28 68 | 69 | - `Editor` 新增实例方法 `setDomObserve`,用于监听编辑器内的非法 `dom` 操作 70 | - `Editor` 新增实例方法 `removeDomObserve`,用于取消监听编辑器内的非法 `dom` 操作 71 | - 修复了 `Image` 和 `Video` 扩展内的图片和视频无法拖拽改变大小的 bug 72 | 73 | ## v0.0.1-beta.27 74 | 75 | - 优化编辑器对非法 `dom` 插入/删除/更新的处理,以适配 `Grammarly` 等第三方插件对 `dom` 的修改 76 | - 其他代码优化 77 | 78 | ## v0.0.1-beta.26 79 | 80 | - 优化 `unicode` 字符删除时的操作逻辑,例如 `emoji` 表情包删除的优化 81 | - 新增 `isSelectionInView` 函数:用以判断光标是否完全在可视范围内 82 | 83 | ## v0.0.1-beta.24 84 | 85 | - 待办扩展优化:勾选的动画效果优化和样式优化(现在复选框始终只会显示在待办的顶部,而不是和之前一样居中) 86 | - 优化不可编辑节点的整体逻辑,在删除、换行、插入等操作时都被视为整体进行处理 87 | - 编辑可编辑下单击附件和数学公式会选中附件和数学公式 88 | - 代码块换行逻辑优化:代码块内最后一行如果没有内容,此时再进行换行会在整个代码块后进行换行 89 | - 表格换行逻辑优化:表格最后一行的最后一列内,如果光标所在的块节点是段落且前一个块节点也是段落,则再次换行时会在整个表格后换行 90 | - 表格格式化规则新增对 `td` 内容的处理,如果 `td` 内没有块节点,会默认使用段落进行包裹 91 | - 列表样式优化:解决了在字体较大时列标显示不全的问题 92 | 93 | ## v0.0.1-beta.22 94 | 95 | - 修复了一个中文输入的 bug 96 | - `Editor` 新增实例方法 `isEmpty`,用以判断编辑器内容是否为空 97 | - 优化了 `placeholder` 占位内容的显示机制,在输入中文时隐藏 `placeholder` 98 | 99 | ## v0.0.1-beta.20 100 | 101 | - 代码优化,解决了 `Grammarly` 插入的问题 102 | - `indent` 扩展新增 `canUseIndent` 指令,用于判断是否可以使用缩进功能 103 | - `indent` 扩展新增键盘事件:`tab` 键按下增加缩进;`shift+tab` 键按下减少缩进 104 | 105 | ## v0.0.1-beta.19 106 | 107 | - 对 `KNode` 的实例方法 `clone` `fullClone` `getFocusNodes` 进行了优化 108 | - 对 `KNode` 的类方法 `flat` `searchByKey` 进行了优化 109 | - `KNode` 的实例方法 `firstTextClosedInNode` 更名为 `firstInTargetNode` 110 | - `KNode` 的实例方法 `lastTextClosedInNode` 更名为 `lastInTargetNode` 111 | - `Editor` 的实例方法 `getLastSelectionNodeInChildren` 更名为 `getLastSelectionNode`,并进行了性能的优化 112 | - `Editor` 的实例方法 `getFirstSelectionNodeInChildren` 更名为 `getFirstSelectionNode`,并进行了性能的优化 113 | - 对 `Editor` 的实例方法 `getFocusNodesBySelection` 进行了优化 114 | - `Editor` 的实例方法 `isSelectionInNode` 更名为 `isSelectionInTargetNode` 115 | - 内部的一些方法和逻辑进行了优化 116 | - 修复了格式化过程中将非块级节点转为块节点时的逻辑处理出现的一些问题,这个问题曾导致内存溢出 117 | 118 | ## v0.0.1-beta.18 119 | 120 | - 新增 `getHTML` 函数获取编辑器 `html` 内容 121 | 122 | ## v0.0.1-beta.17 123 | 124 | - 代码块扩展新增键盘事件:在代码块内按下 `Tab` 键会插入 2 个空格 125 | - 部分代码优化 126 | 127 | ## v0.0.1-beta.16 128 | 129 | - 列表优化:现在会给默认的列表节点设置 `listStyleType` 样式 130 | - 列表扩展 `unsetList` 方法问题修复 131 | - 列表扩展新增 `canCreateInnerList` 和 `createInnertList` 命令 132 | - 列表扩展新增键盘事件:在可以生成内嵌列表时,按下 `Tab` 键会执行 `createInnertList` 命令 133 | 134 | ## v0.0.1-beta.13 135 | 136 | - 优化列表渲染,序标改为外侧,设置左侧内边距 137 | 138 | ## v0.0.1-beta.12 139 | 140 | - kaitify 的第一个发布版本 141 | --------------------------------------------------------------------------------