feat: 新增 WYSIWYG 编辑器功能和相关工具栏事件处理

This commit is contained in:
2026-04-10 16:18:01 +08:00
parent 40b77471b5
commit f20b7aa61c
128 changed files with 16048 additions and 84 deletions

2
.gitignore vendored
View File

@@ -12,3 +12,5 @@ core/judged/judgehub
core/judge_client/judge_client
core/judge_client/judge_client.http
node_modules
dist

View File

@@ -6,9 +6,9 @@
selector: 'textarea.kindeditor',
uploadUrl: '../kindeditor/php/upload_json.php',
accept: '*/*',
mode: 'sv',
mode: 'wysiwyg',
preview: {
mode: 'both',
mode: 'editor',
delay: 0,
actions: [],
markdown: {

View File

@@ -6,10 +6,10 @@
selector: 'textarea.kindeditor',
uploadUrl: '../kindeditor/php/upload_json.php',
accept: '*/*',
mode: 'sv',
mode: 'wysiwyg',
ctrlEnterFormName: 'example',
preview: {
mode: 'both',
mode: 'editor',
delay: 0,
actions: [],
markdown: {

View File

@@ -6,10 +6,25 @@
return;
}
var DEFAULT_CDN = global.HUSTOJ_VDITOR_CDN || 'https://cdn.jsdelivr.net/npm/vditor@3.10.6';
var currentScriptRef = documentRef.currentScript || (function () {
var scripts = documentRef.getElementsByTagName('script');
return scripts.length > 0 ? scripts[scripts.length - 1] : null;
})();
var DEFAULT_CDN = global.HUSTOJ_VDITOR_CDN || getLocalCdnBase();
global.HUSTOJ_VDITOR_CDN = DEFAULT_CDN;
var assetsPromise = null;
var styleInjected = false;
function getLocalCdnBase() {
var scriptSrc = currentScriptRef && currentScriptRef.src;
if (!scriptSrc) {
return 'include/vditor';
}
return scriptSrc.replace(/\/vditor-adapter\.js(?:[?#].*)?$/, '/vditor');
}
function injectAdapterStyle() {
var style;
@@ -85,6 +100,7 @@
function ensureAssets(options) {
var cdn = (options && options.cdn) || DEFAULT_CDN;
global.HUSTOJ_VDITOR_CDN = cdn;
if (global.Vditor) {
injectAdapterStyle();
return Promise.resolve(global.Vditor);
@@ -95,7 +111,7 @@
ensureStyleTag(cdn + '/dist/index.css');
injectAdapterStyle();
assetsPromise = ensureScript(cdn + '/dist/index.min.js').then(function () {
assetsPromise = ensureScript(cdn + '/dist/index.js').then(function () {
return global.Vditor;
});
return assetsPromise;
@@ -447,10 +463,6 @@
'|',
'undo',
'redo',
'|',
'edit-mode',
'both',
'preview',
'fullscreen',
'outline'
];

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 B3log 开源, b3log.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,680 @@
<p align="center">
<img alt="Vditor" src="https://b3log.org/images/brand/vditor-128.png" />
<br>
易于使用的 Markdown 编辑器,为适配不同的应用场景而生
<br><br>
<a title="MIT" target="_blank" href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-orange.svg?style=flat-square"></a>
<a title="npm bundle size" target="_blank" href="https://www.npmjs.com/package/vditor"><img alt="npm bundle size" src="https://img.shields.io/bundlephobia/minzip/vditor?style=flat-square&color=blueviolet"></a>
<a title="Version" target="_blank" href="https://www.npmjs.com/package/vditor"><img src="https://img.shields.io/npm/v/vditor.svg?style=flat-square"></a><br>
<a title="Downloads" target="_blank" href="https://www.npmjs.com/package/vditor"><img src="https://img.shields.io/npm/dt/vditor.svg?style=flat-square&color=97ca00"></a>
<a title="jsdelivr" target="_blank" href="https://www.jsdelivr.com/package/npm/vditor"><img src="https://data.jsdelivr.com/v1/package/npm/vditor/badge"/></a>
<a title="Hits" target="_blank" href="https://github.com/88250/hits"><img src="https://hits.b3log.org/Vanessa219/vditor.svg"></a> <br><br>
<a title="GitHub Watchers" target="_blank" href="https://github.com/Vanessa219/vditor/watchers"><img src="https://img.shields.io/github/watchers/Vanessa219/vditor.svg?label=Watchers&style=social"></a>
<a title="GitHub Stars" target="_blank" href="https://github.com/Vanessa219/vditor/stargazers"><img src="https://img.shields.io/github/stars/Vanessa219/vditor.svg?label=Stars&style=social"></a>
<a title="GitHub Forks" target="_blank" href="https://github.com/Vanessa219/vditor/network/members"><img src="https://img.shields.io/github/forks/Vanessa219/vditor.svg?label=Forks&style=social"></a>
<a title="Author GitHub Followers" target="_blank" href="https://github.com/vanessa219"><img src="https://img.shields.io/github/followers/vanessa219.svg?label=Followers&style=social"></a>
</p>
<p align="center">
<a href="https://github.com/Vanessa219/vditor/blob/master/README_en_US.md">English</a> &nbsp;|&nbsp; <a href="https://b3log.org/vditor/demo/index.html">Demo</a>
</p>
## 💡 简介
[Vditor](https://b3log.org/vditor) 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora和分屏预览模式。它使用 TypeScript 实现,支持原生 JavaScript 以及 Vue、React、Angular 和 Svelte 等框架。
欢迎到 [Vditor 官方讨论区](https://ld246.com/tag/vditor)了解更多。同时也欢迎关注 B3log 开源社区微信公众号 `B3log开源`
![b3logos.jpg](https://b3logfile.com/file/2020/08/b3logos-032af045.jpg)
## 🗺️ 背景
随着 Markdown 排版方式的普及,越来越多的应用开始集成 Markdown 编辑器。目前主流可集成的 Markdown 编辑器现状如下:
* 有的仅支持分屏预览,即编辑区和预览区分离
* 有的同时支持所见即所得和分屏预览,但所见即所得模式下不能完整支持 Markdown 语法排版
* 几乎没有类似 Typora 的即时渲染
而这三点恰好对应了三种应用场景:
* 分屏预览:适配传统的 Markdown 使用场景,适合大屏下编辑排版
* 所见即所得:对不熟悉 Markdown 的用户友好,熟悉 Markdown 的用户也可以无缝使用
* 即时渲染:理论上这是最为优雅的 Markdown 编辑方式,让熟悉 Markdown 的用户能够更专注于内容创作
所以,一个能够**适配应用场景**的 Markdown 编辑器至关重要,它需要考虑到:
* 传统 Markdown 用户的使用场景,提供分屏预览
* 富文本编辑用户的使用场景,提供所见即所得
* 高阶 Markdown 用户的使用场景,提供即时渲染
Vditor 在这些方面做了努力,希望能为现代化的通用 Markdown 编辑领域做出一些贡献。
## ✨ 特性
* 支持三种编辑模式所见即所得wysiwyg、即时渲染ir、分屏预览sv
* 支持大纲、数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、[多媒体](https://ld246.com/article/1589813914768)、语音阅读、标题锚点、代码高亮及复制、graphviz 渲染、[plantuml](https://plantuml.com)UML图
* 导出、图片懒加载、任务列表、多平台预览、多主题切换、复制到微信公众号/知乎功能
* 实现 CommonMark 和 GFM 规范,可对 Markdown 进行格式化和语法树查看,并支持[10+项](https://ld246.com/article/1549638745630#options-preview-markdown)配置
* 工具栏包含 36+ 项操作,除支持扩展外还可对每一项中的[快捷键](https://ld246.com/article/1582778815353)、提示、提示位置、图标、点击事件、类名、子工具栏进行自定义
* 表情/at/话题等自动补全扩展
* 可使用拖拽、剪切板粘贴上传,显示实时上传进度,支持 CORS 跨域上传
* 实时保存内容,防止意外丢失
* 录音支持,用户可直接发布语音
* 粘贴 HTML 自动转换为 Markdown如粘贴中包含外链图片可通过指定接口上传到服务器
* 支持主窗口大小拖拽、字符计数
* 多主题支持,内置黑白绿三套主题
* 多语言支持,内置中、英、韩文本地化
* 支持主流浏览器,对移动端友好
![editor.png](https://b3logfile.com/file/2020/07/editor-b304aa97.png)
![preview.png](https://b3logfile.com/file/2020/05/preview-80846f66.png)
## 🔮 编辑模式
### 所见即所得WYSIWYG
*所见即所得*模式对不熟悉 Markdown 的用户较为友好,熟悉 Markdown 的话也可以无缝使用。
![vditor-wysiwyg](https://b3logfile.com/file/2020/07/wysiwyg-4f216b9b.gif)
### 即时渲染IR
*即时渲染*模式对熟悉 Typora 的用户应该不会感到陌生,理论上这是最优雅的 Markdown 编辑方式。
![vditor-ir](https://b3logfile.com/file/2020/07/ir-67cd956c.gif)
### 分屏预览SV
传统的*分屏预览*模式适合大屏下的 Markdown 编辑。
![vditor-sv](https://b3logfile.com/file/2020/07/sv-595dcb28.gif)
## 🍱 语法支持
* 所有 CommonMark 语法分隔线、ATX 标题、Setext 标题、缩进代码块、围栏代码块、HTML 块、链接引用定义、段落、块引用、列表、反斜杠转义、HTML 实体、行级代码、强调、加粗、链接、图片、行级 HTML、硬换行、软换行和纯文本。
* 所有 GFM 语法表格、任务列表项、删除线、自动链接、XSS 过滤
* 常用 Markdown 扩展语法脚注、ToC、自定义标题 ID
* 图表语法
* 流程图、时序图、甘特图,通过 Mermaid 支持
* Graphviz
* 折线图、饼图、脑图等,通过 ECharts 支持
* 五线谱:通过 abc.js 支持
* 数学公式:数学公式块、行级数学公式,通过 MathJax 和 KaTeX 支持
* YAML Front Matter
* 中文语境优化
* 中西文之间插入空格
* 术语拼写修正
* 中文后跟英文逗号句号等标点替换为中文对应标点
以上大部分特性可以通过开关配置是否启用,开发者可根据自己的应用场景选择搭配。
## 🗃 案例
* [Sym](https://github.com/88250/symphony) 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台
* [Solo](https://github.com/88250/solo) & [Pipe](https://github.com/88250/pipe) B3log 分布式社区的博客端节点,欢迎加入下一代社区网络
* [Tditor](https://tditor.com) 基于React、Vditor、Springboot 一款打造极致文字创作体验的在线Markdown编辑平台
* [Arya](https://github.com/nicejade/markdown-online-editor) 基于 Vue、Vditor所构建的在线 Markdown 编辑器
* [更多案例](https://github.com/Vanessa219/vditor/network/dependents?package_id=UGFja2FnZS0zMTY2Mzg4MzE%3D)
## 🛠️ 使用文档
### CommonJS
* 安装依赖
```shell
npm install vditor --save
```
* 在代码中引入并初始化对象,可参考 [index.js](https://github.com/Vanessa219/vditor/blob/master/demo/index.js)
```ts
import Vditor from 'vditor'
import "~vditor/src/assets/less/index"
const vditor = new Vditor(id, {options...})
```
### HTML script
* 在 HTML 中插入 CSS 和 JavaScript可参考 [demo](https://b3log.org/vditor/demo/index.html)
```html
<!-- ⚠️生产环境请指定版本号,如 https://unpkg.com/vditor@x.x.x/dist... -->
<link rel="stylesheet" href="https://unpkg.com/vditor/dist/index.css" />
<script src="https://unpkg.com/vditor/dist/index.min.js"></script>
```
### 示例代码
* [官方示例](https://b3log.org/vditor/demo/index.html) / [示例源码](https://github.com/Vanessa219/b3log-index/tree/master/src/vditor)
* [CommonJS Editor](https://github.com/Vanessa219/vditor/blob/master/demo/index.js)
* [CommonJS Render](https://github.com/Vanessa219/vditor/blob/master/demo/render.js)
* [在Svelte中使用](https://github.com/HerbertHe/svelte-vditor-demo)
### 主题
#### 编辑器主题
编辑器所展现的外观。内置classicdark 2 套主题。
* 编辑器初始化时可通过 `options.theme` 设置内置主题
* 初始化完成后可通过 `setTheme` 更新编辑器主题
* 可通过修改 [index.less](https://github.com/Vanessa219/vditor/blob/master/src/assets/less/index.less) 中的变量对主题颜色进行定制
* 可参考现有结构和类名在原有基础上进行修改
#### 内容主题
Markdown 输出的 HTML 所展现的外观。内置 ant-design, lightdarkwechat 4 套主题。支持内容主题扩展接口。
* 需在显示元素上添加 `class="vditor-reset"`
* 编辑器初始化时可通过 `options.preview.theme` 设置内置或自己开发的主题列表
* 内容渲染初始化时可通过 `IPreviewOptions.theme` 设置内置或自己开发的主题
* 初始化完成后可通过 `setTheme``setContentTheme` 更新内容主题
#### 代码主题
代码块所展现的外观。内置 github 等 36 套主题。
* 编辑器初始化时可通过 `options.preview.hljs` 对代码块样式、行号、是否启用进行设置
* 内容渲染初始化时可通过 `IPreviewOptions.hljs` 对代码块样式、行号、是否启用进行设置
* 初始化完成后可通过 `setTheme``setCodeTheme` 更新代码主题
### API
#### id
可填入元素 `id` 或元素自身 `HTMLElement`
⚠️:当填入元素自身的 `HTMLElement` 时需设置 `options.cache.id` 或将 `options.cache.enable` 设置为 `false`
#### options
| | 说明 | 默认值 |
| - | - | - |
| i18n | 多语言,参见 ITips | - |
| undoDelay | 历史记录间隔 | - |
| after | 编辑器异步渲染完成后的回调方法 | - |
| height | 编辑器总高度 | 'auto' |
| minHeight | 编辑区域最小高度 | - |
| width | 编辑器总宽度,支持 % | 'auto' |
| placeholder | 输入区域为空时的提示 | '' |
| lang | 语言种类en_US, fr_FR, pt_BR, ja_JP, ko_KR, ru_RU, sv_SE, zh_CN, zh_TW | 'zh_CN' |
| input(value: string) | 输入后触发 | - |
| focus(value: string) | 聚焦后触发 | - |
| blur(value: string) | 失焦后触发 | - |
| keydown(event: KeyboardEvent) | 按下后触发 | - |
| esc(value: string) | <kbd>esc</kbd> 按下后触发 | - |
| ctrlEnter(value: string) | <kbd>⌘/ctrl+enter</kbd> 按下后触发 | - |
| select(value: string) | 编辑器中选中文字后触发 | - |
| tab | <kbd>tab</kbd> 键操作字符串,支持 `\t` 及任意字符串 | - |
| typewriterMode | 是否启用打字机模式 | false |
| cdn | 配置自建 CDN 地址 | `https://unpkg.com/vditor@${VDITOR_VERSION}` |
| mode | 可选模式sv, ir, wysiwyg | 'ir' |
| debugger | 是否显示日志 | false |
| value | 编辑器初始化值 | '' |
| theme | 主题classic, dark | 'classic' |
| icon | 图标风格ant, material | 'ant' |
| customRenders: {language: string, render: (element: HTMLElement, vditor: IVditor) => void}[] | 自定义渲染器 | [] |
#### options.toolbar
* 工具栏,可使用 name 进行简写: `toolbar: ['emoji', 'br', 'bold', '|', 'line']` 。默认值参见 [src/ts/util/Options.ts](https://github.com/Vanessa219/vditor/blob/master/src/ts/util/Options.ts)
* name 可枚举为: `emoji` , `headings` , `bold` , `italic` , `strike` , `|` , `line` , `quote` , `list` , `ordered-list` , `check` ,`outdent` ,`indent` , `code` , `inline-code` , `insert-after` , `insert-before` ,`undo` , `redo` , `upload` , `link` , `table` , `record` , `edit-mode` , `both` , `preview` , `fullscreen` , `outline` , `code-theme` , `content-theme` , `export`, `devtools` , `info` , `help` , `br`
*`name` 不在枚举中时,可以添加自定义按钮,格式如下:
```js
new Vditor('vditor', {
toolbar: [
{
hotkey: '⇧⌘S',
name: 'sponsor',
tipPosition: 's',
tip: '成为赞助者',
className: 'right',
icon: '<svg t="1589994565028" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2808" width="32" height="32"><path d="M506.6 423.6m-29.8 0a29.8 29.8 0 1 0 59.6 0 29.8 29.8 0 1 0-59.6 0Z" fill="#0F0F0F" p-id="2809"></path><path d="M717.8 114.5c-83.5 0-158.4 65.4-211.2 122-52.7-56.6-127.7-122-211.2-122-159.5 0-273.9 129.3-273.9 288.9C21.5 562.9 429.3 913 506.6 913s485.1-350.1 485.1-509.7c0.1-159.5-114.4-288.8-273.9-288.8z" fill="#FAFCFB" p-id="2810"></path><path d="M506.6 926c-22 0-61-20.1-116-59.6-51.5-37-109.9-86.4-164.6-139-65.4-63-217.5-220.6-217.5-324 0-81.4 28.6-157.1 80.6-213.1 53.2-57.2 126.4-88.8 206.3-88.8 40 0 81.8 14.1 124.2 41.9 28.1 18.4 56.6 42.8 86.9 74.2 30.3-31.5 58.9-55.8 86.9-74.2 42.5-27.8 84.3-41.9 124.2-41.9 79.9 0 153.2 31.5 206.3 88.8 52 56 80.6 131.7 80.6 213.1 0 103.4-152.1 261-217.5 324-54.6 52.6-113.1 102-164.6 139-54.8 39.5-93.8 59.6-115.8 59.6zM295.4 127.5c-72.6 0-139.1 28.6-187.3 80.4-47.5 51.2-73.7 120.6-73.7 195.4 0 64.8 78.3 178.9 209.6 305.3 53.8 51.8 111.2 100.3 161.7 136.6 56.1 40.4 88.9 54.8 100.9 54.8s44.7-14.4 100.9-54.8c50.5-36.3 108-84.9 161.7-136.6 131.2-126.4 209.6-240.5 209.6-305.3 0-74.9-26.2-144.2-73.7-195.4-48.2-51.9-114.7-80.4-187.3-80.4-61.8 0-127.8 38.5-201.7 117.9-2.5 2.6-5.9 4.1-9.5 4.1s-7.1-1.5-9.5-4.1C423.2 166 357.2 127.5 295.4 127.5z" fill="#141414" p-id="2811"></path><path d="M353.9 415.6m-33.8 0a33.8 33.8 0 1 0 67.6 0 33.8 33.8 0 1 0-67.6 0Z" fill="#0F0F0F" p-id="2812"></path><path d="M659.3 415.6m-33.8 0a33.8 33.8 0 1 0 67.6 0 33.8 33.8 0 1 0-67.6 0Z" fill="#0F0F0F" p-id="2813"></path><path d="M411.6 538.5c0 52.3 42.8 95 95 95 52.3 0 95-42.8 95-95v-31.7h-190v31.7z" fill="#5B5143" p-id="2814"></path><path d="M506.6 646.5c-59.6 0-108-48.5-108-108v-31.7c0-7.2 5.8-13 13-13h190.1c7.2 0 13 5.8 13 13v31.7c0 59.5-48.5 108-108.1 108z m-82-126.7v18.7c0 45.2 36.8 82 82 82s82-36.8 82-82v-18.7h-164z" fill="#141414" p-id="2815"></path><path d="M450.4 578.9a54.7 27.5 0 1 0 109.4 0 54.7 27.5 0 1 0-109.4 0Z" fill="#EA64F9" p-id="2816"></path><path d="M256 502.7a32.1 27.5 0 1 0 64.2 0 32.1 27.5 0 1 0-64.2 0Z" fill="#EFAFF9" p-id="2817"></path><path d="M703.3 502.7a32.1 27.5 0 1 0 64.2 0 32.1 27.5 0 1 0-64.2 0Z" fill="#EFAFF9" p-id="2818"></path></svg>',
click () {alert('捐赠地址https://ld246.com/sponsor')},
}],
})
```
| | 说明 | 默认值 |
| - | - | - |
| name | 唯一标示 | - |
| icon | svg 图标 | - |
| tip | 提示 | - |
| tipPosition | 提示位置:'n', 'ne', 'nw', 's', 'se', 'sw', 'w', 'e' | - |
| hotkey | 快捷键,格式为<kbd>⇧⌘</kbd>/<kbd>⌘</kbd>/<kbd>⌥⌘</kbd>| - |
| suffix | 插入编辑器中的后缀 | - |
| prefix | 插入编辑器中的前缀 | - |
| click(event: Event, vditor: IVditor) | 自定义按钮点击时触发的事件 | - |
| className | 样式名 | '' |
| toolbar?: Array<options.toolbar> | 子菜单 | - |
#### options.toolbarConfig
| | 说明 | 默认值 |
| - | - | - |
| hide | 是否隐藏工具栏 | false |
| pin | 是否固定工具栏 | false |
#### options.counter
| | 说明 | 默认值 |
| - | - | - |
| enable | 是否启用计数器 | false |
| after(length: number, counter: options.counter): void | 字数统计回调 | - |
| max | 允许输入的最大值 | - |
| type | 统计类型:'markdown', 'text' | 'markdown' |
#### options.cache
| | 说明 | 默认值 |
| - | - | - |
| enable | 是否使用 localStorage 进行缓存 | true |
| id | 缓存 key第一个参数为元素且启用缓存时**必填** | - |
| after(html: string): string | 缓存后的回调 | - |
#### options.comment
⚠️:仅支持 wysiwyg 模式
| | 说明 | 默认值 |
| - | - | - |
| enable | 是否启用评论模式 | false |
| add(id: string, text: string, commentsData: ICommentsData[]) | 添加评论回调 | - |
| remove(ids: string[]) | 删除评论回调 | - |
| scroll(top: number) | 滚动回调 | - |
| adjustTop(commentsData: ICommentsData[]) | 文档修改时,适配评论高度 | - |
#### options.preview
| | 说明 | 默认值 |
| - | - | - |
| delay | 预览 debounce 毫秒间隔 | 1000 |
| maxWidth | 预览区域最大宽度 | 800 |
| mode | 显示模式both, editor | 'both' |
| url | md 解析请求 | - |
| parse(element: HTMLElement) | 预览回调 | - |
| transform(html: string): string | 渲染之前回调 | - |
#### options.preview.hljs
| | 说明 | 默认值 |
| - | - | - |
| defaultLang | 未指定语言时默认使用该语言 | '' |
| enable | 是否启用代码高亮 | true |
| style | 可选值参见[Chroma](https://xyproto.github.io/splash/docs/longer/all.html) | `github` |
| lineNumber | 是否启用行号 | false |
| langs | 自定义指定语言 | [CODE_LANGUAGES](https://github.com/Vanessa219/vditor/blob/53ca8f9a0e511b37b5dae7c6b15eb933e9e02ccd/src/ts/constants.ts#L20) |
| renderMenu(code: HTMLElement, copy: HTMLElement) | 渲染菜单按钮 | - |
#### options.preview.markdown
| | 说明 | 默认值 |
| - | - | - |
| autoSpace | 自动空格 | false |
| gfmAutoLink | 自动链接 | true |
| fixTermTypo | 自动矫正术语 | false |
| toc | 插入目录 | false |
| footnotes | 脚注 | true |
| codeBlockPreview | wysiwyg 和 ir 模式下是否对代码块进行渲染 | true |
| mathBlockPreview | wysiwyg 和 ir 模式下是否对数学公式进行渲染 | true |
| paragraphBeginningSpace | 段落开头空两个 | false |
| sanitize | 是否启用过滤 XSS | true |
| listStyle | 为列表添加 data-style 属性 | false |
| linkBase | 链接相对路径前缀 | '' |
| linkPrefix | 链接强制前缀 | '' |
| mark | 启用 mark 标记 | false |
#### options.preview.theme
| | 说明 | 默认值 |
| - | - | - |
| current | 当前主题 | "light" |
| list | 可选主题列表 | { "ant-design": "Ant Design", dark: "Dark", light: "Light", wechat: "WeChat" } |
| path | 主题样式地址 | `https://unpkg.com/vditor@${VDITOR_VERSION}/dist/css/content-theme` |
#### options.preview.math
| | 说明 | 默认值 |
| - | - | - |
| inlineDigit | 内联数学公式起始 $ 后是否允许数字 | false |
| macros | 使用 MathJax 渲染时传入的宏定义 | {} |
| engine | 数学公式渲染引擎KaTeX, MathJax | 'KaTeX' |
| mathJaxOptions | 数学公式渲染引擎为 MathJax 时的参数 | - |
#### options.preview.actions?: Array<IPreviewAction | IPreviewActionCustom>
默认值为 ["desktop", "tablet", "mobile", "mp-wechat", "zhihu"]。
可从默认值中挑选进行配置,也可使用以下字段进行自定制开发。
| | 说明 | 默认值 |
| - | - | - |
| key | 按钮唯一标识,不能为空 | - |
| text | 按钮文字 | - |
| tooltip | 提示 | - |
| className | 按钮类名 | - |
| click(key: string) | 按钮点击回调事件 | - |
#### options.preview.render.media
| | 说明 | 默认值 |
|--------|-----------|------|
| enable | 是否启用多媒体渲染 | true |
#### options.image
| | 说明 | 默认值 |
| - | - | - |
| isPreview | 是否预览图片 | true |
| preview(bom: Element) => void | 图片预览处理 | - |
#### options.link
| | 说明 | 默认值 |
| - | - | - |
| isOpen | 是否打开链接地址 | true |
| click(bom: Element) => void | 点击链接事件 | - |
#### options.hint
| | 说明 | 默认值 |
| - | - | - |
| parse | 是否进行 md 解析 | true |
| delay | 提示 debounce 毫秒间隔 | 200 |
| emoji | 默认表情,可从[lute/emoji_map](https://github.com/88250/lute/blob/master/parse/emoji_map.go) 中选取,也可自定义 | { '+1': '👍', '-1': '👎', 'heart': '❤️', 'cold_sweat': '😰' } |
| emojiTail | 常用表情提示 | - |
| emojiPath | 表情图片地址 | `https://unpkg.com/vditor@${VDITOR_VERSION}/dist/images/emoji` |
| extend: IHintExtend[] | 对 @/话题等关键字自动补全的扩展 | [] |
```ts
interface IHintData {
html: string;
value: string;
}
interface IHintExtend {
key: string;
hint?(value: string): IHintData[] | Promise<IHintData[]>;
}
```
#### options.upload
* 文件上传的数据结构如下。后端返回的数据结构不一致时,可使用 `format` 进行转换。
```js
// POST data
xhr.send(formData); // formData = FormData.append("file[]", File)
// return data
{
"msg": "",
"code": 0,
"data": {
"errFiles": ['filename', 'filename2'],
"succMap": {
"filename3": "filepath3",
"filename3": "filepath3"
}
}
}
```
* 为了防止站外图片失效, `linkToImgUrl` 可将剪贴板中的站外图片地址传到服务器端进行保存处理,其数据结构如下:
```js
// POST data
xhr.send(JSON.stringify({url: src})); // src 为站外图片地址
// return data
{
msg: '',
code: 0,
data : {
originalURL: '',
url: ''
}
}
```
* `success``format``error` 不会同时触发,具体调用情况如下:
```js
if (xhr.status === 200) {
if (vditor.options.upload.success) {
vditor.options.upload.success(editorElement, xhr.responseText);
} else {
let responseText = xhr.responseText;
if (vditor.options.upload.format) {
responseText = vditor.options.upload.format(files as File [], xhr.responseText);
}
genUploadedLabel(responseText, vditor);
}
} else {
if (vditor.options.upload.error) {
vditor.options.upload.error(xhr.responseText);
} else {
vditor.tip.show(xhr.responseText);
}
}
```
| | 说明 | 默认值 |
| - | - | - |
| url | 上传 url为空则不会触发上传相关事件 | '' |
| max | 上传文件最大 Byte | 10 * 1024 * 1024 |
| linkToImgUrl | 剪切板中包含图片地址时,使用此 url 重新上传 | '' |
| linkToImgCallback(responseText: string) | 图片地址上传回调 | - |
| linkToImgFormat(responseText: string): string | 对图片地址上传的返回值进行格式化 | - |
| success(editor: HTMLPreElement, msg: string) | 上传成功回调 | - |
| error(msg: string) | 上传失败回调 | - |
| token | CORS 上传验证,头为 X-Upload-Token | - |
| withCredentials | 跨站点访问控制 | false |
| headers | 请求头设置 | - |
| filename(name: string): string | 文件名安全处理 | name => name.replace(/\W/g, '') |
| accept | 文件上传类型,同[input accept](https://www.w3schools.com/tags/att_input_accept.asp) | - |
| validate(files: File[]) => string \| boolean | 校验,成功时返回 true 否则返回错误信息 | - |
| handler(files: File[]) => string \| null \| Promise<string> \| Promise<null> | 自定义上传,当发生错误时返回错误信息 | - |
| format(files: File[], responseText: string): string | 对服务端返回的数据进行转换,以满足内置的数据结构 | - |
| file(files: File[]): File[] \| Promise<File[]> | 将上传的文件处理后再返回 | - |
| setHeaders(): { [key: string]: string } | 上传前使用返回值设置头 | - |
| extraData: { [key: string]: string \| Blob } | 为 FormData 添加额外的参数 | - |
| multiple | 上传文件是否为多个 | true |
| fieldName | 上传字段名称 | 'file[]' |
| renderLinkDest?(vditor: IVditor, node: ILuteNode, entering: boolean): [string, number] | 处理剪贴板中的图片地址 | '' |
#### options.resize
| | 说明 | 默认值 |
| - | - | - |
| enable | 是否支持大小拖拽 | false |
| position | 拖拽栏位置:'top', 'bottom' | 'bottom' |
| after(height: number) | 拖拽结束的回调 | - |
#### options.classes
| | 说明 | 默认值 |
| - | - | - |
| preview | 预览元素上的 className | '' |
#### options.fullscreen
| | 说明 | 默认值 |
| - | - | - |
| index | 全屏层级 | 90 |
#### options.outline
| | 说明 | 默认值 |
| - | - | - |
| enable | 初始化是否展现大纲 | false |
| position | 大纲位置:'left', 'right' | 'left' |
#### methods
| | 说明 |
| - | - |
| exportJSON(markdown: string) | 根据 Markdown 获取对应 JSON |
| getValue() | 获取 Markdown 内容 |
| getHTML() | 获取 HTML 内容 |
| insertValue(value: string, render = true) | 在焦点处插入内容,并默认进行 Markdown 渲染 |
| focus() | 聚焦到编辑器 |
| blur() | 让编辑器失焦 |
| disabled() | 禁用编辑器 |
| enable() | 解除编辑器禁用 |
| getSelection(): string | 返回选中的字符串 |
| setValue(markdown: string, clearStack = false) | 设置编辑器内容且选中清空历史栈 |
| clearStack() | 清空撤销和重做记录栈|
| renderPreview(value?: string) | 设置预览区域内容 |
| getCursorPosition():{top: number, left: number} | 获取焦点位置 |
| deleteValue() | 删除选中内容 |
| updateValue(value: string) | 更新选中内容 |
| isUploading() | 上传是否还在进行中 |
| clearCache() | 清除缓存 |
| disabledCache() | 禁用缓存 |
| enableCache() | 启用缓存 |
| html2md(value: string) | HTML 转 md |
| tip(text: string, time: number) | 消息提示。time 为 0 将一直显示 |
| setPreviewMode(mode: "both" \| "editor") | 设置预览模式 |
| setTheme(theme: "dark" \| "classic", contentTheme?: string, codeTheme?: string, contentThemePath?: string) | 设置主题、内容主题及代码块风格 |
| getCurrentMode(): string | 获取编辑器当前编辑模式 |
| destroy() | 销毁编辑器 |
| getCommentIds(): {id: string, top: number}[] | 获取所有评论 |
| hlCommentIds(ids: string[]) | 高亮评论 |
| unHlCommentIds(ids: string[]) | 取消评论高亮 |
| removeCommentIds(removeIds: string[]) | 删除评论 |
| updateToolbarConfig(config: {hide?: boolean, pin?: boolean}) | 更新工具栏配置 |
#### static methods
* 不需要进行编辑操作时,仅需引入 [`method.min.js`](https://unpkg.com/vditor/dist/) 后如下直接调用
```js
Vditor.mermaidRender(document)
```
```js
import VditorPreview from 'vditor/dist/method.min'
VditorPreview.mermaidRender(document)
```
* 需要对页面中的 Markdown 进行渲染时可直接调用 `preview` 方法,参数如下:
```ts
previewElement: HTMLDivElement, // 使用该元素进行渲染
markdown: string, // 需要渲染的 markdown 原文
options?: IPreviewOptions {
mode: "dark" | "light";
anchor?: number; // 为标题添加锚点 0不渲染1渲染于标题前2渲染于标题后默认 0
customEmoji?: { [key: string]: string }; // 自定义 emoji默认为 {}
lang?: (keyof II18nLang); // 语言,默认为 'zh_CN'
emojiPath?: string; // 表情图片路径
hljs?: IHljs; // 参见 options.preview.hljs
speech?: { // 对选中后的内容进行阅读
enable?: boolean,
};
math?: IMath; // 数学公式渲染配置
cdn?: string; // 自建 CDN 地址
transform?(html: string): string; // 在渲染前进行的回调方法
after?(); // 渲染完成后的回调
lazyLoadImage?: string; // 设置为 Loading 图片地址后将启用图片的懒加载
markdown?: options.preview.markdown;
theme?: options.preview.theme;
render?: options.preview.render;
renderers?: ILuteRender; // 自定义渲染 https://ld246.com/article/1588412297062
}
```
* ⚠️ `method.min.js``index.min.js` 不可同时引入
| | 说明 |
| - | - |
| previewImage(oldImgElement: HTMLImageElement, lang: keyof II18n = "zh_CN", theme = "classic") | 点击图片预览 |
| mermaidRender(element: HTMLElement, cdn = options.cdn, theme = options.theme) | 流程图/时序图/甘特图 |
| flowchartRender(element: HTMLElement, cdn = options.cdn) | flowchart 渲染 |
| codeRender(element: HTMLElement, option?: IHljs) | 为 element 中的代码块添加复制按钮 |
| chartRender(element: (HTMLElement \| Document) = document, cdn = options.cdn, theme = options.theme) | 图表渲染 |
| mindmapRender(element: (HTMLElement \| Document) = document, cdn = options.cdn, theme = options.theme) | 脑图渲染 |
| plantumlRender(element: (HTMLElement \| Document) = document, cdn = options.cdn) | plantuml 渲染 |
| abcRender(element: (HTMLElement \| Document) = document, cdn = options.cdn) | 五线谱渲染 |
| md2html(mdText: string, options?: IPreviewOptions): Promise\<string> | Markdown 文本转换为 HTML该方法需使用[异步编程](https://ld246.com/article/1546828434083?r=Vanessa#toc_h3_1) |
| preview(previewElement: HTMLDivElement, markdown: string, options?: IPreviewOptions) | 页面 Markdown 文章渲染 |
| highlightRender(hljsOption?: IHljs, element?: HTMLElement \| Document, cdn = options.cdn) | 为 element 中的代码块进行高亮渲染 |
| mediaRender(element: HTMLElement) | 为[特定链接](https://ld246.com/article/1589813914768)分别渲染为视频、音频、嵌入的 iframe |
| mathRender(element: HTMLElement, options?: {cdn?: string, math?: IMath}) | 对数学公式进行渲染 |
| speechRender(element: HTMLElement, lang?: (keyof II18nLang)) | 对选中的文字进行阅读 |
| graphvizRender(element: HTMLElement, cdn?: string) | 对 graphviz 进行渲染 |
| outlineRender(contentElement: HTMLElement, targetElement: Element) | 对大纲进行渲染 |
| lazyLoadImageRender(element: (HTMLElement \| Document) = document) | 对启用懒加载的图片进行渲染 |
| setCodeTheme(codeTheme: string, cdn = options.cdn) | 设置代码主题codeTheme 参见 options.preview.hljs.style |
| setContentTheme(contentTheme: string, path: string) | 设置内容主题contentTheme 参见 options.preview.theme.list |
## 🏗 开发文档
### 原理相关
* [关于所见即所得 Markdown 编辑器的讨论](https://ld246.com/article/1579414663700)
* [Vditor 实现 Markdown 所见即所得](https://ld246.com/article/1577370404903)
* [Lute 一款对中文语境优化的 Markdown 引擎,支持 Go 和 JavaScript](https://ld246.com/article/1567047822949)
### 环境
1. 安装 [node](https://nodejs.org/) LTS 版本
2. [下载](https://github.com/Vanessa219/vditor/archive/master.zip)最新代码并解压
3. 根目录运行 `npm install`
4. `npm run start` 启动本地服务器,打开 http://localhost:9000
5. 修改代码
6. `npm run build` 打包代码到 dist 目录
### CDN 切换
由于使用了按需加载的机制,默认 CDN 为 [https://unpkg.com/vditor](https://unpkg.com/vditor)@版本号
如果代码有修改或需要使用自建 CDN 的话,可按以下步骤进行操作:
* 初始化时,需对 `options``IPreviewOptions` 中的 `cdn``emojiPath`, `themes` 进行配置
* `highlightRender` , `mathRender` , `abcRender` , `chartRender` , `mermaidRender` `flowchartRender``mindmapRender``graphvizRender``setCodeTheme``setContentTheme` 方法中需添加 cdn 参数
* 将 build 成功的 dist 目录或 [jsDelivr](https://www.jsdelivr.com/package/npm/vditor?path=dist) 中的 dist 目录拷贝至正确的位置
### 升级
版本升级时请**仔细阅读** [CHANGELOG](https://github.com/Vanessa219/vditor/blob/master/CHANGELOG.md) 中的**升级**部分
## Ⓜ️ Markdown 使用指南
* [基础语法](https://ld246.com/article/1583129520165)
* [扩展语法](https://ld246.com/article/1583305480675)
* [速查手册](https://ld246.com/article/1583308420519)
## 🏘️ 社区
* [官网](https://b3log.org/vditor)
* [讨论区](https://ld246.com/tag/vditor)
* [报告问题](https://github.com/Vanessa219/vditor/issues/new)
## 📄 授权
Vditor 使用 [MIT](https://opensource.org/licenses/MIT) 开源协议。
## 🙏 鸣谢
* [Lute](https://github.com/88250/lute):🎼 一款结构化的 Markdown 引擎,支持 Go 和 JavaScript
* [highlight.js](https://github.com/highlightjs/highlight.js)JavaScript syntax highlighter
* [mermaid](https://github.com/knsv/mermaid)Generation of diagram and flowchart from text in a similar manner as Markdown
* [incubator-echarts](https://github.com/apache/incubator-echarts)A powerful, interactive charting and visualization library for browser
* [abcjs](https://github.com/paulrosen/abcjs)JavaScript library for rendering standard music notation in a browser
## 📽️ 历史
我们在开发 [Sym](https://github.com/88250/symphony) 的初期是直接使用 WYSIWYG 富文本编辑器的。那时候基于 HTML 的编辑器非常流行,项目中引用起来也很方便,也符合用户当时的使用习惯。
后来Markdown 的崛起逐步改变了大家的排版方式。再加上我们其他几个项目都是面向程序员用户的,所以迁移到 md 上也是大势所趋。我们选择了 [CodeMirror](https://github.com/codemirror/CodeMirror),这是一款优秀的编辑器,它对开发者提供了丰富的编程接口,对各种浏览器的兼容性也比较好。
再后来,随着我们项目业务需求方面的沉淀,使用 CodeMirror 有时候会感到比较“笨重”。比如要实现 @自动完成用户名列表、插入 Emoji、上传文件等就需要比较深入的二次开发而这些业务需求恰恰是很多项目场景共有且必备的。
终于,我们决定开始在 Sym 中自己实现编辑器。随着几个版本的迭代Sym 的编辑器也日趋成熟。在我们运营的社区[链滴](https://ld246.com)上陆续有人问我们是否能将编辑器单独抽离出来提供给大家使用。与此同时,我们的前端主程 [V](https://ld246.com/member/Vanessa) 同学对于维护分散在各个项目中的编辑器也感到有点力不从心,外加对 TypeScript 的好感,所以就决定使用 ts 来实现一个全新的浏览器端 md 编辑器。
于是Vditor 就这样诞生了。

View File

@@ -0,0 +1,76 @@
{
"name": "vditor",
"version": "3.10.6",
"description": "♏ 易于使用的 Markdown 编辑器,为适配不同的应用场景而生",
"author": "Vanessa <v@b3log.org> (http://vanessa.b3log.org)",
"homepage": "https://b3log.org/vditor",
"jsdelivr": "dist/index.min.js",
"main": "dist/index.js",
"funding": "https://ld246.com/sponsor",
"files": [
"dist/*",
"src/index.ts",
"src/method.ts",
"src/ts/*",
"src/assets/*"
],
"dependencies": {
"diff-match-patch": "^1.0.5"
},
"types": "dist/index.d.ts",
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.14.2",
"@types/diff-match-patch": "^1.0.32",
"@types/jest": "^26.0.23",
"@types/node": "^17.0.19",
"@types/puppeteer": "^2.1.5",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"autoprefixer": "^10.4.2",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"copy-webpack-plugin": "^6.4.1",
"css-loader": "^5.2.4",
"eslint": "^8.9.0",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-prettier": "^4.0.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.1",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.6.3",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"mini-css-extract-plugin": "^2.6.0",
"postcss": "^8.4.12",
"postcss-loader": "^5.2.0",
"prettier": "^2.5.1",
"puppeteer": "^23.0.2",
"style-loader": "^1.3.0",
"terser-webpack-plugin": "^5.3.0",
"ts-jest": "^26.5.6",
"ts-loader": "^6.2.2",
"typescript": "^4.9.5",
"webpack": "^5.66.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.1.0"
},
"license": "MIT",
"repository": "git://github.com/Vanessa219/vditor.git",
"bugs": {
"url": "https://github.com/Vanessa219/vditor/issues"
},
"scripts": {
"lint": "eslint --fix --ext src/**/*.ts",
"test:watch": "jest --watch",
"test": "jest --coverage",
"start": "webpack serve --config webpack.start.js",
"build": "webpack"
},
"keywords": [
"editor",
"markdown",
"b3log"
]
}

View File

@@ -0,0 +1,279 @@
@keyframes slideInDown {
from {
transform: translate3d(0, -100%, 0);
visibility: visible;
}
to {
transform: translate3d(0, 0, 0);
}
}
.vditor {
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
border-radius: 3px;
box-sizing: border-box;
font-family: @font-family-base;
&--fullscreen {
position: fixed;
top: 0;
width: 100% !important;
left: 0;
height: 100vh !important;
z-index: 90;
border-radius: 0;
}
&-content {
display: flex;
min-height: 60px;
flex: 1;
min-width: 1px;
position: relative;
}
&-preview {
flex: 1;
min-width: 1px;
overflow: auto;
margin-left: -1px;
border-left: 1px solid var(--border-color);
box-sizing: border-box;
border-radius: 0 0 3px 0;
background-color: var(--textarea-background-color);
&::-webkit-scrollbar {
display: none;
}
&__action {
text-align: center;
padding: 10px;
background-color: var(--toolbar-background-color);
button {
background-color: var(--toolbar-background-color);
color: var(--toolbar-icon-color);
line-height: 20px;
border: 0;
margin: 0 10px;
cursor: pointer;
padding: 0 7px;
font-size: 12px;
&.vditor-preview__action--current,
&:hover {
color: var(--toolbar-icon-hover-color);
background-color: var(--toolbar-background-color);
}
&:focus {
outline: none;
}
svg {
fill: currentColor;
height: 15px;
width: 15px;
vertical-align: middle;
}
}
}
& > .vditor-reset {
padding: 10px;
margin: 0 auto;
}
img:not(.emoji) {
cursor: pointer;
}
}
&-devtools {
display: none;
background-color: var(--textarea-background-color);
overflow: auto;
flex: 1;
min-width: 1px;
box-shadow: inset 1px 0 var(--border-color);
box-sizing: border-box;
border-radius: 0 0 3px 0;
padding: 10px;
}
&-counter {
padding: 3px;
color: var(--toolbar-icon-color);
background-color: var(--count-background-color);
border-radius: 3px;
font-size: 12px;
user-select: none;
float: right;
margin: 8px 3px 0 0;
&--error {
color: @errorColor;
background-color: rgba(@errorColor, 0.1);
}
}
&-resize {
padding: 3px 0;
cursor: row-resize;
user-select: none;
position: absolute;
width: 100%;
&--top {
top: -3px;
}
&--bottom {
bottom: -3px;
}
& > div {
height: 3px;
background-color: var(--resize-background-color);
transition: @transition;
}
&:hover,
&--selected {
& > div {
background-color: var(--resize-hover-background-color);
}
svg {
color: var(--resize-hover-icon-color);
}
}
svg {
fill: currentColor;
stroke-width: 0;
stroke: currentColor;
width: 13px;
height: 3px;
display: block;
margin: 0 auto;
color: var(--resize-icon-color);
}
}
&-upload {
position: absolute;
height: 3px;
left: 0;
top: -2px;
transition: @transition;
background-color: @blurColor;
}
&-tip {
position: absolute;
font-size: 12px;
top: 10px;
animation-duration: 0.15s;
animation-fill-mode: both;
left: 50%;
z-index: 5;
&--show {
display: block;
animation-name: slideInDown;
}
&__content {
text-align: left;
display: inline-block;
line-height: 16px;
padding: 3px 10px;
border-radius: 3px;
background: var(--toolbar-background-color);
position: relative;
margin-left: -50%;
color: var(--toolbar-icon-color);
max-width: 100%;
box-shadow: var(--panel-shadow);
ul {
margin: 2px 0;
padding: 0 0 0 18px;
}
a {
color: @blurColor;
}
}
&__close {
position: absolute;
color: var(--toolbar-icon-color);
top: -7px;
right: -15px;
font-weight: bold;
cursor: pointer;
&:hover {
color: var(--toolbar-icon-hover-color);
}
}
}
&-img {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
z-index: 100;
&__bar {
border-bottom: 1px solid var(--border-color);
background-color: var(--toolbar-background-color);
text-align: center;
height: 36px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
}
&__btn {
display: flex;
align-items: center;
cursor: pointer;
margin-left: 24px;
user-select: none;
color: var(--toolbar-icon-color);
&:hover {
color: var(--toolbar-icon-hover-color);
}
svg {
height: 14px;
width: 14px;
margin-right: 8px;
fill: currentColor;
}
}
&__img {
flex: 1;
background-color: var(--textarea-background-color);
overflow: auto;
cursor: zoom-out;
img {
max-width: none;
}
}
}
}

View File

@@ -0,0 +1,67 @@
.vditor-hint {
background-color: var(--panel-background-color);
position: absolute;
box-shadow: var(--panel-shadow);
border-radius: 3px;
padding: 5px 0;
z-index: 4;
line-height: 20px;
list-style: none;
font-size: 12px;
margin: 0;
max-width: 250px;
min-width: 80px;
display: none;
.vditor-hint {
margin-top: -31px;
left: 100%;
right: auto;
&.vditor-panel--left {
right: 100%;
left: auto;
}
}
button {
color: var(--toolbar-icon-color);
display: block;
padding: 3px 10px;
border: 0;
border-radius: 0;
line-height: 20px;
width: 100%;
box-sizing: border-box;
text-align: left;
margin: 0;
background-color: transparent;
cursor: pointer;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&:focus {
outline: none;
}
}
&--current,
button:not(.vditor-menu--disabled):hover {
background-color: var(--toolbar-background-color) !important;
color: var(--toolbar-icon-hover-color) !important;
}
&__emoji {
font-size: 16px;
float: left;
margin-right: 3px;
}
img {
height: 20px;
width: 20px;
float: left;
margin-right: 3px;
}
}

View File

@@ -0,0 +1,275 @@
.vditor-ir {
box-sizing: border-box;
flex: 1;
min-width: 1px;
position: relative;
width: 100%;
&__node {
&[data-type="code-block"]:before,
&[data-type="code-block"]:after,
&[data-type="yaml-front-matter"]:before,
&[data-type="yaml-front-matter"]:after,
&[data-type="math-block"]:before,
&[data-type="math-block"]:after {
content: ' ';
color: var(--second-color);
}
&:not(.vditor-ir__node--expand) .vditor-ir__marker {
padding: 0 !important;
}
&:not(.vditor-ir__node--expand)[data-type="a"] {
cursor: pointer;
}
&[data-type="link-ref"],
&[data-type="footnotes-ref"] {
color: @blurColor;
}
&[data-type="html-block"] {
margin-bottom: 1em;
}
.vditor-ir__marker {
width: 0;
overflow: hidden;
display: inline-block;
height: 0;
transition: @transition;
}
&--hidden {
.vditor-ir__marker {
visibility: hidden;
}
}
&--expand {
.vditor-ir__marker {
color: var(--second-color);
display: inline;
height: auto;
width: auto;
&--hide {
display: none;
}
&--heading {
color: var(--ir-heading-color);
}
&--bi {
color: var(--ir-bi-color);
}
&--link {
color: var(--ir-link-color);
}
&--title {
color: var(--ir-title-color);
}
&--bracket {
color: var(--ir-bracket-color);
text-decoration: underline;
}
&--paren {
color: var(--ir-paren-color);
}
&--info {
color: var(--ir-heading-color);
}
&--pre code {
color: var(--textarea-text-color);
height: auto;
text-align: left;
}
}
&[data-type="code-block"]:before,
&[data-type="code-block"]:after {
content: '```';
}
&[data-type="yaml-front-matter"]:before,
&[data-type="yaml-front-matter"]:after {
content: '---';
}
&[data-type="math-block"]:before,
&[data-type="math-block"]:after {
content: '$$';
}
}
span[data-type="code-block-open-marker"],
span[data-type="code-block-close-marker"],
span[data-type="yaml-front-matter-open-marker"],
span[data-type="yaml-front-matter-close-marker"],
span[data-type="math-block-open-marker"],
span[data-type="math-block-close-marker"] {
display: none;
}
}
&__preview {
cursor: pointer;
white-space: initial;
min-height: 27px;
}
&__link {
color: var(--ir-bracket-color);
text-decoration: underline;
}
pre.vditor-reset {
background-color: var(--panel-background-color);
margin: 0;
white-space: pre-wrap;
height: 100%;
box-sizing: border-box;
&[contenteditable="false"] {
opacity: 0.3;
cursor: not-allowed;
}
&:empty::before {
content: attr(placeholder);
color: var(--second-color);
}
&:focus {
outline: none;
background-color: var(--textarea-background-color);
}
&:after {
content: "";
height: var(--editor-bottom);
display: block;
}
pre {
margin: 0;
}
}
hr {
display: inline-block;
margin: 12px 0;
width: 100%;
}
blockquote:empty::before,
pre > code:empty::before,
p:empty::before,
h1:empty::after,
h2:empty::after,
h3:empty::after,
h4:empty::after,
h5:empty::after,
h6:empty::after {
content: ' ';
}
// block title
& .vditor-reset > h1:before,
& .vditor-reset > h2:before,
& .vditor-reset > h3:before,
& .vditor-reset > h4:before,
& .vditor-reset > h5:before,
& .vditor-reset > h6:before,
div[data-type="link-ref-defs-block"]:before,
div[data-type="footnotes-block"]:before,
.vditor-toc:before {
float: left;
padding-right: 4px;
margin-left: -29px;
content: 'H1';
font-size: 0.85rem;
font-weight: normal;
color: var(--second-color);
}
& .vditor-reset > h2:before {
content: 'H2';
}
& .vditor-reset > h3:before {
content: 'H3';
}
& .vditor-reset > h4:before {
content: 'H4';
}
& .vditor-reset > h5:before {
content: 'H5';
}
& .vditor-reset > h6:before {
content: 'H6';
}
div[data-type="link-ref-defs-block"] {
&:before {
content: '"A"';
}
color: var(--blockquote-color);
}
div[data-type="footnotes-block"] {
&:before {
content: '^F';
}
border-top: 2px solid var(--heading-border-color);
padding-top: 24px;
margin-top: 24px;
& > div[data-type="footnotes-def"] {
& > ul, & > ol, & > p, & > blockquote, & > pre, & > table, & > hr {
margin-left: 8px;
}
}
}
.vditor-toc {
&:before {
content: "ToC";
}
span[data-type="toc-h"] {
color: @blurColor;
text-decoration: underline;
}
}
}
@media screen and (max-width: @max-width) {
.vditor-ir {
h1:before,
h2:before,
h3:before,
h4:before,
h5:before,
h6:before,
div[data-type="link-ref-defs-block"]:before,
div[data-type="footnotes-block"]:before,
.vditor-toc:before {
content: none;
}
}
}

View File

@@ -0,0 +1,110 @@
/**
* panel.
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @version 1.0.0.0, Jan 22, 2020
*/
@keyframes scale-in {
0% {
opacity: 0;
transform: scale(0.5);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.vditor {
&-panel {
background-color: var(--panel-background-color);
position: absolute;
box-shadow: var(--panel-shadow);
border-radius: 3px;
padding: 5px;
z-index: 3;
font-size: 14px;
display: none;
user-select: none;
max-width: 320px;
min-width: 80px;
animation-duration: 0.15s;
animation-name: scale-in;
animation-timing-function: cubic-bezier(0.2, 0, 0.13, 1.5);
color: var(--toolbar-icon-color);
&--none {
padding: 0;
animation: none;
min-width: auto;
max-width: none;
white-space: nowrap;
opacity: 0.86;
}
&--arrow:before {
position: absolute;
width: 0;
height: 0;
pointer-events: none;
content: " ";
border: 7px solid transparent;
top: -14px;
left: 5px;
border-bottom-color: var(--panel-background-color);
}
&--left {
right: 0;
&.vditor-panel--arrow:before {
right: 5px;
left: auto;
}
}
}
&-input {
border: 0;
padding: 3px 5px;
background-color: var(--panel-background-color);
font-size: 12px;
color: var(--textarea-text-color);
&:focus {
background-color: var(--toolbar-background-color);
outline: none;
}
}
&-icon {
color: var(--toolbar-icon-color);
cursor: pointer;
float: left;
padding: 4px 5px;
height: 21px;
width: 23px;
background-color: transparent;
border: 0;
box-sizing: border-box;
&:hover,
&--current {
color: var(--toolbar-icon-hover-color);
background-color: transparent;
}
&:focus {
outline: none;
}
svg {
height: 13px !important;
width: 13px !important;
float: left;
fill: currentColor;
pointer-events: none;
}
}
}

View File

@@ -0,0 +1,610 @@
.vditor {
&-reset {
&--anchor {
padding-left: 20px;
}
&--error {
color: @errorColor;
font-size: 12px;
display: block;
line-height: 16px;
}
color: #24292e;
font-variant-ligatures: no-common-ligatures;
font-family: @font-family-base;
word-wrap: break-word;
overflow: auto;
line-height: 1.5;
font-size: 16px;
word-break: break-word;
ul ul ul {
list-style-type: square;
}
ul ul {
list-style-type: circle;
}
ul {
list-style-type: disc;
}
ul,
ol {
padding-left: 2em;
margin-top: 0;
margin-bottom: 16px;
}
li + li {
margin-top: 0.25em;
}
li p {
margin-top: 16px;
}
audio {
max-width: 100%;
&:focus {
outline: none;
}
}
video {
max-height: 90vh;
max-width: 100%;
}
img {
max-width: 100%;
}
img.emoji {
cursor: auto;
max-width: 20px;
vertical-align: sub;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
&:hover .vditor-anchor svg {
visibility: visible;
}
}
h1 {
font-size: 1.75em;
}
h2 {
font-size: 1.55em;
}
h3 {
font-size: 1.38em;
}
h4 {
font-size: 1.25em;
}
h5 {
font-size: 1.13em;
}
h6 {
font-size: 1em;
}
hr {
height: 2px;
padding: 0;
margin: 24px 0;
background-color: #eaecef;
border: 0;
}
p {
margin-top: 0;
margin-bottom: 16px;
}
blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #eaecef;
margin: 0 0 16px 0;
& > :first-child {
margin-top: 0;
}
& > :last-child {
margin-bottom: 0;
}
}
ins > iframe {
border: 0;
}
iframe {
border: 1px solid #d1d5da;
max-width: 100%;
box-sizing: border-box;
&.iframe__video {
min-width: 80%;
min-height: 36vh;
}
}
table {
border-collapse: collapse;
empty-cells: show;
margin-bottom: 16px;
overflow: auto;
border-spacing: 0;
display: block;
word-break: keep-all;
width: 100%;
tr {
background-color: #fafbfc;
border-top: 1px solid #c6cbd1;
}
td,
th {
padding: 6px 13px;
border: 1px solid #dfe2e5;
word-break: normal;
white-space: nowrap;
&:first-child::after {
content: "";
display: inline-block;
vertical-align: top;
min-height: 24px;
}
}
th {
font-weight: 600;
}
tbody tr:nth-child(2n) {
background-color: #fff;
}
}
code:not(.hljs):not(.highlight-chroma) {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
border-radius: 3px;
font-family: @font-family-code;
word-break: break-word;
background-size: 20px 20px;
white-space: pre-wrap;
}
pre {
margin: 1em 0;
& > code {
margin: 0;
font-size: 85%;
padding: 0.5em;
border-radius: 5px;
display: block;
overflow: auto;
white-space: pre;
font-family: @font-family-code;
background-size: 20px 20px;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8AgMAAABHkjHhAAAACVBMVEWAgIBaWlo+Pj7rTFvWAAAAA3RSTlMHCAw+VhR4AAAA+klEQVQoz4WSMW7EQAhFPxKWNh2FCx+HkaZI6RRb5DYbyVfIJXLKDCFoMbaTKSw/8ZnPAPjaH2xgZcUNUDADD7D9LtDBCLZ45fbkvo/30K8yeI64pPwl6znd/3n/Oe93P3ho9qeh72btTFzqkz0rsJle8Zr81OLEwZ1dv/713uWqvu2pl+k0fy7MWtj9r/tN5q/02z89qa/L4Dc2LvM93kezPfXlME/O86EbY/V9GB9ePX8G1/6W+/9h1dq/HGfTfzT3j/xNo7522Bfnqe5jO/fvhVthlfk434v3iO9zG/UOphyPeinPl1J8Gtaa7xPTa/Dk+RIs4deMvwGvcGsmsCvJ0AAAAABJRU5ErkJggg==);
word-break: initial;
word-wrap: normal;
}
&:hover div.vditor-copy {
display: block;
}
}
.language-math,
.language-echarts,
.language-mindmap,
.language-plantuml,
.language-mermaid,
.language-markmap,
.language-abc,
.language-flowchart,
.language-graphviz {
margin-bottom: 16px;
}
.language-math mjx-container:focus {
outline: none;
cursor: context-menu;
}
.language-echarts,
.language-mindmap {
overflow: hidden;
height: 420px;
}
.language-mermaid,
.language-markmap,
.language-flowchart,
.language-graphviz {
text-align: center;
}
.language-graphviz parsererror {
overflow: auto;
}
kbd {
display: inline-block;
padding: 3px 5px;
font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
line-height: 10px;
color: #24292e;
vertical-align: middle;
background-color: #fafbfc;
border: solid 1px #d1d5da;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #d1d5da;
}
summary {
cursor: pointer;
&:focus {
outline: none;
}
}
svg {
height: auto;
width: auto;
stroke-width: initial;
}
p:last-child,
blockquote:last-child,
pre:last-child,
ul:last-child,
ol:last-child,
hr:last-child {
margin-bottom: 0;
}
}
&-comment {
border-bottom: 2px solid rgb(248, 230, 171);
&--focus,
&--hover {
background-color: rgb(250, 241, 209);
border-bottom: 2px solid rgb(255, 198, 10);
.vditor-comment {
border-bottom: 2px solid rgb(255, 198, 10);
}
}
}
&-task {
list-style: none !important;
word-break: break-all;
input {
margin: 0 0.2em 0.25em -1.6em;
font-size: 12px;
vertical-align: middle;
}
}
&-copy {
position: relative;
display: none;
z-index: 1;
textarea {
position: absolute;
left: -100000px;
height: 10px;
}
span {
cursor: pointer;
position: absolute;
right: 15px;
top: 0.5em;
}
svg {
color: #586069;
height: 14px;
width: 14px !important;
display: block;
fill: currentColor;
}
}
&-linenumber {
padding-left: 4em !important;
position: relative;
&__rows {
position: absolute;
pointer-events: none;
top: 0.5em;
left: 0;
width: 3em; /* works for line-numbers below 1000 lines */
user-select: none;
counter-reset: linenumber;
& > span {
pointer-events: none;
display: block;
&::before {
counter-increment: linenumber;
content: counter(linenumber);
color: rgba(158, 150, 150, 0.38);
display: block;
padding-right: 1em;
text-align: right;
}
}
}
}
&-speech {
position: absolute;
display: none;
background-color: #f6f8fa;
border: 1px solid #d1d5da;
border-radius: 3px;
padding: 3px;
cursor: pointer;
color: #586069;
&:hover,
&--current {
color: #4285f4;
}
svg {
height: 14px;
width: 14px;
fill: currentColor;
display: block;
stroke-width: 0;
stroke: currentColor;
}
}
&-anchor {
margin-left: 5px;
&--left {
float: left;
padding-right: 4px;
margin-left: -20px;
}
svg {
visibility: hidden;
}
&:hover {
svg {
visibility: visible;
}
}
&:focus {
outline: none;
}
}
&-linkcard {
margin: 31px auto 16px;
transition: @transition;
cursor: pointer;
max-width: 768px;
padding: 0 10px;
a {
border-radius: 3px;
background-color: #f6f8fa;
overflow: hidden;
max-height: 250px;
display: flex;
text-decoration: none;
flex-wrap: wrap-reverse;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
&:hover {
box-shadow: 0 0 3px rgba(0, 0, 0, 0.13), 0 3px 6px rgba(0, 0, 0, 0.26);
text-decoration: none;
}
&:visited .vditor-linkcard__abstract {
color: rgba(88, 96, 105, 0.36);
}
}
&__info {
padding: 10px;
min-width: 200px;
box-sizing: border-box;
flex: 1;
}
&__title {
font-size: 14px;
font-weight: 400;
color: #24292e;
display: flex;
align-items: center;
img {
cursor: pointer;
height: 20px;
width: 20px;
border-radius: 3px;
flex-shrink: 0;
margin-right: 5px;
}
}
&__abstract {
word-wrap: break-word;
word-break: break-all;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
display: -webkit-box;
font-size: 13px;
color: #586069;
margin: 5px 0;
}
&__site {
font-size: 12px;
color: #4285f4;
}
&__image {
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
max-width: 250px;
min-width: 126px;
cursor: pointer;
background-color: rgba(88, 96, 105, 0.36);
}
}
&-footnotes__goto-ref {
text-decoration: none;
}
&-toc {
margin-bottom: 16px;
user-select: text;
color: @blurColor;
.vditor-outline__action {
display: none;
}
ul {
list-style: none !important;
padding-left: 1em;
}
& > ul {
padding-left: 0;
}
span {
cursor: pointer;
}
li > span > svg {
width: 0;
height: 0;
}
}
&-outline {
width: 250px;
border-right: 1px solid var(--border-color);
background-color: var(--panel-background-color);
display: none;
overflow: auto;
&--right {
border-right: 0;
border-left: 1px solid var(--border-color);
}
&::-webkit-scrollbar {
display: none;
}
ul {
list-style: none !important;
padding-left: 1em;
margin: 0;
}
&__content > ul {
padding-left: 0;
}
li > span {
display: flex;
align-items: center;
padding: 5px 10px;
cursor: pointer;
color: var(--textarea-text-color);
& > svg {
height: 10px;
width: 10px;
flex-shrink: 0;
}
&:hover {
color: var(--toolbar-icon-hover-color);
}
& > span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
&__title {
border-bottom: 1px dashed var(--border-color);
padding: 5px 10px;
color: var(--toolbar-icon-color);
font-size: 12px;
}
&__action {
transition: @transition;
fill: currentColor;
margin-right: 5px;
flex-shrink: 0;
&--close {
transform: rotate(-90deg);
}
}
}
}

View File

@@ -0,0 +1,132 @@
.vditor-sv {
font-family: @font-family-base;
margin: 0 1px 0 0;
overflow: auto;
width: 100%;
flex: 1;
min-width: 1px;
border: 0;
resize: none;
padding: 10px 9px 10px 10px;
box-sizing: border-box;
background-color: var(--panel-background-color);
outline: 0 none;
font-size: 16px;
line-height: 22px;
color: var(--textarea-text-color);
border-radius: 0 0 3px 3px;
font-variant-ligatures: no-common-ligatures;
white-space: pre-wrap;
word-break: break-word;
word-wrap: break-word;
&[contenteditable="false"] {
opacity: 0.3;
cursor: not-allowed;
}
&:empty::before {
content: attr(placeholder);
color: var(--second-color);
}
&:focus {
background-color: var(--textarea-background-color);
}
&:after {
content: "";
height: var(--editor-bottom);
display: block;
}
span[data-type="newline"] + span[data-type="text"]:empty {
display: inherit;
}
.sup {
vertical-align: super;
font-size: smaller;
}
.strong {
font-weight: bold;
}
.em {
font-style: italic;
}
.s {
text-decoration: line-through;
}
.mark:not(.vditor-sv__marker) {
background-color: yellow;
color: black;
}
.h1 {
font-size: 1.75em;
line-height: 44px;
}
.h2 {
font-size: 1.55em;
line-height: 38px;
}
.h3 {
font-size: 1.38em;
line-height: 27px;
}
.h4 {
font-size: 1.25em;
line-height: 25px;
}
.h5 {
font-size: 1.13em;
}
.h6 {
font-size: 1em;
}
&__marker {
color: var(--second-color);
&--heading {
color: var(--ir-heading-color);
}
&--bi {
color: var(--ir-bi-color);
}
&--link {
color: var(--ir-link-color);
}
&--title {
color: var(--ir-title-color);
}
&--bracket {
color: var(--ir-bracket-color);
}
&--paren {
color: var(--ir-paren-color);
}
&--info {
color: var(--ir-heading-color);
}
&--strong {
font-weight: bold;
}
}
}

View File

@@ -0,0 +1,180 @@
.vditor {
&-toolbar {
background-color: var(--toolbar-background-color);
border-bottom: 1px solid var(--border-color);
padding: 0 5px;
line-height: 1;
&--pin {
position: sticky;
top: 0;
z-index: 1;
}
&--hide {
transition: @transition;
height: 5px;
overflow: hidden;
&:hover {
background-color: var(--toolbar-background-color);
height: auto;
overflow: visible;
}
}
&__item {
float: left;
position: relative;
.vditor-tooltipped {
color: var(--toolbar-icon-color);
border: 0;
padding: 10px 5px;
background-color: transparent;
height: var(--toolbar-height);
width: 25px;
box-sizing: border-box;
font-size: 0;
&:focus {
outline: none;
}
&:focus {
cursor: pointer;
color: var(--toolbar-icon-hover-color);
}
}
svg {
fill: currentColor;
display: inline-block;
stroke-width: 0;
stroke: currentColor;
width: 15px;
height: 15px;
}
input {
position: absolute;
width: 25px;
height: var(--toolbar-height);
top: 0;
left: 0;
cursor: pointer;
opacity: .001;
overflow: hidden;
}
}
&__divider {
float: left;
height: calc(var(--toolbar-height) - (var(--toolbar-divider-margin-top) * 2));
border-left: 1px solid var(--second-color);
margin: var(--toolbar-divider-margin-top) 8px;
}
&__br {
width: 100%;
padding: 0 !important;
height: 0 !important;
}
}
&-menu {
&--current {
color: var(--toolbar-icon-hover-color) !important;
}
&--disabled {
color: var(--second-color) !important;
cursor: not-allowed !important;
}
}
&-emojis {
display: inline-block;
overflow: auto;
&::-webkit-scrollbar {
display: none;
}
&__tip {
flex: 1;
min-width: 1px;
width: 200px;
margin-right: 10px;
color: var(--toolbar-icon-color);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&__tail {
margin-top: 5px;
font-size: 12px;
color: var(--toolbar-icon-color);
display: flex;
a {
text-decoration: none;
color: var(--toolbar-icon-color);
&:hover {
color: var(--toolbar-icon-hover-color);
}
}
}
button {
cursor: pointer;
border-radius: 3px;
float: left;
height: 30px;
width: 30px;
text-align: center;
line-height: 26px;
padding: 3px;
box-sizing: border-box;
font-size: 16px;
transition: @transition;
border: 0;
margin: 0;
background-color: transparent;
overflow: hidden;
&:focus {
outline: none;
}
&:hover .vditor-emojis__icon {
display: inline-block;
transform: scale(1.2);
}
}
img {
height: 20px;
width: 20px;
float: left;
margin: 3px 0 0 3px;
}
}
}
@media screen and (max-width: @max-width) {
.vditor-toolbar__item {
padding: 0 12px;
}
.vditor-panel--left.vditor-panel--arrow:before {
right: 17px;
}
}
@media(hover: hover) and (pointer: fine) {
.vditor-toolbar__item .vditor-tooltipped:hover {
color: var(--toolbar-icon-hover-color);
}
}

View File

@@ -0,0 +1,171 @@
/**
* tool tip.
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @version 0.1.0.0, Jan 25, 2019
*/
@keyframes tooltip-appear {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.vditor-tooltipped {
position: relative;
cursor: pointer;
&::after {
position: absolute;
z-index: 1000000;
display: none;
padding: 5px 8px;
font-size: 11px;
font-weight: normal;
-webkit-font-smoothing: subpixel-antialiased;
color: #fff;
text-align: center;
text-decoration: none;
text-shadow: none;
text-transform: none;
letter-spacing: normal;
word-wrap: break-word;
white-space: pre;
pointer-events: none;
content: attr(aria-label);
background: #3b3e43;
border-radius: 3px;
line-height: 16px;
opacity: 0;
}
&::before {
position: absolute;
z-index: 1000001;
display: none;
width: 0;
height: 0;
color: #3b3e43;
pointer-events: none;
content: "";
border: 5px solid transparent;
opacity: 0;
}
&--hover::before,
&--hover::after,
&:hover::before,
&:hover::after,
&:active::before,
&:active::after,
&:focus::before,
&:focus::after {
display: inline-block;
text-decoration: none;
animation-name: tooltip-appear;
animation-duration: 0.15s;
animation-fill-mode: forwards;
animation-timing-function: ease-in;
}
&__s::after,
&__se::after,
&__sw::after {
top: 100%;
right: 50%;
margin-top: 5px;
}
&__s::before,
&__se::before,
&__sw::before {
top: auto;
right: 50%;
bottom: -5px;
margin-right: -5px;
border-bottom-color: #3b3e43;
}
&__se::after {
right: auto;
left: 50%;
margin-left: -15px;
}
&__sw::after {
margin-right: -15px;
}
&__n::after,
&__ne::after,
&__nw::after {
right: 50%;
bottom: 100%;
margin-bottom: 5px;
}
&__n::before,
&__ne::before,
&__nw::before {
top: -5px;
right: 50%;
bottom: auto;
margin-right: -5px;
border-top-color: #3b3e43;
}
&__ne::after {
right: auto;
left: 50%;
margin-left: -15px;
}
&__nw::after {
margin-right: -15px;
}
&__s::after,
&__n::after {
transform: translateX(50%);
}
&__w::after {
right: 100%;
bottom: 50%;
margin-right: 5px;
transform: translateY(50%);
}
&__w::before {
top: 50%;
bottom: 50%;
left: -5px;
margin-top: -5px;
border-left-color: #3b3e43;
}
&__e::after {
bottom: 50%;
left: 100%;
margin-left: 5px;
transform: translateY(50%);
}
&__e::before {
top: 50%;
right: -5px;
bottom: 50%;
margin-top: -5px;
border-right-color: #3b3e43;
}
}
@media screen and (max-width: @max-width) {
.vditor-tooltipped:before,
.vditor-tooltipped:after {
content: none;
}
}

View File

@@ -0,0 +1,195 @@
.vditor-wysiwyg {
box-sizing: border-box;
flex: 1;
position: relative;
width: 100%;
min-width: 1px;
pre.vditor-reset {
background-color: var(--panel-background-color);
margin: 0;
white-space: pre-wrap;
height: 100%;
box-sizing: border-box;
&[contenteditable="false"] {
opacity: 0.3;
cursor: not-allowed;
}
&:empty::before {
content: attr(placeholder);
color: var(--second-color);
}
&:focus {
outline: none;
background-color: var(--textarea-background-color);
}
&:after {
content: "";
height: var(--editor-bottom);
display: block;
}
}
blockquote:empty::before,
pre > code:empty::before,
p:empty::before,
h1:empty::after,
h2:empty::after,
h3:empty::after,
h4:empty::after,
h5:empty::after,
h6:empty::after {
content: ' ';
}
code[data-marker="`"] {
padding-left: 0 !important;
padding-right: 0 !important;
}
&__block {
pre:first-child {
margin-bottom: -1em;
code {
height: auto;
color: var(--textarea-text-color);
height: auto;
text-align: left;
}
}
pre:last-child {
margin-bottom: 1em;
}
}
&__preview {
cursor: pointer;
white-space: initial;
min-height: 27px;
}
& > .vditor-reset > h1:before,
& > .vditor-reset > h2:before,
& > .vditor-reset > h3:before,
& > .vditor-reset > h4:before,
& > .vditor-reset > h5:before,
& > .vditor-reset > h6:before,
div.vditor-wysiwyg__block:before,
div[data-type="link-ref-defs-block"]:before,
div[data-type="footnotes-block"]:before,
.vditor-toc:before {
float: left;
padding-right: 4px;
margin-left: -29px;
content: 'H1';
font-size: 0.85rem;
font-weight: normal;
color: var(--second-color);
}
& > .vditor-reset > h2:before {
content: 'H2';
}
& > .vditor-reset > h3:before {
content: 'H3';
}
& > .vditor-reset > h4:before {
content: 'H4';
}
& > .vditor-reset > h5:before {
content: 'H5';
}
& > .vditor-reset > h6:before {
content: 'H6';
}
div[data-type="link-ref-defs-block"]:before {
content: '"A"';
}
div[data-type="footnotes-block"]:before {
content: '^F';
}
div.vditor-wysiwyg__block:before {
content: "</>";
}
div.vditor-wysiwyg__block[data-type="yaml-front-matter"]:before {
content: "F";
}
div.vditor-wysiwyg__block[data-type="math-block"]:before {
content: "$$";
}
.vditor-toc:before {
content: "ToC";
}
hr {
display: inline-block;
margin: 12px 0;
width: 100%;
}
details {
white-space: initial;
}
a {
cursor: pointer;
}
span[data-type="backslash"] > span {
display: none;
color: var(--second-color);
}
span[data-type="link-ref"],
sup[data-type="footnotes-ref"] {
color: @blurColor;
}
span[data-type="toc-h"] {
color: @blurColor;
text-decoration: underline;
}
div[data-type="footnotes-block"] {
border-top: 2px solid var(--heading-border-color);
padding-top: 24px;
margin-top: 24px;
}
div[data-type="link-ref-defs-block"] {
color: var(--blockquote-color);
}
}
@media screen and (max-width: @max-width) {
.vditor-wysiwyg {
h1:before,
h2:before,
h3:before,
h4:before,
h5:before,
h6:before,
div.vditor-wysiwyg__block:before,
div[data-type="link-ref-defs-block"]:before,
div[data-type="footnotes-block"]:before,
.vditor-toc:before {
content: none;
}
}
}

View File

@@ -0,0 +1,88 @@
@errorColor: #d23f31;
@blurColor: #4285f4;
@font-family-base: "Helvetica Neue", "Luxi Sans", "DejaVu Sans",
"Hiragino Sans GB", "Microsoft Yahei", sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji",
"EmojiSymbols";
@font-family-code: mononoki, Consolas, "Liberation Mono", Menlo, Courier,
monospace, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
"Segoe UI Symbol", "Android Emoji", "EmojiSymbols";
@transition: all 0.15s ease-in-out;
@max-width: 520px;
.vditor {
--border-color: #d1d5da;
--second-color: rgba(88, 96, 105, 0.36);
--panel-background-color: #fff;
--panel-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
--toolbar-background-color: #f6f8fa;
--toolbar-icon-color: #586069;
--toolbar-icon-hover-color: @blurColor;
--toolbar-height: 35px;
--toolbar-divider-margin-top: 8px;
--textarea-background-color: #fafbfc;
--textarea-text-color: #24292e;
--resize-icon-color: var(--toolbar-icon-color);
--resize-background-color: var(--toolbar-background-color);
--resize-hover-icon-color: var(--panel-background-color);
--resize-hover-background-color: var(--toolbar-icon-hover-color);
--count-background-color: rgba(27, 31, 35, 0.05);
--heading-border-color: #eaecef;
--blockquote-color: #6a737d;
--ir-heading-color: #660e7a;
--ir-title-color: #808080;
--ir-bi-color: #0033b3;
--ir-link-color: #008000;
--ir-bracket-color: #0000ff;
--ir-paren-color: #008000;
&--dark {
--border-color: #141414;
--second-color: rgba(185, 185, 185, 0.36);
--panel-background-color: #24292e;
--panel-shadow: 0 1px 2px rgba(255, 255, 255, 0.2);
--toolbar-background-color: #1d2125;
--toolbar-icon-color: #b9b9b9;
--toolbar-icon-hover-color: #fff;
--textarea-background-color: #2f363d;
--textarea-text-color: #d1d5da;
--resize-icon-color: var(--border-color);
--resize-background-color: var(--second-color);
--resize-hover-icon-color: var(--toolbar-icon-hover-color);
--resize-hover-background-color: rgba(185, 185, 185, 0.86);
--count-background-color: rgba(@blurColor, 0.36);
--heading-border-color: var(--textarea-text-color);
--blockquote-color: var(--toolbar-icon-color);
--ir-heading-color: #9876aa;
--ir-title-color: #808080;
--ir-bi-color: #cc7832;
--ir-link-color: #ffc66d;
--ir-bracket-color: #287bde;
--ir-paren-color: #6a8759;
}
}
@import "_tooltipped";
@import "_panel";
@import "_toolbar";
@import "_content";
@import "_hint";
@import "_reset";
@import "_wysiwyg";
@import "_ir";
@import "_sv";

View File

@@ -0,0 +1,553 @@
import "./assets/less/index.less";
import VditorMethod from "./method";
import {Constants, VDITOR_VERSION} from "./ts/constants";
import {DevTools} from "./ts/devtools/index";
import {Hint} from "./ts/hint/index";
import {IR} from "./ts/ir/index";
import {input as irInput} from "./ts/ir/input";
import {processAfterRender} from "./ts/ir/process";
import {getHTML} from "./ts/markdown/getHTML";
import {getMarkdown} from "./ts/markdown/getMarkdown";
import {setLute} from "./ts/markdown/setLute";
import {Outline} from "./ts/outline/index";
import {Preview} from "./ts/preview/index";
import {Resize} from "./ts/resize/index";
import {Editor} from "./ts/sv/index";
import {inputEvent} from "./ts/sv/inputEvent";
import {processAfterRender as processSVAfterRender, processPaste} from "./ts/sv/process";
import {Tip} from "./ts/tip/index";
import {Toolbar} from "./ts/toolbar/index";
import {disableToolbar, hidePanel} from "./ts/toolbar/setToolbar";
import {enableToolbar} from "./ts/toolbar/setToolbar";
import {initUI, UIUnbindListener} from "./ts/ui/initUI";
import {setCodeTheme} from "./ts/ui/setCodeTheme";
import {setContentTheme} from "./ts/ui/setContentTheme";
import {setPreviewMode} from "./ts/ui/setPreviewMode";
import {setTheme} from "./ts/ui/setTheme";
import {Undo} from "./ts/undo/index";
import {Upload} from "./ts/upload/index";
import {addScript, addScriptSync} from "./ts/util/addScript";
import {getSelectText} from "./ts/util/getSelectText";
import {Options} from "./ts/util/Options";
import {processCodeRender} from "./ts/util/processCode";
import {getCursorPosition, getEditorRange, insertHTML} from "./ts/util/selection";
import {afterRenderEvent} from "./ts/wysiwyg/afterRenderEvent";
import {WYSIWYG} from "./ts/wysiwyg/index";
import {input} from "./ts/wysiwyg/input";
import {renderDomByMd} from "./ts/wysiwyg/renderDomByMd";
import {execAfterRender} from "./ts/util/fixBrowserBehavior";
import {accessLocalStorage} from "./ts/util/compatibility";
class Vditor extends VditorMethod {
public readonly version: string;
public vditor: IVditor;
/**
* @param id 要挂载 Vditor 的元素或者元素 ID。
* @param options Vditor 参数
*/
constructor(id: string | HTMLElement, options?: IOptions) {
super();
this.version = VDITOR_VERSION;
if (typeof id === "string") {
if (!options) {
options = {
cache: {
id: `vditor${id}`,
},
};
} else if (!options.cache) {
options.cache = {id: `vditor${id}`};
} else if (!options.cache.id) {
options.cache.id = `vditor${id}`;
}
if (!document.getElementById(id)) {
this.showErrorTip(`Failed to get element by id: ${id}`);
return;
}
id = document.getElementById(id);
}
const getOptions = new Options(options);
const mergedOptions = getOptions.merge();
// 支持自定义国际化
if (!mergedOptions.i18n) {
if (!["en_US", "fr_FR", "pt_BR", "ja_JP", "ko_KR", "ru_RU", "sv_SE", "zh_CN", "zh_TW"].includes(mergedOptions.lang)) {
throw new Error(
"options.lang error, see https://ld246.com/article/1549638745630#options",
);
} else {
const i18nScriptPrefix = "vditorI18nScript";
const i18nScriptID = i18nScriptPrefix + mergedOptions.lang;
document.querySelectorAll(`head script[id^="${i18nScriptPrefix}"]`).forEach((el) => {
if (el.id !== i18nScriptID) {
document.head.removeChild(el);
}
});
addScript(`${mergedOptions.cdn}/dist/js/i18n/${mergedOptions.lang}.js`, i18nScriptID).then(() => {
this.init(id as HTMLElement, mergedOptions);
}).catch(error => {
this.showErrorTip(`GET ${mergedOptions.cdn}/dist/js/i18n/${mergedOptions.lang}.js net::ERR_ABORTED 404 (Not Found)`);
});
}
} else {
window.VditorI18n = mergedOptions.i18n;
this.init(id, mergedOptions);
}
}
private showErrorTip(error: string) {
const tip = new Tip();
document.body.appendChild(tip.element);
tip.show(error, 0)
}
public updateToolbarConfig(options: IToolbarConfig) {
this.vditor.toolbar.updateConfig(this.vditor, options);
}
/** 设置主题 */
public setTheme(
theme: "dark" | "classic",
contentTheme?: string,
codeTheme?: string,
contentThemePath?: string,
) {
this.vditor.options.theme = theme;
setTheme(this.vditor);
if (contentTheme) {
this.vditor.options.preview.theme.current = contentTheme;
setContentTheme(contentTheme, contentThemePath || this.vditor.options.preview.theme.path);
}
if (codeTheme) {
this.vditor.options.preview.hljs.style = codeTheme;
setCodeTheme(codeTheme, this.vditor.options.cdn);
}
}
/** 获取 Markdown 内容 */
public getValue() {
return getMarkdown(this.vditor);
}
/** 获取编辑器当前编辑模式 */
public getCurrentMode() {
return this.vditor.currentMode;
}
/** 聚焦到编辑器 */
public focus() {
if (this.vditor.currentMode === "sv") {
this.vditor.sv.element.focus();
} else if (this.vditor.currentMode === "wysiwyg") {
this.vditor.wysiwyg.element.focus();
} else if (this.vditor.currentMode === "ir") {
this.vditor.ir.element.focus();
}
}
/** 让编辑器失焦 */
public blur() {
if (this.vditor.currentMode === "sv") {
this.vditor.sv.element.blur();
} else if (this.vditor.currentMode === "wysiwyg") {
this.vditor.wysiwyg.element.blur();
} else if (this.vditor.currentMode === "ir") {
this.vditor.ir.element.blur();
}
}
/** 禁用编辑器 */
public disabled() {
hidePanel(this.vditor, ["subToolbar", "hint", "popover"]);
disableToolbar(
this.vditor.toolbar.elements,
Constants.EDIT_TOOLBARS.concat(["undo", "redo", "fullscreen", "edit-mode"]),
);
this.vditor[this.vditor.currentMode].element.setAttribute(
"contenteditable",
"false",
);
}
/** 解除编辑器禁用 */
public enable() {
enableToolbar(
this.vditor.toolbar.elements,
Constants.EDIT_TOOLBARS.concat(["undo", "redo", "fullscreen", "edit-mode"]),
);
this.vditor.undo.resetIcon(this.vditor);
this.vditor[this.vditor.currentMode].element.setAttribute("contenteditable", "true");
}
/** 返回选中的字符串 */
public getSelection() {
if (this.vditor.currentMode === "wysiwyg") {
return getSelectText(this.vditor.wysiwyg.element);
} else if (this.vditor.currentMode === "sv") {
return getSelectText(this.vditor.sv.element);
} else if (this.vditor.currentMode === "ir") {
return getSelectText(this.vditor.ir.element);
}
}
/** 设置预览区域内容 */
public renderPreview(value?: string) {
this.vditor.preview.render(this.vditor, value);
}
/** 获取焦点位置 */
public getCursorPosition() {
return getCursorPosition(this.vditor[this.vditor.currentMode].element);
}
/** 上传是否还在进行中 */
public isUploading() {
return this.vditor.upload.isUploading;
}
/** 清除缓存 */
public clearCache() {
if (this.vditor.options.cache.enable && accessLocalStorage()) {
localStorage.removeItem(this.vditor.options.cache.id);
}
}
/** 禁用缓存 */
public disabledCache() {
this.vditor.options.cache.enable = false;
}
/** 启用缓存 */
public enableCache() {
if (!this.vditor.options.cache.id) {
throw new Error(
"need options.cache.id, see https://ld246.com/article/1549638745630#options",
);
}
this.vditor.options.cache.enable = true;
}
/** HTML 转 md */
public html2md(value: string) {
return this.vditor.lute.HTML2Md(value);
}
/** markdown 转 JSON 输出 */
public exportJSON(value: string) {
return this.vditor.lute.RenderJSON(value);
}
/** 获取 HTML */
public getHTML() {
return getHTML(this.vditor);
}
/** 消息提示。time 为 0 将一直显示 */
public tip(text: string, time?: number) {
this.vditor.tip.show(text, time);
}
/** 设置预览模式 */
public setPreviewMode(mode: "both" | "editor") {
setPreviewMode(mode, this.vditor);
}
/** 删除选中内容 */
public deleteValue() {
if (window.getSelection().isCollapsed) {
return;
}
document.execCommand("delete", false);
}
/** 更新选中内容 */
public updateValue(value: string) {
document.execCommand("insertHTML", false, value);
}
/** 在焦点处插入内容,并默认进行 Markdown 渲染 */
public insertValue(value: string, render = true) {
const range = getEditorRange(this.vditor);
range.collapse(true);
// https://github.com/Vanessa219/vditor/issues/716
// https://github.com/Vanessa219/vditor/issues/917
const tmpElement = document.createElement("template");
tmpElement.innerHTML = value;
range.insertNode(tmpElement.content.cloneNode(true));
range.collapse(false);
if (this.vditor.currentMode === "sv") {
this.vditor.sv.preventInput = true;
if (render) {
inputEvent(this.vditor);
}
} else if (this.vditor.currentMode === "wysiwyg") {
// 由于 https://github.com/Vanessa219/vditor/issues/1566 不能使用 this.vditor.wysiwyg.preventInput = true;
if (render) {
input(this.vditor, getSelection().getRangeAt(0));
}
} else if (this.vditor.currentMode === "ir") {
this.vditor.ir.preventInput = true;
if (render) {
irInput(this.vditor, getSelection().getRangeAt(0), true);
}
}
}
/** 在焦点处插入 Markdown */
public insertMD(md: string) {
// https://github.com/Vanessa219/vditor/issues/1640
if (this.vditor.currentMode === "ir") {
insertHTML(this.vditor.lute.Md2VditorIRDOM(md), this.vditor);
} else if (this.vditor.currentMode === "wysiwyg") {
insertHTML(this.vditor.lute.Md2VditorDOM(md), this.vditor);
} else {
processPaste(this.vditor, md);
}
this.vditor.outline.render(this.vditor);
execAfterRender(this.vditor);
}
/** 设置编辑器内容 */
public setValue(markdown: string, clearStack = false) {
if (this.vditor.currentMode === "sv") {
this.vditor.sv.element.innerHTML = `<div data-block='0'>${this.vditor.lute.SpinVditorSVDOM(markdown)}</div>`;
processSVAfterRender(this.vditor, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
} else if (this.vditor.currentMode === "wysiwyg") {
renderDomByMd(this.vditor, markdown, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
} else {
this.vditor.ir.element.innerHTML = this.vditor.lute.Md2VditorIRDOM(markdown);
this.vditor.ir.element
.querySelectorAll(".vditor-ir__preview[data-render='2']")
.forEach((item: HTMLElement) => {
processCodeRender(item, this.vditor);
});
processAfterRender(this.vditor, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
}
this.vditor.outline.render(this.vditor);
if (!markdown) {
hidePanel(this.vditor, ["emoji", "headings", "submenu", "hint"]);
if (this.vditor.wysiwyg.popover) {
this.vditor.wysiwyg.popover.style.display = "none";
}
this.clearCache();
}
if (clearStack) {
this.clearStack();
}
}
/** 清空 undo & redo 栈 */
public clearStack() {
this.vditor.undo.clearStack(this.vditor);
this.vditor.undo.addToUndoStack(this.vditor);
}
/** 销毁编辑器 */
public destroy() {
this.vditor.element.innerHTML = this.vditor.originalInnerHTML;
this.vditor.element.classList.remove("vditor");
this.vditor.element.removeAttribute("style");
const iconScript = document.getElementById("vditorIconScript")
if (iconScript) {
iconScript.remove();
}
this.clearCache();
UIUnbindListener();
this.vditor.wysiwyg.unbindListener();
}
/** 获取评论 ID */
public getCommentIds() {
if (this.vditor.currentMode !== "wysiwyg") {
return [];
}
return this.vditor.wysiwyg.getComments(this.vditor, true);
}
/** 高亮评论 */
public hlCommentIds(ids: string[]) {
if (this.vditor.currentMode !== "wysiwyg") {
return;
}
const hlItem = (item: Element) => {
item.classList.remove("vditor-comment--hover");
ids.forEach((id) => {
if (item.getAttribute("data-cmtids").indexOf(id) > -1) {
item.classList.add("vditor-comment--hover");
}
});
};
this.vditor.wysiwyg.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
hlItem(item);
});
if (this.vditor.preview.element.style.display !== "none") {
this.vditor.preview.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
hlItem(item);
});
}
}
/** 取消评论高亮 */
public unHlCommentIds(ids: string[]) {
if (this.vditor.currentMode !== "wysiwyg") {
return;
}
const unHlItem = (item: Element) => {
ids.forEach((id) => {
if (item.getAttribute("data-cmtids").indexOf(id) > -1) {
item.classList.remove("vditor-comment--hover");
}
});
};
this.vditor.wysiwyg.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
unHlItem(item);
});
if (this.vditor.preview.element.style.display !== "none") {
this.vditor.preview.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
unHlItem(item);
});
}
}
/** 删除评论 */
public removeCommentIds(removeIds: string[]) {
if (this.vditor.currentMode !== "wysiwyg") {
return;
}
const removeItem = (item: Element, removeId: string) => {
const ids = item.getAttribute("data-cmtids").split(" ");
ids.find((id, index) => {
if (id === removeId) {
ids.splice(index, 1);
return true;
}
});
if (ids.length === 0) {
item.outerHTML = item.innerHTML;
getEditorRange(this.vditor).collapse(true);
} else {
item.setAttribute("data-cmtids", ids.join(" "));
}
};
removeIds.forEach((removeId) => {
this.vditor.wysiwyg.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
removeItem(item, removeId);
});
if (this.vditor.preview.element.style.display !== "none") {
this.vditor.preview.element
.querySelectorAll(".vditor-comment")
.forEach((item) => {
removeItem(item, removeId);
});
}
});
afterRenderEvent(this.vditor, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
}
private init(id: HTMLElement, mergedOptions: IOptions) {
this.vditor = {
currentMode: mergedOptions.mode,
element: id,
hint: new Hint(mergedOptions.hint.extend),
lute: undefined,
options: mergedOptions,
originalInnerHTML: id.innerHTML,
outline: new Outline(window.VditorI18n.outline),
tip: new Tip(),
};
this.vditor.sv = new Editor(this.vditor);
this.vditor.undo = new Undo();
this.vditor.wysiwyg = new WYSIWYG(this.vditor);
this.vditor.ir = new IR(this.vditor);
this.vditor.toolbar = new Toolbar(this.vditor);
if (mergedOptions.resize.enable) {
this.vditor.resize = new Resize(this.vditor);
}
if (this.vditor.toolbar.elements.devtools) {
this.vditor.devtools = new DevTools();
}
if (mergedOptions.upload.url || mergedOptions.upload.handler) {
this.vditor.upload = new Upload();
}
addScript(
mergedOptions._lutePath ||
`${mergedOptions.cdn}/dist/js/lute/lute.min.js`,
"vditorLuteScript",
).then(() => {
this.vditor.lute = setLute({
autoSpace: this.vditor.options.preview.markdown.autoSpace,
gfmAutoLink: this.vditor.options.preview.markdown.gfmAutoLink,
codeBlockPreview: this.vditor.options.preview.markdown
.codeBlockPreview,
emojiSite: this.vditor.options.hint.emojiPath,
emojis: this.vditor.options.hint.emoji,
fixTermTypo: this.vditor.options.preview.markdown.fixTermTypo,
footnotes: this.vditor.options.preview.markdown.footnotes,
headingAnchor: false,
inlineMathDigit: this.vditor.options.preview.math.inlineDigit,
linkBase: this.vditor.options.preview.markdown.linkBase,
linkPrefix: this.vditor.options.preview.markdown.linkPrefix,
listStyle: this.vditor.options.preview.markdown.listStyle,
mark: this.vditor.options.preview.markdown.mark,
mathBlockPreview: this.vditor.options.preview.markdown
.mathBlockPreview,
paragraphBeginningSpace: this.vditor.options.preview.markdown
.paragraphBeginningSpace,
sanitize: this.vditor.options.preview.markdown.sanitize,
toc: this.vditor.options.preview.markdown.toc,
});
this.vditor.preview = new Preview(this.vditor);
initUI(this.vditor);
if (mergedOptions.after) {
mergedOptions.after();
}
if (mergedOptions.icon) {
// 防止初始化 2 个编辑器时加载 2 次
addScriptSync(`${mergedOptions.cdn}/dist/js/icons/${mergedOptions.icon}.js`, "vditorIconScript");
}
});
}
}
export default Vditor;

View File

@@ -0,0 +1,68 @@
import {abcRender} from "./ts/markdown/abcRender";
import * as adapterRender from "./ts/markdown/adapterRender";
import {chartRender} from "./ts/markdown/chartRender";
import {codeRender} from "./ts/markdown/codeRender";
import {flowchartRender} from "./ts/markdown/flowchartRender";
import {graphvizRender} from "./ts/markdown/graphvizRender";
import {highlightRender} from "./ts/markdown/highlightRender";
import {lazyLoadImageRender} from "./ts/markdown/lazyLoadImageRender";
import {mathRender} from "./ts/markdown/mathRender";
import {mediaRender} from "./ts/markdown/mediaRender";
import {mermaidRender} from "./ts/markdown/mermaidRender";
import {markmapRender} from "./ts/markdown/markmapRender";
import {mindmapRender} from "./ts/markdown/mindmapRender";
import {outlineRender} from "./ts/markdown/outlineRender";
import {plantumlRender} from "./ts/markdown/plantumlRender";
import {md2html, previewRender} from "./ts/markdown/previewRender";
import {speechRender} from "./ts/markdown/speechRender";
import {previewImage} from "./ts/preview/image";
import {setCodeTheme} from "./ts/ui/setCodeTheme";
import {setContentTheme} from "./ts/ui/setContentTheme";
class Vditor {
/** 点击图片放大 */
public static adapterRender = adapterRender;
/** 点击图片放大 */
public static previewImage = previewImage;
/** 为 element 中的代码块添加复制按钮 */
public static codeRender = codeRender;
/** 对 graphviz 进行渲染 */
public static graphvizRender = graphvizRender;
/** 为 element 中的代码块进行高亮渲染 */
public static highlightRender = highlightRender;
/** 对数学公式进行渲染 */
public static mathRender = mathRender;
/** 流程图/时序图/甘特图渲染 */
public static mermaidRender = mermaidRender;
/** 支持markdown的思维导图 */
public static markmapRender = markmapRender;
/** flowchart.js 渲染 */
public static flowchartRender = flowchartRender;
/** 图表渲染 */
public static chartRender = chartRender;
/** 五线谱渲染 */
public static abcRender = abcRender;
/** 脑图渲染 */
public static mindmapRender = mindmapRender;
/** plantuml渲染 */
public static plantumlRender = plantumlRender;
/** 大纲渲染 */
public static outlineRender = outlineRender;
/** 为[特定链接](https://github.com/Vanessa219/vditor/issues/7)分别渲染为视频、音频、嵌入的 iframe */
public static mediaRender = mediaRender;
/** 对选中的文字进行阅读 */
public static speechRender = speechRender;
/** 对图片进行懒加载 */
public static lazyLoadImageRender = lazyLoadImageRender;
/** Markdown 文本转换为 HTML该方法需使用[异步编程](https://ld246.com/article/1546828434083?r=Vaness) */
public static md2html = md2html;
/** 页面 Markdown 文章渲染 */
public static preview = previewRender;
/** 设置代码主题 */
public static setCodeTheme = setCodeTheme;
/** 设置内容主题 */
public static setContentTheme = setContentTheme;
}
export default Vditor;

View File

@@ -0,0 +1,69 @@
declare const VDITOR_VERSION: string;
const _VDITOR_VERSION = VDITOR_VERSION;
export {_VDITOR_VERSION as VDITOR_VERSION};
export abstract class Constants {
public static readonly ZWSP: string = "\u200b";
public static readonly DROP_EDITOR: string = "application/editor";
public static readonly MOBILE_WIDTH: number = 520;
public static readonly CLASS_MENU_DISABLED: string = "vditor-menu--disabled";
public static readonly EDIT_TOOLBARS: string[] = ["emoji", "headings", "bold", "italic", "strike", "link", "list",
"ordered-list", "outdent", "indent", "check", "line", "quote", "code", "inline-code", "insert-after",
"insert-before", "upload", "record", "table"];
public static readonly CODE_THEME: string[] = ["abap", "algol", "algol_nu", "arduino", "autumn", "borland", "bw",
"colorful", "dracula", "emacs", "friendly", "fruity", "github", "igor", "lovelace", "manni", "monokai",
"monokailight", "murphy", "native", "paraiso-dark", "paraiso-light", "pastie", "perldoc", "pygments",
"rainbow_dash", "rrt", "solarized-dark", "solarized-dark256", "solarized-light", "swapoff", "tango", "trac",
"vim", "vs", "xcode", "ant-design"];
public static readonly CODE_LANGUAGES: string[] = ["mermaid", "echarts", "mindmap", "plantuml", "abc", "graphviz", "flowchart", "apache",
"js", "ts", "html","markmap",
// common
"properties", "apache", "bash", "c", "csharp", "cpp", "css", "coffeescript", "diff", "go", "xml", "http",
"json", "java", "javascript", "kotlin", "less", "lua", "makefile", "markdown", "nginx", "objectivec", "php",
"php-template", "perl", "plaintext", "python", "python-repl", "r", "ruby", "rust", "scss", "sql", "shell",
"swift", "ini", "typescript", "vbnet", "yaml",
"ada", "clojure", "dart", "erb", "fortran", "gradle", "haskell", "julia", "julia-repl", "lisp", "matlab",
"pgsql", "powershell", "sql_more", "stata", "cmake", "mathematica",
// ext
"solidity", "yul"
];
public static readonly CDN = `https://unpkg.com/vditor@${VDITOR_VERSION}`;
public static readonly MARKDOWN_OPTIONS = {
autoSpace: false,
gfmAutoLink: true,
codeBlockPreview: true,
fixTermTypo: false,
footnotes: true,
linkBase: "",
linkPrefix: "",
listStyle: false,
mark: false,
mathBlockPreview: true,
paragraphBeginningSpace: false,
sanitize: true,
toc: false,
};
public static readonly HLJS_OPTIONS = {
enable: true,
lineNumber: false,
defaultLang: "",
style: "github",
};
public static readonly MATH_OPTIONS: IMath = {
engine: "KaTeX",
inlineDigit: false,
macros: {},
};
public static readonly THEME_OPTIONS = {
current: "light",
list: {
"ant-design": "Ant Design",
"dark": "Dark",
"light": "Light",
"wechat": "WeChat",
},
path: `${Constants.CDN}/dist/css/content-theme`,
};
}

View File

@@ -0,0 +1,83 @@
import {getMarkdown} from "../markdown/getMarkdown";
import {addScript} from "../util/addScript";
declare const echarts: {
init(element: HTMLElement): IEChart;
};
export class DevTools {
public element: HTMLDivElement;
private ASTChart: IEChart;
constructor() {
this.element = document.createElement("div");
this.element.className = "vditor-devtools";
this.element.innerHTML = '<div class="vditor-reset--error"></div><div style="height: 100%;"></div>';
}
public renderEchart(vditor: IVditor) {
if (vditor.devtools.element.style.display !== "block") {
return;
}
addScript(`${vditor.options.cdn}/dist/js/echarts/echarts.min.js?v=5.5.1`, "vditorEchartsScript").then(() => {
if (!this.ASTChart) {
this.ASTChart = echarts.init(vditor.devtools.element.lastElementChild as HTMLDivElement);
}
try {
(this.element.lastElementChild as HTMLElement).style.display = "block";
this.element.firstElementChild.innerHTML = "";
this.ASTChart.setOption({
series: [
{
data: JSON.parse(vditor.lute.RenderEChartsJSON(getMarkdown(vditor))),
initialTreeDepth: -1,
label: {
align: "left",
backgroundColor: "rgba(68, 77, 86, .68)",
borderRadius: 3,
color: "#d1d5da",
fontSize: 12,
lineHeight: 12,
offset: [9, 12],
padding: [2, 4, 2, 4],
position: "top",
verticalAlign: "middle",
},
lineStyle: {
color: "#4285f4",
type: "curve",
width: 1,
},
orient: "vertical",
roam: true,
type: "tree",
},
],
toolbox: {
bottom: 25,
emphasis: {
iconStyle: {
color: "#4285f4",
},
},
feature: {
restore: {
show: true,
},
saveAsImage: {
show: true,
},
},
right: 15,
show: true,
},
});
this.ASTChart.resize();
} catch (e) {
(this.element.lastElementChild as HTMLElement).style.display = "none";
this.element.firstElementChild.innerHTML = e;
}
});
}
}

View File

@@ -0,0 +1,83 @@
import {getHTML} from "../markdown/getHTML";
import {getMarkdown} from "../markdown/getMarkdown";
export const download = (vditor: IVditor, content: string, filename: string) => {
const aElement = document.createElement("a");
if ("download" in aElement) {
aElement.download = filename;
aElement.style.display = "none";
aElement.href = URL.createObjectURL(new Blob([content]));
document.body.appendChild(aElement);
aElement.click();
aElement.remove();
} else {
vditor.tip.show(window.VditorI18n.downloadTip, 0);
}
};
export const exportMarkdown = (vditor: IVditor) => {
const content = getMarkdown(vditor);
download(vditor, content, content.substr(0, 10) + ".md");
};
export const exportPDF = (vditor: IVditor) => {
vditor.tip.show(window.VditorI18n.generate, 3800);
const iframe = document.querySelector("#vditorExportIframe") as HTMLIFrameElement;
iframe.contentDocument.open();
iframe.contentDocument.write(`<link rel="stylesheet" href="${vditor.options.cdn}/dist/index.css"/>
<script src="${vditor.options.cdn}/dist/method.min.js"></script>
<div id="preview" style="width: 800px"></div>
<script>
window.addEventListener("message", (e) => {
if(!e.data) {
return;
}
Vditor.preview(document.getElementById('preview'), e.data, {
cdn: "${vditor.options.cdn}",
markdown: {
theme: ${JSON.stringify(vditor.options.preview.theme)}
},
hljs: {
style: "${vditor.options.preview.hljs.style}"
}
});
setTimeout(() => {
window.print();
}, 3600);
}, false);
</script>`);
iframe.contentDocument.close();
setTimeout(() => {
iframe.contentWindow.postMessage(getMarkdown(vditor), "*");
}, 200);
};
export const exportHTML = (vditor: IVditor) => {
const content = getHTML(vditor);
const html = `<html><head><link rel="stylesheet" type="text/css" href="${vditor.options.cdn}/dist/index.css"/>
<script src="${vditor.options.cdn}/dist/js/i18n/${vditor.options.lang}.js"></script>
<script src="${vditor.options.cdn}/dist/method.min.js"></script></head>
<body><div class="vditor-reset" id="preview">${content}</div>
<script>
const previewElement = document.getElementById('preview')
Vditor.setContentTheme('${vditor.options.preview.theme.current}', '${vditor.options.preview.theme.path}');
Vditor.codeRender(previewElement);
Vditor.highlightRender(${JSON.stringify(vditor.options.preview.hljs)}, previewElement, '${vditor.options.cdn}');
Vditor.mathRender(previewElement, {
cdn: '${vditor.options.cdn}',
math: ${JSON.stringify(vditor.options.preview.math)},
});
Vditor.mermaidRender(previewElement, '${vditor.options.cdn}', '${vditor.options.theme}');
Vditor.markmapRender(previewElement, '${vditor.options.cdn}', '${vditor.options.theme}');
Vditor.flowchartRender(previewElement, '${vditor.options.cdn}');
Vditor.graphvizRender(previewElement, '${vditor.options.cdn}');
Vditor.chartRender(previewElement, '${vditor.options.cdn}', '${vditor.options.theme}');
Vditor.mindmapRender(previewElement, '${vditor.options.cdn}', '${vditor.options.theme}');
Vditor.abcRender(previewElement, '${vditor.options.cdn}');
${vditor.options.preview.render.media.enable ? 'Vditor.mediaRender(previewElement);' : ""}
Vditor.speechRender(previewElement);
</script>
<script src="${vditor.options.cdn}/dist/js/icons/${vditor.options.icon}.js"></script></body></html>`;
download(vditor, html, content.substr(0, 10) + ".html");
};

View File

@@ -0,0 +1,276 @@
import {Constants} from "../constants";
import {processAfterRender} from "../ir/process";
import {code160to32} from "../util/code160to32";
import {isCtrl} from "../util/compatibility";
import {execAfterRender} from "../util/fixBrowserBehavior";
import {hasClosestByAttribute, hasClosestByClassName} from "../util/hasClosest";
import {processCodeRender} from "../util/processCode";
import {getCursorPosition, insertHTML, setSelectionFocus} from "../util/selection";
export class Hint {
public timeId: number;
public element: HTMLDivElement;
public recentLanguage: string;
private splitChar = "";
private lastIndex = -1;
constructor(hintExtends: IHintExtend[]) {
this.timeId = -1;
this.element = document.createElement("div");
this.element.className = "vditor-hint";
this.recentLanguage = "";
hintExtends.push({key: ":"});
}
public render(vditor: IVditor) {
if (!window.getSelection().focusNode) {
return;
}
let currentLineValue: string;
const range = getSelection().getRangeAt(0);
currentLineValue = range.startContainer.textContent.substring(0, range.startOffset) || "";
const key = this.getKey(currentLineValue, vditor.options.hint.extend);
if (typeof key === "undefined") {
this.element.style.display = "none";
clearTimeout(this.timeId);
} else {
if (this.splitChar === ":") {
const emojiHint = key === "" ? vditor.options.hint.emoji : vditor.lute.GetEmojis();
const matchEmojiData: IHintData[] = [];
Object.keys(emojiHint).forEach((keyName) => {
if (keyName.indexOf(key.toLowerCase()) === 0) {
if (emojiHint[keyName].indexOf(".") > -1) {
matchEmojiData.push({
html: `<img src="${emojiHint[keyName]}" title=":${keyName}:"/> :${keyName}:`,
value: `:${keyName}:`,
});
} else {
matchEmojiData.push({
html: `<span class="vditor-hint__emoji">${emojiHint[keyName]}</span>${keyName}`,
value: emojiHint[keyName],
});
}
}
});
this.genHTML(matchEmojiData, key, vditor);
} else {
vditor.options.hint.extend.forEach((item) => {
if (item.key === this.splitChar) {
clearTimeout(this.timeId);
this.timeId = window.setTimeout(async () => {
this.genHTML(await item.hint(key), key, vditor);
}, vditor.options.hint.delay);
}
});
}
}
}
public genHTML(data: IHintData[], key: string, vditor: IVditor) {
if (data.length === 0) {
this.element.style.display = "none";
return;
}
const editorElement = vditor[vditor.currentMode].element;
const textareaPosition = getCursorPosition(editorElement);
const x = textareaPosition.left +
(vditor.options.outline.position === "left" ? vditor.outline.element.offsetWidth : 0);
const y = textareaPosition.top;
let hintsHTML = "";
data.forEach((hintData, i) => {
if (i > 7) {
return;
}
// process high light
let html = hintData.html;
if (key !== "") {
const lastIndex = html.lastIndexOf(">") + 1;
let replaceHtml = html.substr(lastIndex);
const replaceIndex = replaceHtml.toLowerCase().indexOf(key.toLowerCase());
if (replaceIndex > -1) {
replaceHtml = replaceHtml.substring(0, replaceIndex) + "<b>" +
replaceHtml.substring(replaceIndex, replaceIndex + key.length) + "</b>" +
replaceHtml.substring(replaceIndex + key.length);
html = html.substr(0, lastIndex) + replaceHtml;
}
}
hintsHTML += `<button type="button" data-value="${encodeURIComponent(hintData.value)} "
${i === 0 ? "class='vditor-hint--current'" : ""}> ${html}</button>`;
});
this.element.innerHTML = hintsHTML;
const lineHeight = parseInt(document.defaultView.getComputedStyle(editorElement, null)
.getPropertyValue("line-height"), 10);
this.element.style.top = `${y + (lineHeight || 22)}px`;
this.element.style.left = `${x}px`;
this.element.style.display = "block";
this.element.style.right = "auto";
this.element.querySelectorAll("button").forEach((element) => {
element.addEventListener("click", (event) => {
this.fillEmoji(element, vditor);
event.preventDefault();
});
});
// hint 展现在上部
if (this.element.getBoundingClientRect().bottom > window.innerHeight) {
this.element.style.top = `${y - this.element.offsetHeight}px`;
}
if (this.element.getBoundingClientRect().right > window.innerWidth) {
this.element.style.left = "auto";
this.element.style.right = "0";
}
}
public fillEmoji = (element: HTMLElement, vditor: IVditor) => {
this.element.style.display = "none";
const value = decodeURIComponent(element.getAttribute("data-value"));
const range: Range = window.getSelection().getRangeAt(0);
// 代码提示
if (vditor.currentMode === "ir") {
const preBeforeElement = hasClosestByAttribute(range.startContainer, "data-type", "code-block-info");
if (preBeforeElement) {
preBeforeElement.textContent = Constants.ZWSP + value.trimRight();
range.selectNodeContents(preBeforeElement);
range.collapse(false);
processAfterRender(vditor);
preBeforeElement.parentElement.querySelectorAll("code").forEach((item) => {
item.className = "language-" + value.trimRight();
});
processCodeRender(preBeforeElement.parentElement.querySelector(".vditor-ir__preview"), vditor);
this.recentLanguage = value.trimRight();
return;
}
}
if (vditor.currentMode === "wysiwyg" && range.startContainer.nodeType !== 3 ) {
const startContainer = range.startContainer as HTMLElement;
let inputElement: HTMLInputElement;
if (startContainer.classList.contains("vditor-input")) {
inputElement = startContainer as HTMLInputElement;
} else {
inputElement = startContainer.firstElementChild as HTMLInputElement;
}
if (inputElement && inputElement.classList.contains("vditor-input")) {
inputElement.value = value.trimRight();
range.selectNodeContents(inputElement);
range.collapse(false);
// {detail: 1}用于标识这个自定义事件是在编程语言选择后触发的
// 用于在鼠标选择语言后,自动聚焦到代码输入框
inputElement.dispatchEvent(new CustomEvent("input", {detail: 1}));
this.recentLanguage = value.trimRight();
return;
}
}
range.setStart(range.startContainer, this.lastIndex);
range.deleteContents();
if (vditor.options.hint.parse) {
if (vditor.currentMode === "sv") {
insertHTML(vditor.lute.SpinVditorSVDOM(value), vditor);
} else if (vditor.currentMode === "wysiwyg") {
insertHTML(vditor.lute.SpinVditorDOM(value), vditor);
} else {
insertHTML(vditor.lute.SpinVditorIRDOM(value), vditor);
}
} else {
insertHTML(value, vditor);
}
if (this.splitChar === ":" && value.indexOf(":") > -1 && vditor.currentMode !== "sv") {
range.insertNode(document.createTextNode(" "));
}
range.collapse(false);
setSelectionFocus(range);
if (vditor.currentMode === "wysiwyg") {
const preElement = hasClosestByClassName(range.startContainer, "vditor-wysiwyg__block");
if (preElement && preElement.lastElementChild.classList.contains("vditor-wysiwyg__preview")) {
preElement.lastElementChild.innerHTML = preElement.firstElementChild.innerHTML;
processCodeRender(preElement.lastElementChild as HTMLElement, vditor);
}
} else if (vditor.currentMode === "ir") {
const preElement = hasClosestByClassName(range.startContainer, "vditor-ir__marker--pre");
if (preElement && preElement.nextElementSibling.classList.contains("vditor-ir__preview")) {
preElement.nextElementSibling.innerHTML = preElement.innerHTML;
processCodeRender(preElement.nextElementSibling as HTMLElement, vditor);
}
}
execAfterRender(vditor);
}
public select(event: KeyboardEvent, vditor: IVditor) {
if (this.element.querySelectorAll("button").length === 0 ||
this.element.style.display === "none") {
return false;
}
const currentHintElement: HTMLElement = this.element.querySelector(".vditor-hint--current");
if (event.key === "ArrowDown") {
event.preventDefault();
event.stopPropagation();
currentHintElement.removeAttribute("class");
if (!currentHintElement.nextElementSibling) {
this.element.children[0].className = "vditor-hint--current";
} else {
currentHintElement.nextElementSibling.className = "vditor-hint--current";
}
return true;
} else if (event.key === "ArrowUp") {
event.preventDefault();
event.stopPropagation();
currentHintElement.removeAttribute("class");
if (!currentHintElement.previousElementSibling) {
const length = this.element.children.length;
this.element.children[length - 1].className = "vditor-hint--current";
} else {
currentHintElement.previousElementSibling.className = "vditor-hint--current";
}
return true;
} else if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Enter" && !event.isComposing) {
event.preventDefault();
event.stopPropagation();
this.fillEmoji(currentHintElement, vditor);
return true;
}
return false;
}
private getKey(currentLineValue: string, extend: IHintExtend[]) {
this.lastIndex = -1;
this.splitChar = "";
extend.forEach((item) => {
const currentLastIndex = currentLineValue.lastIndexOf(item.key);
if (this.lastIndex < currentLastIndex) {
this.splitChar = item.key;
this.lastIndex = currentLastIndex;
}
});
let key;
if (this.lastIndex === -1) {
return key;
}
const lineArray = currentLineValue.split(this.splitChar);
const lastItem = lineArray[lineArray.length - 1];
const maxLength = 32;
if (lineArray.length > 1 && lastItem.trim() === lastItem) {
if (lineArray.length === 2 && lineArray[0] === "" && lineArray[1].length < maxLength) {
key = lineArray[1];
} else {
const preChar = lineArray[lineArray.length - 2].slice(-1);
if (code160to32(preChar) === " " && lastItem.length < maxLength) {
key = lastItem;
}
}
}
return key;
}
}

View File

@@ -0,0 +1,79 @@
import {hasClosestByClassName, hasTopClosestByClassName} from "../util/hasClosest";
import {setSelectionFocus} from "../util/selection";
const nextIsNode = (range: Range) => {
const startContainer = range.startContainer;
if (startContainer.nodeType === 3 && startContainer.nodeValue.length !== range.startOffset) {
return false;
}
let nextNode: HTMLElement = startContainer.nextSibling as HTMLElement;
while (nextNode && nextNode.textContent === "") {
nextNode = nextNode.nextSibling as HTMLElement;
}
if (!nextNode) {
// *em*|**string**
const markerElement = hasClosestByClassName(startContainer, "vditor-ir__marker");
if (markerElement && !markerElement.nextSibling) {
const parentNextNode = startContainer.parentElement.parentElement.nextSibling as HTMLElement;
if (parentNextNode && parentNextNode.nodeType !== 3 &&
parentNextNode.classList.contains("vditor-ir__node")) {
return parentNextNode;
}
}
return false;
} else if (nextNode && nextNode.nodeType !== 3 && nextNode.classList.contains("vditor-ir__node") &&
!nextNode.getAttribute("data-block")) {
// test|*em*
return nextNode;
}
return false;
};
const previousIsNode = (range: Range) => {
const startContainer = range.startContainer;
const previousNode = startContainer.previousSibling as HTMLElement;
if (startContainer.nodeType === 3 && range.startOffset === 0 && previousNode && previousNode.nodeType !== 3 &&
// *em*|text
previousNode.classList.contains("vditor-ir__node") && !previousNode.getAttribute("data-block")) {
return previousNode;
}
return false;
};
export const expandMarker = (range: Range, vditor: IVditor) => {
vditor.ir.element.querySelectorAll(".vditor-ir__node--expand").forEach((item) => {
item.classList.remove("vditor-ir__node--expand");
});
const nodeElement = hasTopClosestByClassName(range.startContainer, "vditor-ir__node");
const nodeElementEnd = !range.collapsed && hasTopClosestByClassName(range.endContainer, "vditor-ir__node");
// 选中文本为同一个 nodeElement 内时,需要展开
if (!range.collapsed && (!nodeElement || nodeElement !== nodeElementEnd)) {
return;
}
if (nodeElement) {
nodeElement.classList.add("vditor-ir__node--expand");
nodeElement.classList.remove("vditor-ir__node--hidden");
// https://github.com/Vanessa219/vditor/issues/615 safari中光标位置跳动
setSelectionFocus(range);
}
const nextNode = nextIsNode(range);
if (nextNode) {
nextNode.classList.add("vditor-ir__node--expand");
nextNode.classList.remove("vditor-ir__node--hidden");
return;
}
const previousNode = previousIsNode(range);
if (previousNode) {
previousNode.classList.add("vditor-ir__node--expand");
previousNode.classList.remove("vditor-ir__node--hidden");
return;
}
};

View File

@@ -0,0 +1,97 @@
import {Constants} from "../constants";
import {disableToolbar, enableToolbar, removeCurrentToolbar, setCurrentToolbar} from "../toolbar/setToolbar";
import {hasClosestByAttribute, hasClosestByMatchTag} from "../util/hasClosest";
import {hasClosestByHeadings} from "../util/hasClosestByHeadings";
import {getEditorRange, selectIsEditor} from "../util/selection";
export const highlightToolbarIR = (vditor: IVditor) => {
clearTimeout(vditor[vditor.currentMode].hlToolbarTimeoutId);
vditor[vditor.currentMode].hlToolbarTimeoutId = window.setTimeout(() => {
if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
return;
}
if (!selectIsEditor(vditor[vditor.currentMode].element)) {
return;
}
removeCurrentToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS);
enableToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS);
const range = getEditorRange(vditor);
let typeElement = range.startContainer as HTMLElement;
if (range.startContainer.nodeType === 3) {
typeElement = range.startContainer.parentElement;
}
if (typeElement.classList.contains("vditor-reset")) {
typeElement = typeElement.childNodes[range.startOffset] as HTMLElement;
}
const headingElement = vditor.currentMode === "sv" ?
hasClosestByAttribute(typeElement, "data-type", "heading") : hasClosestByHeadings(typeElement);
if (headingElement) {
setCurrentToolbar(vditor.toolbar.elements, ["headings"]);
}
const quoteElement =
vditor.currentMode === "sv" ? hasClosestByAttribute(typeElement, "data-type", "blockquote") :
hasClosestByMatchTag(typeElement, "BLOCKQUOTE");
if (quoteElement) {
setCurrentToolbar(vditor.toolbar.elements, ["quote"]);
}
const strongElement = hasClosestByAttribute(typeElement, "data-type", "strong");
if (strongElement) {
setCurrentToolbar(vditor.toolbar.elements, ["bold"]);
}
const emElement = hasClosestByAttribute(typeElement, "data-type", "em");
if (emElement) {
setCurrentToolbar(vditor.toolbar.elements, ["italic"]);
}
const sElement = hasClosestByAttribute(typeElement, "data-type", "s");
if (sElement) {
setCurrentToolbar(vditor.toolbar.elements, ["strike"]);
}
const aElement = hasClosestByAttribute(typeElement, "data-type", "a");
if (aElement) {
setCurrentToolbar(vditor.toolbar.elements, ["link"]);
}
const liElement = hasClosestByMatchTag(typeElement, "LI");
if (liElement) {
if (liElement.classList.contains("vditor-task")) {
setCurrentToolbar(vditor.toolbar.elements, ["check"]);
} else if (liElement.parentElement.tagName === "OL") {
setCurrentToolbar(vditor.toolbar.elements, ["ordered-list"]);
} else if (liElement.parentElement.tagName === "UL") {
setCurrentToolbar(vditor.toolbar.elements, ["list"]);
}
enableToolbar(vditor.toolbar.elements, ["outdent", "indent"]);
} else {
disableToolbar(vditor.toolbar.elements, ["outdent", "indent"]);
}
const codeBlockElement = hasClosestByAttribute(typeElement, "data-type", "code-block");
if (codeBlockElement) {
disableToolbar(vditor.toolbar.elements, ["headings", "bold", "italic", "strike", "line", "quote",
"list", "ordered-list", "check", "code", "inline-code", "upload", "link", "table", "record"]);
setCurrentToolbar(vditor.toolbar.elements, ["code"]);
}
const codeElement = hasClosestByAttribute(typeElement, "data-type", "code");
if (codeElement) {
disableToolbar(vditor.toolbar.elements, ["headings", "bold", "italic", "strike", "line", "quote",
"list", "ordered-list", "check", "code", "upload", "link", "table", "record"]);
setCurrentToolbar(vditor.toolbar.elements, ["inline-code"]);
}
const tableElement = hasClosestByAttribute(typeElement, "data-type", "table");
if (tableElement) {
disableToolbar(vditor.toolbar.elements, ["headings", "list", "ordered-list", "check", "line",
"quote", "code", "table"]);
}
}, 200);
};

View File

@@ -0,0 +1,265 @@
import {Constants} from "../constants";
import {isCtrl, isFirefox} from "../util/compatibility";
import {
blurEvent,
copyEvent, cutEvent, dblclickEvent,
dropEvent,
focusEvent,
hotkeyEvent,
scrollCenter,
selectEvent,
} from "../util/editorCommonEvent";
import {paste} from "../util/fixBrowserBehavior";
import {hasClosestByAttribute, hasClosestByClassName} from "../util/hasClosest";
import {
getEditorRange, setRangeByWbr,
setSelectionFocus,
} from "../util/selection";
import {clickToc} from "../util/toc";
import {expandMarker} from "./expandMarker";
import {highlightToolbarIR} from "./highlightToolbarIR";
import {input} from "./input";
import {processAfterRender, processHint} from "./process";
import {hidePanel} from "../toolbar/setToolbar";
class IR {
public range: Range;
public element: HTMLPreElement;
public processTimeoutId: number;
public hlToolbarTimeoutId: number;
public composingLock: boolean = false;
public preventInput: boolean;
constructor(vditor: IVditor) {
const divElement = document.createElement("div");
divElement.className = "vditor-ir";
divElement.innerHTML = `<pre class="vditor-reset" placeholder="${vditor.options.placeholder}"
contenteditable="true" spellcheck="false"></pre>`;
this.element = divElement.firstElementChild as HTMLPreElement;
this.bindEvent(vditor);
focusEvent(vditor, this.element);
dblclickEvent(vditor, this.element);
blurEvent(vditor, this.element);
hotkeyEvent(vditor, this.element);
selectEvent(vditor, this.element);
dropEvent(vditor, this.element);
copyEvent(vditor, this.element, this.copy);
cutEvent(vditor, this.element, this.copy);
}
private copy(event: ClipboardEvent, vditor: IVditor) {
const range = getSelection().getRangeAt(0);
if (range.toString() === "") {
return;
}
event.stopPropagation();
event.preventDefault();
const tempElement = document.createElement("div");
tempElement.appendChild(range.cloneContents());
event.clipboardData.setData("text/plain", vditor.lute.VditorIRDOM2Md(tempElement.innerHTML).trim());
event.clipboardData.setData("text/html", "");
}
private bindEvent(vditor: IVditor) {
this.element.addEventListener("paste", (event: ClipboardEvent & { target: HTMLElement }) => {
paste(vditor, event, {
pasteCode: (code: string) => {
document.execCommand("insertHTML", false, code);
},
});
});
this.element.addEventListener("scroll", () => {
hidePanel(vditor, ["hint"]);
});
this.element.addEventListener("compositionstart", (event: InputEvent) => {
this.composingLock = true;
});
this.element.addEventListener("compositionend", (event: InputEvent) => {
if (!isFirefox()) {
input(vditor, getSelection().getRangeAt(0).cloneRange());
}
this.composingLock = false;
});
this.element.addEventListener("input", (event: InputEvent) => {
if (event.inputType === "deleteByDrag" || event.inputType === "insertFromDrop") {
// https://github.com/Vanessa219/vditor/issues/801 编辑器内容拖拽问题
return;
}
if (this.preventInput) {
this.preventInput = false;
processAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: true,
enableInput: true,
});
return;
}
if (this.composingLock || event.data === "" || event.data === "“" || event.data === "《") {
return;
}
input(vditor, getSelection().getRangeAt(0).cloneRange(), false, event);
});
this.element.addEventListener("click", (event: MouseEvent & { target: HTMLInputElement }) => {
if (event.target.tagName === "INPUT") {
if (event.target.checked) {
event.target.setAttribute("checked", "checked");
} else {
event.target.removeAttribute("checked");
}
this.preventInput = true;
processAfterRender(vditor);
return;
}
const range = getEditorRange(vditor);
// 点击后光标落于预览区
let previewElement = hasClosestByClassName(event.target, "vditor-ir__preview");
if (!previewElement) {
previewElement = hasClosestByClassName(
range.startContainer, "vditor-ir__preview");
}
if (previewElement) {
if (previewElement.previousElementSibling.firstElementChild) {
range.selectNodeContents(previewElement.previousElementSibling.firstElementChild);
} else {
// 行内数学公式
range.selectNodeContents(previewElement.previousElementSibling);
}
range.collapse(true);
setSelectionFocus(range);
scrollCenter(vditor);
}
// 点击图片光标选中图片地址
if (event.target.tagName === "IMG") {
const linkElement =
event.target.parentElement.querySelector<HTMLSpanElement>(".vditor-ir__marker--link");
if (linkElement) {
range.selectNode(linkElement);
setSelectionFocus(range);
}
}
// 打开链接
const aElement = hasClosestByAttribute(event.target, "data-type", "a");
if (aElement && (!aElement.classList.contains("vditor-ir__node--expand"))) {
if (vditor.options.link.click) {
vditor.options.link.click(aElement.querySelector(":scope > .vditor-ir__marker--link"));
} else if (vditor.options.link.isOpen) {
window.open(aElement.querySelector(":scope > .vditor-ir__marker--link").textContent);
}
return;
}
if (event.target.isEqualNode(this.element) && this.element.lastElementChild && range.collapsed) {
const lastRect = this.element.lastElementChild.getBoundingClientRect();
if (event.y > lastRect.top + lastRect.height) {
if (this.element.lastElementChild.tagName === "P" &&
this.element.lastElementChild.textContent.trim().replace(Constants.ZWSP, "") === "") {
range.selectNodeContents(this.element.lastElementChild);
range.collapse(false);
} else {
this.element.insertAdjacentHTML("beforeend",
`<p data-block="0">${Constants.ZWSP}<wbr></p>`);
setRangeByWbr(this.element, range);
}
}
}
if (range.toString() === "") {
expandMarker(range, vditor);
} else {
// https://github.com/Vanessa219/vditor/pull/681 当点击选中区域时 eventTarget 与 range 不一致,需延迟等待 range 发生变化
setTimeout(() => {
expandMarker(getEditorRange(vditor), vditor);
});
}
clickToc(event, vditor);
highlightToolbarIR(vditor);
});
this.element.addEventListener("keyup", (event) => {
if (event.isComposing || isCtrl(event)) {
return;
}
if (event.key === "Enter") {
scrollCenter(vditor);
}
highlightToolbarIR(vditor);
if ((event.key === "Backspace" || event.key === "Delete") &&
vditor.ir.element.innerHTML !== "" && vditor.ir.element.childNodes.length === 1 &&
vditor.ir.element.firstElementChild && vditor.ir.element.firstElementChild.tagName === "P"
&& vditor.ir.element.firstElementChild.childElementCount === 0
&& (vditor.ir.element.textContent === "" || vditor.ir.element.textContent === "\n")) {
// 为空时显示 placeholder
vditor.ir.element.innerHTML = "";
return;
}
const range = getEditorRange(vditor);
if (event.key === "Backspace") {
// firefox headings https://github.com/Vanessa219/vditor/issues/211
if (isFirefox() && range.startContainer.textContent === "\n" && range.startOffset === 1) {
range.startContainer.textContent = "";
expandMarker(range, vditor);
}
// 数学公式前是空块,空块前是 table在空块前删除数学公式会多一个 br
this.element.querySelectorAll(".language-math").forEach((item) => {
const brElement = item.querySelector("br");
if (brElement) {
brElement.remove();
}
});
} else if (event.key.indexOf("Arrow") > -1) {
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
processHint(vditor);
}
expandMarker(range, vditor);
} else if (event.keyCode === 229 && event.code === "" && event.key === "Unidentified") {
// https://github.com/Vanessa219/vditor/issues/508 IR 删除到节点需展开
expandMarker(range, vditor);
}
const previewRenderElement = hasClosestByClassName(range.startContainer, "vditor-ir__preview");
if (previewRenderElement) {
if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
if (previewRenderElement.previousElementSibling.firstElementChild) {
range.selectNodeContents(previewRenderElement.previousElementSibling.firstElementChild);
} else {
// 行内数学公式/html entity
range.selectNodeContents(previewRenderElement.previousElementSibling);
}
range.collapse(false);
event.preventDefault();
return true;
}
if (previewRenderElement.tagName === "SPAN" &&
(event.key === "ArrowDown" || event.key === "ArrowRight")) {
if (previewRenderElement.parentElement.getAttribute("data-type") === "html-entity") {
// html entity
previewRenderElement.parentElement.insertAdjacentText("afterend", Constants.ZWSP);
range.setStart(previewRenderElement.parentElement.nextSibling, 1);
} else {
range.selectNodeContents(previewRenderElement.parentElement.lastElementChild);
}
range.collapse(false);
event.preventDefault();
return true;
}
}
});
}
}
export {IR};

View File

@@ -0,0 +1,246 @@
import {Constants} from "../constants";
import {isHeadingMD, isHrMD} from "../util/fixBrowserBehavior";
import {
getTopList,
hasClosestBlock, hasClosestByAttribute,
hasClosestByClassName,
} from "../util/hasClosest";
import {hasClosestByTag} from "../util/hasClosestByHeadings";
import {log} from "../util/log";
import {processCodeRender} from "../util/processCode";
import {getSelectPosition, setRangeByWbr} from "../util/selection";
import {renderToc} from "../util/toc";
import {processAfterRender} from "./process";
import {getMarkdown} from "../markdown/getMarkdown";
export const input = (vditor: IVditor, range: Range, ignoreSpace = false, event?: InputEvent) => {
let blockElement = hasClosestBlock(range.startContainer);
// 前后可以输入空格
if (blockElement && !ignoreSpace && blockElement.getAttribute("data-type") !== "code-block") {
if ((isHrMD(blockElement.innerHTML) && blockElement.previousElementSibling) ||
isHeadingMD(blockElement.innerHTML)) {
return;
}
// 前后空格处理
const startOffset = getSelectPosition(blockElement, vditor.ir.element, range).start;
// 开始可以输入空格
let startSpace = true;
for (let i = startOffset - 1;
// 软换行后有空格
i > blockElement.textContent.substr(0, startOffset).lastIndexOf("\n");
i--) {
if (blockElement.textContent.charAt(i) !== " " &&
// 多个 tab 前删除不形成代码块 https://github.com/Vanessa219/vditor/issues/162 1
blockElement.textContent.charAt(i) !== "\t") {
startSpace = false;
break;
}
}
if (startOffset === 0) {
startSpace = false;
}
// 结尾可以输入空格
let endSpace = true;
for (let i = startOffset - 1; i < blockElement.textContent.length; i++) {
if (blockElement.textContent.charAt(i) !== " " && blockElement.textContent.charAt(i) !== "\n") {
endSpace = false;
break;
}
}
if (startSpace) {
if (typeof vditor.options.input === "function") {
vditor.options.input(getMarkdown(vditor));
}
return;
}
// https://github.com/Vanessa219/vditor/issues/729
if (endSpace && /^#{1,6} $/.test(blockElement.textContent)) {
endSpace = false;
}
if (endSpace) {
const markerElement = hasClosestByClassName(range.startContainer, "vditor-ir__marker");
if (markerElement) {
// inline marker space https://github.com/Vanessa219/vditor/issues/239
} else {
const previousNode = range.startContainer.previousSibling as HTMLElement;
if (previousNode && previousNode.nodeType !== 3 && previousNode.classList.contains("vditor-ir__node--expand")) {
// FireFox https://github.com/Vanessa219/vditor/issues/239
previousNode.classList.remove("vditor-ir__node--expand");
}
if (typeof vditor.options.input === "function") {
vditor.options.input(getMarkdown(vditor));
}
return;
}
}
}
vditor.ir.element.querySelectorAll(".vditor-ir__node--expand").forEach((item) => {
item.classList.remove("vditor-ir__node--expand");
});
if (!blockElement) {
// 使用顶级块元素,应使用 innerHTML
blockElement = vditor.ir.element;
}
// document.exeComment insertHTML 会插入 wbr
if (!blockElement.querySelector("wbr")) {
const previewRenderElement = hasClosestByClassName(range.startContainer, "vditor-ir__preview");
if (previewRenderElement) {
previewRenderElement.previousElementSibling.insertAdjacentHTML("beforeend", "<wbr>");
} else {
range.insertNode(document.createElement("wbr"));
}
}
// 清除浏览器自带的样式
blockElement.querySelectorAll("[style]").forEach((item) => {
item.removeAttribute("style");
});
if (blockElement.getAttribute("data-type") === "link-ref-defs-block") {
// 修改链接引用
blockElement = vditor.ir.element;
}
const isIRElement = blockElement.isEqualNode(vditor.ir.element);
const footnoteElement = hasClosestByAttribute(blockElement, "data-type", "footnotes-block");
let html = "";
if (!isIRElement) {
const blockquoteElement = hasClosestByTag(range.startContainer, "BLOCKQUOTE");
// 列表需要到最顶层
const topListElement = getTopList(range.startContainer);
if (topListElement) {
blockElement = topListElement;
}
// 应到引用层,否则 > --- 会解析为 front-matter列表中有 blockquote 则解析 blockquoteblockquote 中有列表则解析列表
if (blockquoteElement && (!topListElement || (topListElement && !blockquoteElement.contains(topListElement)))) {
blockElement = blockquoteElement;
}
// 修改脚注
if (footnoteElement) {
blockElement = footnoteElement;
}
html = blockElement.outerHTML;
if (blockElement.tagName === "UL" || blockElement.tagName === "OL") {
// 如果为列表的话,需要把上下的列表都重绘
const listPrevElement = blockElement.previousElementSibling;
const listNextElement = blockElement.nextElementSibling;
if (listPrevElement && (listPrevElement.tagName === "UL" || listPrevElement.tagName === "OL")) {
html = listPrevElement.outerHTML + html;
listPrevElement.remove();
}
if (listNextElement && (listNextElement.tagName === "UL" || listNextElement.tagName === "OL")) {
html = html + listNextElement.outerHTML;
listNextElement.remove();
}
// firefox 列表回车不会产生新的 list item https://github.com/Vanessa219/vditor/issues/194
html = html.replace("<div><wbr><br></div>", "<li><p><wbr><br></p></li>");
} else if (blockElement.previousElementSibling &&
blockElement.previousElementSibling.textContent.replace(Constants.ZWSP, "") !== "" &&
event && event.inputType === "insertParagraph") {
// 换行时需要处理上一段落
html = blockElement.previousElementSibling.outerHTML + html;
blockElement.previousElementSibling.remove();
}
if (!blockElement.innerText.startsWith("```")) {
// 添加链接引用
vditor.ir.element.querySelectorAll("[data-type='link-ref-defs-block']").forEach((item) => {
if (item && !(blockElement as HTMLElement).isEqualNode(item)) {
html += item.outerHTML;
item.remove();
}
});
// 添加脚注
vditor.ir.element.querySelectorAll("[data-type='footnotes-block']").forEach((item) => {
if (item && !(blockElement as HTMLElement).isEqualNode(item)) {
html += item.outerHTML;
item.remove();
}
});
}
} else {
html = blockElement.innerHTML;
}
log("SpinVditorIRDOM", html, "argument", vditor.options.debugger);
html = vditor.lute.SpinVditorIRDOM(html);
log("SpinVditorIRDOM", html, "result", vditor.options.debugger);
if (isIRElement) {
blockElement.innerHTML = html;
} else {
blockElement.outerHTML = html;
// 更新正文中的 tip
if (footnoteElement) {
const footnoteItemElement = hasClosestByAttribute(vditor.ir.element.querySelector("wbr"),
"data-type", "footnotes-def");
if (footnoteItemElement) {
const footnoteItemText = footnoteItemElement.textContent;
const marker = footnoteItemText.substring(1, footnoteItemText.indexOf("]:"));
const footnoteRefElement = vditor.ir.element.querySelector(`sup[data-type="footnotes-ref"][data-footnotes-label="${marker}"]`);
if (footnoteRefElement) {
footnoteRefElement.setAttribute("aria-label",
footnoteItemText.substr(marker.length + 3).trim().substr(0, 24));
}
}
}
}
// linkref 合并及添加
let firstLinkRefDefElement: HTMLElement;
const allLinkRefDefsElement = vditor.ir.element.querySelectorAll("[data-type='link-ref-defs-block']");
allLinkRefDefsElement.forEach((item: HTMLElement, index) => {
if (index === 0) {
firstLinkRefDefElement = item;
} else {
firstLinkRefDefElement.insertAdjacentHTML("beforeend", item.innerHTML);
item.remove();
}
});
if (allLinkRefDefsElement.length > 0) {
vditor.ir.element.insertAdjacentElement("beforeend", allLinkRefDefsElement[0]);
}
// 脚注合并后添加的末尾
let firstFootnoteElement: HTMLElement;
const allFootnoteElement = vditor.ir.element.querySelectorAll("[data-type='footnotes-block']");
allFootnoteElement.forEach((item: HTMLElement, index) => {
if (index === 0) {
firstFootnoteElement = item;
} else {
firstFootnoteElement.insertAdjacentHTML("beforeend", item.innerHTML);
item.remove();
}
});
if (allFootnoteElement.length > 0) {
vditor.ir.element.insertAdjacentElement("beforeend", allFootnoteElement[0]);
}
setRangeByWbr(vditor.ir.element, range);
vditor.ir.element.querySelectorAll(".vditor-ir__preview[data-render='2']").forEach((item: HTMLElement) => {
processCodeRender(item, vditor);
});
renderToc(vditor);
processAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: true,
enableInput: true,
});
};

View File

@@ -0,0 +1,222 @@
import {Constants} from "../constants";
import {getMarkdown} from "../markdown/getMarkdown";
import {removeCurrentToolbar} from "../toolbar/setToolbar";
import {accessLocalStorage} from "../util/compatibility";
import {listToggle} from "../util/fixBrowserBehavior";
import {hasClosestBlock, hasClosestByAttribute, hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest";
import {getEditorRange, getSelectPosition, setRangeByWbr, setSelectionFocus} from "../util/selection";
import {highlightToolbarIR} from "./highlightToolbarIR";
import {input} from "./input";
export const processHint = (vditor: IVditor) => {
vditor.hint.render(vditor);
const startContainer = getEditorRange(vditor).startContainer;
// 代码块语言提示
const preBeforeElement = hasClosestByAttribute(startContainer, "data-type", "code-block-info");
if (preBeforeElement) {
if (preBeforeElement.textContent.replace(Constants.ZWSP, "") === "" && vditor.hint.recentLanguage) {
preBeforeElement.textContent = Constants.ZWSP + vditor.hint.recentLanguage;
const range = getEditorRange(vditor);
range.selectNodeContents(preBeforeElement);
} else {
const matchLangData: IHintData[] = [];
const key =
preBeforeElement.textContent.substring(0, getSelectPosition(preBeforeElement, vditor.ir.element).start)
.replace(Constants.ZWSP, "");
(vditor.options.preview.hljs.langs || Constants.CODE_LANGUAGES).forEach((keyName) => {
if (keyName.indexOf(key.toLowerCase()) > -1) {
matchLangData.push({
html: keyName,
value: keyName,
});
}
});
vditor.hint.genHTML(matchLangData, key, vditor);
}
}
};
export const processAfterRender = (vditor: IVditor, options = {
enableAddUndoStack: true,
enableHint: false,
enableInput: true,
}) => {
if (options.enableHint) {
processHint(vditor);
}
clearTimeout(vditor.ir.processTimeoutId);
vditor.ir.processTimeoutId = window.setTimeout(() => {
if (vditor.ir.composingLock) {
return;
}
const text = getMarkdown(vditor);
if (typeof vditor.options.input === "function" && options.enableInput) {
vditor.options.input(text);
}
if (vditor.options.counter.enable) {
vditor.counter.render(vditor, text);
}
if (vditor.options.cache.enable && accessLocalStorage()) {
localStorage.setItem(vditor.options.cache.id, text);
if (vditor.options.cache.after) {
vditor.options.cache.after(text);
}
}
if (vditor.devtools) {
vditor.devtools.renderEchart(vditor);
}
if (options.enableAddUndoStack) {
vditor.undo.addToUndoStack(vditor);
}
}, vditor.options.undoDelay);
};
export const processHeading = (vditor: IVditor, value: string) => {
const range = getEditorRange(vditor);
const headingElement = hasClosestBlock(range.startContainer) || range.startContainer as HTMLElement;
if (headingElement) {
const headingMarkerElement = headingElement.querySelector(".vditor-ir__marker--heading");
if (headingMarkerElement) {
headingMarkerElement.innerHTML = value;
} else {
headingElement.insertAdjacentText("afterbegin", value);
range.selectNodeContents(headingElement);
range.collapse(false);
}
input(vditor, range.cloneRange());
highlightToolbarIR(vditor);
}
};
const removeInline = (range: Range, vditor: IVditor, type: string) => {
const inlineElement = hasClosestByAttribute(range.startContainer, "data-type", type) as HTMLElement;
if (inlineElement) {
inlineElement.firstElementChild.remove();
inlineElement.lastElementChild.remove();
range.insertNode(document.createElement("wbr"));
const tempElement = document.createElement("div");
tempElement.innerHTML = vditor.lute.SpinVditorIRDOM(inlineElement.outerHTML);
inlineElement.outerHTML = tempElement.firstElementChild.innerHTML.trim();
}
};
export const processToolbar = (vditor: IVditor, actionBtn: Element, prefix: string, suffix: string) => {
const range = getEditorRange(vditor);
const commandName = actionBtn.getAttribute("data-type");
let typeElement = range.startContainer as HTMLElement;
if (typeElement.nodeType === 3) {
typeElement = typeElement.parentElement;
}
let useHighlight = true;
// 移除
if (actionBtn.classList.contains("vditor-menu--current")) {
if (commandName === "quote") {
const quoteElement = hasClosestByMatchTag(typeElement, "BLOCKQUOTE");
if (quoteElement) {
range.insertNode(document.createElement("wbr"));
quoteElement.outerHTML = quoteElement.innerHTML.trim() === "" ?
`<p data-block="0">${quoteElement.innerHTML}</p>` : quoteElement.innerHTML;
}
} else if (commandName === "link") {
const aElement = hasClosestByAttribute(range.startContainer, "data-type", "a") as HTMLElement;
if (aElement) {
const aTextElement = hasClosestByClassName(range.startContainer, "vditor-ir__link");
if (aTextElement) {
range.insertNode(document.createElement("wbr"));
aElement.outerHTML = aTextElement.innerHTML;
} else {
aElement.outerHTML = aElement.querySelector(".vditor-ir__link").innerHTML + "<wbr>";
}
}
} else if (commandName === "italic") {
removeInline(range, vditor, "em");
} else if (commandName === "bold") {
removeInline(range, vditor, "strong");
} else if (commandName === "strike") {
removeInline(range, vditor, "s");
} else if (commandName === "inline-code") {
removeInline(range, vditor, "code");
} else if (commandName === "check" || commandName === "list" || commandName === "ordered-list") {
listToggle(vditor, range, commandName);
useHighlight = false;
actionBtn.classList.remove("vditor-menu--current");
}
} else {
// 添加
if (vditor.ir.element.childNodes.length === 0) {
vditor.ir.element.innerHTML = '<p data-block="0"><wbr></p>';
setRangeByWbr(vditor.ir.element, range);
}
const blockElement = hasClosestBlock(range.startContainer);
if (commandName === "line") {
if (blockElement) {
const hrHTML = '<hr data-block="0"><p data-block="0"><wbr>\n</p>';
if (blockElement.innerHTML.trim() === "") {
blockElement.outerHTML = hrHTML;
} else {
blockElement.insertAdjacentHTML("afterend", hrHTML);
}
}
} else if (commandName === "quote") {
if (blockElement) {
range.insertNode(document.createElement("wbr"));
blockElement.outerHTML = `<blockquote data-block="0">${blockElement.outerHTML}</blockquote>`;
useHighlight = false;
actionBtn.classList.add("vditor-menu--current");
}
} else if (commandName === "link") {
let html;
if (range.toString() === "") {
html = `${prefix}<wbr>${suffix}`;
} else {
html = `${prefix}${range.toString()}${suffix.replace(")", "<wbr>)")}`;
}
document.execCommand("insertHTML", false, html);
useHighlight = false;
actionBtn.classList.add("vditor-menu--current");
} else if (commandName === "italic" || commandName === "bold" || commandName === "strike"
|| commandName === "inline-code" || commandName === "code" || commandName === "table") {
let html;
if (range.toString() === "") {
html = `${prefix}<wbr>${suffix}`;
} else {
if (commandName === "code") {
html = `${prefix}\n${range.toString()}<wbr>${suffix}`;
} else if (commandName === "table") {
html = `${prefix}${range.toString()}<wbr>${suffix}`;
} else {
html = `${prefix}${range.toString()}${suffix}<wbr>`;
}
range.deleteContents();
}
if (commandName === "table" || commandName === "code") {
html = "\n" + html + "\n\n";
}
const spanElement = document.createElement("span");
spanElement.innerHTML = html;
range.insertNode(spanElement);
input(vditor, range);
if (commandName === "table") {
range.selectNodeContents(getSelection().getRangeAt(0).startContainer.parentElement);
setSelectionFocus(range);
}
} else if (commandName === "check" || commandName === "list" || commandName === "ordered-list") {
listToggle(vditor, range, commandName, false);
useHighlight = false;
removeCurrentToolbar(vditor.toolbar.elements, ["check", "list", "ordered-list"]);
actionBtn.classList.add("vditor-menu--current");
}
}
setRangeByWbr(vditor.ir.element, range);
processAfterRender(vditor);
if (useHighlight) {
highlightToolbarIR(vditor);
}
};

View File

@@ -0,0 +1,238 @@
import {Constants} from "../constants";
import {hidePanel} from "../toolbar/setToolbar";
import {isCtrl} from "../util/compatibility";
import {
fixBlockquote, fixCJKPosition,
fixCodeBlock, fixCursorDownInlineMath,
fixDelete, fixFirefoxArrowUpTable, fixGSKeyBackspace, fixHR,
fixList,
fixMarkdown,
fixTab,
fixTable,
fixTask,
insertAfterBlock, insertBeforeBlock, isFirstCell, isLastCell,
} from "../util/fixBrowserBehavior";
import {
hasClosestBlock,
hasClosestByAttribute,
hasClosestByClassName,
hasClosestByMatchTag,
} from "../util/hasClosest";
import {hasClosestByHeadings} from "../util/hasClosestByHeadings";
import {matchHotKey} from "../util/hotKey";
import {getEditorRange, getSelectPosition, setSelectionFocus} from "../util/selection";
import {keydownToc} from "../util/toc";
import {expandMarker} from "./expandMarker";
import {processAfterRender, processHeading} from "./process";
export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => {
vditor.ir.composingLock = event.isComposing;
if (event.isComposing) {
return false;
}
// 添加第一次记录 undo 的光标
if (event.key.indexOf("Arrow") === -1 && event.key !== "Meta" && event.key !== "Control" && event.key !== "Alt" &&
event.key !== "Shift" && event.key !== "CapsLock" && event.key !== "Escape" && !/^F\d{1,2}$/.test(event.key)) {
vditor.undo.recordFirstPosition(vditor, event);
}
const range = getEditorRange(vditor);
const startContainer = range.startContainer;
if (!fixGSKeyBackspace(event, vditor, startContainer)) {
return false;
}
fixCJKPosition(range, vditor, event);
fixHR(range);
// 仅处理以下快捷键操作
if (event.key !== "Enter" && event.key !== "Tab" && event.key !== "Backspace" && event.key.indexOf("Arrow") === -1
&& !isCtrl(event) && event.key !== "Escape" && event.key !== "Delete") {
return false;
}
// 斜体、粗体、内联代码块中换行
const newlineElement = hasClosestByAttribute(startContainer, "data-newline", "1");
if (!isCtrl(event) && !event.altKey && !event.shiftKey && event.key === "Enter" && newlineElement
&& range.startOffset < newlineElement.textContent.length) {
const beforeMarkerElement = newlineElement.previousElementSibling;
if (beforeMarkerElement) {
range.insertNode(document.createTextNode(beforeMarkerElement.textContent));
range.collapse(false);
}
const afterMarkerElement = newlineElement.nextSibling;
if (afterMarkerElement) {
range.insertNode(document.createTextNode(afterMarkerElement.textContent));
range.collapse(true);
}
}
const pElement = hasClosestByMatchTag(startContainer, "P");
// md 处理
if (fixMarkdown(event, vditor, pElement, range)) {
return true;
}
// li
if (fixList(range, vditor, pElement, event)) {
return true;
}
// blockquote
if (fixBlockquote(vditor, range, event, pElement)) {
return true;
}
// 代码块
const preRenderElement = hasClosestByClassName(startContainer, "vditor-ir__marker--pre");
if (preRenderElement && preRenderElement.tagName === "PRE") {
const codeRenderElement = preRenderElement.firstChild as HTMLElement;
if (fixCodeBlock(vditor, event, preRenderElement, range)) {
return true;
}
// 数学公式上无元素,按上或左将添加新块
if ((codeRenderElement.getAttribute("data-type") === "math-block"
|| codeRenderElement.getAttribute("data-type") === "html-block") &&
insertBeforeBlock(vditor, event, range, codeRenderElement, preRenderElement.parentElement)) {
return true;
}
// 代码块下无元素或者为代码块/table 元素,添加空块
if (insertAfterBlock(vditor, event, range, codeRenderElement, preRenderElement.parentElement)) {
return true;
}
}
// 代码块语言
const preBeforeElement = hasClosestByAttribute(startContainer, "data-type", "code-block-info");
if (preBeforeElement) {
if (event.key === "Enter" || event.key === "Tab") {
range.selectNodeContents(preBeforeElement.nextElementSibling.firstChild);
range.collapse(true);
event.preventDefault();
hidePanel(vditor, ["hint"]);
return true;
}
if (event.key === "Backspace") {
const start = getSelectPosition(preBeforeElement, vditor.ir.element).start;
if (start === 1) { // 删除零宽空格
range.setStart(startContainer, 0);
}
if (start === 2) { // 删除时清空自动补全语言
vditor.hint.recentLanguage = "";
}
}
if (insertBeforeBlock(vditor, event, range, preBeforeElement, preBeforeElement.parentElement)) {
// 上无元素,按上或左将添加新块
hidePanel(vditor, ["hint"]);
return true;
}
}
// table
const cellElement = hasClosestByMatchTag(startContainer, "TD") ||
hasClosestByMatchTag(startContainer, "TH");
if (event.key.indexOf("Arrow") > -1 && cellElement) {
const tableElement = isFirstCell(cellElement);
if (tableElement && insertBeforeBlock(vditor, event, range, cellElement, tableElement)) {
return true;
}
const table2Element = isLastCell(cellElement);
if (table2Element && insertAfterBlock(vditor, event, range, cellElement, table2Element)) {
return true;
}
}
if (fixTable(vditor, event, range)) {
return true;
}
// task list
if (fixTask(vditor, range, event)) {
return true;
}
// tab
if (fixTab(vditor, range, event)) {
return true;
}
const headingElement = hasClosestByHeadings(startContainer);
if (headingElement) {
// enter++: 标题变大
if (matchHotKey("⌘=", event)) {
const headingMarkerElement = headingElement.querySelector(".vditor-ir__marker--heading");
if (headingMarkerElement && headingMarkerElement.textContent.trim().length > 1) {
processHeading(vditor, headingMarkerElement.textContent.substr(1));
}
event.preventDefault();
return true;
}
// enter++: 标题变小
if (matchHotKey("⌘-", event)) {
const headingMarkerElement = headingElement.querySelector(".vditor-ir__marker--heading");
if (headingMarkerElement && headingMarkerElement.textContent.trim().length < 6) {
processHeading(vditor, headingMarkerElement.textContent.trim() + "# ");
}
event.preventDefault();
return true;
}
}
const blockElement = hasClosestBlock(startContainer);
if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey && range.toString() === "") {
if (fixDelete(vditor, range, event, pElement)) {
return true;
}
if (blockElement && blockElement.previousElementSibling
&& blockElement.tagName !== "UL" && blockElement.tagName !== "OL"
&& (blockElement.previousElementSibling.getAttribute("data-type") === "code-block" ||
blockElement.previousElementSibling.getAttribute("data-type") === "math-block")) {
const rangeStart = getSelectPosition(blockElement, vditor.ir.element, range).start;
if (rangeStart === 0 || (rangeStart === 1 && blockElement.innerText.startsWith(Constants.ZWSP))) {
// 当前块删除后光标落于代码渲染块上,当前块会被删除,因此需要阻止事件,不能和 keyup 中的代码块处理合并
range.selectNodeContents(blockElement.previousElementSibling.querySelector(".vditor-ir__marker--pre code"));
range.collapse(false);
expandMarker(range, vditor);
if (blockElement.textContent.trim().replace(Constants.ZWSP, "") === "") {
// 当前块为空且不是最后一个时,需要删除
blockElement.remove();
processAfterRender(vditor);
}
event.preventDefault();
return true;
}
}
// 光标位于标题前marker 后
if (headingElement) {
const headingLength = headingElement.firstElementChild.textContent.length;
if (getSelectPosition(headingElement, vditor.ir.element).start === headingLength && headingLength !== 0) {
range.setStart(headingElement.firstElementChild.firstChild, headingLength - 1);
range.collapse(true);
setSelectionFocus(range);
}
}
}
if ((event.key === "ArrowUp" || event.key === "ArrowDown") && blockElement) {
// https://github.com/Vanessa219/vditor/issues/358
blockElement.querySelectorAll(".vditor-ir__node").forEach((item: HTMLElement) => {
if (!item.contains(startContainer)) {
item.classList.add("vditor-ir__node--hidden");
}
});
if (fixFirefoxArrowUpTable(event, blockElement, range)) {
return true;
}
}
fixCursorDownInlineMath(range, event.key);
if (blockElement && keydownToc(blockElement, vditor, event, range)) {
event.preventDefault();
return true;
}
return false;
};

View File

@@ -0,0 +1,27 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {abcRenderAdapter} from "./adapterRender";
declare const ABCJS: {
renderAbc(element: HTMLElement, text: string): void;
};
export const abcRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN) => {
const abcElements = abcRenderAdapter.getElements(element);
if (abcElements.length > 0) {
addScript(`${cdn}/dist/js/abcjs/abcjs_basic.min.js`, "vditorAbcjsScript").then(() => {
abcElements.forEach((item: HTMLDivElement) => {
if (item.parentElement.classList.contains("vditor-wysiwyg__pre") ||
item.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
if (item.getAttribute("data-processed") === "true") {
return;
}
ABCJS.renderAbc(item, abcRenderAdapter.getCode(item).trim());
item.style.overflowX = "auto";
item.setAttribute("data-processed", "true");
});
});
}
};

View File

@@ -0,0 +1,37 @@
export const mathRenderAdapter = {
getCode: (mathElement: Element) => mathElement.textContent,
getElements: (element: HTMLElement) => element.querySelectorAll(".language-math"),
};
export const mermaidRenderAdapter = {
/** 不仅要返回code并且需要将 code 设置为 el 的 innerHTML */
getCode: (el: Element) => el.textContent,
getElements: (element: HTMLElement) => element.querySelectorAll(".language-mermaid"),
};
export const markmapRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (element: HTMLElement) => element.querySelectorAll(".language-markmap"),
};
export const mindmapRenderAdapter = {
getCode: (el: Element) => el.getAttribute("data-code"),
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-mindmap"),
};
export const chartRenderAdapter = {
getCode: (el: HTMLElement) => el.innerText,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-echarts"),
};
export const abcRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-abc"),
};
export const graphvizRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-graphviz"),
};
export const flowchartRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-flowchart"),
};
export const plantumlRenderAdapter = {
getCode: (el: Element) => el.textContent,
getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-plantuml"),
};

View File

@@ -0,0 +1,19 @@
export const anchorRender = (type: number) => {
document.querySelectorAll(".vditor-anchor").forEach((anchor: HTMLLinkElement) => {
if (type === 1) {
anchor.classList.add("vditor-anchor--left");
}
anchor.onclick = () => {
const id = anchor.getAttribute("href").substr(1);
const top = document.getElementById("vditorAnchor-" + id).offsetTop;
document.querySelector("html").scrollTop = top;
};
});
window.onhashchange = () => {
const element = document.getElementById("vditorAnchor-" + decodeURIComponent(window.location.hash.substr(1)));
if (element) {
document.querySelector("html").scrollTop = element.offsetTop;
}
};
};

View File

@@ -0,0 +1,38 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {chartRenderAdapter} from "./adapterRender";
import {looseJsonParse} from "../util/function";
declare const echarts: {
init(element: HTMLElement, theme?: string): IEChart;
};
export const chartRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN, theme: string) => {
const echartsElements = chartRenderAdapter.getElements(element);
if (echartsElements.length > 0) {
addScript(`${cdn}/dist/js/echarts/echarts.min.js?v=5.5.1`, "vditorEchartsScript").then(() => {
echartsElements.forEach(async (e: HTMLDivElement) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
const text = chartRenderAdapter.getCode(e).trim();
if (!text) {
return;
}
try {
if (e.getAttribute("data-processed") === "true") {
return;
}
const option = await looseJsonParse(text);
echarts.init(e, theme === "dark" ? "dark" : undefined).setOption(option);
e.setAttribute("data-processed", "true");
} catch (error) {
e.className = "vditor-reset--error";
e.innerHTML = `echarts render error: <br>${error}`;
}
});
});
}
};

View File

@@ -0,0 +1,60 @@
import {code160to32} from "../util/code160to32";
import {Constants} from "../constants";
export const codeRender = (element: HTMLElement, option?: IHljs) => {
Array.from<HTMLElement>(element.querySelectorAll("pre > code")).filter((e, index) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return false;
}
if (e.classList.contains("language-mermaid") || e.classList.contains("language-flowchart") ||
e.classList.contains("language-echarts") || e.classList.contains("language-mindmap") ||
e.classList.contains("language-plantuml") || e.classList.contains("language-markmap") ||
e.classList.contains("language-abc") || e.classList.contains("language-graphviz") ||
e.classList.contains("language-math")) {
return false;
}
if (e.style.maxHeight.indexOf("px") > -1) {
return false;
}
// 避免预览区在渲染后由于代码块过多产生性能问题 https://github.com/b3log/vditor/issues/67
if (element.classList.contains("vditor-preview") && index > 5) {
return false;
}
return true;
}).forEach((e) => {
let codeText = e.innerText;
if (e.classList.contains("highlight-chroma")) {
const codeElement = e.cloneNode(true) as HTMLElement;
codeElement.querySelectorAll(".highlight-ln").forEach((item: HTMLElement) => {
item.remove();
});
codeText = codeElement.innerText;
} else if (codeText.endsWith("\n")) {
codeText = codeText.substr(0, codeText.length - 1)
}
let iconHTML = '<svg><use xlink:href="#vditor-icon-copy"></use></svg>';
if (!document.getElementById("vditorIconScript")) {
iconHTML = '<svg viewBox="0 0 32 32"><path d="M22.545-0h-17.455c-1.6 0-2.909 1.309-2.909 2.909v20.364h2.909v-20.364h17.455v-2.909zM26.909 5.818h-16c-1.6 0-2.909 1.309-2.909 2.909v20.364c0 1.6 1.309 2.909 2.909 2.909h16c1.6 0 2.909-1.309 2.909-2.909v-20.364c0-1.6-1.309-2.909-2.909-2.909zM26.909 29.091h-16v-20.364h16v20.364z"></path></svg>';
}
const divElement = document.createElement("div");
divElement.className = "vditor-copy";
divElement.innerHTML = `<span aria-label="${window.VditorI18n?.copy || "复制"}"
onmouseover="this.setAttribute('aria-label', '${window.VditorI18n?.copy || "复制"}')"
class="vditor-tooltipped vditor-tooltipped__w"
onclick="this.previousElementSibling.select();document.execCommand('copy');this.setAttribute('aria-label', '${window.VditorI18n?.copied || "已复制"}');this.previousElementSibling.blur()">${iconHTML}</span>`;
const textarea = document.createElement("textarea");
textarea.value = code160to32(codeText);
divElement.insertAdjacentElement("afterbegin", textarea);
if (option && option.renderMenu) {
option.renderMenu(e, divElement);
}
e.before(divElement);
e.style.maxHeight = (window.outerHeight - 40) + "px";
// https://github.com/Vanessa219/vditor/issues/1356
e.insertAdjacentHTML("afterend", `<span style="position: absolute">${Constants.ZWSP}</span>`)
});
};

View File

@@ -0,0 +1,25 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {flowchartRenderAdapter} from "./adapterRender";
declare const flowchart: {
parse(text: string): { drawSVG: (type: HTMLElement) => void };
};
export const flowchartRender = (element: HTMLElement, cdn = Constants.CDN) => {
const flowchartElements = flowchartRenderAdapter.getElements(element);
if (flowchartElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/flowchart.js/flowchart.min.js`, "vditorFlowchartScript").then(() => {
flowchartElements.forEach((item: HTMLElement) => {
if (item.getAttribute("data-processed") === "true") {
return;
}
const flowchartObj = flowchart.parse(flowchartRenderAdapter.getCode(item));
item.innerHTML = "";
flowchartObj.drawSVG(item);
item.setAttribute("data-processed", "true");
});
});
};

View File

@@ -0,0 +1,11 @@
import {getMarkdown} from "./getMarkdown";
export const getHTML = (vditor: IVditor) => {
if (vditor.currentMode === "sv") {
return vditor.lute.Md2HTML(getMarkdown(vditor));
} else if (vditor.currentMode === "wysiwyg") {
return vditor.lute.VditorDOM2HTML(vditor.wysiwyg.element.innerHTML);
} else if (vditor.currentMode === "ir") {
return vditor.lute.VditorIRDOM2HTML(vditor.ir.element.innerHTML);
}
};

View File

@@ -0,0 +1,12 @@
import {code160to32} from "../util/code160to32";
export const getMarkdown = (vditor: IVditor) => {
if (vditor.currentMode === "sv") {
return code160to32(`${vditor.sv.element.textContent}\n`.replace(/\n\n$/, "\n"));
} else if (vditor.currentMode === "wysiwyg") {
return vditor.lute.VditorDOM2Md(vditor.wysiwyg.element.innerHTML);
} else if (vditor.currentMode === "ir") {
return vditor.lute.VditorIRDOM2Md(vditor.ir.element.innerHTML);
}
return "";
};

View File

@@ -0,0 +1,49 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {graphvizRenderAdapter} from "./adapterRender";
declare class Viz {
public renderSVGElement: (code: string) => Promise<any>;
constructor({ }: { worker: Worker });
}
export const graphvizRender = (element: HTMLElement, cdn = Constants.CDN) => {
const graphvizElements = graphvizRenderAdapter.getElements(element);
if (graphvizElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/graphviz/viz.js`, "vditorGraphVizScript").then(() => {
graphvizElements.forEach((e: HTMLDivElement) => {
const code = graphvizRenderAdapter.getCode(e);
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
if (e.getAttribute("data-processed") === "true" || code.trim() === "") {
return;
}
try {
const blob = new Blob([`importScripts('${(document.getElementById("vditorGraphVizScript") as HTMLScriptElement).src.replace("viz.js", "full.render.js")}');`],
{ type: "application/javascript" });
const url = window.URL || window.webkitURL;
const blobUrl = url.createObjectURL(blob);
const worker = new Worker(blobUrl);
new Viz({ worker })
.renderSVGElement(code).then((result: HTMLElement) => {
e.innerHTML = result.outerHTML;
}).catch((error) => {
e.innerHTML = `graphviz render error: <br>${error}`;
e.className = "vditor-reset--error";
});
} catch (e) {
console.error("graphviz error", e);
}
e.setAttribute("data-processed", "true");
});
});
};

View File

@@ -0,0 +1,90 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {addStyle} from "../util/addStyle";
declare const hljs: {
highlightElement(element: Element): void;
};
export const highlightRender = (hljsOption?: IHljs, element: HTMLElement | Document = document,
cdn = Constants.CDN) => {
let style = hljsOption.style;
if (!Constants.CODE_THEME.includes(style)) {
style = "github";
}
const vditorHljsStyle = document.getElementById("vditorHljsStyle") as HTMLLinkElement;
const href = `${cdn}/dist/js/highlight.js/styles/${style}.css`;
if (vditorHljsStyle && vditorHljsStyle.getAttribute('href') !== href) {
vditorHljsStyle.remove();
}
addStyle(`${cdn}/dist/js/highlight.js/styles/${style}.css`, "vditorHljsStyle");
if (hljsOption.enable === false) {
return;
}
const codes = element.querySelectorAll("pre > code");
if (codes.length === 0) {
return;
}
addScript(`${cdn}/dist/js/highlight.js/highlight.pack.js`, "vditorHljsScript").then(() => {
addScript(`${cdn}/dist/js/highlight.js/solidity.min.js`, "vditorHljsSolidityScript").then(() => {
addScript(`${cdn}/dist/js/highlight.js/yul.min.js`, "vditorHljsYulScript").then(() => {
element.querySelectorAll("pre > code").forEach((block) => {
// ir & wysiwyg 区域不渲染
if (block.parentElement.classList.contains("vditor-ir__marker--pre") ||
block.parentElement.classList.contains("vditor-wysiwyg__pre")) {
return;
}
if (block.classList.contains("language-mermaid") || block.classList.contains("language-flowchart") ||
block.classList.contains("language-echarts") || block.classList.contains("language-mindmap") ||
block.classList.contains("language-plantuml") ||
block.classList.contains("language-abc") || block.classList.contains("language-graphviz") ||
block.classList.contains("language-math")) {
return;
}
if (hljsOption.defaultLang !== "" && block.className.indexOf("language-") === -1) {
block.classList.add("language-" + hljsOption.defaultLang)
}
hljs.highlightElement(block);
if (!hljsOption.lineNumber) {
return;
}
block.classList.add("vditor-linenumber");
let linenNumberTemp: HTMLDivElement = block.querySelector(".vditor-linenumber__temp");
if (!linenNumberTemp) {
linenNumberTemp = document.createElement("div");
linenNumberTemp.className = "vditor-linenumber__temp";
block.insertAdjacentElement("beforeend", linenNumberTemp);
}
const whiteSpace = getComputedStyle(block).whiteSpace;
let isSoftWrap = false;
if (whiteSpace === "pre-wrap" || whiteSpace === "pre-line") {
isSoftWrap = true;
}
let lineNumberHTML = "";
const lineList = block.textContent.split(/\r\n|\r|\n/g);
lineList.pop();
lineList.map((line) => {
let lineHeight = "";
if (isSoftWrap) {
linenNumberTemp.textContent = line || "\n";
lineHeight = ` style="height:${linenNumberTemp.getBoundingClientRect().height}px"`;
}
lineNumberHTML += `<span${lineHeight}></span>`;
});
linenNumberTemp.style.display = "none";
lineNumberHTML = `<span class="vditor-linenumber__rows">${lineNumberHTML}</span>`;
block.insertAdjacentHTML("beforeend", lineNumberHTML);
});
})
})
});
};

View File

@@ -0,0 +1,56 @@
declare global {
interface Window {
vditorImageIntersectionObserver: IntersectionObserver;
}
}
export const lazyLoadImageRender = (element: (HTMLElement | Document) = document) => {
const loadImg = (it: HTMLImageElement) => {
const testImage = document.createElement("img");
testImage.src = it.getAttribute("data-src");
testImage.addEventListener("load", () => {
if (!it.getAttribute("style") && !it.getAttribute("class") &&
!it.getAttribute("width") && !it.getAttribute("height")) {
if (testImage.naturalHeight > testImage.naturalWidth &&
testImage.naturalWidth / testImage.naturalHeight <
document.querySelector(".vditor-reset").clientWidth / (window.innerHeight - 40) &&
testImage.naturalHeight > (window.innerHeight - 40)) {
it.style.height = (window.innerHeight - 40) + "px";
}
}
it.src = testImage.src;
});
it.removeAttribute("data-src");
};
if (!("IntersectionObserver" in window)) {
element.querySelectorAll("img").forEach((imgElement: HTMLImageElement) => {
if (imgElement.getAttribute("data-src")) {
loadImg(imgElement);
}
});
return false;
}
if (window.vditorImageIntersectionObserver) {
window.vditorImageIntersectionObserver.disconnect();
element.querySelectorAll("img").forEach((imgElement) => {
window.vditorImageIntersectionObserver.observe(imgElement);
});
} else {
window.vditorImageIntersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entrie: IntersectionObserverEntry & { target: HTMLImageElement }) => {
if ((typeof entrie.isIntersecting === "undefined"
? entrie.intersectionRatio !== 0
: entrie.isIntersecting)
&& entrie.target.getAttribute("data-src")) {
loadImg(entrie.target);
}
});
});
element.querySelectorAll("img").forEach((imgElement) => {
window.vditorImageIntersectionObserver.observe(imgElement);
});
}
};

View File

@@ -0,0 +1,57 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {markmapRenderAdapter} from "./adapterRender";
declare const window: any;
const enabled: Record<string, boolean> = {};
const transform = (transformer: any,content: string)=>{
const result = transformer.transform(content);
const keys = Object.keys(result.features).filter((key) => !enabled[key]);
keys.forEach((key) => {
enabled[key] = true;
});
const { styles, scripts } = transformer.getAssets(keys);
const { markmap } = window;
if (styles) markmap.loadCSS(styles);
if (scripts) markmap.loadJS(scripts);
return result;
}
const init = (el: HTMLElement,code: string) => {
const { Transformer, Markmap, deriveOptions , globalCSS} = window.markmap;
const transformer = new Transformer();
el.innerHTML = '<svg style="width:100%"></svg>';
const svg = el.firstChild as SVGElement;
const mm = Markmap.create(svg, null);
const { root, frontmatter } = transform(transformer, code);
const markmapOptions = frontmatter?.markmap;
const frontmatterOptions = deriveOptions(markmapOptions);
mm.setData(root, frontmatterOptions);
mm.fit();
}
export const markmapRender = (element: HTMLElement, cdn = Constants.CDN, theme: string) => {
const markmapElements = markmapRenderAdapter.getElements(element);
if (markmapElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/markmap/markmap.min.js`, "vditorMermaidScript").then(() => {
markmapElements.forEach((item) => {
const code = markmapRenderAdapter.getCode(item);
if (item.getAttribute("data-processed") === "true" || code.trim() === "") {
return;
}
const render = document.createElement("div")
render.className = "language-markmap"
item.parentNode.appendChild(render)
init(render,code)
if(item.parentNode.childNodes[0].nodeName == "CODE"){
item.parentNode.removeChild(item.parentNode.childNodes[0])
}
});
});
};

View File

@@ -0,0 +1,148 @@
import {Constants} from "../constants";
import {addScript, addScriptSync} from "../util/addScript";
import {addStyle} from "../util/addStyle";
import {code160to32} from "../util/code160to32";
import {mathRenderAdapter} from "./adapterRender";
declare const katex: {
renderToString(math: string, option: {
displayMode: boolean;
output: string;
macros: object;
}): string;
};
declare global {
interface Window {
MathJax: any;
}
}
export const mathRender = (element: HTMLElement, options?: { cdn?: string, math?: IMath }) => {
const mathElements = mathRenderAdapter.getElements(element);
if (mathElements.length === 0) {
return;
}
const defaultOptions = {
cdn: Constants.CDN,
math: {
engine: "KaTeX",
inlineDigit: false,
macros: {},
},
};
if (options && options.math) {
options.math =
Object.assign({}, defaultOptions.math, options.math);
}
options = Object.assign({}, defaultOptions, options);
if (options.math.engine === "KaTeX") {
addStyle(`${options.cdn}/dist/js/katex/katex.min.css?v=0.16.9`, "vditorKatexStyle");
addScript(`${options.cdn}/dist/js/katex/katex.min.js?v=0.16.9`, "vditorKatexScript").then(() => {
addScript(`${options.cdn}/dist/js/katex/mhchem.min.js?v=0.16.9`, "vditorKatexChemScript").then(() => {
mathElements.forEach((mathElement) => {
if (mathElement.parentElement.classList.contains("vditor-wysiwyg__pre") ||
mathElement.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
if (mathElement.getAttribute("data-math")) {
return;
}
const math = code160to32(mathRenderAdapter.getCode(mathElement));
mathElement.setAttribute("data-math", math);
try {
mathElement.innerHTML = katex.renderToString(math, {
displayMode: mathElement.tagName === "DIV",
output: "html",
macros: options.math.macros,
});
} catch (e) {
mathElement.innerHTML = e.message;
mathElement.className = "language-math vditor-reset--error";
}
mathElement.addEventListener("copy", (event: ClipboardEvent) => {
event.stopPropagation();
event.preventDefault();
const vditorMathElement = (event.currentTarget as HTMLElement).closest(".language-math");
event.clipboardData.setData("text/html", vditorMathElement.innerHTML);
event.clipboardData.setData("text/plain",
vditorMathElement.getAttribute("data-math"));
});
});
});
});
} else if (options.math.engine === "MathJax") {
const chainAsync = (fns: any) => {
if (fns.length === 0) {
return;
}
let curr = 0;
const last = fns[fns.length - 1];
const next = () => {
const fn = fns[curr++];
fn === last ? fn() : fn(next);
};
next();
};
if (!window.MathJax) {
window.MathJax = {
loader: {
paths: {mathjax: `${options.cdn}/dist/js/mathjax`},
},
startup: {
typeset: false,
},
tex: {
macros: options.math.macros,
},
};
// https://github.com/Vanessa219/vditor/issues/1453
Object.assign(window.MathJax, options.math.mathJaxOptions);
}
// 循环加载会抛异常
addScriptSync(`${options.cdn}/dist/js/mathjax/tex-svg-full.js`, "protyleMathJaxScript");
const renderMath = (mathElement: Element, next?: () => void) => {
const math = code160to32(mathElement.textContent).trim();
const mathOptions = window.MathJax.getMetricsFor(mathElement);
mathOptions.display = mathElement.tagName === "DIV";
window.MathJax.tex2svgPromise(math, mathOptions).then((node: Element) => {
mathElement.innerHTML = "";
mathElement.setAttribute("data-math", math);
mathElement.append(node);
window.MathJax.startup.document.clear();
window.MathJax.startup.document.updateDocument();
const errorTextElement = node.querySelector('[data-mml-node="merror"]');
if (errorTextElement && errorTextElement.textContent.trim() !== "") {
mathElement.innerHTML = errorTextElement.textContent.trim();
mathElement.className = "vditor-reset--error";
}
if (next) {
next();
}
});
};
window.MathJax.startup.promise.then(() => {
const chains: any[] = [];
for (let i = 0; i < mathElements.length; i++) {
const mathElement = mathElements[i];
if (!mathElement.parentElement.classList.contains("vditor-wysiwyg__pre") &&
!mathElement.parentElement.classList.contains("vditor-ir__marker--pre") &&
!mathElement.getAttribute("data-math") && code160to32(mathElement.textContent).trim()) {
chains.push((next: () => void) => {
if (i === mathElements.length - 1) {
renderMath(mathElement);
} else {
renderMath(mathElement, next);
}
});
}
}
chainAsync(chains);
});
}
};

View File

@@ -0,0 +1,105 @@
import {getSearch} from "../util/function";
const videoRender = (element: HTMLElement, url: string) => {
element.insertAdjacentHTML("afterend", `<video controls="controls" src="${url}"></video>`);
element.remove();
};
const audioRender = (element: HTMLElement, url: string) => {
element.insertAdjacentHTML("afterend", `<audio controls="controls" src="${url}"></audio>`);
element.remove();
};
const iframeRender = (element: HTMLElement, url: string) => {
const youtubeMatch = url.match(/\/\/(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([\w|-]{11})(?:(?:[\?&]t=)(\S+))?/);
const youkuMatch = url.match(/\/\/v\.youku\.com\/v_show\/id_(\w+)=*\.html/);
const qqMatch = url.match(/\/\/v\.qq\.com\/x\/cover\/.*\/([^\/]+)\.html\??.*/);
const coubMatch = url.match(/(?:www\.|\/\/)coub\.com\/view\/(\w+)/);
const facebookMatch = url.match(/(?:www\.|\/\/)facebook\.com\/([^\/]+)\/videos\/([0-9]+)/);
const dailymotionMatch = url.match(/.+dailymotion.com\/(video|hub)\/(\w+)\?/);
const bilibiliMatch = url.match(/(?:www\.|\/\/)bilibili\.com\/video\/(\w+)/);
const tedMatch = url.match(/(?:www\.|\/\/)ted\.com\/talks\/(\w+)/);
if (youtubeMatch && youtubeMatch[1].length === 11) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="//www.youtube.com/embed/${youtubeMatch[1] +
(youtubeMatch[2] ? "?start=" + youtubeMatch[2] : "")}"></iframe>`);
element.remove();
} else if (youkuMatch && youkuMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="//player.youku.com/embed/${youkuMatch[1]}"></iframe>`);
element.remove();
} else if (qqMatch && qqMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="https://v.qq.com/txp/iframe/player.html?vid=${qqMatch[1]}"></iframe>`);
element.remove();
} else if (coubMatch && coubMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video"
src="//coub.com/embed/${coubMatch[1]}?muted=false&autostart=false&originalSize=true&startWithHD=true"></iframe>`);
element.remove();
} else if (facebookMatch && facebookMatch[0]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video"
src="https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(facebookMatch[0])}"></iframe>`);
element.remove();
} else if (dailymotionMatch && dailymotionMatch[2]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video"
src="https://www.dailymotion.com/embed/video/${dailymotionMatch[2]}"></iframe>`);
element.remove();
} else if (url.indexOf("bilibili.com") > -1 && (url.indexOf("bvid=") > -1 || (bilibiliMatch && bilibiliMatch[1]))) {
const params: IObject = {
bvid: getSearch("bvid", url) || (bilibiliMatch && bilibiliMatch[1]),
page: "1",
high_quality: "1",
as_wide: "1",
allowfullscreen: "true",
autoplay: "0"
};
new URL(url.startsWith("http") ? url : "https:" + url).search.split("&").forEach((item, index) => {
if (!item) {
return;
}
if (index === 0) {
item = item.substr(1);
}
const keyValue = item.split("=");
params[keyValue[0]] = keyValue[1];
});
let src = "https://player.bilibili.com/player.html?";
const keys = Object.keys(params);
keys.forEach((key, index) => {
src += `${key}=${params[key]}`;
if (index < keys.length - 1) {
src += "&";
}
});
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="${src}"></iframe>`);
element.remove();
} else if (tedMatch && tedMatch[1]) {
element.insertAdjacentHTML("afterend",
`<iframe class="iframe__video" src="//embed.ted.com/talks/${tedMatch[1]}"></iframe>`);
element.remove();
}
};
export const mediaRender = (element: HTMLElement) => {
if (!element) {
return;
}
element.querySelectorAll("a").forEach((aElement) => {
const url = aElement.getAttribute("href");
if (!url) {
return;
}
if (url.match(/^.+.(mp4|m4v|ogg|ogv|webm)$/)) {
videoRender(aElement, url);
} else if (url.match(/^.+.(mp3|wav|flac)$/)) {
audioRender(aElement, url);
} else {
iframeRender(aElement, url);
}
});
};

View File

@@ -0,0 +1,60 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {mermaidRenderAdapter} from "./adapterRender";
import {genUUID} from "../util/function";
declare const mermaid: {
initialize(options: any): void,
render(id: string, text: string): { svg: string }
};
export const mermaidRender = (element: HTMLElement, cdn = Constants.CDN, theme: string) => {
const mermaidElements = mermaidRenderAdapter.getElements(element);
if (mermaidElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/mermaid/mermaid.min.js`, "vditorMermaidScript").then(() => {
const config: any = {
securityLevel: "loose", // 升级后无 https://github.com/siyuan-note/siyuan/issues/3587可使用该选项
altFontFamily: "sans-serif",
fontFamily: "sans-serif",
startOnLoad: false,
flowchart: {
htmlLabels: true,
useMaxWidth: !0
},
sequence: {
useMaxWidth: true,
diagramMarginX: 8,
diagramMarginY: 8,
boxMargin: 8,
showSequenceNumbers: true // Mermaid 时序图增加序号 https://github.com/siyuan-note/siyuan/pull/6992 https://mermaid.js.org/syntax/sequenceDiagram.html#sequencenumbers
},
gantt: {
leftPadding: 75,
rightPadding: 20
}
};
if (theme === "dark") {
config.theme = "dark";
}
mermaid.initialize(config);
mermaidElements.forEach(async (item) => {
const code = mermaidRenderAdapter.getCode(item);
if (item.getAttribute("data-processed") === "true" || code.trim() === "") {
return;
}
const id = "mermaid"+genUUID()
try {
const mermaidData = await mermaid.render(id, item.textContent);
item.innerHTML = mermaidData.svg;
} catch (e) {
const errorElement = document.querySelector("#" + id);
item.innerHTML = `${errorElement.outerHTML}<br>
<div style="text-align: left"><small>${e.message.replace(/\n/, "<br>")}</small></div>`;
errorElement.parentElement.remove();
}
item.setAttribute("data-processed", "true");
});
});
};

View File

@@ -0,0 +1,74 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {mindmapRenderAdapter} from "./adapterRender";
declare const echarts: {
init(element: HTMLElement, theme?: string): IEChart;
};
export const mindmapRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN, theme: string) => {
const mindmapElements = mindmapRenderAdapter.getElements(element);
if (mindmapElements.length > 0) {
addScript(`${cdn}/dist/js/echarts/echarts.min.js?v=5.5.1`, "vditorEchartsScript").then(() => {
mindmapElements.forEach((e: HTMLDivElement) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
const text = mindmapRenderAdapter.getCode(e);
if (!text) {
return;
}
try {
if (e.getAttribute("data-processed") === "true") {
return;
}
echarts.init(e, theme === "dark" ? "dark" : undefined).setOption({
series: [
{
data: [JSON.parse(decodeURIComponent(text))],
initialTreeDepth: -1,
itemStyle: {
borderWidth: 0,
color: "#4285f4",
},
label: {
backgroundColor: "#f6f8fa",
borderColor: "#d1d5da",
borderRadius: 5,
borderWidth: 0.5,
color: "#586069",
lineHeight: 20,
offset: [-5, 0],
padding: [0, 5],
position: "insideRight",
},
lineStyle: {
color: "#d1d5da",
width: 1,
},
roam: true,
symbol: (value: number, params: { data?: { children?: object } }) => {
if (params?.data?.children) {
return "circle";
} else {
return "path://";
}
},
type: "tree",
},
],
tooltip: {
trigger: "item",
triggerOn: "mousemove",
},
});
e.setAttribute("data-processed", "true");
} catch (error) {
e.className = "vditor-reset--error";
e.innerHTML = `mindmap render error: <br>${error}`;
}
});
});
}
};

View File

@@ -0,0 +1,110 @@
import {hasClosestByHeadings} from "../util/hasClosestByHeadings";
import {mathRender} from "./mathRender";
export const outlineRender = (contentElement: HTMLElement, targetElement: Element, vditor?: IVditor) => {
let tocHTML = "";
const ids: string[] = [];
Array.from(contentElement.children).forEach((item: HTMLElement, index: number) => {
if (hasClosestByHeadings(item)) {
if (vditor) {
const lastIndex = item.id.lastIndexOf("_");
item.id = item.id.substring(0, lastIndex === -1 ? undefined : lastIndex) + "_" + index;
}
ids.push(item.id);
tocHTML += item.outerHTML.replace("<wbr>", "");
}
});
if (tocHTML === "") {
targetElement.innerHTML = "";
return "";
}
const tempElement = document.createElement("div");
if (vditor) {
vditor.lute.SetToC(true);
if (vditor.currentMode === "wysiwyg" && !vditor.preview.element.contains(contentElement)) {
tempElement.innerHTML = vditor.lute.SpinVditorDOM("<p>[ToC]</p>" + tocHTML);
} else if (vditor.currentMode === "ir" && !vditor.preview.element.contains(contentElement)) {
tempElement.innerHTML = vditor.lute.SpinVditorIRDOM("<p>[ToC]</p>" + tocHTML);
} else {
tempElement.innerHTML = vditor.lute.HTML2VditorDOM("<p>[ToC]</p>" + tocHTML);
}
vditor.lute.SetToC(vditor.options.preview.markdown.toc);
} else {
targetElement.classList.add("vditor-outline");
const lute = Lute.New();
lute.SetToC(true);
tempElement.innerHTML = lute.HTML2VditorDOM("<p>[ToC]</p>" + tocHTML);
}
const headingsElement = tempElement.firstElementChild.querySelectorAll("li > span[data-target-id]");
headingsElement.forEach((item, index) => {
if (item.nextElementSibling && item.nextElementSibling.tagName === "UL") {
let iconHTML = "<svg class='vditor-outline__action'><use xlink:href='#vditor-icon-down'></use></svg>";
if (!document.getElementById("vditorIconScript")) {
iconHTML = '<svg class="vditor-outline__action" viewBox="0 0 32 32"><path d="M3.76 6.12l12.24 12.213 12.24-12.213 3.76 3.76-16 16-16-16 3.76-3.76z"></path></svg>';
}
item.innerHTML = `${iconHTML}<span>${item.innerHTML}</span>`;
} else {
item.innerHTML = `<svg></svg><span>${item.innerHTML}</span>`;
}
item.setAttribute("data-target-id", ids[index]);
});
tocHTML = tempElement.firstElementChild.innerHTML;
if (headingsElement.length === 0) {
targetElement.innerHTML = "";
return tocHTML;
}
targetElement.innerHTML = tocHTML;
if (vditor) {
mathRender(targetElement as HTMLElement, {
cdn: vditor.options.cdn,
math: vditor.options.preview.math,
});
}
targetElement.firstElementChild.addEventListener("click", (event: Event) => {
let target = event.target as HTMLElement;
while (target && !target.isEqualNode(targetElement)) {
if (target.classList.contains("vditor-outline__action")) {
if (target.classList.contains("vditor-outline__action--close")) {
target.classList.remove("vditor-outline__action--close");
target.parentElement.nextElementSibling.setAttribute("style", "display:block");
} else {
target.classList.add("vditor-outline__action--close");
target.parentElement.nextElementSibling.setAttribute("style", "display:none");
}
event.preventDefault();
event.stopPropagation();
break;
} else if (target.getAttribute("data-target-id")) {
event.preventDefault();
event.stopPropagation();
const idElement = document.getElementById(target.getAttribute("data-target-id"));
if (!idElement) {
return;
}
if (vditor) {
if (vditor.options.height === "auto") {
let windowScrollY = idElement.offsetTop + vditor.element.offsetTop;
if (!vditor.options.toolbarConfig.pin) {
windowScrollY += vditor.toolbar.element.offsetHeight;
}
window.scrollTo(window.scrollX, windowScrollY);
} else {
if (vditor.element.offsetTop < window.scrollY) {
window.scrollTo(window.scrollX, vditor.element.offsetTop);
}
if (vditor.preview.element.contains(contentElement)) {
contentElement.parentElement.scrollTop = idElement.offsetTop;
} else {
contentElement.scrollTop = idElement.offsetTop;
}
}
} else {
window.scrollTo(window.scrollX, idElement.offsetTop);
}
break;
}
target = target.parentElement;
}
});
return tocHTML;
};

View File

@@ -0,0 +1,32 @@
import {Constants} from "../constants";
import {addScript} from "../util/addScript";
import {plantumlRenderAdapter} from "./adapterRender";
declare const plantumlEncoder: {
encode(options: string): string,
};
export const plantumlRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN) => {
const plantumlElements = plantumlRenderAdapter.getElements(element);
if (plantumlElements.length === 0) {
return;
}
addScript(`${cdn}/dist/js/plantuml/plantuml-encoder.min.js`, "vditorPlantumlScript").then(() => {
plantumlElements.forEach((e: HTMLDivElement) => {
if (e.parentElement.classList.contains("vditor-wysiwyg__pre") ||
e.parentElement.classList.contains("vditor-ir__marker--pre")) {
return;
}
const text = plantumlRenderAdapter.getCode(e).trim();
if (!text) {
return;
}
try {
e.innerHTML = `<object type="image/svg+xml" data="https://www.plantuml.com/plantuml/svg/~1${plantumlEncoder.encode(text)}"/>`;
} catch (error) {
e.className = "vditor-reset--error";
e.innerHTML = `plantuml render error: <br>${error}`;
}
});
});
};

View File

@@ -0,0 +1,167 @@
import {Constants} from "../constants";
import {setContentTheme} from "../ui/setContentTheme";
import {addScript, addScriptSync} from "../util/addScript";
import {hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest";
import {merge} from "../util/merge";
import {abcRender} from "./abcRender";
import {anchorRender} from "./anchorRender";
import {chartRender} from "./chartRender";
import {codeRender} from "./codeRender";
import {flowchartRender} from "./flowchartRender";
import {graphvizRender} from "./graphvizRender";
import {highlightRender} from "./highlightRender";
import {lazyLoadImageRender} from "./lazyLoadImageRender";
import {mathRender} from "./mathRender";
import {mediaRender} from "./mediaRender";
import {mermaidRender} from "./mermaidRender";
import {markmapRender} from "../markdown/markmapRender";
import {mindmapRender} from "./mindmapRender";
import {plantumlRender} from "./plantumlRender";
import {setLute} from "./setLute";
import {speechRender} from "./speechRender";
const mergeOptions = (options?: IPreviewOptions) => {
const defaultOption: IPreviewOptions = {
anchor: 0,
cdn: Constants.CDN,
customEmoji: {},
emojiPath: `${Constants.CDN}/dist/images/emoji`,
hljs: Constants.HLJS_OPTIONS,
icon: "ant",
lang: "zh_CN",
markdown: Constants.MARKDOWN_OPTIONS,
math: Constants.MATH_OPTIONS,
mode: "light",
speech: {
enable: false,
},
render: {
media: {
enable: true,
}
},
theme: Constants.THEME_OPTIONS,
};
if (options.cdn) {
if (!options.theme?.path) {
defaultOption.theme.path = `${options.cdn}/dist/css/content-theme`
}
if (!options.emojiPath) {
defaultOption.emojiPath = `${options.cdn}/dist/images/emoji`;
}
}
return merge(defaultOption, options);
};
export const md2html = (mdText: string, options?: IPreviewOptions) => {
const mergedOptions = mergeOptions(options);
return addScript(`${mergedOptions.cdn}/dist/js/lute/lute.min.js`, "vditorLuteScript").then(() => {
const lute = setLute({
autoSpace: mergedOptions.markdown.autoSpace,
gfmAutoLink: mergedOptions.markdown.gfmAutoLink,
codeBlockPreview: mergedOptions.markdown.codeBlockPreview,
emojiSite: mergedOptions.emojiPath,
emojis: mergedOptions.customEmoji,
fixTermTypo: mergedOptions.markdown.fixTermTypo,
footnotes: mergedOptions.markdown.footnotes,
headingAnchor: mergedOptions.anchor !== 0,
inlineMathDigit: mergedOptions.math.inlineDigit,
lazyLoadImage: mergedOptions.lazyLoadImage,
linkBase: mergedOptions.markdown.linkBase,
linkPrefix: mergedOptions.markdown.linkPrefix,
listStyle: mergedOptions.markdown.listStyle,
mark: mergedOptions.markdown.mark,
mathBlockPreview: mergedOptions.markdown.mathBlockPreview,
paragraphBeginningSpace: mergedOptions.markdown.paragraphBeginningSpace,
sanitize: mergedOptions.markdown.sanitize,
toc: mergedOptions.markdown.toc,
});
if (options?.renderers) {
lute.SetJSRenderers({
renderers: {
Md2HTML: options.renderers,
},
});
}
lute.SetHeadingID(true);
return lute.Md2HTML(mdText);
});
};
export const previewRender = async (previewElement: HTMLDivElement, markdown: string, options?: IPreviewOptions) => {
const mergedOptions: IPreviewOptions = mergeOptions(options);
let html = await md2html(markdown, mergedOptions);
if (mergedOptions.transform) {
html = mergedOptions.transform(html);
}
previewElement.innerHTML = html;
previewElement.classList.add("vditor-reset");
if (!mergedOptions.i18n) {
if (!["en_US", "fr_FR", "pt_BR", "ja_JP", "ko_KR", "ru_RU", "sv_SE", "zh_CN", "zh_TW"].includes(mergedOptions.lang)) {
throw new Error(
"options.lang error, see https://ld246.com/article/1549638745630#options",
);
} else {
const i18nScriptPrefix = "vditorI18nScript";
const i18nScriptID = i18nScriptPrefix + mergedOptions.lang;
document.querySelectorAll(`head script[id^="${i18nScriptPrefix}"]`).forEach((el) => {
if (el.id !== i18nScriptID) {
document.head.removeChild(el);
}
});
await addScript(`${mergedOptions.cdn}/dist/js/i18n/${mergedOptions.lang}.js`, i18nScriptID);
}
} else {
window.VditorI18n = mergedOptions.i18n;
}
if (mergedOptions.icon) {
await addScript(`${mergedOptions.cdn}/dist/js/icons/${mergedOptions.icon}.js`, "vditorIconScript");
}
setContentTheme(mergedOptions.theme.current, mergedOptions.theme.path);
if (mergedOptions.anchor === 1) {
previewElement.classList.add("vditor-reset--anchor");
}
codeRender(previewElement, mergedOptions.hljs);
highlightRender(mergedOptions.hljs, previewElement, mergedOptions.cdn);
mathRender(previewElement, {
cdn: mergedOptions.cdn,
math: mergedOptions.math,
});
mermaidRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
markmapRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
flowchartRender(previewElement, mergedOptions.cdn);
graphvizRender(previewElement, mergedOptions.cdn);
chartRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
mindmapRender(previewElement, mergedOptions.cdn, mergedOptions.mode);
plantumlRender(previewElement, mergedOptions.cdn);
abcRender(previewElement, mergedOptions.cdn);
if (mergedOptions.render.media.enable) {
mediaRender(previewElement);
}
if (mergedOptions.speech.enable) {
speechRender(previewElement);
}
if (mergedOptions.anchor !== 0) {
anchorRender(mergedOptions.anchor);
}
if (mergedOptions.after) {
mergedOptions.after();
}
if (mergedOptions.lazyLoadImage) {
lazyLoadImageRender(previewElement);
}
previewElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => {
const spanElement = hasClosestByMatchTag(event.target, "SPAN");
if (spanElement && hasClosestByClassName(spanElement, "vditor-toc")) {
const headingElement =
previewElement.querySelector("#" + spanElement.getAttribute("data-target-id")) as HTMLElement;
if (headingElement) {
window.scrollTo(window.scrollX, headingElement.offsetTop);
}
return;
}
});
};

View File

@@ -0,0 +1,24 @@
export const setLute = (options: ILuteOptions) => {
const lute: Lute = Lute.New();
lute.PutEmojis(options.emojis);
lute.SetEmojiSite(options.emojiSite);
lute.SetHeadingAnchor(options.headingAnchor);
lute.SetInlineMathAllowDigitAfterOpenMarker(options.inlineMathDigit);
lute.SetAutoSpace(options.autoSpace);
lute.SetToC(options.toc);
lute.SetFootnotes(options.footnotes);
lute.SetFixTermTypo(options.fixTermTypo);
lute.SetVditorCodeBlockPreview(options.codeBlockPreview);
lute.SetVditorMathBlockPreview(options.mathBlockPreview);
lute.SetSanitize(options.sanitize);
lute.SetChineseParagraphBeginningSpace(options.paragraphBeginningSpace);
lute.SetRenderListStyle(options.listStyle);
lute.SetLinkBase(options.linkBase);
lute.SetLinkPrefix(options.linkPrefix);
lute.SetMark(options.mark);
lute.SetGFMAutoLink(options.gfmAutoLink);
if (options.lazyLoadImage) {
lute.SetImageLazyLoading(options.lazyLoadImage);
}
return lute;
};

View File

@@ -0,0 +1,107 @@
import {setSelectionFocus} from "../util/selection";
declare global {
interface Window {
vditorSpeechRange: Range;
}
}
export const speechRender = (element: HTMLElement, lang: keyof II18n = "zh_CN") => {
if (typeof speechSynthesis === "undefined" || typeof SpeechSynthesisUtterance === "undefined") {
return;
}
const getVoice = () => {
const voices = speechSynthesis.getVoices();
let currentVoice: SpeechSynthesisVoice;
let defaultVoice;
voices.forEach((item) => {
if (item.lang === lang.replace("_", "-")) {
currentVoice = item;
}
if (item.default) {
defaultVoice = item;
}
});
if (!currentVoice) {
currentVoice = defaultVoice;
}
return currentVoice;
};
let playSVG = '<svg><use xlink:href="#vditor-icon-play"></use></svg>';
let pauseSVG = '<svg><use xlink:href="#vditor-icon-pause"></use></svg>';
if (!document.getElementById("vditorIconScript")) {
playSVG = '<svg viewBox="0 0 32 32"><path d="M3.436 0l25.128 16-25.128 16v-32z"></path></svg>';
pauseSVG = '<svg viewBox="0 0 32 32"><path d="M20.617 0h9.128v32h-9.128v-32zM2.255 32v-32h9.128v32h-9.128z"></path></svg>';
}
let speechDom: HTMLButtonElement = document.querySelector(".vditor-speech");
if (!speechDom) {
speechDom = document.createElement("button");
speechDom.className = "vditor-speech";
element.insertAdjacentElement("beforeend", speechDom);
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = getVoice;
}
}
const voice = getVoice();
const utterThis = new SpeechSynthesisUtterance();
utterThis.voice = voice;
utterThis.onend = utterThis.onerror = () => {
speechDom.style.display = "none";
speechSynthesis.cancel();
speechDom.classList.remove("vditor-speech--current");
speechDom.innerHTML = playSVG;
};
element.addEventListener(window.ontouchstart !== undefined ? "touchend" : "click", (event) => {
const target = event.target as HTMLElement
if (target.classList.contains("vditor-speech") || target.parentElement.classList.contains("vditor-speech")) {
if (!speechDom.classList.contains("vditor-speech--current")) {
utterThis.text = speechDom.getAttribute("data-text");
speechSynthesis.speak(utterThis);
speechDom.classList.add("vditor-speech--current");
speechDom.innerHTML = pauseSVG;
} else {
if (speechSynthesis.speaking) {
if (speechSynthesis.paused) {
speechSynthesis.resume();
speechDom.innerHTML = pauseSVG;
} else {
speechSynthesis.pause();
speechDom.innerHTML = playSVG;
}
}
}
setSelectionFocus(window.vditorSpeechRange);
element.focus();
return;
}
speechDom.style.display = "none";
speechSynthesis.cancel();
speechDom.classList.remove("vditor-speech--current");
speechDom.innerHTML = playSVG;
if (getSelection().rangeCount === 0) {
return;
}
const range = getSelection().getRangeAt(0)
const text = range.toString().trim();
if (!text) {
return;
}
window.vditorSpeechRange = range.cloneRange();
const rect = range.getBoundingClientRect();
speechDom.innerHTML = playSVG;
speechDom.style.display = "block";
speechDom.style.top = (rect.top + rect.height + document.querySelector("html").scrollTop - 20) + "px";
if (window.ontouchstart !== undefined) {
speechDom.style.left = ((event as TouchEvent).changedTouches[(event as TouchEvent).changedTouches.length - 1].pageX + 2) + "px";
} else {
speechDom.style.left = ((event as MouseEvent).clientX + 2) + "px";
}
speechDom.setAttribute("data-text", text);
});
};

View File

@@ -0,0 +1,45 @@
import {Constants} from "../constants";
import {outlineRender} from "../markdown/outlineRender";
import {setPadding} from "../ui/initUI";
import {setSelectionFocus} from "../util/selection";
export class Outline {
public element: HTMLElement;
constructor(outlineLabel: string) {
this.element = document.createElement("div");
this.element.className = "vditor-outline";
this.element.innerHTML = `<div class="vditor-outline__title">${outlineLabel}</div>
<div class="vditor-outline__content"></div>`;
}
public render(vditor: IVditor) {
let html = "";
if (vditor.preview.element.style.display === "block") {
html = outlineRender(vditor.preview.previewElement,
this.element.lastElementChild, vditor);
} else {
html = outlineRender(vditor[vditor.currentMode].element, this.element.lastElementChild, vditor);
}
return html;
}
public toggle(vditor: IVditor, show = true, focus = true) {
const btnElement = vditor.toolbar.elements.outline?.firstElementChild;
if (show && window.innerWidth >= Constants.MOBILE_WIDTH) {
this.element.style.display = "block";
this.render(vditor);
btnElement?.classList.add("vditor-menu--current");
} else {
this.element.style.display = "none";
btnElement?.classList.remove("vditor-menu--current");
}
if (focus && getSelection().rangeCount > 0) {
const range = getSelection().getRangeAt(0);
if (vditor[vditor.currentMode].element.contains(range.startContainer)) {
setSelectionFocus(range);
}
}
setPadding(vditor);
}
}

View File

@@ -0,0 +1,50 @@
export const previewImage = (oldImgElement: HTMLImageElement, lang: keyof II18n = "zh_CN", theme = "classic") => {
const oldImgRect = oldImgElement.getBoundingClientRect();
const height = 36;
document.body.insertAdjacentHTML("beforeend", `<div class="vditor vditor-img${theme === "dark" ? " vditor--dark" : ""}">
<div class="vditor-img__bar">
<span class="vditor-img__btn" data-deg="0">
<svg><use xlink:href="#vditor-icon-redo"></use></svg>
${window.VditorI18n.spin}
</span>
<span class="vditor-img__btn" onclick="this.parentElement.parentElement.outerHTML = '';document.body.style.overflow = ''">
X &nbsp;${window.VditorI18n.close}
</span>
</div>
<div class="vditor-img__img" onclick="this.parentElement.outerHTML = '';document.body.style.overflow = ''">
<img style="width: ${oldImgElement.width}px;height:${oldImgElement.height}px;transform: translate3d(${oldImgRect.left}px, ${oldImgRect.top - height}px, 0)" src="${oldImgElement.getAttribute("src")}">
</div>
</div>`);
document.body.style.overflow = "hidden";
// 图片从原始位置移动到预览正中间的动画效果
const imgElement = document.querySelector(".vditor-img img") as HTMLImageElement;
const translate3d = `translate3d(${Math.max(0, window.innerWidth - oldImgElement.naturalWidth) / 2}px, ${Math.max(0, window.innerHeight - height - oldImgElement.naturalHeight) / 2}px, 0)`;
setTimeout(() => {
imgElement.setAttribute("style", `transition: transform .3s ease-in-out;transform: ${translate3d}`);
setTimeout(() => {
imgElement.parentElement.scrollTo(
(imgElement.parentElement.scrollWidth - imgElement.parentElement.clientWidth) / 2,
(imgElement.parentElement.scrollHeight - imgElement.parentElement.clientHeight) / 2);
}, 400);
});
// 旋转
const btnElement = document.querySelector(".vditor-img__btn");
btnElement.addEventListener("click", () => {
const deg = parseInt(btnElement.getAttribute("data-deg"), 10) + 90;
if ((deg / 90) % 2 === 1 && oldImgElement.naturalWidth > imgElement.parentElement.clientHeight) {
imgElement.style.transform = `translate3d(${
Math.max(0, window.innerWidth - oldImgElement.naturalWidth) / 2}px, ${
oldImgElement.naturalWidth / 2 - oldImgElement.naturalHeight / 2}px, 0) rotateZ(${deg}deg)`;
} else {
imgElement.style.transform = `${translate3d} rotateZ(${deg}deg)`;
}
btnElement.setAttribute("data-deg", deg.toString());
setTimeout(() => {
imgElement.parentElement.scrollTo(
(imgElement.parentElement.scrollWidth - imgElement.parentElement.clientWidth) / 2,
(imgElement.parentElement.scrollHeight - imgElement.parentElement.clientHeight) / 2);
}, 400);
});
};

View File

@@ -0,0 +1,285 @@
import {abcRender} from "../markdown/abcRender";
import {chartRender} from "../markdown/chartRender";
import {codeRender} from "../markdown/codeRender";
import {flowchartRender} from "../markdown/flowchartRender";
import {getMarkdown} from "../markdown/getMarkdown";
import {graphvizRender} from "../markdown/graphvizRender";
import {highlightRender} from "../markdown/highlightRender";
import {mathRender} from "../markdown/mathRender";
import {mediaRender} from "../markdown/mediaRender";
import {mermaidRender} from "../markdown/mermaidRender";
import {markmapRender} from "../markdown/markmapRender";
import {mindmapRender} from "../markdown/mindmapRender";
import {plantumlRender} from "../markdown/plantumlRender";
import {getEventName} from "../util/compatibility";
import {hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest";
import {hasClosestByTag} from "../util/hasClosestByHeadings";
import {setSelectionFocus} from "../util/selection";
import {previewImage} from "./image";
export class Preview {
public element: HTMLElement;
public previewElement: HTMLElement;
private mdTimeoutId: number;
constructor(vditor: IVditor) {
this.element = document.createElement("div");
this.element.className = `vditor-preview`;
this.previewElement = document.createElement("div");
this.previewElement.className = "vditor-reset";
if (vditor.options.classes.preview) {
this.previewElement.classList.add(vditor.options.classes.preview);
}
this.previewElement.style.maxWidth = vditor.options.preview.maxWidth + "px";
this.previewElement.addEventListener("copy", (event: ClipboardEvent & { target: HTMLElement }) => {
if (event.target.tagName === "TEXTAREA") {
// https://github.com/Vanessa219/vditor/issues/901
return;
}
const tempElement = document.createElement("div");
tempElement.className = "vditor-reset";
tempElement.appendChild(getSelection().getRangeAt(0).cloneContents());
this.copyToX(vditor, tempElement, "default");
event.preventDefault();
});
this.previewElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => {
const spanElement = hasClosestByMatchTag(event.target, "SPAN");
if (spanElement && hasClosestByClassName(spanElement, "vditor-toc")) {
const headingElement =
this.previewElement.querySelector("#" + spanElement.getAttribute("data-target-id")) as HTMLElement;
if (headingElement) {
this.element.scrollTop = headingElement.offsetTop;
}
return;
}
if (event.target.tagName === "A") {
if (vditor.options.link.click) {
vditor.options.link.click(event.target);
} else if (vditor.options.link.isOpen) {
window.open(event.target.getAttribute("href"));
}
event.preventDefault();
return;
}
if (event.target.tagName === "IMG") {
if (vditor.options.image.preview) {
vditor.options.image.preview(event.target)
} else if (vditor.options.image.isPreview) {
previewImage(event.target as HTMLImageElement, vditor.options.lang, vditor.options.theme);
}
}
});
this.element.appendChild(this.previewElement);
const actions = vditor.options.preview.actions;
if (actions.length === 0) {
return;
}
const actionElement = document.createElement("div");
actionElement.className = "vditor-preview__action";
const actionHtml: string[] = [];
for (let i = 0; i < actions.length; i++) {
const action = actions[i];
if (typeof action === "object") {
actionHtml.push(`<button type="button" data-type="${action.key}" class="${action.className}"${action.tooltip ? ` aria-label="${action.tooltip}"` : ""}">${action.text}</button>`);
continue;
}
switch (action) {
case "desktop":
actionHtml.push(`<button type="button" class="vditor-preview__action--current" data-type="desktop">Desktop</button>`);
break;
case "tablet":
actionHtml.push(`<button type="button" data-type="tablet">Tablet</button>`);
break;
case "mobile":
actionHtml.push(`<button type="button" data-type="mobile">Mobile/Wechat</button>`);
break;
case "mp-wechat":
actionHtml.push(`<button type="button" data-type="mp-wechat" class="vditor-tooltipped vditor-tooltipped__w" aria-label="复制到公众号"><svg><use xlink:href="#vditor-icon-mp-wechat"></use></svg></button>`);
break;
case "zhihu":
actionHtml.push(`<button type="button" data-type="zhihu" class="vditor-tooltipped vditor-tooltipped__w" aria-label="复制到知乎"><svg><use xlink:href="#vditor-icon-zhihu"></use></svg></button>`);
break;
}
}
actionElement.innerHTML = actionHtml.join("");
actionElement.addEventListener(getEventName(), (event) => {
const btn = hasClosestByTag(event.target as HTMLElement, "BUTTON");
if (!btn) {
return;
}
const type = btn.getAttribute("data-type");
const actionCustom = actions.find((w: IPreviewActionCustom) => w?.key === type) as IPreviewActionCustom;
if (actionCustom) {
actionCustom.click(type);
return;
}
if (type === "mp-wechat" || type === "zhihu") {
this.copyToX(vditor, this.previewElement.cloneNode(true) as HTMLElement, type);
return;
}
if (type === "desktop") {
this.previewElement.style.width = "auto";
} else if (type === "tablet") {
this.previewElement.style.width = "780px";
} else {
this.previewElement.style.width = "360px";
}
if (this.previewElement.scrollWidth > this.previewElement.parentElement.clientWidth) {
this.previewElement.style.width = "auto";
}
this.render(vditor);
actionElement.querySelectorAll("button").forEach((item) => {
item.classList.remove("vditor-preview__action--current");
});
btn.classList.add("vditor-preview__action--current");
});
this.element.insertBefore(actionElement, this.previewElement);
}
public render(vditor: IVditor, value?: string) {
clearTimeout(this.mdTimeoutId);
if (this.element.style.display === "none") {
if (this.element.getAttribute("data-type") === "renderPerformance") {
vditor.tip.hide();
}
return;
}
if (value) {
this.previewElement.innerHTML = value;
return;
}
if (getMarkdown(vditor)
.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") === "") {
this.previewElement.innerHTML = "";
return;
}
const renderStartTime = new Date().getTime();
const markdownText = getMarkdown(vditor);
this.mdTimeoutId = window.setTimeout(() => {
if (vditor.options.preview.url) {
const xhr = new XMLHttpRequest();
xhr.open("POST", vditor.options.preview.url);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
const responseJSON = JSON.parse(xhr.responseText);
if (responseJSON.code !== 0) {
vditor.tip.show(responseJSON.msg);
return;
}
if (vditor.options.preview.transform) {
responseJSON.data = vditor.options.preview.transform(responseJSON.data);
}
this.previewElement.innerHTML = responseJSON.data;
this.afterRender(vditor, renderStartTime);
} else {
let html = vditor.lute.Md2HTML(markdownText);
if (vditor.options.preview.transform) {
html = vditor.options.preview.transform(html);
}
this.previewElement.innerHTML = html;
this.afterRender(vditor, renderStartTime);
}
}
};
xhr.send(JSON.stringify({markdownText}));
} else {
let html = vditor.lute.Md2HTML(markdownText);
if (vditor.options.preview.transform) {
html = vditor.options.preview.transform(html);
}
this.previewElement.innerHTML = html;
this.afterRender(vditor, renderStartTime);
}
}, vditor.options.preview.delay);
}
private afterRender(vditor: IVditor, startTime: number) {
if (vditor.options.preview.parse) {
vditor.options.preview.parse(this.element);
}
const time = (new Date().getTime() - startTime);
if ((new Date().getTime() - startTime) > 2600) {
// https://github.com/b3log/vditor/issues/67
vditor.tip.show(window.VditorI18n.performanceTip.replace("${x}", time.toString()));
vditor.preview.element.setAttribute("data-type", "renderPerformance");
} else if (vditor.preview.element.getAttribute("data-type") === "renderPerformance") {
vditor.tip.hide();
vditor.preview.element.removeAttribute("data-type");
}
const cmtFocusElement = vditor.preview.element.querySelector(".vditor-comment--focus");
if (cmtFocusElement) {
cmtFocusElement.classList.remove("vditor-comment--focus");
}
codeRender(vditor.preview.previewElement, vditor.options.preview.hljs);
highlightRender(vditor.options.preview.hljs, vditor.preview.previewElement,
vditor.options.cdn);
mermaidRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme);
markmapRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme);
flowchartRender(vditor.preview.previewElement, vditor.options.cdn);
graphvizRender(vditor.preview.previewElement, vditor.options.cdn);
chartRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme);
mindmapRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme);
plantumlRender(vditor.preview.previewElement, vditor.options.cdn);
abcRender(vditor.preview.previewElement, vditor.options.cdn);
if (vditor.options.preview.render.media.enable) {
mediaRender(vditor.preview.previewElement);
}
vditor.options.customRenders.forEach((item) => {
item.render(vditor.preview.previewElement, vditor);
})
// toc render
const editorElement = vditor.preview.element;
let tocHTML = vditor.outline.render(vditor);
if (tocHTML === "") {
tocHTML = "[ToC]";
}
editorElement.querySelectorAll('[data-type="toc-block"]').forEach((item: HTMLElement) => {
item.innerHTML = tocHTML;
mathRender(item, {
cdn: vditor.options.cdn,
math: vditor.options.preview.math,
});
});
mathRender(vditor.preview.previewElement, {
cdn: vditor.options.cdn,
math: vditor.options.preview.math,
});
}
private copyToX(vditor: IVditor, copyElement: HTMLElement, type = "mp-wechat") {
// fix math render
if (type !== "zhihu") {
copyElement.querySelectorAll(".katex-html .base").forEach((item: HTMLElement) => {
item.style.display = "initial";
});
} else {
copyElement.querySelectorAll(".language-math").forEach((item: HTMLElement) => {
item.outerHTML = `<img class="Formula-image" data-eeimg="true" src="//www.zhihu.com/equation?tex=" alt="${item.getAttribute("data-math")}\\" style="display: block; margin: 0 auto; max-width: 100%;">`;
});
}
// 防止背景色被粘贴到公众号中
copyElement.style.backgroundColor = "#fff";
// 代码背景
copyElement.querySelectorAll("code").forEach((item) => {
item.style.backgroundImage = "none";
});
this.element.append(copyElement);
const range = copyElement.ownerDocument.createRange();
range.selectNode(copyElement);
setSelectionFocus(range);
document.execCommand("copy");
copyElement.remove();
vditor.tip.show(['zhihu', 'mp-wechat'].includes(type) ? `已复制,可到${type === "zhihu" ? "知乎" : "微信公众号平台"}进行粘贴` : `已复制到剪切板`);
}
}

View File

@@ -0,0 +1,57 @@
export class Resize {
public element: HTMLElement;
constructor(vditor: IVditor) {
this.element = document.createElement("div");
this.element.className = `vditor-resize vditor-resize--${vditor.options.resize.position}`;
this.element.innerHTML = `<div><svg><use xlink:href="#vditor-icon-resize"></use></svg></div>`;
this.bindEvent(vditor);
}
private bindEvent(vditor: IVditor) {
this.element.addEventListener("mousedown", (event: MouseEvent) => {
const documentSelf = document;
const y = event.clientY;
const height = vditor.element.offsetHeight;
const minHeight = 63 + vditor.element.querySelector(".vditor-toolbar").clientHeight;
documentSelf.ondragstart = () => false;
if (window.captureEvents) {
window.captureEvents();
}
this.element.classList.add("vditor-resize--selected");
documentSelf.onmousemove = (moveEvent: MouseEvent) => {
if (vditor.options.resize.position === "top") {
vditor.element.style.height = Math.max(minHeight, height + (y - moveEvent.clientY)) + "px";
} else {
vditor.element.style.height = Math.max(minHeight, height + (moveEvent.clientY - y)) + "px";
}
if (vditor.options.typewriterMode) {
vditor.sv.element.style.paddingBottom =
vditor.sv.element.parentElement.offsetHeight / 2 + "px";
}
};
documentSelf.onmouseup = () => {
if (vditor.options.resize.after) {
vditor.options.resize.after(vditor.element.offsetHeight - height);
}
if (window.captureEvents) {
window.captureEvents();
}
documentSelf.onmousemove = null;
documentSelf.onmouseup = null;
documentSelf.ondragstart = null;
documentSelf.onselectstart = null;
documentSelf.onselect = null;
this.element.classList.remove("vditor-resize--selected");
};
});
}
}

View File

@@ -0,0 +1,27 @@
/**
* 合并脚注
* @param elements vditor.sv.element
* @param afterCombine 每个脚注块合并完成后的回调, param: root为合并后的脚注块
*/
export const combineFootnote = (elements: HTMLElement, afterCombine?: (root: HTMLElement) => void ) => {
elements.querySelectorAll("[data-type=footnotes-link]").forEach((el: Element) => {
const root = el.parentElement
let footnote = root.nextSibling
// 寻找所有该脚注的块
while (footnote) {
if (footnote.textContent.startsWith(" ")) {
// 解析到四个空格加入到root并继续解析
const thisNode = footnote
thisNode.childNodes.forEach(node => {
root.append(node.cloneNode(true))
})
footnote = footnote.nextSibling
thisNode.remove()
} else {
// 非空格停止解析
break
}
}
afterCombine && afterCombine(root)
})
}

View File

@@ -0,0 +1,125 @@
import {isCtrl, isFirefox} from "../util/compatibility";
import {
blurEvent,
copyEvent, cutEvent,
dropEvent,
focusEvent,
hotkeyEvent,
scrollCenter,
selectEvent,
} from "../util/editorCommonEvent";
import {paste} from "../util/fixBrowserBehavior";
import {getSelectText} from "../util/getSelectText";
import {inputEvent} from "./inputEvent";
import {processAfterRender} from "./process";
class Editor {
public range: Range;
public element: HTMLPreElement;
public composingLock: boolean = false;
public processTimeoutId: number;
public hlToolbarTimeoutId: number;
public preventInput: boolean;
constructor(vditor: IVditor) {
this.element = document.createElement("pre");
this.element.className = "vditor-sv vditor-reset";
this.element.setAttribute("placeholder", vditor.options.placeholder);
this.element.setAttribute("contenteditable", "true");
this.element.setAttribute("spellcheck", "false");
this.bindEvent(vditor);
focusEvent(vditor, this.element);
blurEvent(vditor, this.element);
hotkeyEvent(vditor, this.element);
selectEvent(vditor, this.element);
dropEvent(vditor, this.element);
copyEvent(vditor, this.element, this.copy);
cutEvent(vditor, this.element, this.copy);
}
private copy(event: ClipboardEvent, vditor: IVditor) {
event.stopPropagation();
event.preventDefault();
event.clipboardData.setData("text/plain", getSelectText(vditor[vditor.currentMode].element));
}
private bindEvent(vditor: IVditor) {
this.element.addEventListener("paste", (event: ClipboardEvent & { target: HTMLElement }) => {
paste(vditor, event, {
pasteCode: (code: string) => {
document.execCommand("insertHTML", false, code);
},
});
});
this.element.addEventListener("scroll", () => {
if (vditor.preview.element.style.display !== "block") {
return;
}
const textScrollTop = this.element.scrollTop;
const textHeight = this.element.clientHeight;
const textScrollHeight = this.element.scrollHeight - parseFloat(this.element.style.paddingBottom || "0");
const preview = vditor.preview.element;
if ((textScrollTop / textHeight > 0.5)) {
preview.scrollTop = (textScrollTop + textHeight) *
preview.scrollHeight / textScrollHeight - textHeight;
} else {
preview.scrollTop = textScrollTop *
preview.scrollHeight / textScrollHeight;
}
});
this.element.addEventListener("compositionstart", (event: InputEvent) => {
this.composingLock = true;
});
this.element.addEventListener("compositionend", (event: InputEvent) => {
if (!isFirefox()) {
inputEvent(vditor, event);
}
this.composingLock = false;
});
this.element.addEventListener("input", (event: InputEvent) => {
if (event.inputType === "deleteByDrag" || event.inputType === "insertFromDrop") {
// https://github.com/Vanessa219/vditor/issues/801 编辑器内容拖拽问题
return;
}
if (this.composingLock || event.data === "" || event.data === "“" || event.data === "《") {
return;
}
if (this.preventInput) {
this.preventInput = false;
processAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: true,
enableInput: true,
});
return;
}
inputEvent(vditor, event);
});
this.element.addEventListener("keyup", (event) => {
if (event.isComposing || isCtrl(event)) {
return;
}
if ((event.key === "Backspace" || event.key === "Delete") &&
vditor.sv.element.innerHTML !== "" && vditor.sv.element.childNodes.length === 1 &&
vditor.sv.element.firstElementChild && vditor.sv.element.firstElementChild.tagName === "DIV"
&& vditor.sv.element.firstElementChild.childElementCount === 2
&& (vditor.sv.element.firstElementChild.textContent === "" || vditor.sv.element.textContent === "\n")) {
// 为空时显示 placeholder
vditor.sv.element.innerHTML = "";
return;
}
if (event.key === "Enter") {
scrollCenter(vditor);
}
});
}
}
export {Editor};

View File

@@ -0,0 +1,188 @@
import {scrollCenter} from "../util/editorCommonEvent";
import {hasClosestByAttribute} from "../util/hasClosest";
import {getSelectPosition, setRangeByWbr} from "../util/selection";
import {getSideByType, processAfterRender, processSpinVditorSVDOM} from "./process";
import {combineFootnote} from "./combineFootnote";
export const inputEvent = (vditor: IVditor, event?: InputEvent) => {
const range = getSelection().getRangeAt(0).cloneRange();
let startContainer = range.startContainer;
if (range.startContainer.nodeType !== 3 && (range.startContainer as HTMLElement).tagName === "DIV") {
startContainer = range.startContainer.childNodes[range.startOffset - 1];
}
let blockElement = hasClosestByAttribute(startContainer, "data-block", "0");
// 不调用 lute 解析
if (blockElement && event && (event.inputType === "deleteContentBackward" || event.data === " ")) {
// 开始可以输入空格
const startOffset = getSelectPosition(blockElement, vditor.sv.element, range).start;
let startSpace = true;
for (let i = startOffset - 1;
// 软换行后有空格
i > blockElement.textContent.substr(0, startOffset).lastIndexOf("\n"); i--) {
if (blockElement.textContent.charAt(i) !== " " &&
// 多个 tab 前删除不形成代码块 https://github.com/Vanessa219/vditor/issues/162 1
blockElement.textContent.charAt(i) !== "\t") {
startSpace = false;
break;
}
}
if (startOffset === 0) {
startSpace = false;
}
if (startSpace) {
processAfterRender(vditor);
return;
}
if (event.inputType === "deleteContentBackward") {
// https://github.com/Vanessa219/vditor/issues/584 代码块 marker 删除
const codeBlockMarkerElement =
hasClosestByAttribute(startContainer, "data-type", "code-block-open-marker") ||
hasClosestByAttribute(startContainer, "data-type", "code-block-close-marker");
if (codeBlockMarkerElement) {
if (codeBlockMarkerElement.getAttribute("data-type") === "code-block-close-marker") {
const openMarkerElement = getSideByType(startContainer, "code-block-open-marker");
if (openMarkerElement) {
openMarkerElement.textContent = codeBlockMarkerElement.textContent;
processAfterRender(vditor);
return;
}
}
if (codeBlockMarkerElement.getAttribute("data-type") === "code-block-open-marker") {
const openMarkerElement = getSideByType(startContainer, "code-block-close-marker", false);
if (openMarkerElement) {
openMarkerElement.textContent = codeBlockMarkerElement.textContent;
processAfterRender(vditor);
return;
}
}
}
// https://github.com/Vanessa219/vditor/issues/877 数学公式输入删除生成节点
const mathBlockMarkerElement =
hasClosestByAttribute(startContainer, "data-type", "math-block-open-marker");
if (mathBlockMarkerElement) {
const mathBlockCloseElement = mathBlockMarkerElement.nextElementSibling.nextElementSibling;
if (mathBlockCloseElement && mathBlockCloseElement.getAttribute("data-type") === "math-block-close-marker") {
mathBlockCloseElement.remove();
processAfterRender(vditor);
}
return;
}
blockElement.querySelectorAll('[data-type="code-block-open-marker"]').forEach((item: HTMLElement) => {
if (item.textContent.length === 1) {
item.remove();
}
});
blockElement.querySelectorAll('[data-type="code-block-close-marker"]').forEach((item: HTMLElement) => {
if (item.textContent.length === 1) {
item.remove();
}
});
// 标题删除
const headingElement = hasClosestByAttribute(startContainer, "data-type", "heading-marker");
if (headingElement && headingElement.textContent.indexOf("#") === -1) {
processAfterRender(vditor);
return;
}
}
// 删除或空格不解析,否则会 format 回去
if ((event.data === " " || event.inputType === "deleteContentBackward") &&
(hasClosestByAttribute(startContainer, "data-type", "padding") // 场景b 前进行删除 [> 1. a\n> b]
|| hasClosestByAttribute(startContainer, "data-type", "li-marker") // 场景:删除最后一个字符 [* 1\n* ]
|| hasClosestByAttribute(startContainer, "data-type", "task-marker") // 场景:删除最后一个字符 [* [ ] ]
|| hasClosestByAttribute(startContainer, "data-type", "blockquote-marker") // 场景:删除最后一个字符 [> ]
)) {
processAfterRender(vditor);
return;
}
}
if (blockElement && blockElement.textContent.trimRight() === "$$") {
// 内联数学公式
processAfterRender(vditor);
return;
}
if (!blockElement) {
blockElement = vditor.sv.element;
}
if (blockElement.firstElementChild?.getAttribute("data-type") === "link-ref-defs-block") {
// 修改链接引用
blockElement = vditor.sv.element;
}
if (hasClosestByAttribute(startContainer, "data-type", "footnotes-link")) {
// 修改脚注角标
blockElement = vditor.sv.element;
}
// 添加光标位置
if (blockElement.textContent.indexOf(Lute.Caret) === -1) {
// 点击工具栏会插入 Caret
range.insertNode(document.createTextNode(Lute.Caret));
}
// 清除浏览器自带的样式
blockElement.querySelectorAll("[style]").forEach((item) => { // 不可前置,否则会影响 newline 的样式
item.removeAttribute("style");
});
blockElement.querySelectorAll("font").forEach((item) => { // 不可前置,否则会影响光标的位置
item.outerHTML = item.innerHTML;
});
let html = blockElement.textContent;
const isSVElement = blockElement.isEqualNode(vditor.sv.element);
if (isSVElement) {
html = blockElement.textContent;
} else {
// 添加前一个块元素
if (blockElement.previousElementSibling) {
html = blockElement.previousElementSibling.textContent + html;
blockElement.previousElementSibling.remove();
}
if (blockElement.previousElementSibling && html.indexOf("---\n") === 0) {
// 确认 yaml-front 是否为首行
html = blockElement.previousElementSibling.textContent + html;
blockElement.previousElementSibling.remove();
}
// 添加链接引用
let footnotes = ""
vditor.sv.element.querySelectorAll("[data-type='link-ref-defs-block']").forEach((item, index) => {
if (item && !(blockElement as HTMLElement).isEqualNode(item.parentElement)) {
footnotes += item.parentElement.textContent + "\n";
item.parentElement.remove();
}
});
// 添加脚注到文章头便于lute处理
vditor.sv.element.querySelectorAll("[data-type='footnotes-link']").forEach((item, index) => {
if (item && !(blockElement as HTMLElement).isEqualNode(item.parentElement)) {
footnotes += item.parentElement.textContent + "\n";
item.parentElement.remove();
}
});
html = footnotes + html;
}
html = processSpinVditorSVDOM(html, vditor);
if (isSVElement) {
blockElement.innerHTML = html;
} else {
blockElement.outerHTML = html;
}
vditor.sv.element.querySelectorAll("[data-type='link-ref-defs-block']").forEach((item) => {
vditor.sv.element.insertAdjacentElement("beforeend", item.parentElement)
})
// 合并脚注
combineFootnote(vditor.sv.element, (root: HTMLElement) => {
vditor.sv.element.insertAdjacentElement("beforeend", root)
})
setRangeByWbr(vditor.sv.element, range);
scrollCenter(vditor);
processAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: true,
enableInput: true,
});
};

View File

@@ -0,0 +1,215 @@
import {getMarkdown} from "../markdown/getMarkdown";
import {accessLocalStorage} from "../util/compatibility";
import {scrollCenter} from "../util/editorCommonEvent";
import {hasClosestBlock, hasClosestByAttribute} from "../util/hasClosest";
import {hasClosestByTag} from "../util/hasClosestByHeadings";
import {log} from "../util/log";
import {getEditorRange, setRangeByWbr} from "../util/selection";
import {inputEvent} from "./inputEvent";
import {combineFootnote} from "./combineFootnote";
export const processPaste = (vditor: IVditor, text: string) => {
const range = getEditorRange(vditor);
range.extractContents();
range.insertNode(document.createTextNode(Lute.Caret));
range.insertNode(document.createTextNode(text));
let blockElement = hasClosestByAttribute(range.startContainer, "data-block", "0");
if (!blockElement) {
blockElement = vditor.sv.element;
}
let spinHTML = vditor.lute.SpinVditorSVDOM(blockElement.textContent)
spinHTML = "<div data-block='0'>" +
spinHTML.replace(/<span data-type="newline"><br \/><span style="display: none">\n<\/span><\/span><span data-type="newline"><br \/><span style="display: none">\n<\/span><\/span></g, '<span data-type="newline"><br /><span style="display: none">\n</span></span><span data-type="newline"><br /><span style="display: none">\n</span></span></div><div data-block="0"><') +
"</div>";
if (blockElement.isEqualNode(vditor.sv.element)) {
blockElement.innerHTML = spinHTML;
} else {
blockElement.outerHTML = spinHTML;
}
combineFootnote(vditor.sv.element)
setRangeByWbr(vditor.sv.element, range);
scrollCenter(vditor);
};
export const getSideByType = (spanNode: Node, type: string, isPrevious = true) => {
let sideElement = spanNode as Element;
if (sideElement.nodeType === 3) {
sideElement = sideElement.parentElement;
}
while (sideElement) {
if (sideElement.getAttribute("data-type") === type) {
return sideElement;
}
if (isPrevious) {
sideElement = sideElement.previousElementSibling;
} else {
sideElement = sideElement.nextElementSibling;
}
}
return false;
};
export const processSpinVditorSVDOM = (html: string, vditor: IVditor) => {
log("SpinVditorSVDOM", html, "argument", vditor.options.debugger);
const spinHTML = vditor.lute.SpinVditorSVDOM(html)
html = "<div data-block='0'>" +
spinHTML.replace(/<span data-type="newline"><br \/><span style="display: none">\n<\/span><\/span><span data-type="newline"><br \/><span style="display: none">\n<\/span><\/span></g, '<span data-type="newline"><br /><span style="display: none">\n</span></span><span data-type="newline"><br /><span style="display: none">\n</span></span></div><div data-block="0"><') +
"</div>";
log("SpinVditorSVDOM", html, "result", vditor.options.debugger);
return html;
};
export const processPreviousMarkers = (spanElement: HTMLElement) => {
const spanType = spanElement.getAttribute("data-type");
let previousElement = spanElement.previousElementSibling;
// 有内容的子列表/标题,在其 marker 后换行
let markerText = (spanType && spanType !== "text" && spanType !== "table" && spanType !== "heading-marker" &&
spanType !== "newline" && spanType !== "yaml-front-matter-open-marker" && spanType !== "yaml-front-matter-close-marker"
&& spanType !== "code-block-info" && spanType !== "code-block-close-marker" && spanType !== "code-block-open-marker") ?
spanElement.textContent : "";
let hasNL = false;
if (spanType === "newline") {
hasNL = true;
}
while (previousElement && !hasNL) {
const previousType = previousElement.getAttribute("data-type");
if (previousType === "li-marker" || previousType === "blockquote-marker" || previousType === "task-marker" ||
previousType === "padding") {
const previousText = previousElement.textContent;
if (previousType === "li-marker" &&
(spanType === "code-block-open-marker" || spanType === "code-block-info")) {
// https://github.com/Vanessa219/vditor/issues/586
markerText = previousText.replace(/\S/g, " ") + markerText;
} else if (spanType === "code-block-close-marker" &&
previousElement.nextElementSibling.isSameNode(spanElement)) {
// https://github.com/Vanessa219/vditor/issues/594
const openMarker = getSideByType(spanElement, "code-block-open-marker");
if (openMarker && openMarker.previousElementSibling) {
previousElement = openMarker.previousElementSibling;
markerText = previousText + markerText;
}
} else {
markerText = previousText + markerText;
}
} else if (previousType === "newline") {
hasNL = true;
}
previousElement = previousElement.previousElementSibling;
}
return markerText;
};
export const processAfterRender = (vditor: IVditor, options = {
enableAddUndoStack: true,
enableHint: false,
enableInput: true,
}) => {
if (options.enableHint) {
vditor.hint.render(vditor);
}
vditor.preview.render(vditor);
const text = getMarkdown(vditor);
if (typeof vditor.options.input === "function" && options.enableInput) {
vditor.options.input(text);
}
if (vditor.options.counter.enable) {
vditor.counter.render(vditor, text);
}
if (vditor.options.cache.enable && accessLocalStorage()) {
localStorage.setItem(vditor.options.cache.id, text);
if (vditor.options.cache.after) {
vditor.options.cache.after(text);
}
}
if (vditor.devtools) {
vditor.devtools.renderEchart(vditor);
}
clearTimeout(vditor.sv.processTimeoutId);
vditor.sv.processTimeoutId = window.setTimeout(() => {
if (options.enableAddUndoStack && !vditor.sv.composingLock) {
vditor.undo.addToUndoStack(vditor);
}
}, vditor.options.undoDelay);
};
export const processHeading = (vditor: IVditor, value: string) => {
const range = getEditorRange(vditor);
const headingElement = hasClosestByTag(range.startContainer, "SPAN");
if (headingElement && headingElement.textContent.trim() !== "") {
value = "\n" + value;
}
range.collapse(true);
document.execCommand("insertHTML", false, value);
};
export const processToolbar = (vditor: IVditor, actionBtn: Element, prefix: string, suffix: string) => {
const range = getEditorRange(vditor);
const commandName = actionBtn.getAttribute("data-type");
// 添加
if (vditor.sv.element.childNodes.length === 0) {
vditor.sv.element.innerHTML = `<span data-type="p" data-block="0"><span data-type="text"><wbr></span></span><span data-type="newline"><br><span style="display: none">
</span></span>`;
setRangeByWbr(vditor.sv.element, range);
}
const blockElement = hasClosestBlock(range.startContainer);
const spanElement = hasClosestByTag(range.startContainer, "SPAN");
if (!blockElement) {
return;
}
if (commandName === "link") {
let html;
if (range.toString() === "") {
html = `${prefix}${Lute.Caret}${suffix}`;
} else {
html = `${prefix}${range.toString()}${suffix.replace(")", Lute.Caret + ")")}`;
}
document.execCommand("insertHTML", false, html);
return;
} else if (commandName === "italic" || commandName === "bold" || commandName === "strike" ||
commandName === "inline-code" || commandName === "code" || commandName === "table" || commandName === "line") {
let html;
// https://github.com/Vanessa219/vditor/issues/563 代码块不需要后面的 ```
if (range.toString() === "") {
html = `${prefix}${Lute.Caret}${commandName === "code" ? "" : suffix}`;
} else {
html = `${prefix}${range.toString()}${Lute.Caret}${commandName === "code" ? "" : suffix}`;
}
if (commandName === "table" || (commandName === "code" && spanElement && spanElement.textContent !== "")) {
html = "\n\n" + html;
} else if (commandName === "line") {
html = `\n\n${prefix}\n${Lute.Caret}`;
}
document.execCommand("insertHTML", false, html);
return;
} else if (commandName === "check" || commandName === "list" || commandName === "ordered-list" ||
commandName === "quote") {
if (spanElement) {
let marker = "* ";
if (commandName === "check") {
marker = "* [ ] ";
} else if (commandName === "ordered-list") {
marker = "1. ";
} else if (commandName === "quote") {
marker = "> ";
}
const newLine = getSideByType(spanElement, "newline");
if (newLine) {
newLine.insertAdjacentText("afterend", marker);
} else {
blockElement.insertAdjacentText("afterbegin", marker);
}
inputEvent(vditor);
return;
}
}
setRangeByWbr(vditor.sv.element, range);
processAfterRender(vditor);
};

View File

@@ -0,0 +1,202 @@
import {isCtrl} from "../util/compatibility";
import {fixTab} from "../util/fixBrowserBehavior";
import {hasClosestByAttribute} from "../util/hasClosest";
import {hasClosestByTag} from "../util/hasClosestByHeadings";
import {getEditorRange, getSelectPosition} from "../util/selection";
import {inputEvent} from "./inputEvent";
import {processAfterRender, processPreviousMarkers} from "./process";
export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => {
vditor.sv.composingLock = event.isComposing;
if (event.isComposing) {
return false;
}
if (event.key.indexOf("Arrow") === -1 && event.key !== "Meta" && event.key !== "Control" && event.key !== "Alt" &&
event.key !== "Shift" && event.key !== "CapsLock" && event.key !== "Escape" && !/^F\d{1,2}$/.test(event.key)) {
vditor.undo.recordFirstPosition(vditor, event);
}
// 仅处理以下快捷键操作
if (event.key !== "Enter" && event.key !== "Tab" && event.key !== "Backspace" && event.key.indexOf("Arrow") === -1
&& !isCtrl(event) && event.key !== "Escape") {
return false;
}
const range = getEditorRange(vditor);
let startContainer = range.startContainer;
if (range.startContainer.nodeType !== 3 && (range.startContainer as HTMLElement).tagName === "DIV") {
startContainer = range.startContainer.childNodes[range.startOffset - 1];
}
const textElement = hasClosestByAttribute(startContainer, "data-type", "text");
// blockquote
let blockquoteMarkerElement = hasClosestByAttribute(startContainer, "data-type", "blockquote-marker");
if (!blockquoteMarkerElement && range.startOffset === 0 && textElement && textElement.previousElementSibling &&
textElement.previousElementSibling.getAttribute("data-type") === "blockquote-marker") {
blockquoteMarkerElement = textElement.previousElementSibling as HTMLElement;
}
// 回车逐个删除 blockquote marker 标记
if (blockquoteMarkerElement) {
if (event.key === "Enter" && !isCtrl(event) && !event.altKey &&
blockquoteMarkerElement.nextElementSibling.textContent.trim() === "" &&
getSelectPosition(blockquoteMarkerElement, vditor.sv.element, range).start ===
blockquoteMarkerElement.textContent.length) {
if (blockquoteMarkerElement.previousElementSibling?.getAttribute("data-type") === "padding") {
// 列表中存在多行 BQ 时,标记回车需跳出列表
blockquoteMarkerElement.previousElementSibling.setAttribute("data-action", "enter-remove");
}
blockquoteMarkerElement.remove();
processAfterRender(vditor);
event.preventDefault();
return true;
}
}
// list item
const listMarkerElement = hasClosestByAttribute(startContainer, "data-type", "li-marker") as HTMLElement;
const taskMarkerElement = hasClosestByAttribute(startContainer, "data-type", "task-marker") as HTMLElement;
let listLastMarkerElement = listMarkerElement;
if (!listLastMarkerElement) {
if (taskMarkerElement && taskMarkerElement.nextElementSibling.getAttribute("data-type") !== "task-marker") {
listLastMarkerElement = taskMarkerElement;
}
}
if (!listLastMarkerElement && range.startOffset === 0 && textElement && textElement.previousElementSibling &&
(textElement.previousElementSibling.getAttribute("data-type") === "li-marker" ||
textElement.previousElementSibling.getAttribute("data-type") === "task-marker")) {
listLastMarkerElement = textElement.previousElementSibling as HTMLElement;
}
if (listLastMarkerElement) {
const startIndex = getSelectPosition(listLastMarkerElement, vditor.sv.element, range).start;
const isTask = listLastMarkerElement.getAttribute("data-type") === "task-marker";
let listFirstMarkerElement = listLastMarkerElement;
if (isTask) {
listFirstMarkerElement = listLastMarkerElement.previousElementSibling.previousElementSibling
.previousElementSibling as HTMLElement;
}
if (startIndex === listLastMarkerElement.textContent.length) {
// 回车清空列表标记符
if (event.key === "Enter" && !isCtrl(event) && !event.altKey && !event.shiftKey &&
listLastMarkerElement.nextElementSibling.textContent.trim() === "") {
if (listFirstMarkerElement.previousElementSibling?.getAttribute("data-type") === "padding") {
listFirstMarkerElement.previousElementSibling.remove();
inputEvent(vditor);
} else {
if (isTask) {
listFirstMarkerElement.remove();
listLastMarkerElement.previousElementSibling.previousElementSibling.remove();
listLastMarkerElement.previousElementSibling.remove();
}
listLastMarkerElement.nextElementSibling.remove();
listLastMarkerElement.remove();
processAfterRender(vditor);
}
event.preventDefault();
return true;
}
// 第一个 marker 后 tab 进行缩进
if (event.key === "Tab") {
if (event.shiftKey) {
if (listFirstMarkerElement.previousElementSibling.getAttribute("data-type") === "padding") {
listFirstMarkerElement.previousElementSibling.remove();
}
} else {
listFirstMarkerElement.insertAdjacentHTML("beforebegin",
`<span data-type="padding">${listFirstMarkerElement.textContent.replace(/\S/g, " ")}</span>`);
}
if (/^\d/.test(listFirstMarkerElement.textContent)) {
listFirstMarkerElement.textContent = listFirstMarkerElement.textContent.replace(/^\d{1,}/, "1");
range.selectNodeContents(listLastMarkerElement.firstChild);
range.collapse(false);
}
inputEvent(vditor);
event.preventDefault();
return true;
}
}
}
// tab
if (fixTab(vditor, range, event)) {
return true;
}
const blockElement = hasClosestByAttribute(startContainer, "data-block", "0");
const spanElement = hasClosestByTag(startContainer, "SPAN");
// 回车
if (event.key === "Enter" && !isCtrl(event) && !event.altKey && !event.shiftKey && blockElement) {
let isFirst = false;
const newLineMatch = blockElement.textContent.match(/^\n+/);
if (getSelectPosition(blockElement, vditor.sv.element).start <= (newLineMatch ? newLineMatch[0].length : 0)) {
// 允许段落开始换行
isFirst = true;
}
let newLineText = "\n";
if (spanElement) {
if (spanElement.previousElementSibling?.getAttribute("data-action") === "enter-remove") {
// https://github.com/Vanessa219/vditor/issues/596
spanElement.previousElementSibling.remove();
processAfterRender(vditor);
event.preventDefault();
return true;
} else {
newLineText += processPreviousMarkers(spanElement);
}
}
range.insertNode(document.createTextNode(newLineText));
range.collapse(false);
if (blockElement && blockElement.textContent.trim() !== "" && !isFirst) {
inputEvent(vditor);
} else {
processAfterRender(vditor);
}
event.preventDefault();
return true;
}
// 删除后光标前有 newline 的处理
if (event.key === "Backspace" && !isCtrl(event) && !event.altKey && !event.shiftKey) {
if (spanElement && spanElement.previousElementSibling?.getAttribute("data-type") === "newline" &&
getSelectPosition(spanElement, vditor.sv.element, range).start === 1 &&
// 飘号的处理需在 inputEvent 中,否则上下飘号对不齐
spanElement.getAttribute("data-type").indexOf("code-block-") === -1) {
// 光标在每一行的第一个字符后
range.setStart(spanElement, 0);
range.extractContents();
if (spanElement.textContent.trim() !== "") {
inputEvent(vditor);
} else {
processAfterRender(vditor);
}
event.preventDefault();
return true;
}
// 每一段第一个字符前
if (blockElement && getSelectPosition(blockElement, vditor.sv.element, range).start === 0 &&
blockElement.previousElementSibling) {
range.extractContents();
let previousLastElement = blockElement.previousElementSibling.lastElementChild;
if (previousLastElement.getAttribute("data-type") === "newline") {
previousLastElement.remove();
previousLastElement = blockElement.previousElementSibling.lastElementChild;
}
// 场景:末尾无法删除 [```\ntext\n```\n\n]
if (previousLastElement.getAttribute("data-type") !== "newline") {
previousLastElement.insertAdjacentHTML("afterend", blockElement.innerHTML);
blockElement.remove();
}
if (blockElement.textContent.trim() !== "" && !blockElement.previousElementSibling?.querySelector('[data-type="code-block-open-marker"]')) {
inputEvent(vditor);
} else {
if (previousLastElement.getAttribute("data-type") !== "newline") {
// https://github.com/Vanessa219/vditor/issues/597
range.selectNodeContents(previousLastElement.lastChild);
range.collapse(false);
}
processAfterRender(vditor);
}
event.preventDefault();
return true;
}
}
return false;
};

View File

@@ -0,0 +1,39 @@
export class Tip {
public element: HTMLElement;
constructor() {
this.element = document.createElement("div");
this.element.className = "vditor-tip";
}
public show(text: string, time: number = 6000) {
this.element.className = "vditor-tip vditor-tip--show";
if (time === 0) {
this.element.innerHTML = `<div class="vditor-tip__content">${text}
<div class="vditor-tip__close">X</div></div>`;
this.element.querySelector(".vditor-tip__close").addEventListener("click", () => {
this.hide();
});
} else {
this.element.innerHTML = `<div class="vditor-tip__content">${text}</div>`;
setTimeout(() => {
this.hide();
}, time);
}
// 需在动画结束后才能确定位置
this.element.removeAttribute("style")
setTimeout(() => {
const rect = this.element.getBoundingClientRect();
if (rect.top < 46) {
this.element.style.position = "fixed"
this.element.style.top = "46px"
}
}, 150);
}
public hide() {
this.element.className = "vditor-messageElementtip";
this.element.innerHTML = "";
}
}

View File

@@ -0,0 +1,28 @@
import {Constants} from "../constants";
import {setPreviewMode} from "../ui/setPreviewMode";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Both extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
if (vditor.options.preview.mode === "both") {
this.element.children[0].classList.add("vditor-menu--current");
}
this.element.children[0].addEventListener(getEventName(), (event) => {
const btnElement = this.element.firstElementChild;
if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
event.preventDefault();
if (vditor.currentMode !== "sv") {
return;
}
if (vditor.options.preview.mode === "both") {
setPreviewMode("editor", vditor);
} else {
setPreviewMode("both", vditor);
}
});
}
}

View File

@@ -0,0 +1,8 @@
export class Br {
public element: HTMLElement;
constructor() {
this.element = document.createElement("div");
this.element.className = "vditor-toolbar__br";
}
}

View File

@@ -0,0 +1,36 @@
import {Constants} from "../constants";
import {setCodeTheme} from "../ui/setCodeTheme";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {hidePanel, toggleSubMenu} from "./setToolbar";
export class CodeTheme extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const actionBtn = this.element.children[0] as HTMLElement;
const panelElement = document.createElement("div");
panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`;
let innerHTML = "";
Constants.CODE_THEME.forEach((theme) => {
innerHTML += `<button>${theme}</button>`;
});
panelElement.innerHTML =
`<div style="overflow: auto;max-height:${window.innerHeight / 2}px">${innerHTML}</div>`;
panelElement.addEventListener(getEventName(), (event: MouseEvent & { target: HTMLElement }) => {
if (event.target.tagName === "BUTTON") {
hidePanel(vditor, ["subToolbar"]);
vditor.options.preview.hljs.style = event.target.textContent;
setCodeTheme(event.target.textContent, vditor.options.cdn);
event.preventDefault();
event.stopPropagation();
}
});
this.element.appendChild(panelElement);
toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level);
}
}

View File

@@ -0,0 +1,35 @@
import {setContentTheme} from "../ui/setContentTheme";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {hidePanel, toggleSubMenu} from "./setToolbar";
export class ContentTheme extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const actionBtn = this.element.children[0] as HTMLElement;
const panelElement = document.createElement("div");
panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`;
let innerHTML = "";
Object.keys(vditor.options.preview.theme.list).forEach((key) => {
innerHTML += `<button data-type="${key}">${vditor.options.preview.theme.list[key]}</button>`;
});
panelElement.innerHTML =
`<div style="overflow: auto;max-height:${window.innerHeight / 2}px">${innerHTML}</div>`;
panelElement.addEventListener(getEventName(), (event: MouseEvent & { target: HTMLElement }) => {
if (event.target.tagName === "BUTTON") {
hidePanel(vditor, ["subToolbar"]);
vditor.options.preview.theme.current = event.target.getAttribute("data-type");
setContentTheme(vditor.options.preview.theme.current, vditor.options.preview.theme.path);
event.preventDefault();
event.stopPropagation();
}
});
this.element.appendChild(panelElement);
toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level);
}
}

View File

@@ -0,0 +1,40 @@
export class Counter {
public element: HTMLElement;
constructor(vditor: IVditor) {
this.element = document.createElement("span");
this.element.className = "vditor-counter vditor-tooltipped vditor-tooltipped__nw";
this.render(vditor, "");
}
public render(vditor: IVditor, mdText: string) {
let length = mdText.endsWith("\n") ? mdText.length - 1 : mdText.length;
if (vditor.options.counter.type === "text" && vditor[vditor.currentMode]) {
const tempElement = vditor[vditor.currentMode].element.cloneNode(true) as HTMLElement;
tempElement.querySelectorAll(".vditor-wysiwyg__preview").forEach((item) => {
item.remove();
});
length = tempElement.textContent.length;
}
if (typeof vditor.options.counter.max === "number") {
if (length > vditor.options.counter.max) {
this.element.className = "vditor-counter vditor-counter--error";
} else {
this.element.className = "vditor-counter";
}
this.element.innerHTML = `${length}/${vditor.options.counter.max}`;
} else {
this.element.innerHTML = `${length}`;
}
this.element.setAttribute("aria-label", vditor.options.counter.type);
if (vditor.options.counter.after) {
vditor.options.counter.after(length, {
enable: vditor.options.counter.enable,
max: vditor.options.counter.max,
type: vditor.options.counter.type,
});
}
}
}

View File

@@ -0,0 +1,17 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Custom extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].innerHTML = menuItem.icon;
this.element.children[0].addEventListener(getEventName(), (event: Event & { currentTarget: HTMLElement }) => {
event.preventDefault();
if (event.currentTarget.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
menuItem.click(event, vditor);
});
}
}

View File

@@ -0,0 +1,28 @@
import {Constants} from "../constants";
import {setPadding} from "../ui/initUI";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Devtools extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.firstElementChild.addEventListener(getEventName(), (event) => {
const btnElement = this.element.firstElementChild;
if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
event.preventDefault();
if (btnElement.classList.contains("vditor-menu--current")) {
btnElement.classList.remove("vditor-menu--current");
vditor.devtools.element.style.display = "none";
setPadding(vditor);
} else {
btnElement.classList.add("vditor-menu--current");
vditor.devtools.element.style.display = "block";
setPadding(vditor);
vditor.devtools.renderEchart(vditor);
}
});
}
}

View File

@@ -0,0 +1,8 @@
export class Divider {
public element: HTMLElement;
constructor() {
this.element = document.createElement("div");
this.element.className = "vditor-toolbar__divider";
}
}

View File

@@ -0,0 +1,195 @@
import {Constants} from "../constants";
import {processAfterRender} from "../ir/process";
import {getMarkdown} from "../markdown/getMarkdown";
import {mathRender} from "../markdown/mathRender";
import {processAfterRender as processSVAfterRender, processSpinVditorSVDOM} from "../sv/process";
import {setPadding, setTypewriterPosition} from "../ui/initUI";
import {getEventName, updateHotkeyTip} from "../util/compatibility";
import {highlightToolbar} from "../util/highlightToolbar";
import {processCodeRender} from "../util/processCode";
import {renderToc} from "../util/toc";
import {renderDomByMd} from "../wysiwyg/renderDomByMd";
import {MenuItem} from "./MenuItem";
import {
disableToolbar,
enableToolbar,
hidePanel,
hideToolbar,
removeCurrentToolbar,
showToolbar, toggleSubMenu,
} from "./setToolbar";
import {combineFootnote} from "../sv/combineFootnote";
export const setEditMode = (vditor: IVditor, type: string, event: Event | string) => {
let markdownText;
if (typeof event !== "string") {
hidePanel(vditor, ["subToolbar", "hint"]);
event.preventDefault();
markdownText = getMarkdown(vditor);
} else {
markdownText = event;
}
if (vditor.currentMode === type && typeof event !== "string") {
return;
}
if (vditor.devtools) {
vditor.devtools.renderEchart(vditor);
}
if (vditor.options.preview.mode === "both" && type === "sv") {
vditor.preview.element.style.display = "block";
} else {
vditor.preview.element.style.display = "none";
}
enableToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS);
removeCurrentToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS);
disableToolbar(vditor.toolbar.elements, ["outdent", "indent"]);
if (type === "ir") {
hideToolbar(vditor.toolbar.elements, ["both"]);
showToolbar(vditor.toolbar.elements, ["outdent", "indent", "outline", "insert-before", "insert-after"]);
vditor.sv.element.style.display = "none";
vditor.wysiwyg.element.parentElement.style.display = "none";
vditor.ir.element.parentElement.style.display = "block";
vditor.lute.SetVditorIR(true);
vditor.lute.SetVditorWYSIWYG(false);
vditor.lute.SetVditorSV(false);
vditor.currentMode = "ir";
vditor.ir.element.innerHTML = vditor.lute.Md2VditorIRDOM(markdownText);
processAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
setPadding(vditor);
vditor.ir.element.querySelectorAll(".vditor-ir__preview[data-render='2']").forEach((item: HTMLElement) => {
processCodeRender(item, vditor);
});
vditor.ir.element.querySelectorAll(".vditor-toc").forEach((item: HTMLElement) => {
mathRender(item, {
cdn: vditor.options.cdn,
math: vditor.options.preview.math,
});
});
} else if (type === "wysiwyg") {
hideToolbar(vditor.toolbar.elements, ["both"]);
showToolbar(vditor.toolbar.elements, ["outdent", "indent", "outline", "insert-before", "insert-after"]);
vditor.sv.element.style.display = "none";
vditor.wysiwyg.element.parentElement.style.display = "block";
vditor.ir.element.parentElement.style.display = "none";
vditor.lute.SetVditorIR(false);
vditor.lute.SetVditorWYSIWYG(true);
vditor.lute.SetVditorSV(false);
vditor.currentMode = "wysiwyg";
setPadding(vditor);
renderDomByMd(vditor, markdownText, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
vditor.wysiwyg.element.querySelectorAll(".vditor-toc").forEach((item: HTMLElement) => {
mathRender(item, {
cdn: vditor.options.cdn,
math: vditor.options.preview.math,
});
});
vditor.wysiwyg.popover.style.display = "none";
} else if (type === "sv") {
showToolbar(vditor.toolbar.elements, ["both"]);
hideToolbar(vditor.toolbar.elements, ["outdent", "indent", "outline", "insert-before", "insert-after"]);
vditor.wysiwyg.element.parentElement.style.display = "none";
vditor.ir.element.parentElement.style.display = "none";
if (vditor.options.preview.mode === "both") {
vditor.sv.element.style.display = "block";
} else if (vditor.options.preview.mode === "editor") {
vditor.sv.element.style.display = "block";
}
vditor.lute.SetVditorIR(false);
vditor.lute.SetVditorWYSIWYG(false);
vditor.lute.SetVditorSV(true);
vditor.currentMode = "sv";
let svHTML = processSpinVditorSVDOM(markdownText, vditor);
if (svHTML === "<div data-block='0'></div>") {
// https://github.com/Vanessa219/vditor/issues/654 SV 模式 Placeholder 显示问题
svHTML = "";
}
vditor.sv.element.innerHTML = svHTML;
combineFootnote(vditor.sv.element)
processSVAfterRender(vditor, {
enableAddUndoStack: true,
enableHint: false,
enableInput: false,
});
setPadding(vditor);
}
vditor.undo.resetIcon(vditor);
if (typeof event !== "string") {
// 初始化不 focus
vditor[vditor.currentMode].element.focus();
highlightToolbar(vditor);
}
renderToc(vditor);
setTypewriterPosition(vditor);
if (vditor.toolbar.elements["edit-mode"]) {
vditor.toolbar.elements["edit-mode"].querySelectorAll("button").forEach((item) => {
item.classList.remove("vditor-menu--current");
});
vditor.toolbar.elements["edit-mode"].querySelector(`button[data-mode="${vditor.currentMode}"]`).classList.add("vditor-menu--current");
}
vditor.outline.toggle(vditor, vditor.currentMode !== "sv" && vditor.options.outline.enable, typeof event !== "string");
};
export class EditMode extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const panelElement = document.createElement("div");
panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`;
panelElement.innerHTML = `<button data-mode="wysiwyg">${window.VditorI18n.wysiwyg} &lt;${updateHotkeyTip("⌥⌘7")}></button>
<button data-mode="ir">${window.VditorI18n.instantRendering} &lt;${updateHotkeyTip("⌥⌘8")}></button>
<button data-mode="sv">${window.VditorI18n.splitView} &lt;${updateHotkeyTip("⌥⌘9")}></button>`;
this.element.appendChild(panelElement);
this._bindEvent(vditor, panelElement, menuItem);
}
public _bindEvent(vditor: IVditor, panelElement: HTMLElement, menuItem: IMenuItem) {
const actionBtn = this.element.children[0] as HTMLElement;
toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level);
panelElement.children.item(0).addEventListener(getEventName(), (event: Event) => {
// wysiwyg
setEditMode(vditor, "wysiwyg", event);
event.preventDefault();
event.stopPropagation();
});
panelElement.children.item(1).addEventListener(getEventName(), (event: Event) => {
// ir
setEditMode(vditor, "ir", event);
event.preventDefault();
event.stopPropagation();
});
panelElement.children.item(2).addEventListener(getEventName(), (event: Event) => {
// markdown
setEditMode(vditor, "sv", event);
event.preventDefault();
event.stopPropagation();
});
}
}

View File

@@ -0,0 +1,74 @@
import {getEventName} from "../util/compatibility";
import {execAfterRender} from "../util/fixBrowserBehavior";
import {hasClosestByTag} from "../util/hasClosestByHeadings";
import {getEditorRange, insertHTML, setSelectionFocus} from "../util/selection";
import {MenuItem} from "./MenuItem";
import {toggleSubMenu} from "./setToolbar";
export class Emoji extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const panelElement = document.createElement("div");
panelElement.className = "vditor-panel vditor-panel--arrow";
let commonEmojiHTML = "";
Object.keys(vditor.options.hint.emoji).forEach((key) => {
const emojiValue = vditor.options.hint.emoji[key];
if (emojiValue.indexOf(".") > -1) {
commonEmojiHTML += `<button data-value=":${key}: " data-key=":${key}:"><img
data-value=":${key}: " data-key=":${key}:" class="vditor-emojis__icon" src="${emojiValue}"/></button>`;
} else {
commonEmojiHTML += `<button data-value="${emojiValue} "
data-key="${key}"><span class="vditor-emojis__icon">${emojiValue}</span></button>`;
}
});
panelElement.innerHTML = `<div class="vditor-emojis" style="max-height: ${
vditor.options.height === "auto" ? "auto" : vditor.options.height as number - 80
}px">${commonEmojiHTML}</div><div class="vditor-emojis__tail">
<span class="vditor-emojis__tip"></span><span>${vditor.options.hint.emojiTail || ""}</span>
</div>`;
this.element.appendChild(panelElement);
toggleSubMenu(vditor, panelElement, this.element.firstElementChild, menuItem.level);
this.bindEvent(vditor);
}
private bindEvent(vditor: IVditor) {
this.element.lastElementChild.addEventListener(getEventName(), (event: Event & { target: Element }) => {
const btnElement = hasClosestByTag(event.target, "BUTTON");
if (btnElement) {
event.preventDefault();
const value = btnElement.getAttribute("data-value");
const range = getEditorRange(vditor);
let html = value;
if (vditor.currentMode === "wysiwyg") {
html = vditor.lute.SpinVditorDOM(value);
} else if (vditor.currentMode === "ir") {
html = vditor.lute.SpinVditorIRDOM(value);
}
if (value.indexOf(":") > -1 && vditor.currentMode !== "sv") {
const tempElement = document.createElement("div");
tempElement.innerHTML = html;
html = tempElement.firstElementChild.firstElementChild.outerHTML + " ";
insertHTML(html, vditor);
} else {
range.extractContents();
range.insertNode(document.createTextNode(value));
}
range.collapse(false);
setSelectionFocus(range);
(this.element.lastElementChild as HTMLElement).style.display = "none";
execAfterRender(vditor);
}
});
this.element.lastElementChild.addEventListener("mouseover", (event: Event & { target: Element }) => {
const btnElement = hasClosestByTag(event.target, "BUTTON");
if (btnElement) {
this.element.querySelector(".vditor-emojis__tip").innerHTML = btnElement.getAttribute("data-key");
}
});
}
}

View File

@@ -0,0 +1,41 @@
import {exportHTML, exportMarkdown, exportPDF} from "../export";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {hidePanel, toggleSubMenu} from "./setToolbar";
export class Export extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const actionBtn = this.element.children[0] as HTMLElement;
const panelElement = document.createElement("div");
panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`;
panelElement.innerHTML = `<button data-type="markdown">Markdown</button>
<button data-type="pdf">PDF</button>
<button data-type="html">HTML</button>`;
panelElement.addEventListener(getEventName(), (event: MouseEvent & { target: HTMLElement }) => {
const btnElement = event.target;
if (btnElement.tagName === "BUTTON") {
switch (btnElement.getAttribute("data-type")) {
case "markdown":
exportMarkdown(vditor);
break;
case "pdf":
exportPDF(vditor);
break;
case "html":
exportHTML(vditor);
break;
default:
break;
}
hidePanel(vditor, ["subToolbar"]);
event.preventDefault();
event.stopPropagation();
}
});
this.element.appendChild(panelElement);
toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level);
}
}

View File

@@ -0,0 +1,66 @@
import {setPadding, setTypewriterPosition} from "../ui/initUI";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Fullscreen extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this._bindEvent(vditor, menuItem);
}
public _bindEvent(vditor: IVditor, menuItem: IMenuItem) {
this.element.children[0].addEventListener(getEventName(), function (event) {
event.preventDefault();
if (vditor.element.className.includes("vditor--fullscreen")) {
if (!menuItem.level) {
this.innerHTML = menuItem.icon;
}
vditor.element.style.zIndex = "";
document.body.style.overflow = "";
vditor.element.classList.remove("vditor--fullscreen");
Object.keys(vditor.toolbar.elements).forEach((key) => {
const svgElement = vditor.toolbar.elements[key].firstChild as HTMLElement;
if (svgElement) {
svgElement.className = svgElement.className.replace("__s", "__n");
vditor.options.toolbar.forEach(item => {
if (typeof item !== "string" && item.tipPosition && item.name === svgElement.dataset.type) {
svgElement.className = `vditor-tooltipped vditor-tooltipped__${item.tipPosition}`;
}
})
}
});
if (vditor.counter) {
vditor.counter.element.className = vditor.counter.element.className.replace("__s", "__n");
}
} else {
if (!menuItem.level) {
this.innerHTML = '<svg><use xlink:href="#vditor-icon-contract"></use></svg>';
}
vditor.element.style.zIndex = vditor.options.fullscreen.index.toString();
document.body.style.overflow = "hidden";
vditor.element.classList.add("vditor--fullscreen");
Object.keys(vditor.toolbar.elements).forEach((key) => {
const svgElement = vditor.toolbar.elements[key].firstChild as HTMLElement;
if (svgElement) {
svgElement.className = svgElement.className.replace("__n", "__s");
}
});
if (vditor.counter) {
vditor.counter.element.className = vditor.counter.element.className.replace("__n", "__s");
}
}
if (vditor.devtools) {
vditor.devtools.renderEchart(vditor);
}
if (menuItem.click) {
menuItem.click(event, vditor);
}
setPadding(vditor);
setTypewriterPosition(vditor);
});
}
}

View File

@@ -0,0 +1,73 @@
import {Constants} from "../constants";
import {processHeading} from "../ir/process";
import {processHeading as processHeadingSV} from "../sv/process";
import {getEventName, updateHotkeyTip} from "../util/compatibility";
import {afterRenderEvent} from "../wysiwyg/afterRenderEvent";
import {removeHeading, setHeading} from "../wysiwyg/setHeading";
import {MenuItem} from "./MenuItem";
import {hidePanel} from "./setToolbar";
export class Headings extends MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
const panelElement = document.createElement("div");
panelElement.className = "vditor-hint vditor-panel--arrow";
panelElement.innerHTML = `<button data-tag="h1" data-value="# ">${window.VditorI18n.heading1} ${updateHotkeyTip("&lt;⌥⌘1>")}</button>
<button data-tag="h2" data-value="## ">${window.VditorI18n.heading2} &lt;${updateHotkeyTip("⌥⌘2")}></button>
<button data-tag="h3" data-value="### ">${window.VditorI18n.heading3} &lt;${updateHotkeyTip("⌥⌘3")}></button>
<button data-tag="h4" data-value="#### ">${window.VditorI18n.heading4} &lt;${updateHotkeyTip("⌥⌘4")}></button>
<button data-tag="h5" data-value="##### ">${window.VditorI18n.heading5} &lt;${updateHotkeyTip("⌥⌘5")}></button>
<button data-tag="h6" data-value="###### ">${window.VditorI18n.heading6} &lt;${updateHotkeyTip("⌥⌘6")}></button>`;
this.element.appendChild(panelElement);
this._bindEvent(vditor, panelElement);
}
public _bindEvent(vditor: IVditor, panelElement: HTMLElement) {
const actionBtn = this.element.children[0] as HTMLElement;
actionBtn.addEventListener(getEventName(), (event) => {
event.preventDefault();
// https://github.com/Vanessa219/vditor/issues/1391
clearTimeout(vditor.wysiwyg.afterRenderTimeoutId);
clearTimeout(vditor.ir.processTimeoutId);
clearTimeout(vditor.sv.processTimeoutId);
if (actionBtn.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
actionBtn.blur();
if (actionBtn.classList.contains("vditor-menu--current")) {
if (vditor.currentMode === "wysiwyg") {
removeHeading(vditor);
afterRenderEvent(vditor);
} else if (vditor.currentMode === "ir") {
processHeading(vditor, "");
}
actionBtn.classList.remove("vditor-menu--current");
} else {
hidePanel(vditor, ["subToolbar"]);
panelElement.style.display = "block";
}
});
for (let i = 0; i < 6; i++) {
panelElement.children.item(i).addEventListener(getEventName(), (event: Event) => {
event.preventDefault();
if (vditor.currentMode === "wysiwyg") {
setHeading(vditor, (event.target as HTMLElement).getAttribute("data-tag"));
afterRenderEvent(vditor);
actionBtn.classList.add("vditor-menu--current");
} else if (vditor.currentMode === "ir") {
processHeading(vditor, (event.target as HTMLElement).getAttribute("data-value"));
actionBtn.classList.add("vditor-menu--current");
} else {
processHeadingSV(vditor, (event.target as HTMLElement).getAttribute("data-value"));
}
panelElement.style.display = "none";
});
}
}
}

View File

@@ -0,0 +1,30 @@
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Help extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
vditor.tip.show(`<div style="margin-bottom:14px;font-size: 14px;line-height: 22px;min-width:300px;max-width: 360px;display: flex;">
<div style="margin-top: 14px;flex: 1">
<div>Markdown 使用指南</div>
<ul style="list-style: none">
<li><a href="https://ld246.com/article/1583308420519" target="_blank">语法速查手册</a></li>
<li><a href="https://ld246.com/article/1583129520165" target="_blank">基础语法</a></li>
<li><a href="https://ld246.com/article/1583305480675" target="_blank">扩展语法</a></li>
<li><a href="https://ld246.com/article/1582778815353" target="_blank">键盘快捷键</a></li>
</ul>
</div>
<div style="margin-top: 14px;flex: 1">
<div>Vditor 支持</div>
<ul style="list-style: none">
<li><a href="https://github.com/Vanessa219/vditor/issues" target="_blank">Issues</a></li>
<li><a href="https://ld246.com/tag/vditor" target="_blank">官方讨论区</a></li>
<li><a href="https://ld246.com/article/1549638745630" target="_blank">开发手册</a></li>
<li><a href="https://ld246.com/guide/markdown" target="_blank">演示地址</a></li>
</ul>
</div></div>`, 0);
});
}
}

View File

@@ -0,0 +1,25 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {listIndent} from "../util/fixBrowserBehavior";
import {hasClosestByMatchTag} from "../util/hasClosest";
import {getEditorRange} from "../util/selection";
import {MenuItem} from "./MenuItem";
export class Indent extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) ||
vditor.currentMode === "sv") {
return;
}
const range = getEditorRange(vditor);
const liElement = hasClosestByMatchTag(range.startContainer, "LI");
if (liElement) {
listIndent(vditor, liElement, range);
}
});
}
}

View File

@@ -0,0 +1,43 @@
import {VDITOR_VERSION} from "../constants";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Info extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
vditor.tip.show(`<div style="max-width: 520px; font-size: 14px;line-height: 22px;margin-bottom: 14px;">
<p style="text-align: center;margin: 14px 0">
<em>下一代的 Markdown 编辑器,为未来而构建</em>
</p>
<div style="display: flex;margin-bottom: 14px;flex-wrap: wrap;align-items: center">
<img src="https://unpkg.com/vditor/dist/images/logo.png" style="margin: 0 auto;height: 68px"/>
<div>&nbsp;&nbsp;</div>
<div style="flex: 1;min-width: 250px">
Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora和分屏预览模式。
它使用 TypeScript 实现,支持原生 JavaScript 以及 Vue、React、Angular 和 Svelte 等框架。
</div>
</div>
<div style="display: flex;flex-wrap: wrap;">
<ul style="list-style: none;flex: 1;min-width:148px">
<li>
项目地址:<a href="https://b3log.org/vditor" target="_blank">b3log.org/vditor</a>
</li>
<li>
开源协议MIT
</li>
</ul>
<ul style="list-style: none;margin-right: 18px">
<li>
组件版本Vditor v${VDITOR_VERSION} / Lute v${Lute.Version}
</li>
<li>
赞助捐赠:<a href="https://ld246.com/sponsor" target="_blank">https://ld246.com/sponsor</a>
</li>
</ul>
</div>
</div>`, 0);
});
}
}

View File

@@ -0,0 +1,18 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {insertEmptyBlock} from "../util/fixBrowserBehavior";
import {MenuItem} from "./MenuItem";
export class InsertAfter extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) ||
vditor.currentMode === "sv") {
return;
}
insertEmptyBlock(vditor, "afterend");
});
}
}

View File

@@ -0,0 +1,18 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {insertEmptyBlock} from "../util/fixBrowserBehavior";
import {MenuItem} from "./MenuItem";
export class InsertBefore extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) ||
vditor.currentMode === "sv") {
return;
}
insertEmptyBlock(vditor, "beforebegin");
});
}
}

View File

@@ -0,0 +1,54 @@
import {Constants} from "../constants";
import {processToolbar} from "../ir/process";
import {processToolbar as processToolbarSV} from "../sv/process";
import {getEventName} from "../util/compatibility";
import {updateHotkeyTip} from "../util/compatibility";
import {toolbarEvent} from "../wysiwyg/toolbarEvent";
export class MenuItem {
public element: HTMLElement;
constructor(vditor: IVditor, menuItem: IMenuItem) {
this.element = document.createElement("div");
if (menuItem.className) {
this.element.classList.add(...menuItem.className.split(" "));
}
let hotkey = menuItem.hotkey ? ` <${updateHotkeyTip(menuItem.hotkey)}>` : "";
if (menuItem.level === 2) {
hotkey = menuItem.hotkey ? ` &lt;${updateHotkeyTip(menuItem.hotkey)}&gt;` : "";
}
const tip = menuItem.tip ? menuItem.tip + hotkey : `${window.VditorI18n[menuItem.name]}${hotkey}`;
const tagName = menuItem.name === "upload" ? "div" : "button";
if (menuItem.level === 2) {
this.element.innerHTML = `<${tagName} data-type="${menuItem.name}">${tip}</${tagName}>`;
} else {
this.element.classList.add("vditor-toolbar__item");
const iconElement = document.createElement(tagName);
iconElement.setAttribute("data-type", menuItem.name);
iconElement.className = `vditor-tooltipped vditor-tooltipped__${menuItem.tipPosition}`;
iconElement.setAttribute("aria-label", tip);
iconElement.innerHTML = menuItem.icon;
this.element.appendChild(iconElement);
}
if (!menuItem.prefix) {
return;
}
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
if (vditor.currentMode === "wysiwyg") {
toolbarEvent(vditor, this.element.children[0], event);
} else if (vditor.currentMode === "ir") {
processToolbar(vditor, this.element.children[0],
menuItem.prefix || "", menuItem.suffix || "");
} else {
processToolbarSV(vditor, this.element.children[0],
menuItem.prefix || "", menuItem.suffix || "");
}
});
}
}

View File

@@ -0,0 +1,24 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import { listOutdent} from "../util/fixBrowserBehavior";
import {hasClosestByMatchTag} from "../util/hasClosest";
import {getEditorRange} from "../util/selection";
import {MenuItem} from "./MenuItem";
export class Outdent extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) ||
vditor.currentMode === "sv") {
return;
}
const range = getEditorRange(vditor);
const liElement = hasClosestByMatchTag(range.startContainer, "LI");
if (liElement) {
listOutdent(vditor, liElement, range, liElement.parentElement);
}
});
}
}

View File

@@ -0,0 +1,21 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Outline extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
if (vditor.options.outline) {
this.element.firstElementChild.classList.add("vditor-menu--current");
}
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
const btnElement = vditor.toolbar.elements.outline.firstElementChild;
if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
vditor.options.outline.enable = !this.element.firstElementChild.classList.contains("vditor-menu--current");
vditor.outline.toggle(vditor, vditor.options.outline.enable);
});
}
}

View File

@@ -0,0 +1,55 @@
import {Constants} from "../constants";
import {setPadding} from "../ui/initUI";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {disableToolbar, enableToolbar, hidePanel} from "./setToolbar";
export class Preview extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this._bindEvent(vditor);
}
public _bindEvent(vditor: IVditor) {
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
const btnElement = this.element.firstElementChild;
if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
const toolbars = Constants.EDIT_TOOLBARS.concat(["both", "edit-mode", "devtools"]);
if (btnElement.classList.contains("vditor-menu--current")) {
btnElement.classList.remove("vditor-menu--current");
if (vditor.currentMode === "sv") {
vditor.sv.element.style.display = "block";
if (vditor.options.preview.mode === "both") {
vditor.preview.element.style.display = "block";
} else {
vditor.preview.element.style.display = "none";
}
} else {
vditor[vditor.currentMode].element.parentElement.style.display = "block";
vditor.preview.element.style.display = "none";
}
enableToolbar(vditor.toolbar.elements, toolbars);
vditor.outline.render(vditor);
} else {
disableToolbar(vditor.toolbar.elements, toolbars);
vditor.preview.element.style.display = "block";
if (vditor.currentMode === "sv") {
vditor.sv.element.style.display = "none";
} else {
vditor[vditor.currentMode].element.parentElement.style.display = "none";
}
vditor.preview.render(vditor);
btnElement.classList.add("vditor-menu--current");
hidePanel(vditor, ["subToolbar", "hint", "popover"]);
setTimeout(() => {
vditor.outline.render(vditor);
}, vditor.options.preview.delay + 10);
}
setPadding(vditor);
});
}
}

View File

@@ -0,0 +1,60 @@
import {Constants} from "../constants";
import {uploadFiles} from "../upload/index";
import {getEventName} from "../util/compatibility";
import {RecordMedia} from "../util/RecordMedia";
import {MenuItem} from "./MenuItem";
export class Record extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
this._bindEvent(vditor);
}
public _bindEvent(vditor: IVditor) {
let mediaRecorder: RecordMedia;
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
const editorElement = vditor[vditor.currentMode].element;
if (!mediaRecorder) {
navigator.mediaDevices.getUserMedia({audio: true}).then((mediaStream: MediaStream) => {
mediaRecorder = new RecordMedia(mediaStream);
mediaRecorder.recorder.onaudioprocess = (e: AudioProcessingEvent) => {
// Do nothing if not recording:
if (!mediaRecorder.isRecording) {
return;
}
// Copy the data from the input buffers;
const left = e.inputBuffer.getChannelData(0);
const right = e.inputBuffer.getChannelData(1);
mediaRecorder.cloneChannelData(left, right);
};
mediaRecorder.startRecordingNewWavFile();
vditor.tip.show(window.VditorI18n.recording);
editorElement.setAttribute("contenteditable", "false");
this.element.children[0].classList.add("vditor-menu--current");
}).catch(() => {
vditor.tip.show(window.VditorI18n["record-tip"]);
});
return;
}
if (mediaRecorder.isRecording) {
mediaRecorder.stopRecording();
vditor.tip.hide();
const file: File = new File([mediaRecorder.buildWavFileBlob()],
`record${(new Date()).getTime()}.wav`, {type: "video/webm"});
uploadFiles(vditor, [file]);
this.element.children[0].classList.remove("vditor-menu--current");
} else {
vditor.tip.show(window.VditorI18n.recording);
editorElement.setAttribute("contenteditable", "false");
mediaRecorder.startRecordingNewWavFile();
this.element.children[0].classList.add("vditor-menu--current");
}
});
}
}

View File

@@ -0,0 +1,18 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {disableToolbar} from "./setToolbar";
export class Redo extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
disableToolbar({redo: this.element}, ["redo"]);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
vditor.undo.redo(vditor);
});
}
}

View File

@@ -0,0 +1,18 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
import {disableToolbar} from "./setToolbar";
export class Undo extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
disableToolbar({undo: this.element}, ["undo"]);
this.element.children[0].addEventListener(getEventName(), (event) => {
event.preventDefault();
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
vditor.undo.undo(vditor);
});
}
}

View File

@@ -0,0 +1,41 @@
import {Constants} from "../constants";
import {uploadFiles} from "../upload/index";
import {getEventName} from "../util/compatibility";
import {MenuItem} from "./MenuItem";
export class Upload extends MenuItem {
constructor(vditor: IVditor, menuItem: IMenuItem) {
super(vditor, menuItem);
let inputHTML = '<input type="file"';
if (vditor.options.upload.multiple) {
inputHTML += ' multiple="multiple"';
}
if (vditor.options.upload.accept) {
inputHTML += ` accept="${vditor.options.upload.accept}"`;
}
this.element.children[0].innerHTML = `${(menuItem.icon || '<svg><use xlink:href="#vditor-icon-upload"></use></svg>')}${inputHTML}>`;
this._bindEvent(vditor);
}
public _bindEvent(vditor: IVditor) {
this.element.children[0].addEventListener(getEventName(), (event) => {
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
event.stopPropagation();
event.preventDefault();
return;
}
});
this.element.querySelector("input").addEventListener("change",
(event: InputEvent & { target: HTMLInputElement }) => {
if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) {
event.stopPropagation();
event.preventDefault();
return;
}
if (event.target.files.length === 0) {
return;
}
uploadFiles(vditor, event.target.files, event.target);
});
}
}

View File

@@ -0,0 +1,192 @@
import {getEventName} from "../util/compatibility";
import {Both} from "./Both";
import {Br} from "./Br";
import {CodeTheme} from "./CodeTheme";
import {ContentTheme} from "./ContentTheme";
import {Counter} from "./Counter";
import {Custom} from "./Custom";
import {Devtools} from "./Devtools";
import {Divider} from "./Divider";
import {EditMode} from "./EditMode";
import {Emoji} from "./Emoji";
import {Export} from "./Export";
import {Fullscreen} from "./Fullscreen";
import {Headings} from "./Headings";
import {Help} from "./Help";
import {Indent} from "./Indent";
import {Info} from "./Info";
import {InsertAfter} from "./InsertAfter";
import {InsertBefore} from "./InsertBefore";
import {MenuItem} from "./MenuItem";
import {Outdent} from "./Outdent";
import {Outline} from "./Outline";
import {Preview} from "./Preview";
import {Record} from "./Record";
import {Redo} from "./Redo";
import {toggleSubMenu} from "./setToolbar";
import {Undo} from "./Undo";
import {Upload} from "./Upload";
export class Toolbar {
public elements: { [key: string]: HTMLElement };
public element: HTMLElement;
constructor(vditor: IVditor) {
const options = vditor.options;
this.elements = {};
this.element = document.createElement("div");
this.element.className = "vditor-toolbar";
options.toolbar.forEach((menuItem: IMenuItem, i: number) => {
const itemElement = this.genItem(vditor, menuItem, i);
this.element.appendChild(itemElement);
if (menuItem.toolbar) {
const panelElement = document.createElement("div");
panelElement.className = "vditor-hint vditor-panel--arrow";
panelElement.addEventListener(getEventName(), (event) => {
panelElement.style.display = "none";
});
menuItem.toolbar.forEach((subMenuItem: IMenuItem, subI: number) => {
subMenuItem.level = 2;
panelElement.appendChild(this.genItem(vditor, subMenuItem, i + subI));
});
itemElement.appendChild(panelElement);
toggleSubMenu(vditor, panelElement, itemElement.children[0], 2);
}
});
if (vditor.options.toolbarConfig.hide) {
this.element.classList.add("vditor-toolbar--hide");
}
if (vditor.options.toolbarConfig.pin) {
this.element.classList.add("vditor-toolbar--pin");
}
if (vditor.options.counter.enable) {
vditor.counter = new Counter(vditor);
this.element.appendChild(vditor.counter.element);
}
}
public updateConfig(vditor: IVditor, options: IToolbarConfig) {
vditor.options.toolbarConfig = Object.assign({
hide: false,
pin: false,
}, options);
if (vditor.options.toolbarConfig.hide) {
this.element.classList.add("vditor-toolbar--hide");
} else {
this.element.classList.remove("vditor-toolbar--hide");
}
if (vditor.options.toolbarConfig.pin) {
this.element.classList.add("vditor-toolbar--pin");
} else {
this.element.classList.remove("vditor-toolbar--pin");
}
}
private genItem(vditor: IVditor, menuItem: IMenuItem, index: number) {
let menuItemObj;
switch (menuItem.name) {
case "bold":
case "italic":
case "more":
case "strike":
case "line":
case "quote":
case "list":
case "ordered-list":
case "check":
case "code":
case "inline-code":
case "link":
case "table":
menuItemObj = new MenuItem(vditor, menuItem);
break;
case "emoji":
menuItemObj = new Emoji(vditor, menuItem);
break;
case "headings":
menuItemObj = new Headings(vditor, menuItem);
break;
case "|":
menuItemObj = new Divider();
break;
case "br":
menuItemObj = new Br();
break;
case "undo":
menuItemObj = new Undo(vditor, menuItem);
break;
case "redo":
menuItemObj = new Redo(vditor, menuItem);
break;
case "help":
menuItemObj = new Help(vditor, menuItem);
break;
case "both":
menuItemObj = new Both(vditor, menuItem);
break;
case "preview":
menuItemObj = new Preview(vditor, menuItem);
break;
case "fullscreen":
menuItemObj = new Fullscreen(vditor, menuItem);
break;
case "upload":
menuItemObj = new Upload(vditor, menuItem);
break;
case "record":
menuItemObj = new Record(vditor, menuItem);
break;
case "info":
menuItemObj = new Info(vditor, menuItem);
break;
case "edit-mode":
menuItemObj = new EditMode(vditor, menuItem);
break;
case "devtools":
menuItemObj = new Devtools(vditor, menuItem);
break;
case "outdent":
menuItemObj = new Outdent(vditor, menuItem);
break;
case "indent":
menuItemObj = new Indent(vditor, menuItem);
break;
case "outline":
menuItemObj = new Outline(vditor, menuItem);
break;
case "insert-after":
menuItemObj = new InsertAfter(vditor, menuItem);
break;
case "insert-before":
menuItemObj = new InsertBefore(vditor, menuItem);
break;
case "code-theme":
menuItemObj = new CodeTheme(vditor, menuItem);
break;
case "content-theme":
menuItemObj = new ContentTheme(vditor, menuItem);
break;
case "export":
menuItemObj = new Export(vditor, menuItem);
break;
default:
menuItemObj = new Custom(vditor, menuItem);
break;
}
if (!menuItemObj) {
return;
}
let key = menuItem.name;
if (key === "br" || key === "|") {
key = key + index;
}
this.elements[key] = menuItemObj.element;
return menuItemObj.element;
}
}

View File

@@ -0,0 +1,120 @@
import {Constants} from "../constants";
import {getEventName} from "../util/compatibility";
export const removeCurrentToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
const itemElement = toolbar[name].children[0];
if (itemElement && itemElement.classList.contains("vditor-menu--current")) {
itemElement.classList.remove("vditor-menu--current");
}
});
};
export const setCurrentToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
const itemElement = toolbar[name].children[0];
if (itemElement && !itemElement.classList.contains("vditor-menu--current")) {
itemElement.classList.add("vditor-menu--current");
}
});
};
export const enableToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
const itemElement = toolbar[name].children[0];
if (itemElement && itemElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
itemElement.classList.remove(Constants.CLASS_MENU_DISABLED);
}
});
};
export const disableToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
const itemElement = toolbar[name].children[0];
if (itemElement && !itemElement.classList.contains(Constants.CLASS_MENU_DISABLED)) {
itemElement.classList.add(Constants.CLASS_MENU_DISABLED);
}
});
};
export const hideToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
if (toolbar[name]) {
toolbar[name].style.display = "none";
}
});
};
export const showToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => {
names.forEach((name) => {
if (!toolbar[name]) {
return;
}
if (toolbar[name]) {
toolbar[name].style.display = "block";
}
});
};
// "subToolbar", "hint", "popover"
export const hidePanel = (vditor: IVditor, panels: string[], exceptElement?: HTMLElement) => {
if (panels.includes("subToolbar")) {
vditor.toolbar.element.querySelectorAll(".vditor-hint").forEach((item: HTMLElement) => {
if (exceptElement && item.isEqualNode(exceptElement)) {
return;
}
item.style.display = "none";
});
if (vditor.toolbar.elements.emoji) {
(vditor.toolbar.elements.emoji.lastElementChild as HTMLElement).style.display = "none";
}
}
if (panels.includes("hint")) {
vditor.hint.element.style.display = "none";
}
if (vditor.wysiwyg.popover && panels.includes("popover")) {
vditor.wysiwyg.popover.style.display = "none";
}
};
export const toggleSubMenu = (vditor: IVditor, panelElement: HTMLElement, actionBtn: Element, level: number) => {
actionBtn.addEventListener(getEventName(), (event) => {
event.preventDefault();
event.stopPropagation();
if (actionBtn.classList.contains(Constants.CLASS_MENU_DISABLED)) {
return;
}
vditor.toolbar.element.querySelectorAll(".vditor-hint--current").forEach((item) => {
item.classList.remove("vditor-hint--current");
});
if (panelElement.style.display === "block") {
panelElement.style.display = "none";
} else {
hidePanel(vditor, ["subToolbar", "hint", "popover"], actionBtn.parentElement.parentElement);
if (!actionBtn.classList.contains("vditor-tooltipped")) {
actionBtn.classList.add("vditor-hint--current");
}
panelElement.style.display = "block";
if (vditor.toolbar.element.getBoundingClientRect().right - actionBtn.getBoundingClientRect().right < 250) {
panelElement.classList.add("vditor-panel--left");
} else {
panelElement.classList.remove("vditor-panel--left");
}
}
});
};

View File

@@ -0,0 +1,180 @@
import {Constants} from "../constants";
import {setEditMode} from "../toolbar/EditMode";
import {hidePanel} from "../toolbar/setToolbar";
import {accessLocalStorage} from "../util/compatibility";
import {setContentTheme} from "./setContentTheme";
import {setTheme} from "./setTheme";
export const initUI = (vditor: IVditor) => {
vditor.element.innerHTML = "";
vditor.element.classList.add("vditor");
// 支持 RTL
if (vditor.options.rtl) {
vditor.element.setAttribute("dir", "rtl")
}
setTheme(vditor);
setContentTheme(vditor.options.preview.theme.current, vditor.options.preview.theme.path);
if (typeof vditor.options.height === "number") {
vditor.element.style.height = vditor.options.height + "px";
} else {
vditor.element.style.height = vditor.options.height;
}
if (typeof vditor.options.minHeight === "number") {
vditor.element.style.minHeight = vditor.options.minHeight + "px";
}
if (typeof vditor.options.width === "number") {
vditor.element.style.width = vditor.options.width + "px";
} else {
vditor.element.style.width = vditor.options.width;
}
vditor.element.appendChild(vditor.toolbar.element);
const contentElement = document.createElement("div");
contentElement.className = "vditor-content";
if (vditor.options.outline.position === "left") {
contentElement.appendChild(vditor.outline.element);
}
contentElement.appendChild(vditor.wysiwyg.element.parentElement);
contentElement.appendChild(vditor.sv.element);
contentElement.appendChild(vditor.ir.element.parentElement);
contentElement.appendChild(vditor.preview.element);
if (vditor.toolbar.elements.devtools) {
contentElement.appendChild(vditor.devtools.element);
}
if (vditor.options.outline.position === "right") {
vditor.outline.element.classList.add("vditor-outline--right");
contentElement.appendChild(vditor.outline.element);
}
if (vditor.upload) {
contentElement.appendChild(vditor.upload.element);
}
if (vditor.options.resize.enable) {
contentElement.appendChild(vditor.resize.element);
}
contentElement.appendChild(vditor.hint.element);
contentElement.appendChild(vditor.tip.element);
vditor.element.appendChild(contentElement);
contentElement.addEventListener("click", () => {
hidePanel(vditor, ["subToolbar"]);
});
if (vditor.toolbar.elements.export) {
// for export pdf
vditor.element.insertAdjacentHTML("beforeend",
'<iframe id="vditorExportIframe" style="width: 100%;height: 0;border: 0"></iframe>');
}
setEditMode(vditor, vditor.options.mode, afterRender(vditor));
document.execCommand("DefaultParagraphSeparator", false, "p");
if (navigator.userAgent.indexOf("iPhone") > -1 && typeof window.visualViewport !== "undefined") {
// https://github.com/Vanessa219/vditor/issues/379
let pendingUpdate = false;
const viewportHandler = (event: Event) => {
if (pendingUpdate) {
return;
}
pendingUpdate = true;
requestAnimationFrame(() => {
pendingUpdate = false;
const layoutViewport = vditor.toolbar.element;
layoutViewport.style.transform = "none";
if (layoutViewport.getBoundingClientRect().top < 0) {
layoutViewport.style.transform = `translate(0, ${-layoutViewport.getBoundingClientRect().top}px)`;
}
});
};
window.visualViewport.addEventListener("scroll", viewportHandler);
window.visualViewport.addEventListener("resize", viewportHandler);
}
};
export const setPadding = (vditor: IVditor) => {
const minPadding = window.innerWidth <= Constants.MOBILE_WIDTH ? 10 : 35;
if (vditor.wysiwyg.element.parentElement.style.display !== "none") {
const padding = (vditor.wysiwyg.element.parentElement.clientWidth
- vditor.options.preview.maxWidth) / 2;
vditor.wysiwyg.element.style.padding = `10px ${Math.max(minPadding, padding)}px`;
}
if (vditor.ir.element.parentElement.style.display !== "none") {
const padding = (vditor.ir.element.parentElement.clientWidth
- vditor.options.preview.maxWidth) / 2;
vditor.ir.element.style.padding = `10px ${Math.max(minPadding, padding)}px`;
}
if (vditor.preview.element.style.display !== "block") {
vditor.toolbar.element.style.paddingLeft = Math.max(5,
parseInt(vditor[vditor.currentMode].element.style.paddingLeft || "0", 10) +
(vditor.options.outline.position === "left" ? vditor.outline.element.offsetWidth : 0)) + "px";
} else {
vditor.toolbar.element.style.paddingLeft = (5 +
(vditor.options.outline.position === "left" ? vditor.outline.element.offsetWidth : 0)) + "px";
}
};
export const setTypewriterPosition = (vditor: IVditor) => {
if (!vditor.options.typewriterMode) {
return;
}
let height: number = window.innerHeight;
if (typeof vditor.options.height === "number") {
height = vditor.options.height;
if (typeof vditor.options.minHeight === "number") {
height = Math.max(height, vditor.options.minHeight);
}
height = Math.min(window.innerHeight, height);
} else {
height = vditor.element.clientHeight;
}
if (vditor.element.classList.contains("vditor--fullscreen")) {
height = window.innerHeight;
}
// 由于 Firefox padding-bottom bug只能使用 :after
vditor[vditor.currentMode].element.style.setProperty("--editor-bottom",
((height - vditor.toolbar.element.offsetHeight) / 2) + "px");
};
let resizeCb: () => void;
export function UIUnbindListener() {
window.removeEventListener("resize", resizeCb);
}
const afterRender = (vditor: IVditor) => {
setTypewriterPosition(vditor);
UIUnbindListener();
window.addEventListener("resize", resizeCb = () => {
setPadding(vditor);
setTypewriterPosition(vditor);
});
// set default value
let initValue = accessLocalStorage() && localStorage.getItem(vditor.options.cache.id);
if (!vditor.options.cache.enable || !initValue) {
if (vditor.options.value) {
initValue = vditor.options.value;
} else if (vditor.originalInnerHTML) {
initValue = vditor.lute.HTML2Md(vditor.originalInnerHTML);
} else if (!vditor.options.cache.enable) {
initValue = "";
}
}
return initValue || "";
};

View File

@@ -0,0 +1,16 @@
import {Constants} from "../constants";
import {addStyle} from "../util/addStyle";
export const setCodeTheme = (codeTheme: string, cdn = Constants.CDN) => {
if (!Constants.CODE_THEME.includes(codeTheme)) {
codeTheme = "github";
}
const vditorHljsStyle = document.getElementById("vditorHljsStyle") as HTMLLinkElement;
const href = `${cdn}/dist/js/highlight.js/styles/${codeTheme}.css`;
if (!vditorHljsStyle) {
addStyle(href, "vditorHljsStyle");
} else if (vditorHljsStyle.getAttribute('href') !== href) {
vditorHljsStyle.remove();
addStyle(href, "vditorHljsStyle");
}
};

View File

@@ -0,0 +1,15 @@
import {addStyle} from "../util/addStyle";
export const setContentTheme = (contentTheme: string, path: string) => {
if (!contentTheme || !path) {
return;
}
const vditorContentTheme = document.getElementById("vditorContentTheme") as HTMLLinkElement;
const cssPath = `${path}/${contentTheme}.css`;
if (!vditorContentTheme) {
addStyle(cssPath, "vditorContentTheme");
} else if (vditorContentTheme.getAttribute("href") !== cssPath) {
vditorContentTheme.remove();
addStyle(cssPath, "vditorContentTheme");
}
};

View File

@@ -0,0 +1,31 @@
import {removeCurrentToolbar} from "../toolbar/setToolbar";
import {setCurrentToolbar} from "../toolbar/setToolbar";
export const setPreviewMode = (mode: "both" | "editor", vditor: IVditor) => {
if (vditor.options.preview.mode === mode) {
return;
}
vditor.options.preview.mode = mode;
switch (mode) {
case "both":
vditor.sv.element.style.display = "block";
vditor.preview.element.style.display = "block";
vditor.preview.render(vditor);
setCurrentToolbar(vditor.toolbar.elements, ["both"]);
break;
case "editor":
vditor.sv.element.style.display = "block";
vditor.preview.element.style.display = "none";
removeCurrentToolbar(vditor.toolbar.elements, ["both"]);
break;
default:
break;
}
if (vditor.devtools) {
vditor.devtools.renderEchart(vditor);
}
};

View File

@@ -0,0 +1,7 @@
export const setTheme = (vditor: IVditor) => {
if (vditor.options.theme === "dark") {
vditor.element.classList.add("vditor--dark");
} else {
vditor.element.classList.remove("vditor--dark");
}
};

View File

@@ -0,0 +1,260 @@
import * as DiffMatchPatch from "diff-match-patch";
import {disableToolbar, enableToolbar, hidePanel} from "../toolbar/setToolbar";
import {isFirefox, isSafari} from "../util/compatibility";
import {scrollCenter} from "../util/editorCommonEvent";
import {execAfterRender} from "../util/fixBrowserBehavior";
import {highlightToolbar} from "../util/highlightToolbar";
import {processCodeRender} from "../util/processCode";
import {setRangeByWbr, setSelectionFocus} from "../util/selection";
import {renderToc} from "../util/toc";
interface IUndo {
hasUndo: boolean;
lastText: string;
redoStack: DiffMatchPatch.patch_obj[][];
undoStack: DiffMatchPatch.patch_obj[][];
}
class Undo {
private stackSize = 50;
private dmp: DiffMatchPatch.diff_match_patch;
private wysiwyg: IUndo;
private ir: IUndo;
private sv: IUndo;
constructor() {
this.resetStack();
// @ts-ignore
this.dmp = new DiffMatchPatch();
}
public clearStack(vditor: IVditor) {
this.resetStack();
this.resetIcon(vditor);
}
public resetIcon(vditor: IVditor) {
if (!vditor.toolbar) {
return;
}
if (this[vditor.currentMode].undoStack.length > 1) {
enableToolbar(vditor.toolbar.elements, ["undo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["undo"]);
}
if (this[vditor.currentMode].redoStack.length !== 0) {
enableToolbar(vditor.toolbar.elements, ["redo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["redo"]);
}
}
public undo(vditor: IVditor) {
if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
return;
}
if (this[vditor.currentMode].undoStack.length < 2) {
return;
}
const state = this[vditor.currentMode].undoStack.pop();
if (!state) {
return;
}
this[vditor.currentMode].redoStack.push(state);
this.renderDiff(state, vditor);
this[vditor.currentMode].hasUndo = true;
// undo 操作后,需要关闭 hint
hidePanel(vditor, ["hint"]);
}
public redo(vditor: IVditor) {
if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
return;
}
const state = this[vditor.currentMode].redoStack.pop();
if (!state) {
return;
}
this[vditor.currentMode].undoStack.push(state);
this.renderDiff(state, vditor, true);
}
public recordFirstPosition(vditor: IVditor, event: KeyboardEvent) {
if (getSelection().rangeCount === 0) {
return;
}
if (this[vditor.currentMode].undoStack.length !== 1 || this[vditor.currentMode].undoStack[0].length === 0 ||
this[vditor.currentMode].redoStack.length > 0) {
return;
}
if (isFirefox() && event.key === "Backspace") {
// Firefox 第一次删除无效
return;
}
if (isSafari()) {
// Safari keydown 在 input 之后,不需要重复记录历史
return;
}
const text = this.addCaret(vditor);
if (text.replace("<wbr>", "").replace(" vditor-ir__node--expand", "")
!== this[vditor.currentMode].undoStack[0][0].diffs[0][1].replace("<wbr>", "")) {
// 当还不没有存入 undo 栈时,按下 ctrl 后会覆盖 lastText
return;
}
this[vditor.currentMode].undoStack[0][0].diffs[0][1] = text;
this[vditor.currentMode].lastText = text;
// 不能添加 setSelectionFocus(cloneRange); 否则 windows chrome 首次输入会烂
}
public addToUndoStack(vditor: IVditor) {
// afterRenderEvent.ts 已经 debounce
const text = this.addCaret(vditor, true);
const diff = this.dmp.diff_main(text, this[vditor.currentMode].lastText, true);
const patchList = this.dmp.patch_make(text, this[vditor.currentMode].lastText, diff);
if (patchList.length === 0 && this[vditor.currentMode].undoStack.length > 0) {
return;
}
this[vditor.currentMode].lastText = text;
this[vditor.currentMode].undoStack.push(patchList);
if (this[vditor.currentMode].undoStack.length > this.stackSize) {
this[vditor.currentMode].undoStack.shift();
}
if (this[vditor.currentMode].hasUndo) {
this[vditor.currentMode].redoStack = [];
this[vditor.currentMode].hasUndo = false;
disableToolbar(vditor.toolbar.elements, ["redo"]);
}
if (this[vditor.currentMode].undoStack.length > 1) {
enableToolbar(vditor.toolbar.elements, ["undo"]);
}
}
private renderDiff(state: DiffMatchPatch.patch_obj[], vditor: IVditor, isRedo: boolean = false) {
let text;
if (isRedo) {
const redoPatchList = this.dmp.patch_deepCopy(state).reverse();
redoPatchList.forEach((patch) => {
patch.diffs.forEach((diff) => {
diff[0] = -diff[0];
});
});
text = this.dmp.patch_apply(redoPatchList, this[vditor.currentMode].lastText)[0];
} else {
text = this.dmp.patch_apply(state, this[vditor.currentMode].lastText)[0];
}
this[vditor.currentMode].lastText = text;
vditor[vditor.currentMode].element.innerHTML = text;
if (vditor.currentMode !== "sv") {
vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`)
.forEach((blockElement: HTMLElement) => {
processCodeRender(blockElement, vditor);
});
}
if (!vditor[vditor.currentMode].element.querySelector("wbr")) {
// Safari 第一次输入没有光标,需手动定位到结尾
const range = getSelection().getRangeAt(0);
range.setEndBefore(vditor[vditor.currentMode].element);
range.collapse(false);
} else {
setRangeByWbr(
vditor[vditor.currentMode].element, vditor[vditor.currentMode].element.ownerDocument.createRange());
scrollCenter(vditor);
}
renderToc(vditor);
execAfterRender(vditor, {
enableAddUndoStack: false,
enableHint: false,
enableInput: true,
});
highlightToolbar(vditor);
vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`)
.forEach((item: HTMLElement) => {
processCodeRender(item, vditor);
});
if (this[vditor.currentMode].undoStack.length > 1) {
enableToolbar(vditor.toolbar.elements, ["undo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["undo"]);
}
if (this[vditor.currentMode].redoStack.length !== 0) {
enableToolbar(vditor.toolbar.elements, ["redo"]);
} else {
disableToolbar(vditor.toolbar.elements, ["redo"]);
}
}
private resetStack() {
this.ir = {
hasUndo: false,
lastText: "",
redoStack: [],
undoStack: [],
};
this.sv = {
hasUndo: false,
lastText: "",
redoStack: [],
undoStack: [],
};
this.wysiwyg = {
hasUndo: false,
lastText: "",
redoStack: [],
undoStack: [],
};
}
private addCaret(vditor: IVditor, setFocus = false) {
let cloneRange: Range;
if (getSelection().rangeCount !== 0 && !vditor[vditor.currentMode].element.querySelector("wbr")) {
const range = getSelection().getRangeAt(0);
if (vditor[vditor.currentMode].element.contains(range.startContainer)) {
cloneRange = range.cloneRange();
const wbrElement = document.createElement("span");
wbrElement.className = "vditor-wbr";
range.insertNode(wbrElement);
}
}
// 移除数学公式、echart 渲染 https://github.com/siyuan-note/siyuan/issues/537
const cloneElement = vditor.ir.element.cloneNode(true) as HTMLElement;
cloneElement.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='1']`)
.forEach((item: HTMLElement) => {
if (!item.firstElementChild) {
return
}
if (item.firstElementChild.classList.contains("language-echarts") ||
item.firstElementChild.classList.contains("language-plantuml") ||
item.firstElementChild.classList.contains("language-mindmap")) {
item.firstElementChild.removeAttribute("_echarts_instance_");
item.firstElementChild.removeAttribute("data-processed");
item.firstElementChild.innerHTML = item.previousElementSibling.firstElementChild.innerHTML;
item.setAttribute("data-render", "2");
} else if (item.firstElementChild.classList.contains("language-math")) {
item.setAttribute("data-render", "2");
item.firstElementChild.textContent = item.firstElementChild.getAttribute("data-math");
item.firstElementChild.removeAttribute("data-math");
}
});
const text = vditor[vditor.currentMode].element.innerHTML;
vditor[vditor.currentMode].element.querySelectorAll(".vditor-wbr").forEach((item) => {
item.remove();
// 使用 item.outerHTML = "" 会产生 https://github.com/Vanessa219/vditor/pull/686;
});
if (setFocus && cloneRange) {
setSelectionFocus(cloneRange);
}
return text.replace('<span class="vditor-wbr"></span>', "<wbr>");
}
}
export {Undo};

View File

@@ -0,0 +1,10 @@
export const getElement = (vditor: IVditor) => {
switch (vditor.currentMode) {
case "ir":
return vditor.ir.element;
case "wysiwyg":
return vditor.wysiwyg.element;
case "sv":
return vditor.sv.element;
}
};

View File

@@ -0,0 +1,260 @@
import {getEditorRange, setSelectionFocus} from "../util/selection";
import {getElement} from "./getElement";
import {setHeaders} from "./setHeaders";
class Upload {
public element: HTMLElement;
public isUploading: boolean;
public range: Range;
constructor() {
this.isUploading = false;
this.element = document.createElement("div");
this.element.className = "vditor-upload";
}
}
const validateFile = (vditor: IVditor, files: File[]) => {
vditor.tip.hide();
const uploadFileList = [];
let errorTip = "";
let uploadingStr = "";
const lang: keyof II18n | "" = vditor.options.lang;
const options: IOptions = vditor.options;
for (let iMax = files.length, i = 0; i < iMax; i++) {
const file = files[i];
let validate = true;
if (!file.name) {
errorTip += `<li>${window.VditorI18n.nameEmpty}</li>`;
validate = false;
}
if (file.size > vditor.options.upload.max) {
errorTip += `<li>${file.name} ${window.VditorI18n.over} ${vditor.options.upload.max / 1024 / 1024}M</li>`;
validate = false;
}
const lastIndex = file.name.lastIndexOf(".");
const fileExt = file.name.substr(lastIndex);
const filename = vditor.options.upload.filename(file.name.substr(0, lastIndex)) + fileExt;
if (vditor.options.upload.accept) {
const isAccept = vditor.options.upload.accept.split(",").some((item) => {
const type = item.trim();
if (type.indexOf(".") === 0) {
if (fileExt.toLowerCase() === type.toLowerCase()) {
return true;
}
} else {
if (file.type.split("/")[0] === type.split("/")[0]) {
return true;
}
}
return false;
});
if (!isAccept) {
errorTip += `<li>${file.name} ${window.VditorI18n.fileTypeError}</li>`;
validate = false;
}
}
if (validate) {
uploadFileList.push(file);
uploadingStr += `<li>${filename} ${window.VditorI18n.uploading}</li>`;
}
}
vditor.tip.show(`<ul>${errorTip}${uploadingStr}</ul>`);
return uploadFileList;
};
const genUploadedLabel = (responseText: string, vditor: IVditor) => {
const editorElement = getElement(vditor);
editorElement.focus();
const response = JSON.parse(responseText);
let errorTip = "";
if (response.code === 1) {
errorTip = `${response.msg}`;
}
if (response.data.errFiles && response.data.errFiles.length > 0) {
errorTip = `<ul><li>${errorTip}</li>`;
response.data.errFiles.forEach((data: string) => {
const lastIndex = data.lastIndexOf(".");
const filename = vditor.options.upload.filename(data.substr(0, lastIndex)) + data.substr(lastIndex);
errorTip += `<li>${filename} ${window.VditorI18n.uploadError}</li>`;
});
errorTip += "</ul>";
}
if (errorTip) {
vditor.tip.show(errorTip);
} else {
vditor.tip.hide();
}
let succFileText = "";
Object.keys(response.data.succMap).forEach((key) => {
const path = response.data.succMap[key];
const lastIndex = key.lastIndexOf(".");
let type = key.substr(lastIndex);
const filename = vditor.options.upload.filename(key.substr(0, lastIndex)) + type;
type = type.toLowerCase();
if (type.indexOf(".wav") === 0 || type.indexOf(".mp3") === 0 || type.indexOf(".ogg") === 0) {
if (vditor.currentMode === "wysiwyg") {
succFileText += `<div class="vditor-wysiwyg__block" data-type="html-block"
data-block="0"><pre><code>&lt;audio controls="controls" src="${path}"&gt;&lt;/audio&gt;</code></pre><pre class="vditor-wysiwyg__preview" data-render="1"><audio controls="controls" src="${path}"></audio></pre></div>\n`;
} else if (vditor.currentMode === "ir") {
succFileText += `<audio controls="controls" src="${path}"></audio>\n`;
} else {
succFileText += `[${filename}](${path})\n`;
}
} else if (type.indexOf(".apng") === 0
|| type.indexOf(".bmp") === 0
|| type.indexOf(".gif") === 0
|| type.indexOf(".ico") === 0 || type.indexOf(".cur") === 0
|| type.indexOf(".jpg") === 0 || type.indexOf(".jpeg") === 0 || type.indexOf(".jfif") === 0 || type.indexOf(".pjp") === 0 || type.indexOf(".pjpeg") === 0
|| type.indexOf(".png") === 0
|| type.indexOf(".svg") === 0
|| type.indexOf(".webp") === 0) {
if (vditor.currentMode === "wysiwyg") {
succFileText += `<img alt="${filename}" src="${path}">\n`;
} else {
succFileText += `![${filename}](${path})\n`;
}
} else {
if (vditor.currentMode === "wysiwyg") {
succFileText += `<a href="${path}">${filename}</a>\n`;
} else {
succFileText += `[${filename}](${path})\n`;
}
}
});
setSelectionFocus(vditor.upload.range);
document.execCommand("insertHTML", false, succFileText);
vditor.upload.range = getSelection().getRangeAt(0).cloneRange();
};
const uploadFiles =
async (vditor: IVditor, files: FileList | DataTransferItemList | File[], element?: HTMLInputElement) => {
// FileList | DataTransferItemList | File[] => File[]
let fileList = [];
const filesMax = vditor.options.upload.multiple === true ? files.length : 1;
for (let i = 0; i < filesMax; i++) {
let fileItem = files[i];
if (fileItem instanceof DataTransferItem) {
fileItem = fileItem.getAsFile();
}
fileList.push(fileItem);
}
if (vditor.options.upload.handler) {
const isValidate = await vditor.options.upload.handler(fileList);
if (element) {
element.value = "";
}
if (typeof isValidate === "string") {
vditor.tip.show(isValidate);
return;
}
return;
}
if (!vditor.options.upload.url || !vditor.upload) {
if (element) {
element.value = "";
}
vditor.tip.show("please config: options.upload.url");
return;
}
if (vditor.options.upload.file) {
fileList = await vditor.options.upload.file(fileList);
}
if (vditor.options.upload.validate) {
const isValidate = vditor.options.upload.validate(fileList);
if (typeof isValidate === "string") {
vditor.tip.show(isValidate);
return;
}
}
const editorElement = getElement(vditor);
vditor.upload.range = getEditorRange(vditor);
const validateResult = validateFile(vditor, fileList);
if (validateResult.length === 0) {
if (element) {
element.value = "";
}
return;
}
const formData = new FormData();
const extraData = vditor.options.upload.extraData;
for (const key of Object.keys(extraData)) {
formData.append(key, extraData[key]);
}
for (let i = 0, iMax = validateResult.length; i < iMax; i++) {
formData.append(vditor.options.upload.fieldName, validateResult[i]);
}
const xhr = new XMLHttpRequest();
xhr.open("POST", vditor.options.upload.url);
if (vditor.options.upload.token) {
xhr.setRequestHeader("X-Upload-Token", vditor.options.upload.token);
}
if (vditor.options.upload.withCredentials) {
xhr.withCredentials = true;
}
setHeaders(vditor, xhr);
vditor.upload.isUploading = true;
editorElement.setAttribute("contenteditable", "false");
xhr.onreadystatechange = () => {
if (xhr.readyState === XMLHttpRequest.DONE) {
vditor.upload.isUploading = false;
editorElement.setAttribute("contenteditable", "true");
if (xhr.status >= 200 && xhr.status < 300) {
if (vditor.options.upload.success) {
vditor.options.upload.success(editorElement, xhr.responseText);
} else {
let responseText = xhr.responseText;
if (vditor.options.upload.format) {
responseText = vditor.options.upload.format(files as File [], xhr.responseText);
}
genUploadedLabel(responseText, vditor);
}
} else {
if (vditor.options.upload.error) {
vditor.options.upload.error(xhr.responseText);
} else {
vditor.tip.show(xhr.responseText);
}
}
if (element) {
element.value = "";
}
vditor.upload.element.style.display = "none";
}
};
xhr.upload.onprogress = (event: ProgressEvent) => {
if (!event.lengthComputable) {
return;
}
const progress = event.loaded / event.total * 100;
vditor.upload.element.style.display = "block";
const progressBar = vditor.upload.element;
progressBar.style.width = progress + "%";
};
xhr.send(formData);
};
export {Upload, uploadFiles};

View File

@@ -0,0 +1,10 @@
export const setHeaders = (vditor: IVditor, xhr: XMLHttpRequest) => {
if (vditor.options.upload.setHeaders) {
vditor.options.upload.headers = vditor.options.upload.setHeaders();
}
if (vditor.options.upload.headers) {
Object.keys(vditor.options.upload.headers).forEach((key) => {
xhr.setRequestHeader(key, vditor.options.upload.headers[key]);
});
}
};

View File

@@ -0,0 +1,454 @@
import {Constants} from "../constants";
import {merge} from "./merge";
export class Options {
public options: IOptions;
private defaultOptions: IOptions = {
rtl: false,
after: undefined,
cache: {
enable: true,
},
cdn: Constants.CDN,
classes: {
preview: "",
},
comment: {
enable: false,
},
counter: {
enable: false,
type: "markdown",
},
customRenders: [],
debugger: false,
fullscreen: {
index: 90,
},
height: "auto",
hint: {
delay: 200,
emoji: {
"+1": "👍",
"-1": "👎",
"confused": "😕",
"eyes": "👀️",
"heart": "❤️",
"rocket": "🚀️",
"smile": "😄",
"tada": "🎉️",
},
emojiPath: `${Constants.CDN}/dist/images/emoji`,
extend: [],
parse: true,
},
icon: "ant",
lang: "zh_CN",
mode: "ir",
outline: {
enable: false,
position: "left",
},
placeholder: "",
preview: {
actions: ["desktop", "tablet", "mobile", "mp-wechat", "zhihu"],
delay: 1000,
hljs: Constants.HLJS_OPTIONS,
markdown: Constants.MARKDOWN_OPTIONS,
math: Constants.MATH_OPTIONS,
maxWidth: 800,
mode: "both",
theme: Constants.THEME_OPTIONS,
render: {
media: {
enable: true,
}
}
},
link: {
isOpen: true,
},
image: {
isPreview: true,
},
resize: {
enable: false,
position: "bottom",
},
theme: "classic",
toolbar: [
"emoji",
"headings",
"bold",
"italic",
"strike",
"link",
"|",
"list",
"ordered-list",
"check",
"outdent",
"indent",
"|",
"quote",
"line",
"code",
"inline-code",
"insert-before",
"insert-after",
"|",
"upload",
"record",
"table",
"|",
"undo",
"redo",
"|",
"fullscreen",
"edit-mode",
{
name: "more",
toolbar: [
"both",
"code-theme",
"content-theme",
"export",
"outline",
"preview",
"devtools",
"info",
"help",
],
},
],
toolbarConfig: {
hide: false,
pin: false,
},
typewriterMode: false,
undoDelay: 800,
upload: {
extraData: {},
fieldName: "file[]",
filename: (name: string) => name.replace(/\W/g, ""),
linkToImgUrl: "",
max: 10 * 1024 * 1024,
multiple: true,
url: "",
withCredentials: false,
},
value: "",
width: "auto",
};
constructor(options: IOptions) {
this.options = options;
}
public merge(): IOptions {
if (this.options) {
if (this.options.toolbar) {
this.options.toolbar = this.mergeToolbar(this.options.toolbar);
} else {
this.options.toolbar = this.mergeToolbar(this.defaultOptions.toolbar);
}
if (this.options.preview?.theme?.list) {
this.defaultOptions.preview.theme.list = this.options.preview.theme.list;
}
if (this.options.preview?.render?.media?.enable) {
this.defaultOptions.preview.render.media.enable = this.options.preview.render.media.enable;
}
if (this.options.hint?.emoji) {
this.defaultOptions.hint.emoji = this.options.hint.emoji;
}
if (this.options.comment) {
this.defaultOptions.comment = this.options.comment;
}
if (this.options.cdn) {
if (!this.options.preview?.theme?.path) {
this.defaultOptions.preview.theme.path = `${this.options.cdn}/dist/css/content-theme`
}
if (!this.options.hint?.emojiPath) {
this.defaultOptions.hint.emojiPath = `${this.options.cdn}/dist/images/emoji`;
}
}
}
const mergedOptions = merge(this.defaultOptions, this.options);
if (mergedOptions.cache.enable && !mergedOptions.cache.id) {
throw new Error(
"need options.cache.id, see https://ld246.com/article/1549638745630#options",
);
}
return mergedOptions;
}
private mergeToolbar(toolbar: Array<string | IMenuItem>) {
const toolbarItem = [
{
icon: '<svg><use xlink:href="#vditor-icon-export"></use></svg>',
name: "export",
tipPosition: "ne",
},
{
hotkey: "⌘E",
icon: '<svg><use xlink:href="#vditor-icon-emoji"></use></svg>',
name: "emoji",
tipPosition: "ne",
},
{
hotkey: "⌘H",
icon:
'<svg><use xlink:href="#vditor-icon-headings"></use></svg>',
name: "headings",
tipPosition: "ne",
},
{
hotkey: "⌘B",
icon: '<svg><use xlink:href="#vditor-icon-bold"></use></svg>',
name: "bold",
prefix: "**",
suffix: "**",
tipPosition: "ne",
},
{
hotkey: "⌘I",
icon: '<svg><use xlink:href="#vditor-icon-italic"></use></svg>',
name: "italic",
prefix: "*",
suffix: "*",
tipPosition: "ne",
},
{
hotkey: "⌘D",
icon: '<svg><use xlink:href="#vditor-icon-strike"></use></svg>',
name: "strike",
prefix: "~~",
suffix: "~~",
tipPosition: "ne",
},
{
hotkey: "⌘K",
icon: '<svg><use xlink:href="#vditor-icon-link"></use></svg>',
name: "link",
prefix: "[",
suffix: "](https://)",
tipPosition: "n",
},
{
name: "|",
},
{
hotkey: "⌘L",
icon: '<svg><use xlink:href="#vditor-icon-list"></use></svg>',
name: "list",
prefix: "* ",
tipPosition: "n",
},
{
hotkey: "⌘O",
icon:
'<svg><use xlink:href="#vditor-icon-ordered-list"></use></svg>',
name: "ordered-list",
prefix: "1. ",
tipPosition: "n",
},
{
hotkey: "⌘J",
icon: '<svg><use xlink:href="#vditor-icon-check"></use></svg>',
name: "check",
prefix: "* [ ] ",
tipPosition: "n",
},
{
hotkey: "⇧⌘I",
icon:
'<svg><use xlink:href="#vditor-icon-outdent"></use></svg>',
name: "outdent",
tipPosition: "n",
},
{
hotkey: "⇧⌘O",
icon: '<svg><use xlink:href="#vditor-icon-indent"></use></svg>',
name: "indent",
tipPosition: "n",
},
{
name: "|",
},
{
hotkey: "⌘;",
icon: '<svg><use xlink:href="#vditor-icon-quote"></use></svg>',
name: "quote",
prefix: "> ",
tipPosition: "n",
},
{
hotkey: "⇧⌘H",
icon: '<svg><use xlink:href="#vditor-icon-line"></use></svg>',
name: "line",
prefix: "---",
tipPosition: "n",
},
{
hotkey: "⌘U",
icon: '<svg><use xlink:href="#vditor-icon-code"></use></svg>',
name: "code",
prefix: "```",
suffix: "\n```",
tipPosition: "n",
},
{
hotkey: "⌘G",
icon:
'<svg><use xlink:href="#vditor-icon-inline-code"></use></svg>',
name: "inline-code",
prefix: "`",
suffix: "`",
tipPosition: "n",
},
{
hotkey: "⇧⌘B",
icon: '<svg><use xlink:href="#vditor-icon-before"></use></svg>',
name: "insert-before",
tipPosition: "n",
},
{
hotkey: "⇧⌘E",
icon: '<svg><use xlink:href="#vditor-icon-after"></use></svg>',
name: "insert-after",
tipPosition: "n",
},
{
name: "|",
},
{
icon: '<svg><use xlink:href="#vditor-icon-upload"></use></svg>',
name: "upload",
tipPosition: "n",
},
{
icon: '<svg><use xlink:href="#vditor-icon-record"></use></svg>',
name: "record",
tipPosition: "n",
},
{
hotkey: "⌘M",
icon: '<svg><use xlink:href="#vditor-icon-table"></use></svg>',
name: "table",
prefix: "| col1",
suffix:
" | col2 | col3 |\n| --- | --- | --- |\n| | | |\n| | | |",
tipPosition: "n",
},
{
name: "|",
},
{
hotkey: "⌘Z",
icon: '<svg><use xlink:href="#vditor-icon-undo"></use></svg>',
name: "undo",
tipPosition: "nw",
},
{
hotkey: "⌘Y",
icon: '<svg><use xlink:href="#vditor-icon-redo"></use></svg>',
name: "redo",
tipPosition: "nw",
},
{
name: "|",
},
{
icon: '<svg><use xlink:href="#vditor-icon-more"></use></svg>',
name: "more",
tipPosition: "e",
},
{
hotkey: "⌘'",
icon:
'<svg><use xlink:href="#vditor-icon-fullscreen"></use></svg>',
name: "fullscreen",
tipPosition: "nw",
},
{
icon: '<svg><use xlink:href="#vditor-icon-edit"></use></svg>',
name: "edit-mode",
tipPosition: "nw",
},
{
hotkey: "⌘P",
icon: '<svg><use xlink:href="#vditor-icon-both"></use></svg>',
name: "both",
tipPosition: "nw",
},
{
icon:
'<svg><use xlink:href="#vditor-icon-preview"></use></svg>',
name: "preview",
tipPosition: "nw",
},
{
icon:
'<svg><use xlink:href="#vditor-icon-align-center"></use></svg>',
name: "outline",
tipPosition: "nw",
},
{
icon: '<svg><use xlink:href="#vditor-icon-theme"></use></svg>',
name: "content-theme",
tipPosition: "nw",
},
{
icon:
'<svg><use xlink:href="#vditor-icon-code-theme"></use></svg>',
name: "code-theme",
tipPosition: "nw",
},
{
icon: '<svg><use xlink:href="#vditor-icon-bug"></use></svg>',
name: "devtools",
tipPosition: "nw",
},
{
icon: '<svg><use xlink:href="#vditor-icon-info"></use></svg>',
name: "info",
tipPosition: "nw",
},
{
icon: '<svg><use xlink:href="#vditor-icon-help"></use></svg>',
name: "help",
tipPosition: "nw",
},
{
name: "br",
},
];
const toolbarResult: IMenuItem[] = [];
toolbar.forEach((menuItem: IMenuItem) => {
let currentMenuItem = menuItem;
toolbarItem.forEach((defaultMenuItem: IMenuItem) => {
if (
typeof menuItem === "string" &&
defaultMenuItem.name === menuItem
) {
currentMenuItem = defaultMenuItem;
}
if (
typeof menuItem === "object" &&
defaultMenuItem.name === menuItem.name
) {
currentMenuItem = Object.assign({}, defaultMenuItem, menuItem);
}
});
if (menuItem.toolbar) {
currentMenuItem.toolbar = this.mergeToolbar(menuItem.toolbar);
}
toolbarResult.push(currentMenuItem);
});
return toolbarResult;
}
}

View File

@@ -0,0 +1,170 @@
export class RecordMedia {
public SAMPLE_RATE = 5000; // 44100 suggested by demos;
public DEFAULT_SAMPLE_RATE: number;
public isRecording = false;
public readyFlag = false;
public leftChannel: Float32List[] = [];
public rightChannel: Float32List[] = [];
public recordingLength = 0;
// This needs to be public so the 'onaudioprocess' event handler can be defined externally.
public recorder: ScriptProcessorNode;
constructor(e: MediaStream) {
let context;
// creates the audio context
if (typeof AudioContext !== "undefined") {
context = new AudioContext();
} else if (webkitAudioContext) {
context = new webkitAudioContext();
} else {
return;
}
this.DEFAULT_SAMPLE_RATE = context.sampleRate;
// creates a gain node
const volume = context.createGain();
// creates an audio node from the microphone incoming stream
const audioInput = context.createMediaStreamSource(e);
// connect the stream to the gain node
audioInput.connect(volume);
/* From the spec: The size of the buffer controls how frequently the audioprocess event is
dispatched and how many sample-frames need to be processed each call.
Lower values for buffer size will result in a lower (better) latency.
Higher values will be necessary to avoid audio breakup and glitches */
this.recorder = context.createScriptProcessor(2048, 2, 1);
// The onaudioprocess event needs to be defined externally, so make sure it is not set:
this.recorder.onaudioprocess = null;
// we connect the recorder
volume.connect(this.recorder);
this.recorder.connect(context.destination);
this.readyFlag = true;
}
// Publicly accessible methods:
public cloneChannelData(leftChannelData: Float32List, rightChannelData: Float32List) {
this.leftChannel.push(new Float32Array(leftChannelData));
this.rightChannel.push(new Float32Array(rightChannelData));
this.recordingLength += 2048;
}
public startRecordingNewWavFile() {
if (this.readyFlag) {
this.isRecording = true;
this.leftChannel.length = this.rightChannel.length = 0;
this.recordingLength = 0;
}
}
public stopRecording() {
this.isRecording = false;
}
public buildWavFileBlob() {
// we flat the left and right channels down
const leftBuffer = this.mergeBuffers(this.leftChannel);
const rightBuffer = this.mergeBuffers(this.rightChannel);
// Interleave the left and right channels together:
let interleaved: Float32Array = new Float32Array(leftBuffer.length);
for (let i = 0; i < leftBuffer.length; ++i) {
interleaved[i] = 0.5 * (leftBuffer[i] + rightBuffer[i]);
}
// Downsample the audio data if necessary:
if (this.DEFAULT_SAMPLE_RATE > this.SAMPLE_RATE) {
interleaved = this.downSampleBuffer(interleaved, this.SAMPLE_RATE);
}
const totalByteCount = (44 + interleaved.length * 2);
const buffer = new ArrayBuffer(totalByteCount);
const view = new DataView(buffer);
// Build the RIFF chunk descriptor:
this.writeUTFBytes(view, 0, "RIFF");
view.setUint32(4, totalByteCount, true);
this.writeUTFBytes(view, 8, "WAVE");
// Build the FMT sub-chunk:
this.writeUTFBytes(view, 12, "fmt "); // subchunk1 ID is format
view.setUint32(16, 16, true); // The sub-chunk size is 16.
view.setUint16(20, 1, true); // The audio format is 1.
view.setUint16(22, 1, true); // Number of interleaved channels.
view.setUint32(24, this.SAMPLE_RATE, true); // Sample rate.
view.setUint32(28, this.SAMPLE_RATE * 2, true); // Byte rate.
view.setUint16(32, 2, true); // Block align
view.setUint16(34, 16, true); // Bits per sample.
// Build the data sub-chunk:
const subChunk2ByteCount = interleaved.length * 2;
this.writeUTFBytes(view, 36, "data");
view.setUint32(40, subChunk2ByteCount, true);
// Write the PCM samples to the view:
const lng = interleaved.length;
let index = 44;
const volume = 1;
for (let j = 0; j < lng; j++) {
view.setInt16(index, interleaved[j] * (0x7FFF * volume), true);
index += 2;
}
return new Blob([view], {type: "audio/wav"});
}
private downSampleBuffer(buffer: Float32Array, rate: number) {
if (rate === this.DEFAULT_SAMPLE_RATE) {
return buffer;
}
if (rate > this.DEFAULT_SAMPLE_RATE) {
// throw "downsampling rate show be smaller than original sample rate";
return buffer;
}
const sampleRateRatio = this.DEFAULT_SAMPLE_RATE / rate;
const newLength = Math.round(buffer.length / sampleRateRatio);
const result = new Float32Array(newLength);
let offsetResult = 0;
let offsetBuffer = 0;
while (offsetResult < result.length) {
const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio);
let accum = 0;
let count = 0;
for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
accum += buffer[i];
count++;
}
result[offsetResult] = accum / count;
offsetResult++;
offsetBuffer = nextOffsetBuffer;
}
return result;
}
private mergeBuffers(desiredChannelBuffer: Float32List[]) {
const result = new Float32Array(this.recordingLength);
let offset = 0;
const lng = desiredChannelBuffer.length;
for (let i = 0; i < lng; ++i) {
const buffer = desiredChannelBuffer[i];
result.set(buffer, offset);
offset += buffer.length;
}
return result;
}
private writeUTFBytes(view: DataView, offset: number, value: string) {
const lng = value.length;
for (let i = 0; i < lng; i++) {
view.setUint8(offset + i, value.charCodeAt(i));
}
}
}

View File

@@ -0,0 +1,43 @@
export const addScriptSync = (path: string, id: string) => {
if (document.getElementById(id)) {
return false;
}
const xhrObj = new XMLHttpRequest();
xhrObj.open("GET", path, false);
xhrObj.setRequestHeader("Accept",
"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01");
xhrObj.send("");
const scriptElement = document.createElement("script");
scriptElement.type = "text/javascript";
scriptElement.text = xhrObj.responseText;
scriptElement.id = id;
document.head.appendChild(scriptElement);
};
export const addScript = (path: string, id: string) => {
return new Promise((resolve, reject) => {
if (document.getElementById(id)) {
// 脚本加载后再次调用直接返回
resolve(true);
return false;
}
const scriptElement = document.createElement("script");
scriptElement.src = path;
scriptElement.async = true;
// 循环调用时 Chrome 不会重复请求 js
document.head.appendChild(scriptElement);
scriptElement.onerror = (event) => {
reject(event);
}
scriptElement.onload = () => {
if (document.getElementById(id)) {
// 循环调用需清除 DOM 中的 script 标签
scriptElement.remove();
resolve(true);
return false;
}
scriptElement.id = id;
resolve(true);
};
});
};

Some files were not shown because too many files have changed in this diff Show More