✨ feat: 新增 WYSIWYG 编辑器功能和相关工具栏事件处理
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,3 +12,5 @@ core/judged/judgehub
|
|||||||
core/judge_client/judge_client
|
core/judge_client/judge_client
|
||||||
core/judge_client/judge_client.http
|
core/judge_client/judge_client.http
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
@@ -6,9 +6,9 @@
|
|||||||
selector: 'textarea.kindeditor',
|
selector: 'textarea.kindeditor',
|
||||||
uploadUrl: '../kindeditor/php/upload_json.php',
|
uploadUrl: '../kindeditor/php/upload_json.php',
|
||||||
accept: '*/*',
|
accept: '*/*',
|
||||||
mode: 'sv',
|
mode: 'wysiwyg',
|
||||||
preview: {
|
preview: {
|
||||||
mode: 'both',
|
mode: 'editor',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
actions: [],
|
actions: [],
|
||||||
markdown: {
|
markdown: {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
selector: 'textarea.kindeditor',
|
selector: 'textarea.kindeditor',
|
||||||
uploadUrl: '../kindeditor/php/upload_json.php',
|
uploadUrl: '../kindeditor/php/upload_json.php',
|
||||||
accept: '*/*',
|
accept: '*/*',
|
||||||
mode: 'sv',
|
mode: 'wysiwyg',
|
||||||
ctrlEnterFormName: 'example',
|
ctrlEnterFormName: 'example',
|
||||||
preview: {
|
preview: {
|
||||||
mode: 'both',
|
mode: 'editor',
|
||||||
delay: 0,
|
delay: 0,
|
||||||
actions: [],
|
actions: [],
|
||||||
markdown: {
|
markdown: {
|
||||||
|
|||||||
@@ -6,10 +6,25 @@
|
|||||||
return;
|
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 assetsPromise = null;
|
||||||
var styleInjected = false;
|
var styleInjected = false;
|
||||||
|
|
||||||
|
function getLocalCdnBase() {
|
||||||
|
var scriptSrc = currentScriptRef && currentScriptRef.src;
|
||||||
|
|
||||||
|
if (!scriptSrc) {
|
||||||
|
return 'include/vditor';
|
||||||
|
}
|
||||||
|
|
||||||
|
return scriptSrc.replace(/\/vditor-adapter\.js(?:[?#].*)?$/, '/vditor');
|
||||||
|
}
|
||||||
|
|
||||||
function injectAdapterStyle() {
|
function injectAdapterStyle() {
|
||||||
var style;
|
var style;
|
||||||
|
|
||||||
@@ -85,6 +100,7 @@
|
|||||||
|
|
||||||
function ensureAssets(options) {
|
function ensureAssets(options) {
|
||||||
var cdn = (options && options.cdn) || DEFAULT_CDN;
|
var cdn = (options && options.cdn) || DEFAULT_CDN;
|
||||||
|
global.HUSTOJ_VDITOR_CDN = cdn;
|
||||||
if (global.Vditor) {
|
if (global.Vditor) {
|
||||||
injectAdapterStyle();
|
injectAdapterStyle();
|
||||||
return Promise.resolve(global.Vditor);
|
return Promise.resolve(global.Vditor);
|
||||||
@@ -95,7 +111,7 @@
|
|||||||
|
|
||||||
ensureStyleTag(cdn + '/dist/index.css');
|
ensureStyleTag(cdn + '/dist/index.css');
|
||||||
injectAdapterStyle();
|
injectAdapterStyle();
|
||||||
assetsPromise = ensureScript(cdn + '/dist/index.min.js').then(function () {
|
assetsPromise = ensureScript(cdn + '/dist/index.js').then(function () {
|
||||||
return global.Vditor;
|
return global.Vditor;
|
||||||
});
|
});
|
||||||
return assetsPromise;
|
return assetsPromise;
|
||||||
@@ -447,10 +463,6 @@
|
|||||||
'|',
|
'|',
|
||||||
'undo',
|
'undo',
|
||||||
'redo',
|
'redo',
|
||||||
'|',
|
|
||||||
'edit-mode',
|
|
||||||
'both',
|
|
||||||
'preview',
|
|
||||||
'fullscreen',
|
'fullscreen',
|
||||||
'outline'
|
'outline'
|
||||||
];
|
];
|
||||||
|
|||||||
21
web/include/vditor/LICENSE
Normal file
21
web/include/vditor/LICENSE
Normal 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.
|
||||||
680
web/include/vditor/README.md
Normal file
680
web/include/vditor/README.md
Normal 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> | <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开源`:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 🗺️ 背景
|
||||||
|
|
||||||
|
随着 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,如粘贴中包含外链图片可通过指定接口上传到服务器
|
||||||
|
* 支持主窗口大小拖拽、字符计数
|
||||||
|
* 多主题支持,内置黑白绿三套主题
|
||||||
|
* 多语言支持,内置中、英、韩文本地化
|
||||||
|
* 支持主流浏览器,对移动端友好
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 🔮 编辑模式
|
||||||
|
|
||||||
|
### 所见即所得(WYSIWYG)
|
||||||
|
|
||||||
|
*所见即所得*模式对不熟悉 Markdown 的用户较为友好,熟悉 Markdown 的话也可以无缝使用。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 即时渲染(IR)
|
||||||
|
|
||||||
|
*即时渲染*模式对熟悉 Typora 的用户应该不会感到陌生,理论上这是最优雅的 Markdown 编辑方式。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 分屏预览(SV)
|
||||||
|
|
||||||
|
传统的*分屏预览*模式适合大屏下的 Markdown 编辑。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 🍱 语法支持
|
||||||
|
|
||||||
|
* 所有 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)
|
||||||
|
|
||||||
|
### 主题
|
||||||
|
|
||||||
|
#### 编辑器主题
|
||||||
|
|
||||||
|
编辑器所展现的外观。内置classic,dark 2 套主题。
|
||||||
|
|
||||||
|
* 编辑器初始化时可通过 `options.theme` 设置内置主题
|
||||||
|
* 初始化完成后可通过 `setTheme` 更新编辑器主题
|
||||||
|
* 可通过修改 [index.less](https://github.com/Vanessa219/vditor/blob/master/src/assets/less/index.less) 中的变量对主题颜色进行定制
|
||||||
|
* 可参考现有结构和类名在原有基础上进行修改
|
||||||
|
|
||||||
|
#### 内容主题
|
||||||
|
|
||||||
|
Markdown 输出的 HTML 所展现的外观。内置 ant-design, light,dark,wechat 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 就这样诞生了。
|
||||||
76
web/include/vditor/package.json
Normal file
76
web/include/vditor/package.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
279
web/include/vditor/src/assets/less/_content.less
Normal file
279
web/include/vditor/src/assets/less/_content.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
web/include/vditor/src/assets/less/_hint.less
Normal file
67
web/include/vditor/src/assets/less/_hint.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
275
web/include/vditor/src/assets/less/_ir.less
Normal file
275
web/include/vditor/src/assets/less/_ir.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
web/include/vditor/src/assets/less/_panel.less
Normal file
110
web/include/vditor/src/assets/less/_panel.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
610
web/include/vditor/src/assets/less/_reset.less
Normal file
610
web/include/vditor/src/assets/less/_reset.less
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
132
web/include/vditor/src/assets/less/_sv.less
Normal file
132
web/include/vditor/src/assets/less/_sv.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
web/include/vditor/src/assets/less/_toolbar.less
Normal file
180
web/include/vditor/src/assets/less/_toolbar.less
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
171
web/include/vditor/src/assets/less/_tooltipped.less
Normal file
171
web/include/vditor/src/assets/less/_tooltipped.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
195
web/include/vditor/src/assets/less/_wysiwyg.less
Normal file
195
web/include/vditor/src/assets/less/_wysiwyg.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
web/include/vditor/src/assets/less/index.less
Normal file
88
web/include/vditor/src/assets/less/index.less
Normal 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";
|
||||||
553
web/include/vditor/src/index.ts
Normal file
553
web/include/vditor/src/index.ts
Normal 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;
|
||||||
68
web/include/vditor/src/method.ts
Normal file
68
web/include/vditor/src/method.ts
Normal 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;
|
||||||
69
web/include/vditor/src/ts/constants.ts
Normal file
69
web/include/vditor/src/ts/constants.ts
Normal 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`,
|
||||||
|
};
|
||||||
|
}
|
||||||
83
web/include/vditor/src/ts/devtools/index.ts
Normal file
83
web/include/vditor/src/ts/devtools/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
83
web/include/vditor/src/ts/export/index.ts
Normal file
83
web/include/vditor/src/ts/export/index.ts
Normal 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");
|
||||||
|
};
|
||||||
276
web/include/vditor/src/ts/hint/index.ts
Normal file
276
web/include/vditor/src/ts/hint/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
web/include/vditor/src/ts/ir/expandMarker.ts
Normal file
79
web/include/vditor/src/ts/ir/expandMarker.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
97
web/include/vditor/src/ts/ir/highlightToolbarIR.ts
Normal file
97
web/include/vditor/src/ts/ir/highlightToolbarIR.ts
Normal 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);
|
||||||
|
};
|
||||||
265
web/include/vditor/src/ts/ir/index.ts
Normal file
265
web/include/vditor/src/ts/ir/index.ts
Normal 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};
|
||||||
246
web/include/vditor/src/ts/ir/input.ts
Normal file
246
web/include/vditor/src/ts/ir/input.ts
Normal 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 则解析 blockquote;blockquote 中有列表则解析列表
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
222
web/include/vditor/src/ts/ir/process.ts
Normal file
222
web/include/vditor/src/ts/ir/process.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
238
web/include/vditor/src/ts/ir/processKeydown.ts
Normal file
238
web/include/vditor/src/ts/ir/processKeydown.ts
Normal 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;
|
||||||
|
};
|
||||||
27
web/include/vditor/src/ts/markdown/abcRender.ts
Normal file
27
web/include/vditor/src/ts/markdown/abcRender.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
37
web/include/vditor/src/ts/markdown/adapterRender.ts
Normal file
37
web/include/vditor/src/ts/markdown/adapterRender.ts
Normal 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"),
|
||||||
|
};
|
||||||
19
web/include/vditor/src/ts/markdown/anchorRender.ts
Normal file
19
web/include/vditor/src/ts/markdown/anchorRender.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
38
web/include/vditor/src/ts/markdown/chartRender.ts
Normal file
38
web/include/vditor/src/ts/markdown/chartRender.ts
Normal 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}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
60
web/include/vditor/src/ts/markdown/codeRender.ts
Normal file
60
web/include/vditor/src/ts/markdown/codeRender.ts
Normal 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>`)
|
||||||
|
});
|
||||||
|
};
|
||||||
25
web/include/vditor/src/ts/markdown/flowchartRender.ts
Normal file
25
web/include/vditor/src/ts/markdown/flowchartRender.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
11
web/include/vditor/src/ts/markdown/getHTML.ts
Normal file
11
web/include/vditor/src/ts/markdown/getHTML.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
12
web/include/vditor/src/ts/markdown/getMarkdown.ts
Normal file
12
web/include/vditor/src/ts/markdown/getMarkdown.ts
Normal 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 "";
|
||||||
|
};
|
||||||
49
web/include/vditor/src/ts/markdown/graphvizRender.ts
Normal file
49
web/include/vditor/src/ts/markdown/graphvizRender.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
90
web/include/vditor/src/ts/markdown/highlightRender.ts
Normal file
90
web/include/vditor/src/ts/markdown/highlightRender.ts
Normal 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);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
56
web/include/vditor/src/ts/markdown/lazyLoadImageRender.ts
Normal file
56
web/include/vditor/src/ts/markdown/lazyLoadImageRender.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
57
web/include/vditor/src/ts/markdown/markmapRender.ts
Normal file
57
web/include/vditor/src/ts/markdown/markmapRender.ts
Normal 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])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
148
web/include/vditor/src/ts/markdown/mathRender.ts
Normal file
148
web/include/vditor/src/ts/markdown/mathRender.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
105
web/include/vditor/src/ts/markdown/mediaRender.ts
Normal file
105
web/include/vditor/src/ts/markdown/mediaRender.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
60
web/include/vditor/src/ts/markdown/mermaidRender.ts
Normal file
60
web/include/vditor/src/ts/markdown/mermaidRender.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
74
web/include/vditor/src/ts/markdown/mindmapRender.ts
Normal file
74
web/include/vditor/src/ts/markdown/mindmapRender.ts
Normal 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}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
110
web/include/vditor/src/ts/markdown/outlineRender.ts
Normal file
110
web/include/vditor/src/ts/markdown/outlineRender.ts
Normal 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;
|
||||||
|
};
|
||||||
32
web/include/vditor/src/ts/markdown/plantumlRender.ts
Normal file
32
web/include/vditor/src/ts/markdown/plantumlRender.ts
Normal 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}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
167
web/include/vditor/src/ts/markdown/previewRender.ts
Normal file
167
web/include/vditor/src/ts/markdown/previewRender.ts
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
24
web/include/vditor/src/ts/markdown/setLute.ts
Normal file
24
web/include/vditor/src/ts/markdown/setLute.ts
Normal 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;
|
||||||
|
};
|
||||||
107
web/include/vditor/src/ts/markdown/speechRender.ts
Normal file
107
web/include/vditor/src/ts/markdown/speechRender.ts
Normal 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);
|
||||||
|
});
|
||||||
|
};
|
||||||
45
web/include/vditor/src/ts/outline/index.ts
Normal file
45
web/include/vditor/src/ts/outline/index.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
web/include/vditor/src/ts/preview/image.ts
Normal file
50
web/include/vditor/src/ts/preview/image.ts
Normal 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 ${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);
|
||||||
|
});
|
||||||
|
};
|
||||||
285
web/include/vditor/src/ts/preview/index.ts
Normal file
285
web/include/vditor/src/ts/preview/index.ts
Normal 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" ? "知乎" : "微信公众号平台"}进行粘贴` : `已复制到剪切板`);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
web/include/vditor/src/ts/resize/index.ts
Normal file
57
web/include/vditor/src/ts/resize/index.ts
Normal 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");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
27
web/include/vditor/src/ts/sv/combineFootnote.ts
Normal file
27
web/include/vditor/src/ts/sv/combineFootnote.ts
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
125
web/include/vditor/src/ts/sv/index.ts
Normal file
125
web/include/vditor/src/ts/sv/index.ts
Normal 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};
|
||||||
188
web/include/vditor/src/ts/sv/inputEvent.ts
Normal file
188
web/include/vditor/src/ts/sv/inputEvent.ts
Normal 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,
|
||||||
|
});
|
||||||
|
};
|
||||||
215
web/include/vditor/src/ts/sv/process.ts
Normal file
215
web/include/vditor/src/ts/sv/process.ts
Normal 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);
|
||||||
|
};
|
||||||
202
web/include/vditor/src/ts/sv/processKeydown.ts
Normal file
202
web/include/vditor/src/ts/sv/processKeydown.ts
Normal 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;
|
||||||
|
};
|
||||||
39
web/include/vditor/src/ts/tip/index.ts
Normal file
39
web/include/vditor/src/ts/tip/index.ts
Normal 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 = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
28
web/include/vditor/src/ts/toolbar/Both.ts
Normal file
28
web/include/vditor/src/ts/toolbar/Both.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
8
web/include/vditor/src/ts/toolbar/Br.ts
Normal file
8
web/include/vditor/src/ts/toolbar/Br.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export class Br {
|
||||||
|
public element: HTMLElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.element = document.createElement("div");
|
||||||
|
this.element.className = "vditor-toolbar__br";
|
||||||
|
}
|
||||||
|
}
|
||||||
36
web/include/vditor/src/ts/toolbar/CodeTheme.ts
Normal file
36
web/include/vditor/src/ts/toolbar/CodeTheme.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
web/include/vditor/src/ts/toolbar/ContentTheme.ts
Normal file
35
web/include/vditor/src/ts/toolbar/ContentTheme.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
web/include/vditor/src/ts/toolbar/Counter.ts
Normal file
40
web/include/vditor/src/ts/toolbar/Counter.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
web/include/vditor/src/ts/toolbar/Custom.ts
Normal file
17
web/include/vditor/src/ts/toolbar/Custom.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
28
web/include/vditor/src/ts/toolbar/Devtools.ts
Normal file
28
web/include/vditor/src/ts/toolbar/Devtools.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
8
web/include/vditor/src/ts/toolbar/Divider.ts
Normal file
8
web/include/vditor/src/ts/toolbar/Divider.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export class Divider {
|
||||||
|
public element: HTMLElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.element = document.createElement("div");
|
||||||
|
this.element.className = "vditor-toolbar__divider";
|
||||||
|
}
|
||||||
|
}
|
||||||
195
web/include/vditor/src/ts/toolbar/EditMode.ts
Normal file
195
web/include/vditor/src/ts/toolbar/EditMode.ts
Normal 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} <${updateHotkeyTip("⌥⌘7")}></button>
|
||||||
|
<button data-mode="ir">${window.VditorI18n.instantRendering} <${updateHotkeyTip("⌥⌘8")}></button>
|
||||||
|
<button data-mode="sv">${window.VditorI18n.splitView} <${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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
74
web/include/vditor/src/ts/toolbar/Emoji.ts
Normal file
74
web/include/vditor/src/ts/toolbar/Emoji.ts
Normal 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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
41
web/include/vditor/src/ts/toolbar/Export.ts
Normal file
41
web/include/vditor/src/ts/toolbar/Export.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
66
web/include/vditor/src/ts/toolbar/Fullscreen.ts
Normal file
66
web/include/vditor/src/ts/toolbar/Fullscreen.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
73
web/include/vditor/src/ts/toolbar/Headings.ts
Normal file
73
web/include/vditor/src/ts/toolbar/Headings.ts
Normal 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("<⌥⌘1>")}</button>
|
||||||
|
<button data-tag="h2" data-value="## ">${window.VditorI18n.heading2} <${updateHotkeyTip("⌥⌘2")}></button>
|
||||||
|
<button data-tag="h3" data-value="### ">${window.VditorI18n.heading3} <${updateHotkeyTip("⌥⌘3")}></button>
|
||||||
|
<button data-tag="h4" data-value="#### ">${window.VditorI18n.heading4} <${updateHotkeyTip("⌥⌘4")}></button>
|
||||||
|
<button data-tag="h5" data-value="##### ">${window.VditorI18n.heading5} <${updateHotkeyTip("⌥⌘5")}></button>
|
||||||
|
<button data-tag="h6" data-value="###### ">${window.VditorI18n.heading6} <${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";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
web/include/vditor/src/ts/toolbar/Help.ts
Normal file
30
web/include/vditor/src/ts/toolbar/Help.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
25
web/include/vditor/src/ts/toolbar/Indent.ts
Normal file
25
web/include/vditor/src/ts/toolbar/Indent.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
43
web/include/vditor/src/ts/toolbar/Info.ts
Normal file
43
web/include/vditor/src/ts/toolbar/Info.ts
Normal 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> </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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
web/include/vditor/src/ts/toolbar/InsertAfter.ts
Normal file
18
web/include/vditor/src/ts/toolbar/InsertAfter.ts
Normal 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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
web/include/vditor/src/ts/toolbar/InsertBefore.ts
Normal file
18
web/include/vditor/src/ts/toolbar/InsertBefore.ts
Normal 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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
54
web/include/vditor/src/ts/toolbar/MenuItem.ts
Normal file
54
web/include/vditor/src/ts/toolbar/MenuItem.ts
Normal 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 ? ` <${updateHotkeyTip(menuItem.hotkey)}>` : "";
|
||||||
|
}
|
||||||
|
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 || "");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
24
web/include/vditor/src/ts/toolbar/Outdent.ts
Normal file
24
web/include/vditor/src/ts/toolbar/Outdent.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
21
web/include/vditor/src/ts/toolbar/Outline.ts
Normal file
21
web/include/vditor/src/ts/toolbar/Outline.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
55
web/include/vditor/src/ts/toolbar/Preview.ts
Normal file
55
web/include/vditor/src/ts/toolbar/Preview.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
60
web/include/vditor/src/ts/toolbar/Record.ts
Normal file
60
web/include/vditor/src/ts/toolbar/Record.ts
Normal 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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
web/include/vditor/src/ts/toolbar/Redo.ts
Normal file
18
web/include/vditor/src/ts/toolbar/Redo.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
web/include/vditor/src/ts/toolbar/Undo.ts
Normal file
18
web/include/vditor/src/ts/toolbar/Undo.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
41
web/include/vditor/src/ts/toolbar/Upload.ts
Normal file
41
web/include/vditor/src/ts/toolbar/Upload.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
192
web/include/vditor/src/ts/toolbar/index.ts
Normal file
192
web/include/vditor/src/ts/toolbar/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
120
web/include/vditor/src/ts/toolbar/setToolbar.ts
Normal file
120
web/include/vditor/src/ts/toolbar/setToolbar.ts
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
180
web/include/vditor/src/ts/ui/initUI.ts
Normal file
180
web/include/vditor/src/ts/ui/initUI.ts
Normal 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 || "";
|
||||||
|
};
|
||||||
16
web/include/vditor/src/ts/ui/setCodeTheme.ts
Normal file
16
web/include/vditor/src/ts/ui/setCodeTheme.ts
Normal 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");
|
||||||
|
}
|
||||||
|
};
|
||||||
15
web/include/vditor/src/ts/ui/setContentTheme.ts
Normal file
15
web/include/vditor/src/ts/ui/setContentTheme.ts
Normal 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");
|
||||||
|
}
|
||||||
|
};
|
||||||
31
web/include/vditor/src/ts/ui/setPreviewMode.ts
Normal file
31
web/include/vditor/src/ts/ui/setPreviewMode.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
7
web/include/vditor/src/ts/ui/setTheme.ts
Normal file
7
web/include/vditor/src/ts/ui/setTheme.ts
Normal 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");
|
||||||
|
}
|
||||||
|
};
|
||||||
260
web/include/vditor/src/ts/undo/index.ts
Normal file
260
web/include/vditor/src/ts/undo/index.ts
Normal 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};
|
||||||
10
web/include/vditor/src/ts/upload/getElement.ts
Normal file
10
web/include/vditor/src/ts/upload/getElement.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
260
web/include/vditor/src/ts/upload/index.ts
Normal file
260
web/include/vditor/src/ts/upload/index.ts
Normal 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><audio controls="controls" src="${path}"></audio></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 += `\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};
|
||||||
10
web/include/vditor/src/ts/upload/setHeaders.ts
Normal file
10
web/include/vditor/src/ts/upload/setHeaders.ts
Normal 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]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
454
web/include/vditor/src/ts/util/Options.ts
Normal file
454
web/include/vditor/src/ts/util/Options.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
170
web/include/vditor/src/ts/util/RecordMedia.ts
Normal file
170
web/include/vditor/src/ts/util/RecordMedia.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
web/include/vditor/src/ts/util/addScript.ts
Normal file
43
web/include/vditor/src/ts/util/addScript.ts
Normal 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
Reference in New Issue
Block a user