feat: 完成todo功能

This commit is contained in:
hahaaha 2020-11-24 17:11:18 +08:00
parent ad179c686d
commit fcceeba68c
114 changed files with 1784 additions and 310 deletions

View File

@ -19,7 +19,6 @@
"regenerator": true,
"useESModules": false
}
],
"istanbul"
]
]
}

41
.github/workflows/e2e.yml vendored Normal file
View File

@ -0,0 +1,41 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
# github actions 中文文档 https://docs.github.com/cn/actions/getting-started-with-github-actions
name: Cypress tests
on:
push:
branches:
- 'master'
- 'dev'
- 'feature-*'
- 'fix-*'
- 'hotfix-*'
paths:
- '.github/workflows/*'
- 'src/**'
- 'test/**'
- 'examples/**'
- 'build/**'
- 'cypress/**'
jobs:
test-e2e:
runs-on: ubuntu-latest
container: cypress/browsers:node12.13.0-chrome78-ff70
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm install
- name: Run build
run: npm run build:dev
- uses: cypress-io/github-action@v2
with:
browser: chrome
start: npm run example
wait-on: 'http://localhost:8881/examples/index.html'

View File

@ -17,7 +17,7 @@ module.exports = {
publish: false,
},
plugins: {
'@release-it/conventional-changelog': {
'./conventional-changelog.js': {
preset: 'angular',
infile: 'CHANGELOG.md',
},

View File

@ -1,3 +1,51 @@
## [4.5.2](https://github.com/wangeditor-team/wangEditor/compare/v4.5.1...v4.5.2) (2020-11-27)
### Bug Fixes
* 错误提示类型优化 ([61cc9b4](https://github.com/wangeditor-team/wangEditor/commit/61cc9b4d5081ce7e0733753e138ae2ff4f157921))
* 多实例全屏的问题 ([83f6b43](https://github.com/wangeditor-team/wangEditor/commit/83f6b43db87a4671d46d302f975a5e6bf7b8b070))
* 去掉测试全屏的代码 ([03a3f81](https://github.com/wangeditor-team/wangEditor/commit/03a3f811a01255bb5aeb8f6a64985919df76e271))
* 添加 custom alert 的 html 文档 ([baba963](https://github.com/wangeditor-team/wangEditor/commit/baba96388c819e8b51288fb8997d14998f3c7447))
* 添加居中样式 ([8db384a](https://github.com/wangeditor-team/wangEditor/commit/8db384ab19987943e255b22fe77144c0be9ebf8a))
* fix wrap wrap in firefox ([7ebdbbf](https://github.com/wangeditor-team/wangEditor/commit/7ebdbbf3dbccf5a83d02518698d74ef643a1576b))
* fix: 某些情况下,无法成功粘贴 ([dbfe2eb](https://github.com/wangeditor-team/wangEditor/commit/dbfe2eb3db0426a22fe3296fa3e62c1dc44b6537)), closes [#2530](https://github.com/wangeditor-team/wangEditor/issues/2530) [#2530](https://github.com/wangeditor-team/wangEditor/issues/2530)
* img 添加 重置 效果 ([10df1bb](https://github.com/wangeditor-team/wangEditor/commit/10df1bbcda00b723299a4935077a3636f4a09906))
## [4.5.1](https://github.com/wangeditor-team/wangEditor/compare/v4.5.0...v4.5.1) (2020-11-26)
### Bug Fixes
* setJSON的表格不成功的问题解决 ([f57395b](https://github.com/wangeditor-team/wangEditor/commit/f57395b3445fe05debdeaf4eaae7ddd1ce44da1e))
* uploadImgAccept 类型 ([18b7a42](https://github.com/wangeditor-team/wangEditor/commit/18b7a42e02a6079502d3ce7583524f3f391a082f))
* 去掉console.log ([6197747](https://github.com/wangeditor-team/wangEditor/commit/6197747700ce99616831624688f6395b4baaae9b))
* 变量名优化 ([5d20096](https://github.com/wangeditor-team/wangEditor/commit/5d20096319a63c11bd9071dfe107245fac632597))
* 完善了设置字体大小、样式、背景、文字颜色等菜单的功能 ([3072543](https://github.com/wangeditor-team/wangEditor/commit/3072543efdff2cb36f594ac396eb6c2c61815d13))
* 添加自定义setJSON表格按钮 ([7bd76c6](https://github.com/wangeditor-team/wangEditor/commit/7bd76c6ebab4011e40fab4d78fa59c74903df7b6))
### Features
* 🎸 support custom accept for image [#1655](https://github.com/wangeditor-team/wangEditor/issues/1655) ([5af4dcd](https://github.com/wangeditor-team/wangEditor/commit/5af4dcd505a41a3f4fbe6b1e885c0005bcf887d8))
# [4.6.0](https://github.com/wangeditor-team/wangEditor/compare/v4.5.0...v4.6.0) (2020-11-25)
### Bug Fixes
* setJSON的表格不成功的问题解决 ([f57395b](https://github.com/wangeditor-team/wangEditor/commit/f57395b3445fe05debdeaf4eaae7ddd1ce44da1e))
* uploadImgAccept 类型 ([18b7a42](https://github.com/wangeditor-team/wangEditor/commit/18b7a42e02a6079502d3ce7583524f3f391a082f))
* 去掉console.log ([6197747](https://github.com/wangeditor-team/wangEditor/commit/6197747700ce99616831624688f6395b4baaae9b))
* 变量名优化 ([5d20096](https://github.com/wangeditor-team/wangEditor/commit/5d20096319a63c11bd9071dfe107245fac632597))
* 完善了设置字体大小、样式、背景、文字颜色等菜单的功能 ([3072543](https://github.com/wangeditor-team/wangEditor/commit/3072543efdff2cb36f594ac396eb6c2c61815d13))
* 添加自定义setJSON表格按钮 ([7bd76c6](https://github.com/wangeditor-team/wangEditor/commit/7bd76c6ebab4011e40fab4d78fa59c74903df7b6))
### Features
* 🎸 support custom accept for image [#1655](https://github.com/wangeditor-team/wangEditor/issues/1655) ([5af4dcd](https://github.com/wangeditor-team/wangEditor/commit/5af4dcd505a41a3f4fbe6b1e885c0005bcf887d8))
# [4.5.0](https://github.com/wangeditor-team/wangEditor/compare/v4.4.2...v4.5.0) (2020-11-20)

View File

@ -19,13 +19,27 @@
## 基本使用
npm 安装 `npm i wangeditor --save` ,几行代码即可创建一个编辑器
### NPM
```bash
npm i wangeditor --save
```
安装后几行代码即可创建一个编辑器:
```js
import E from "wangeditor";
const editor = new E("#div1");
editor.create();
```
### CDN
```html
<script type="text/javascript" src="https://unpkg.com/wangeditor/dist/wangEditor.min.js"></script>
<script type="text/javascript">
const E = window.wangEditor
const editor = new E('#div1')
// 或者 const editor = new E(document.getElementById('div1'))
editor.create()
</script>
```
更多使用方法,可参考[文档](http://www.wangeditor.com/doc/)。

89
conventional-changelog.js Normal file
View File

@ -0,0 +1,89 @@
const { EOL } = require('os')
const fs = require('fs')
const { Plugin } = require('release-it')
const conventionalChangelog = require('conventional-changelog')
const concat = require('concat-stream')
const prependFile = require('prepend-file')
class ConventionalChangelog extends Plugin {
getInitialOptions(options, namespace) {
options[namespace].tagName = options.git.tagName
return options[namespace]
}
async bump(version) {
this.setContext({ version })
const { previousTag, currentTag } = await this.getConventionalConfig()
this.setContext({ previousTag, currentTag })
const changelog = await this.generateChangelog()
this.setContext({ changelog })
}
async getConventionalConfig() {
const version = this.getContext('version')
const previousTag = this.config.getContext('latestTag')
const tagTemplate =
this.options.tagName || ((previousTag || '').match(/^v/) ? 'v${version}' : '${version}')
const currentTag = tagTemplate.replace('${version}', version)
return { version, previousTag, currentTag }
}
getChangelogStream(options = {}) {
const { version, previousTag, currentTag } = this.getContext()
return conventionalChangelog(
Object.assign(options, this.options),
{ version, previousTag, currentTag },
{
debug: this.config.isDebug ? this.debug : null,
}
)
}
generateChangelog(options) {
return new Promise((resolve, reject) => {
const resolver = result => resolve(result.toString().trim())
const changelogStream = this.getChangelogStream(options)
changelogStream.pipe(concat(resolver))
changelogStream.on('error', reject)
})
}
async writeChangelog() {
const { infile } = this.options
let { changelog } = this.getContext()
let hasInfile = false
try {
fs.accessSync(infile)
hasInfile = true
} catch (err) {
this.debug(err)
}
if (!hasInfile) {
changelog = await this.generateChangelog({ releaseCount: 0 })
this.debug({ changelog })
}
await prependFile(infile, changelog + EOL + EOL)
if (!hasInfile) {
await this.exec(`git add ${infile}`)
}
}
async beforeRelease() {
const { infile } = this.options
const { isDryRun } = this.config
this.log.exec(`Writing changelog to ${infile}`, isDryRun)
if (infile && !isDryRun) {
await this.writeChangelog()
}
}
}
module.exports = ConventionalChangelog

View File

@ -1,4 +1,5 @@
{
"baseUrl": "http://localhost:8881",
"defaultCommandTimeout": 8000
"defaultCommandTimeout": 8000,
"video": false
}

View File

@ -62,12 +62,12 @@ describe('表情', () => {
cy.get('@emotionList')
.eq(0)
.as('emotion')
.click()
.click({ timeout: 1000 })
.then($el => {
const img = $el.find('img')
const src = (img.get(0) as HTMLImageElement).src
cy.get('@Editable').find('img').should('have.attr', 'src', src)
cy.get('@Editable').find('img', { timeout: 20000 }).should('have.attr', 'src', src)
})
})
})

View File

@ -7,8 +7,7 @@ describe('插入视频', () => {
cy.get('@Editable').clear()
})
const videoUrl =
'https://player.vimeo.com/external/288190258.sd.mp4?s=8bbc2d25f23cf412a99bd2dbdfa08688fd973ce8&profile_id=164&oauth2_token_id=57447761'
const videoUrl = 'https://www.bilibili.com/video/BV14p4y1v776/'
it('点击菜单打开插入视频的面板', () => {
cy.getByClass('toolbar').children().eq(17).as('videoMenu').click()
@ -33,15 +32,14 @@ describe('插入视频', () => {
cy.get('@Editable').find('video').should('not.exist')
cy.getByClass('toolbar').children().eq(17).as('imgMenu').click()
const videoEl = `<video src="${videoUrl}" controls></video>`
const videoEl = `<iframe src="${videoUrl}" controls></iframe>`
cy.get('@imgMenu').find('.w-e-panel-container').as('Panel').find('input').type(videoEl)
cy.get('@Panel').find('.w-e-button-container button').click()
cy.get('@Editable')
.find('video')
cy.get('.w-e-text-container iframe', { timeout: 10000 })
.should('be.visible')
.then($video => {
const video = $video.get(0)
const video = $video.get(0) as HTMLVideoElement
expect(video.src).to.eq(videoUrl)
})
})

View File

@ -0,0 +1,37 @@
describe('添加todo', () => {
beforeEach(() => {
cy.visit('/examples/index.html')
cy.getByClass('text-container').children().first().as('Editable')
cy.get('@Editable').clear()
})
const text = 'text1234'
it('点击todo菜单插入todo样式', () => {
cy.get('@Editable').type(text)
cy.get('@Editable').contains(text)
cy.getByClass('toolbar').children().eq(23).click()
cy.getByClass('toolbar').children().eq(23).should('have.class', 'w-e-active')
cy.get('@Editable').find('ul').should('contain.text', text).find('input')
})
it('再次点击todo菜单移除todo', () => {
cy.get('@Editable').type(text)
cy.get('@Editable').contains(text)
cy.getByClass('toolbar').children().eq(23).as('todo').click()
cy.getByClass('toolbar').children().eq(23).should('have.class', 'w-e-active')
cy.get('@Editable').find('ul').should('contain.text', text).find('input')
cy.get('@todo').click()
cy.get('@todo').should('not.have.class', 'w-e-active')
cy.get('@Editable').find('input').should('not.exist')
cy.get('@Editable').find('p').contains(text)
})
})

View File

@ -7,6 +7,7 @@
- 语言:`typescript`
- 依赖框架和工具:无(编辑器作为第三方工具,应该控制自身的代码体积和依赖,让用户简单实用)
- 打包工具:`webpack`
- 测试工具 `jest` `cypress`
## 主要目录

View File

@ -2,6 +2,24 @@
欢迎非团队成员贡献源码,我们会及时审核、合并。
- fork 源码
- 修改代码,并提交
- fork 源码,下载到本地,并 `npm i`
- 修改代码,运行 `npm start` 自测
- 运行 `npm run all-check` 执行最后的检查
- 提交代码到你的 github 仓库
- 提交 Pull Request ,合并到 `feature-third-contribution` 分支(注意,其他分支不予通过!!!)
注意,提交 Pull Request 时,请一定说明你这次改动的目的。模板可参考:
```md
## 遇到了什么问题
*请详细描述问题,或者贴一个 issue 链接*
## 你的预期是什么
*请详细描述,你修改代码之后的样子*
## 是否进行了详细的自测?
*是/否*
```

View File

@ -4,7 +4,7 @@
## 检查 PR
- 是否要往 `dev` 分支合并,而不是其他的分支
- 是否要往 `dev` 分支(或其他指定的分支)合并,而不是其他的分支
- commits 描述是否符合开发规范
- github actions 检查是否成功
@ -22,14 +22,26 @@
代码走查如果有问题,会在 Pull Request 上回复评论意见,并通知开发者。
开发者根据评论意见,继续修改,然后重新提交,重新代码走查。
## 合并代码
## 反馈评审意见
代码走查没有问题,则通过检查,并 Merge Pull Request 合并到 dev 分支
第一,针对每一行,提交评审意见
如果合并出现冲突,开发者需要重新修改代码,重新提交 Pull Request 。
![](./imgs/cr1.png)
成功合并了 dev 分支之后,确保 github actions 的任务能执行通过。如果 actions 任务有问题,要查看日志,解决问题
第二,待所有代码都审核完,统一提交本次 review 的意见,通知任务负责人
## 回归测试 dev 分支
![](./imgs/cr2.png)
打开浏览器访问 `http://106.55.153.217:8881/dev/examples/index.html` ,测试功能。
第三,待任务负责人修改之后,重新审核。
## 通过审核
提交 Approve review ,通知任务负责人。
![](./imgs/cr3.png)
## 不用合并代码
代码审核通过之后,**不用**合并代码,通知负责人即可。
负责人等待所有 code review 结束之后,群里通知作者(群主)合并代码。

View File

@ -28,7 +28,11 @@ git config user.email xxx@xxx.com
## 运行代码
打开两个控制台,进入项目目录,分别运行 `npm run dev``npm run example` ,然后浏览器访问 `http://127.0.0.1:8881/examples/` 即可。
```sh
npm install
npm run start
# 浏览器访问 http://localhost:8881/examples/index.html
```
## 创建分支
@ -36,8 +40,10 @@ git config user.email xxx@xxx.com
- `master` 主干分支,当前正在运行的代码。**不可**直接往 `master` 提交代码。
- `dev` 开发分支,当前正在开发、但尚未发布的代码。**不可**直接往 `dev` 提交代码,但可以合并其他分支。
- `server` 开发分支,用于部署 server 端功能,**不可**直接往 `server` 提交代码,但可以合并其他分支。
- `feature-xxx` 开发新功能
- `fix-xxx` bug 修复
- `hotfix-xxx` 高优紧急 bug 修复,修复完需紧急上线
- `doc-xxx` 仅修改文档,不修改代码
例如你要开发一个图片上传的功能,可以根据 master 分支拉一个新的分支 `git checkout -b feature-upload-img`
@ -56,7 +62,7 @@ git config user.email xxx@xxx.com
写完代码之后,一定要进行自测:
- 运行 `npm run test` 进行单元测试
- 运行 `npm run dev` 和 `npm run example` 打开页面,进行功能测试
- 运行 `npm start` 打开页面,进行功能测试
- 自己的功能正常
- 其他功能不影响
@ -91,7 +97,7 @@ git config user.email xxx@xxx.com
## 自动部署远程测试页
说明:只有以 `feature-``fix-` 开头的分支,才具有这个功能。
说明:只有以 `feature-` `fix-` `hotfix-` 开头的分支,才具有这个功能。
当提交完自己的分支之后github actions 会自动触发部署到腾讯云测试机。
查看 [actions 列表](https://github.com/wangeditor-team/wangEditor/actions),待所有任务运行完成之后。
@ -107,7 +113,15 @@ git config user.email xxx@xxx.com
然后,一定要自己先看一看 PR 的 **Files Changed** ,看是否符合自己的预期,重要!!如果不符合预期,则把这个 PR 关掉,再重新修改代码,重新提交 PR 。
拿到 Pull Request 的链接,然后
将 Pull Request 的链接贴到任务卡片中,这样其他人就能看到了。
- 如果你是开发小组成员,将 PR 链接粘贴到任务卡片中,并通知导师来代码走查
- 如果你不是开发小组成员,可在 QQ 群 @ 通知群主,并给出 PR 链接
## 剩下的步骤
剩下的步骤,也非常重要,加入团队之后可以从团队知识库中找到说明。
- 交叉测试
- code review
## 非团队人员贡献源码
自己测试,自己 code review具体参考 [contribution.md](./contribution.md)

BIN
docs/imgs/cr1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
docs/imgs/cr2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
docs/imgs/cr3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

View File

@ -1,25 +1,54 @@
# 发布到 npm
发布项目要有节奏,每周定期发布。除非遇到紧急 bug ,需紧急应对。
## 技术方案
- release-it 发布 tag 和 ChangeLog
- github actions 监控 tag 提交,自动发布到 npm
### release-it
`v4.1.0` 版本开始,采用 [release-it](https://github.com/release-it/release-it) 来执行发布操作。
不过release-it 仅仅用来创建 tag 并 push ,真正 publish 到 npm 是在 github actions 中实现的,代码见 `.github/workflows/npm-publish.yml`
## set upstream
### set upstream
切换到 master 分支,执行 `git branch --set-upstream-to=origin/master master`
## 明确发布的范围
打开以下页面,分别找到“待发布”的任务
- bugs https://github.com/wangeditor-team/wangEditor/projects/1
- feature https://www.teambition.com/project/5eb8b4e2ce8c00002237bb81/tasks/view/all
要确定这些任务的 pr ,都已经被合并到了 `dev` 分支。否则,终止发布,联系任务负责人确认。
## 合并代码
到 https://github.com/wangeditor-team/wangEditor/pulls 创建 pr ,分别将以下分支,合并到 `master` 分支。
- `dev`
- `feature-third-contribution` (其他人贡献的代码,会合并到这里)
然后,等待 master 分支的 [github actions](https://github.com/wangeditor-team/wangEditor/actions) 执行完成,主要 jest 和 cypress 测试的流程。
## 触发 release
执行 `npm run release` 。主要进行如下步骤(配置见 `.release-it.js`
本地执行 `npm run release` 。主要进行如下步骤(配置见 `.release-it.js`
- 必须是 master 分支
- 首先 `git pull origin master`
- 然后 `npm run all-check`
- 创建 tag 并 push (它会自动推荐 tag 的版本号,我们默认用即可)
**【注意】千万不要随意执行 `npm run release` **
## release 注意事项
**【注意1】选择版本时一定要慎重** 日常的小改动,选择 `patch` 版本。
如果要选择 `minor` 甚至 `major` 版本,请一定与作者联系确定!!!
**【注意2】千万不要随意执行 `npm run release` **
如果想要体验一下发布过程,可执行 `npm run just-try-release` ,这个随便玩。
## 发布到 npm
@ -28,19 +57,26 @@
待 github actions 执行完成,只要没有报错,即表示发布完成。
## 合并代码到开发分支
提交 pr ,把 master 的代码合并到以下分支,以便接接下来开发。
- `dev`
- `feature-third-contribution`
## 回归测试
### 自动流程
发布完成之后,访问 http://106.55.153.217:8881/publish-npm-test/ 即可得到最新版本的 demo 。
github actions 在发布完 npm 之后,会自动安装最新版,并打包出一个测试页 http://106.55.153.217:8881/publish-npm-test/ 用于回归测试。
## 修改任务状态
PS配置代码见 `.github/workflows/npm-publish.yml`
将任务列表中,“待发布”的任务,拖拽到“已发布”阶段。并通知任务负责人。
### 手动测试
如果修改的是 issue ,则回复“已修复,请更新到最新版本”,并关闭。
可下载测试 demo `git clone git@github.com:wangeditor-team/we-demo.git` ,安装最新的包,本地运行。
## 看是否需要修改文档
## 合并代码到 dev
刚刚已发布的任务,如果是新增的功能或者配置,可能需要修改用户文档。
和任务负责人确认一下,如果需要,则尽快修改文档。
发布完成之后,将 master 代码合并到 dev 分支,并提交。
以便后续开发 merge 时,代码更简洁。
【注意】必须先发布功能,再修改文档。顺序不能反了。

View File

@ -5,14 +5,11 @@
单元测试是一个项目最基础的测试,也是一个项目质量保证的第一关,所以确保一定的单元测试覆盖率还是很重要的。我们项目的单元测试使用了业界内比较受欢迎的 `jest` ,文档完善,生态发展也不错,也比较适合来写单元测试。
### 目录介绍
目前我们的单元测试放在根目录下的 `test` 目录,下面是具体的目录介绍:
- config 主要用来存放与编辑器默认配置相关的测试文件;
- editor 主要用来存放与编辑器构造器相关的测试文件;
- fns 主要用来存放一些帮助方便测试的 `utils`
- menus 主要用来存放菜单功能的测试文件;
- text 主要用来存放文本处理相关的测试文件;
- utils 主要用来存放项目 `utils` 相关的测试文件;
- helpers 用来存放配合测试的一些 utils
- setup 用来引入一些更加方便我们测试的 `jest` 库,比如在这里我们就引入了 `jest-dom` 来配合我们测试 HTML 相关的部分;
- unit 主要用来存放所有单元测试文件,里面就基本按我们项目核心代码的目录结构存放对应的单元测试,比如 editor 目录就是存放编辑器相关的测试文件,以此类推。
可以看到,单元测试目录结构基本是跟项目核心代码的结构保持一致的,这样也让我可以很清晰地定位到相关功能的测试文件。
可以看到单元测试目录结构职责还是比较分明unit下的测试文件基本是跟项目核心代码的结构保持一致的这样也让我可以很清晰地定位到相关功能的测试文件。
### 编写测试
可以根据你需要测试的功能选择对应的目录创建测试文件,目前的目录结构基本满足了编辑器所有功能了。如果之前没有写过或者学习过单元测试的同学,可以先去 [jest](https://jestjs.io/) 官网先进行一定的学习,再加入到单元测试中来。

View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>wangEditor example</title>
<style>
</style>
</head>
<body>
<p>wangEditor demo</p>
<div id="div1">
<p>欢迎使用 <b>wangEditor</b> 富文本编辑器</p>
<p><img src="http://www.wangeditor.com/imgs/logo.jpeg"/></p>
</div>
<script src="../dist/wangEditor.js"></script>
<script>
// 改为使用var声明才能在window对象上获取到编辑器实例方便e2e测试
var E = window.wangEditor
var editor = new E('#div1')
editor.config.customAlert = function (str, code, err) {
console.log('customAlert', str, code, err)
alert(`str:${str}, code:${code}, err: ${err}`)
}
editor.config.linkCheck = () => {
return false
}
editor.create()
</script>
</body>
</html>

35
examples/form.html Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>wangEditor example</title>
<style>
</style>
</head>
<body>
<p>
wangEditor demo
</p>
<form onsubmit="console.log('submit')">
<div id="div1">
<p>欢迎使用 <b>wangEditor</b> 富文本编辑器</p>
<p>
<img src="http://www.wangeditor.com/imgs/logo.jpeg"/>
</p>
</div>
<button type="submit">submit</button>
</form>
<script src="../dist/wangEditor.js"></script>
<script>
// 改为使用var声明才能在window对象上获取到编辑器实例方便e2e测试
var E = window.wangEditor
var editor = new E('#div1')
editor.create()
</script>
</body>
</html>

View File

@ -14,14 +14,13 @@
wangEditor getJSON
</p>
<div id="div1">
<p>我是一行文字</p>
<p>我是一行<b>这里加粗</b>文字</p>
<p>我是一行<a href="123" target="_blank">链接</a>文字</p>
<p><img src="https://www.tslang.cn/assets/images/fork_me_ribbon.svg" style="width: 200px;" /></p>
</div>
<button onclick="setJsonOne()">setJSON</button>
<button onclick="setJsonTwo()">setJSON自定义 JSON</button>
<button onclick="getJson()">getJSON</button>
<button onclick="setTable()">setJSON Table</button>
<script src="../dist/wangEditor.js"></script>
<script>
@ -34,7 +33,14 @@
console.log(editor.txt.getJSON())
let myJson = editor.txt.getJSON()
function getJson(){
myJson = editor.txt.getJSON()
console.log(myJson, JSON.stringify(myJson))
}
function setJsonOne() {
console.log(myJson)
editor.txt.setJSON(myJson)
}
@ -69,6 +75,11 @@
}
])
}
function setTable(){
let tableJson = [{"tag":"p","attrs":[],"children":["setJSON表格设置成功"]},{"tag":"table","attrs":[{"name":"border","value":"0"},{"name":"width","value":"100%"},{"name":"cellpadding","value":"0"},{"name":"cellspacing","value":"0"}],"children":[{"tag":"tbody","attrs":[],"children":[{"tag":"tr","attrs":[],"children":[{"tag":"th","attrs":[],"children":["1"]},{"tag":"th","attrs":[],"children":["2"]}]},{"tag":"tr","attrs":[],"children":[{"tag":"td","attrs":[],"children":["3"]},{"tag":"td","attrs":[],"children":["2"]}]}]}]},{"tag":"p","attrs":[],"children":[{"tag":"br","attrs":[],"children":[]}]}]
editor.txt.setJSON(tableJson)
}
</script>
</body>

38
examples/table.html Normal file
View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>wangEditor example</title>
<style>
</style>
</head>
<body>
<p> wangEditor demo </p>
<table>
<tbody>
<tr>
<td>
<div id="div1">
<p>欢迎使用 <b>wangEditor</b> 富文本编辑器</p>
<p>
<img src="http://www.wangeditor.com/imgs/logo.jpeg"/>
</p>
</div>
</td>
</tr>
</tbody>
</table>
<script src="../dist/wangEditor.js"></script>
<script>
// 改为使用var声明才能在window对象上获取到编辑器实例方便e2e测试
var E = window.wangEditor
var editor = new E('#div1')
editor.create()
</script>
</body>
</html>

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>wangEditor example</title>
<style>
</style>
</head>
<body>
<p>
wangEditor demo
</p>
<div id="div1">
<p>欢迎使用 <b>wangEditor</b> 富文本编辑器</p>
<p>
<img src="//www.baidu.com/img/flexible/logo/pc/result@2.png" />
</p>
</div>
<script src="../dist/wangEditor.js"></script>
<script>
// 改为使用var声明才能在window对象上获取到编辑器实例方便e2e测试
var E = window.wangEditor
var editor = new E('#div1')
// 测试如果输入'测试'返回false停止插入
editor.config.onlineVideoCheck = function (video) {
if (video === '测试') {
return '测试禁止插入';
}
return true;
}
editor.config.onlineVideoCallback = function (video) {
console.log('video', video)
}
editor.create()
</script>
</body>
</html>

View File

@ -2,11 +2,14 @@ module.exports = {
roots: ['<rootDir>/test'],
testRegex: 'test/(.+)\\.test\\.(js?|ts?)$',
transform: {
'^.+\\.(css|less)$': '<rootDir>/test/fns/styleMock.js',
'^.+\\.(css|less)$': '<rootDir>/test/helpers/styleMock.js',
'^.+\\.ts?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^@/(.*)$': '<rootDir>/src/unit/$1',
},
collectCoverageFrom: ['src/**/*.ts', 'src/**/*.js'],
setupFilesAfterEnv: ['./test/setup/index.ts']
}

View File

@ -1,6 +1,6 @@
{
"name": "wangeditor",
"version": "4.5.0",
"version": "4.5.2",
"description": "wangEditor - 轻量级 web 富文本编辑器,配置方便,使用简单,开源免费",
"homepage": "http://www.wangeditor.com/",
"keywords": [
@ -16,6 +16,7 @@
"example": "cross-env PORT=8881 nodemon server/www.js",
"server": "cross-env NODE_ENV=prd_dev pm2 start server/pm2.conf.json",
"build": "cross-env NODE_ENV=production webpack --config build/webpack.prod.js",
"build:dev": "cross-env NODE_ENV=development webpack --config build/webpack.dev.js",
"build:un-min": "cross-env NODE_ENV=production webpack --config build/webpack.un-min.prod.js",
"build:analyzer": "cross-env NODE_ENV=production_analyzer webpack --config build/webpack.prod.js",
"lint": "eslint '{src,test,cypress,build}/**/*.{js,ts}'",
@ -48,6 +49,7 @@
"@babel/preset-env": "^7.11.5",
"@cypress/code-coverage": "^3.8.3",
"@release-it/conventional-changelog": "^2.0.0",
"@testing-library/jest-dom": "^5.11.6",
"@types/jest": "^25.2.1",
"@types/jquery": "^3.3.38",
"@types/lodash": "^4.14.150",
@ -59,7 +61,9 @@
"clean-webpack-plugin": "^3.0.0",
"commitlint": "^11.0.0",
"commitlint-config-cz": "^0.13.2",
"concat-stream": "^2.0.0",
"concurrently": "^5.3.0",
"conventional-changelog": "^3.1.24",
"cross-env": "^7.0.2",
"css-loader": "^3.5.3",
"cypress": "^5.5.0",
@ -86,6 +90,7 @@
"nodemon": "^2.0.6",
"nyc": "^15.1.0",
"postcss-loader": "^3.0.0",
"prepend-file": "^2.0.0",
"prettier": "^2.0.5",
"release-it": "^14.2.0",
"style-loader": "^1.2.1",

View File

@ -5,6 +5,24 @@
margin: 0;
box-sizing: border-box;
background-color: #fff;
h1 {
font-size: 2em !important;
}
h2 {
font-size: 1.5em !important;
}
h3 {
font-size: 1.17em !important;
}
h4 {
font-size: 1em !important;
}
h5 {
font-size: 0.83em !important;
}
p {
font-size: 1em !important;
}
/*表情菜单样式*/
.eleImg{
cursor: pointer;

View File

@ -97,7 +97,8 @@
box-shadow: 0 2px 8px 0 rgba(0,0,0,.15);
border-radius: 4px;
padding: 4px 5px 6px;
position: absolute;
justify-content: center;
align-items: center;
}
// 下箭头
.w-e-tooltip-up::after {
@ -128,4 +129,4 @@
color: #ccc;
text-decoration: underline;
}
}
}

View File

@ -11,6 +11,19 @@ export type TCatalog = {
text: string
}
/**
*
* @param alertInfo alert info
* @param alertType
* @param debugInfo debug info
*/
function customAlert(alertInfo: string, alertType: string, debugInfo?: string): void {
window.alert(alertInfo)
if (debugInfo) {
console.error('wangEditor: ' + debugInfo)
}
}
export default {
onchangeTimeout: 200,
@ -19,4 +32,5 @@ export default {
onblur: EMPTY_FN,
onCatalogChange: null,
customAlert,
}

View File

@ -31,6 +31,9 @@ export default {
// 插入图片成功之后的回调函数
linkImgCallback: EMPTY_FN,
// accept
uploadImgAccept: ['jpg', 'jpeg', 'png', 'gif', 'bmp'],
// 服务端地址
uploadImgServer: '',

View File

@ -12,6 +12,7 @@ import imageConfig, { UploadImageHooksType } from './image'
import textConfig from './text'
import langConfig from './lang'
import historyConfig from './history'
import videoConfig from './video'
// 字典类型
export type DicType = {
@ -45,6 +46,7 @@ export type ConfigType = {
zIndexFullScreen: number
showFullScreen: boolean
showLinkImg: boolean
uploadImgAccept: string[]
uploadImgServer: string
uploadImgShowBase64: boolean
uploadImgMaxSize: number
@ -57,7 +59,7 @@ export type ConfigType = {
uploadImgTimeout: number
withCredentials: boolean
customUploadImg: Function | null
customAlert: Function | null
customAlert: Function
onCatalogChange: Function | null
@ -70,6 +72,9 @@ export type ConfigType = {
historyMaxSize: number
focus: boolean
onlineVideoCheck: Function
onlineVideoCallback: Function
}
export type Resource = {
@ -98,6 +103,7 @@ const defaultConfig = Object.assign(
textConfig,
langConfig,
historyConfig,
videoConfig,
//链接校验的配置函数
{
linkCheck: function (text: string, link: string): string | boolean {

View File

@ -3,6 +3,7 @@ export default {
languages: {
'zh-CN': {
wangEditor: {
: '重置',
: '插入',
: '默认',
: '创建',
@ -103,6 +104,7 @@ export default {
},
en: {
wangEditor: {
: 'reset',
: 'insert',
: 'default',
: 'create',

View File

@ -59,6 +59,7 @@ export default {
'splitLine',
'undo',
'redo',
'todo',
],
fontNames: [

View File

@ -7,6 +7,6 @@ export default {
focus: true,
height: 300,
placeholder: '请输入正文',
zIndexFullScreen: 10000,
zIndexFullScreen: 10002,
showFullScreen: true,
}

16
src/config/video.ts Normal file
View File

@ -0,0 +1,16 @@
/**
* @description
* @author hutianhao
*/
import { EMPTY_FN } from '../utils/const'
export default {
// 插入网络视频前的回调函数
onlineVideoCheck: (video: string): string | boolean => {
return true
},
// 插入网络视频成功之后的回调函数
onlineVideoCallback: EMPTY_FN,
}

View File

@ -9,7 +9,7 @@ import { UA, toArray } from '../../../../utils/util'
/**
*
*/
function compileType(data: string) {
export function compileType(data: string) {
switch (data) {
case 'childList':
return 'node'
@ -23,7 +23,7 @@ function compileType(data: string) {
/**
*
*/
function compileValue(data: MutationRecord) {
export function compileValue(data: MutationRecord) {
switch (data.type) {
case 'attributes':
return (data.target as Element).getAttribute(data.attributeName as string) || ''
@ -37,7 +37,7 @@ function compileValue(data: MutationRecord) {
/**
* addedNodes/removedNodes
*/
function complieNodes(data: MutationRecord) {
export function complieNodes(data: MutationRecord) {
const temp: DiffNodes = {}
if (data.addedNodes.length) {
temp.add = toArray(data.addedNodes)
@ -51,7 +51,7 @@ function complieNodes(data: MutationRecord) {
/**
* addedNodes/removedNodes
*/
function compliePosition(data: MutationRecord) {
export function compliePosition(data: MutationRecord) {
let temp: TargetPosition
if (data.previousSibling) {
temp = {

View File

@ -240,14 +240,33 @@ class SelectionAndRange {
}
/**
*
* ,
* firefox下的文本节点会自动补充一个br元素
* firefox下的文本节点会自动移动到br前面
* @param {Node} node
* @param {Boolean} toStart true光标在开始位置 false在结束位置
* @param {number} position
*/
public moveCursor(node: Node, toStart: boolean = false) {
public moveCursor(node: Node, position?: number) {
const range = this.getRange()
const pos = toStart ? 0 : node.childNodes.length
//对文本节点特殊处理
let len: number
if (node.nodeType === 3) {
len = node.nodeValue?.length as number
// 在firefox下文本节点下会自带一个br导致的自动换行问题
if (UA.isFirefox && len !== 0) {
len = len - 1
}
} else {
len = node.childNodes.length
// 在firefox下文本节点下会自带一个br导致的自动换行问题
if (UA.isFirefox && len !== 0) {
if (node.childNodes[len - 1].nodeName === 'BR') {
len = len - 1
}
}
}
// 如果position变量存在取positon,position不存在取默认的len
let pos: number = position || position === 0 ? position : len
if (!range) {
return
}
@ -257,6 +276,15 @@ class SelectionAndRange {
this.restoreSelection()
}
}
/**
*
*/
public getCursorPos(): number | undefined {
const selection = window.getSelection()
return selection?.anchorOffset
}
}
export default SelectionAndRange

View File

@ -8,7 +8,7 @@ import DropListMenu from '../menu-constructors/DropListMenu'
import $ from '../../utils/dom-core'
import Editor from '../../editor/index'
import { MenuActive } from '../menu-constructors/Menu'
import { hexToRgb } from '../../utils/util'
class BackColor extends DropListMenu implements MenuActive {
constructor(editor: Editor) {
const $elem = $(
@ -41,7 +41,29 @@ class BackColor extends DropListMenu implements MenuActive {
*/
public command(value: string): void {
const editor = this.editor
const isEmptySelection = editor.selection.isSelectionEmpty()
const $selectionElem = editor.selection.getSelectionContainerElem()?.elems[0]
const isSpan = $selectionElem?.nodeName.toLowerCase() !== 'p'
const bgColor = $selectionElem?.style.backgroundColor
const isSameColor = hexToRgb(value) === bgColor
if (isEmptySelection) {
if (isSpan && !isSameColor) {
const $elems = editor.selection.getSelectionRangeTopNodes(editor)
editor.selection.createRangeByElem($elems[0])
editor.selection.moveCursor($elems[0].elems[0])
}
// 插入空白选区
editor.selection.createEmptyRange()
}
editor.cmd.do('backColor', value)
if (isEmptySelection) {
// 需要将选区范围折叠起来
editor.selection.collapseRange()
editor.selection.restoreSelection()
}
}
/**

View File

@ -91,7 +91,7 @@ export default function (editor: Editor, text: string, languageType: string): Pa
'"'
)}</textarea>
<div class="w-e-button-container">
<button id="${btnOkId}" class="right">${
<button type="button" id="${btnOkId}" class="right">${
isActive(editor) ? t('修改') : t('插入')
}</button>
</div>

View File

@ -41,7 +41,28 @@ class FontColor extends DropListMenu implements MenuActive {
*/
public command(value: string): void {
const editor = this.editor
const isEmptySelection = editor.selection.isSelectionEmpty()
const $selectionElem = editor.selection.getSelectionContainerElem()?.elems[0]
const isFont = $selectionElem?.nodeName.toLowerCase() !== 'p'
const isSameColor = $selectionElem?.getAttribute('color') === value
if (isEmptySelection) {
if (isFont && !isSameColor) {
const $elems = editor.selection.getSelectionRangeTopNodes(editor)
editor.selection.createRangeByElem($elems[0])
editor.selection.moveCursor($elems[0].elems[0])
}
// 插入空白选区
editor.selection.createEmptyRange()
}
editor.cmd.do('foreColor', value)
if (isEmptySelection) {
// 需要将选区范围折叠起来
editor.selection.collapseRange()
editor.selection.restoreSelection()
}
}
/**

View File

@ -37,7 +37,29 @@ class FontSize extends DropListMenu implements MenuActive {
*/
public command(value: string): void {
const editor = this.editor
const isEmptySelection = editor.selection.isSelectionEmpty()
const $selectionElem = editor.selection.getSelectionContainerElem()?.elems[0]
const isFont = $selectionElem?.nodeName.toLowerCase() !== 'p'
const isSameSize = $selectionElem?.getAttribute('size') === value
if (isEmptySelection) {
if (isFont && !isSameSize) {
const $elems = editor.selection.getSelectionRangeTopNodes(editor)
editor.selection.createRangeByElem($elems[0])
editor.selection.moveCursor($elems[0].elems[0])
}
// 插入空白选区
editor.selection.createEmptyRange()
}
editor.cmd.do('fontSize', value)
if (isEmptySelection) {
// 需要将选区范围折叠起来
editor.selection.collapseRange()
editor.selection.restoreSelection()
}
}
/**

View File

@ -37,7 +37,29 @@ class FontStyle extends DropListMenu implements MenuActive {
*/
public command(value: string): void {
const editor = this.editor
const isEmptySelection = editor.selection.isSelectionEmpty()
const $selectionElem = editor.selection.getSelectionContainerElem()?.elems[0]
const isFont = $selectionElem?.nodeName.toLowerCase() !== 'p'
const isSameValue = $selectionElem?.getAttribute('face') === value
if (isEmptySelection) {
if (isFont && !isSameValue) {
const $elems = editor.selection.getSelectionRangeTopNodes(editor)
editor.selection.createRangeByElem($elems[0])
editor.selection.moveCursor($elems[0].elems[0])
}
// 插入空白选区
editor.selection.createEmptyRange()
}
editor.cmd.do('fontName', value)
if (isEmptySelection) {
// 需要将选区范围折叠起来
editor.selection.collapseRange()
editor.selection.restoreSelection()
}
}
/**

View File

@ -12,6 +12,9 @@ import Editor from '../../../editor/index'
*/
function createShowHideFn(editor: Editor) {
let tooltip: Tooltip | null
const t = (text: string, prefix: string = ''): string => {
return editor.i18next.t(prefix + text)
}
/**
* tooltip
@ -56,6 +59,16 @@ function createShowHideFn(editor: Editor) {
$node.attr('width', '100%')
$node.removeAttr('height')
// 返回 true表示执行完之后隐藏 tooltip。否则不隐藏。
return true
},
},
{
$elem: $(`<span>${t('重置')}</span>`),
onClick: (editor: Editor, $node: DomElement) => {
$node.removeAttr('width')
$node.removeAttr('height')
// 返回 true表示执行完之后隐藏 tooltip。否则不隐藏。
return true
},

View File

@ -44,22 +44,24 @@ export default function (editor: Editor): PanelConf {
} else if (check === true) {
//用户通过了开发者的校验
if (flag === false) {
alert(
config.customAlert(
`${t('您插入的网络图片无法识别', 'validate.')}${t(
'请替换为支持的图片类型',
'validate.'
)}jpg | png | gif ...`
)}jpg | png | gif ...`,
'warning'
)
} else return true
} else {
//用户未能通过开发者的校验,开发者希望我们提示这一字符串
alert(check)
config.customAlert(check, 'error')
}
return false
}
// tabs 配置 -----------------------------------------
const fileMultipleAttr = config.uploadImgMaxLength === 1 ? '' : 'multiple="multiple"'
const accepts: string = config.uploadImgAccept.map((item: string) => `image/${item}`).join(',')
const tabsConf: PanelTabConf[] = [
// first tab
{
@ -71,7 +73,7 @@ export default function (editor: Editor): PanelConf {
<i class="w-e-icon-upload2"></i>
</div>
<div style="display:none;">
<input id="${upFileId}" type="file" ${fileMultipleAttr} accept="image/jpg,image/jpeg,image/png,image/gif,image/bmp"/>
<input id="${upFileId}" type="file" ${fileMultipleAttr} accept="${accepts}"/>
</div>
</div>`,
// 事件绑定
@ -126,7 +128,10 @@ export default function (editor: Editor): PanelConf {
placeholder="${t('图片链接')}"/>
</td>
<div class="w-e-button-container">
<button id="${linkBtnId}" class="right">${t('插入', '')}</button>
<button type="button" id="${linkBtnId}" class="right">${t(
'插入',
''
)}</button>
</div>
</div>`,
events: [

View File

@ -20,24 +20,6 @@ class UploadImg {
this.editor = editor
}
/**
*
* @param alertInfo alert info
* @param debugInfo debug info
*/
private alert(alertInfo: string, debugInfo?: string): void {
const customAlert = this.editor.config.customAlert
if (customAlert) {
customAlert(alertInfo)
} else {
window.alert(alertInfo)
}
if (debugInfo) {
console.error('wangEditor: ' + debugInfo)
}
}
/**
*
* @param src
@ -62,8 +44,9 @@ class UploadImg {
img = null
}
img.onerror = () => {
this.alert(
config.customAlert(
t('插入图片错误'),
'error',
`wangEditor: ${t('插入图片错误')}${t('图片链接')} "${src}"${t('下载链接失败')}`
)
img = null
@ -155,11 +138,11 @@ class UploadImg {
})
// 抛出验证信息
if (errInfos.length) {
this.alert(`${t('图片验证未通过')}: \n` + errInfos.join('\n'))
config.customAlert(`${t('图片验证未通过')}: \n` + errInfos.join('\n'), 'warning')
return
}
if (resultFiles.length > maxLength) {
this.alert(t('一次最多上传') + maxLength + t('张图片'))
config.customAlert(t('一次最多上传') + maxLength + t('张图片'), 'warning')
return
}
@ -219,7 +202,7 @@ class UploadImg {
if (hooks.before) return hooks.before(xhr, editor, resultFiles)
},
onTimeout: xhr => {
this.alert(t('上传图片超时'))
config.customAlert(t('上传图片超时'), 'error')
if (hooks.timeout) hooks.timeout(xhr, editor)
},
onProgress: (percent, e) => {
@ -230,15 +213,17 @@ class UploadImg {
}
},
onError: xhr => {
this.alert(
config.customAlert(
t('上传图片错误'),
'error',
`${t('上传图片错误')}${t('服务器返回状态')}: ${xhr.status}`
)
if (hooks.error) hooks.error(xhr, editor)
},
onFail: (xhr, resultStr) => {
this.alert(
config.customAlert(
t('上传图片失败'),
'error',
t('上传图片返回结果错误') + `${t('返回结果')}: ` + resultStr
)
if (hooks.fail) hooks.fail(xhr, editor, resultStr)
@ -251,8 +236,9 @@ class UploadImg {
}
if (result.errno != '0') {
// 返回格式不对,应该为 { errno: 0, data: [...] }
this.alert(
config.customAlert(
t('上传图片失败'),
'error',
`${t('上传图片返回结果错误')}${t('返回结果')} errno=${result.errno}`
)
if (hooks.fail) hooks.fail(xhr, editor, result)
@ -271,7 +257,7 @@ class UploadImg {
})
if (typeof xhr === 'string') {
// 上传被阻止
this.alert(xhr)
config.customAlert(xhr, 'error')
}
// 阻止以下代码执行,重要!!!

View File

@ -79,7 +79,7 @@ export default function (editor: Editor, text: string, link: string): PanelConf
return true
} else {
//用户未能通过开发者的校验,开发者希望我们提示这一字符串
alert(check)
editor.config.customAlert(check, 'warning')
}
return false
}
@ -95,25 +95,25 @@ export default function (editor: Editor, text: string, link: string): PanelConf
title: editor.i18next.t('menus.panelMenus.link.链接'),
// 模板
tpl: `<div>
<input
id="${inputTextId}"
type="text"
class="block"
value="${text}"
<input
id="${inputTextId}"
type="text"
class="block"
value="${text}"
placeholder="${editor.i18next.t('menus.panelMenus.link.链接文字')}"/>
</td>
<input
id="${inputLinkId}"
type="text"
class="block"
value="${link}"
<input
id="${inputLinkId}"
type="text"
class="block"
value="${link}"
placeholder="${editor.i18next.t('如')} https://..."/>
</td>
<div class="w-e-button-container">
<button id="${btnOkId}" class="right">
<button type="button" id="${btnOkId}" class="right">
${editor.i18next.t('插入')}
</button>
<button id="${btnDelId}" class="gray right" style="display:${delBtnDisplay}">
<button type="button" id="${btnDelId}" class="gray right" style="display:${delBtnDisplay}">
${editor.i18next.t('menus.panelMenus.link.取消链接')}
</button>
</div>

View File

@ -26,6 +26,7 @@ import Redo from './redo/index'
import Table from './table/index'
import Code from './code'
import SplitLine from './split-line/index'
import Todo from './todo'
export type MenuListType = {
[key: string]: any
@ -55,4 +56,5 @@ export default {
table: Table,
code: Code,
splitLine: SplitLine,
todo: Todo,
}

View File

@ -18,7 +18,7 @@ function bindEvent(editor: Editor) {
const $newLine = $('<p><br></p>')
$newLine.insertAfter($topSelectElem)
// 将光标移动br前面
editor.selection.moveCursor($newLine.getNode(), true)
editor.selection.moveCursor($newLine.getNode(), 0)
}
// 当blockQuote中没有内容回车后移除blockquote

View File

@ -55,7 +55,7 @@ class Quote extends BtnMenu implements MenuActive {
// 兼容firefoxfirefox下空行情况下选区会在br后造成自动换行的问题
moveNode.textContent
? editor.selection.moveCursor(moveNode)
: editor.selection.moveCursor(moveNode, true)
: editor.selection.moveCursor(moveNode, 0)
// 即时更新btn状态
this.tryChangeActive()
// 防止最后一行无法跳出

View File

@ -38,7 +38,9 @@ export default function (editor: Editor): PanelConf {
}</span>
</div>
<div class="w-e-button-container">
<button id="${insertBtnId}" class="right">${t('插入')}</button>
<button type="button" id="${insertBtnId}" class="right">${t(
'插入'
)}</button>
</div>
</div>`,
events: [

View File

@ -0,0 +1,104 @@
import Editor from '../../../editor/index'
import $ from '../../../utils/dom-core'
import { getCursorNextNode, isAllTodo } from '../util'
import createTodo from '../todo'
/**
* todolist
* @param editor
*/
function bindEvent(editor: Editor) {
/**
* todo的自定义回车事件
* @param e
*/
function todoEnter(e: Event) {
// 判断是否为todo节点
if (isAllTodo(editor)) {
e.preventDefault()
const selection = editor.selection
const $topSelectElem = selection.getSelectionRangeTopNodes(editor)[0]
const $li = $topSelectElem.childNodes()?.get(0)
const selectionNode = window.getSelection()?.anchorNode as Node
// 回车时内容为空时,删去此行
if ($topSelectElem.text() === '') {
const $p = $(`<p><br></p>`)
$p.insertAfter($topSelectElem)
selection.moveCursor($p.getNode())
$topSelectElem.remove()
return
}
const pos = selection.getCursorPos() as number
const CursorNextNode = getCursorNextNode($li?.getNode() as Node, selectionNode, pos)
const todo = createTodo($(CursorNextNode))
const $inputcontainer = todo.getInputContainer()
const todoLiElem = $inputcontainer.parent().getNode()
const $newTodo = todo.getTodo()
const contentSection = $inputcontainer.getNode().nextSibling
// 处理光标在最前面时回车input不显示的问题
if ($li?.text() === '') {
$li?.append($(`<br>`))
}
$newTodo.insertAfter($topSelectElem)
// 处理在google中光标在最后面的input不显示的问题(必须插入之后移动光标)
if (!contentSection || contentSection?.textContent === '') {
// 防止多个br出现的情况
if (contentSection?.nodeName !== 'BR') {
const $br = $(`<br>`)
$br.insertAfter($inputcontainer)
}
selection.moveCursor(todoLiElem, 1)
} else {
selection.moveCursor(todoLiElem)
}
}
}
/**
* input产生的问题
*/
function delDown(e: Event) {
if (isAllTodo(editor)) {
const selection = editor.selection
const $topSelectElem = selection.getSelectionRangeTopNodes(editor)[0]
const $li = $topSelectElem.childNodes()?.getNode()
const $p = $(`<p></p>`)
const p = $p.getNode()
const selectionNode = window.getSelection()?.anchorNode as Node
const pos = selection.getCursorPos()
const prevNode = selectionNode.previousSibling
// 处理内容为空的情况
if ($topSelectElem.text() === '') {
e.preventDefault()
const $newP = $(`<p><br></p>`)
$newP.insertAfter($topSelectElem)
$topSelectElem.remove()
selection.moveCursor($newP.getNode(), 0)
return
}
// 处理有内容时,光标在最前面的情况
if (
prevNode?.nodeName === 'SPAN' &&
prevNode.childNodes[0].nodeName === 'INPUT' &&
pos === 0
) {
e.preventDefault()
$li?.childNodes.forEach((v, index) => {
if (index === 0) return
p.appendChild(v.cloneNode(true))
})
$p.insertAfter($topSelectElem)
$topSelectElem.remove()
}
}
}
editor.txt.eventHooks.enterDownEvents.push(todoEnter)
editor.txt.eventHooks.deleteDownEvents.push(delDown)
}
export default bindEvent

86
src/menus/todo/index.ts Normal file
View File

@ -0,0 +1,86 @@
import $, { DomElement } from '../../utils/dom-core'
import BtnMenu from '../menu-constructors/BtnMenu'
import Editor from '../../editor/index'
import { MenuActive } from '../menu-constructors/Menu'
import { isAllTodo } from './util'
import bindEvent from './bind-event'
import createTodo from './todo'
class Todo extends BtnMenu implements MenuActive {
constructor(editor: Editor) {
const $elem = $(
`<div class="w-e-menu">
<i class="w-e-icon-checkbox-checked"></i>
</div>`
)
super($elem, editor)
bindEvent(editor)
}
/**
*
*/
public clickHandler(): void {
const editor = this.editor
if (!isAllTodo(editor)) {
// 设置todolist
this.setTodo()
} else {
// 取消设置todolist
this.cancelTodo()
this.tryChangeActive()
}
}
tryChangeActive() {
if (isAllTodo(this.editor)) {
this.active()
} else {
this.unActive()
}
}
/**
* todo
*/
private setTodo() {
const editor = this.editor
const topNodeElem: DomElement[] = editor.selection.getSelectionRangeTopNodes(editor)
// 增加垫片防止无法删除的情况
if (topNodeElem[0].prev().length === 0) {
$(`<p style="height:0px;"><br></p>`).insertBefore(topNodeElem[0])
}
topNodeElem.forEach($node => {
const nodeName = $node?.getNodeName()
if (nodeName === 'P') {
const todo = createTodo($node)
const todoNode = todo.getTodo()
const child = todoNode.children()?.getNode() as Node
todoNode.insertAfter($node)
editor.selection.moveCursor(child)
$node.remove()
}
})
this.tryChangeActive()
}
/**
* todo
*/
private cancelTodo() {
const editor = this.editor
const $topNodeElems: DomElement[] = editor.selection.getSelectionRangeTopNodes(editor)
$topNodeElems.forEach($topNodeElem => {
let content = $topNodeElem.childNodes()?.childNodes()?.clone(true) as DomElement
const $p = $(`<p></p>`)
$p.append(content)
$p.insertAfter($topNodeElem)
// 移除input
$p.childNodes()?.get(0).remove()
editor.selection.moveCursor($p.getNode())
$topNodeElem.remove()
})
}
}
export default Todo

55
src/menus/todo/todo.ts Normal file
View File

@ -0,0 +1,55 @@
import $, { DomElement } from '../../utils/dom-core'
export class todo {
private template: string
private checked: boolean
private $todo: DomElement
private $child: DomElement
constructor($orginElem?: DomElement) {
this.template = `<ul data-todo-key="w-e-text-todo" style="margin:0 0 0 20px;position:relative;"><li style="list-style:none;"><span style="position: absolute;left: -18px;top: 2px;" contenteditable="false"><input type="checkbox" style="margin-right:3px;"></span></li></ul>`
this.checked = false
this.$todo = $(this.template)
this.$child = $orginElem?.childNodes()?.clone(true) as DomElement
}
public init() {
const $input = this.getInput()
const $child = this.$child
const $inputContainer = this.getInputContainer()
if ($child) {
$child.insertAfter($inputContainer)
}
$input.on('click', () => {
if (this.checked) {
$input?.removeAttr('checked')
} else {
$input?.attr('checked', '')
}
this.checked = !this.checked
})
}
public getInput(): DomElement {
const $todo = this.$todo
const $input = $todo.find('input')
return $input
}
public getInputContainer(): DomElement {
const $inputContainer = this.getInput().parent()
return $inputContainer
}
public getTodo(): DomElement {
return this.$todo
}
}
function createTodo($orginElem?: DomElement): todo {
const t = new todo($orginElem)
t.init()
return t
}
export default createTodo

86
src/menus/todo/util.ts Normal file
View File

@ -0,0 +1,86 @@
import { DomElement } from '../../utils/dom-core'
import Editor from '../../editor'
/**
* todo
* @param editor
*/
function isTodo($topSelectElem: DomElement) {
return $topSelectElem.attr('data-todo-key') === 'w-e-text-todo'
}
/**
* todo
* @param editor
*/
function isAllTodo(editor: Editor): boolean | undefined {
const $topSelectElems = editor.selection.getSelectionRangeTopNodes(editor)
// 排除为[]的情况
if ($topSelectElems.length === 0) return
return $topSelectElems.every($topSelectElem => {
return isTodo($topSelectElem)
})
}
/**
*
* @param node
* @param textNode
* @param pos
*/
function getCursorNextNode(node: Node, textNode: Node, pos: number): Node | undefined {
if (!node.hasChildNodes()) return
const newNode = node.cloneNode() as ChildNode
let end = false
if (textNode.nodeValue === '') {
end = true
}
let delArr: Node[] = []
node.childNodes.forEach(v => {
//选中后
if (!v.contains(textNode) && end) {
newNode.appendChild(v.cloneNode(true))
delArr.push(v)
}
// 选中
if (v.contains(textNode)) {
if (v.nodeType === 1) {
const childNode = getCursorNextNode(v, textNode, pos) as Node
if (childNode && childNode.textContent !== '') newNode?.appendChild(childNode)
}
if (v.nodeType === 3) {
if (textNode.isEqualNode(v)) {
const textContent = dealTextNode(v, pos)
newNode.textContent = textContent
} else {
newNode.textContent = v.nodeValue
}
}
end = true
}
})
// 删除选中后原来的节点
delArr.forEach(v => {
const node = v as ChildNode
node.remove()
})
return newNode
}
/**
*
* @param node
* @param pos
*/
function dealTextNode(node: Node, pos: number) {
let content = node.nodeValue
let oldContent = content?.slice(0, pos) as string
content = content?.slice(pos) as string
node.nodeValue = oldContent
return content
}
export { getCursorNextNode, isTodo, isAllTodo }

View File

@ -7,11 +7,16 @@ import Editor from '../../editor/index'
import { PanelConf } from '../menu-constructors/Panel'
import { getRandom } from '../../utils/util'
import $ from '../../utils/dom-core'
import { videoRegex } from '../../utils/const'
export default function (editor: Editor, video: string): PanelConf {
// panel 中需要用到的id
const inputIFrameId = getRandom('input-iframe')
const btnOkId = getRandom('btn-ok')
const i18nPrefix = 'menus.panelMenus.video.'
const t = (text: string, prefix: string = i18nPrefix): string => {
return editor.i18next.t(prefix + text)
}
/**
*
@ -19,6 +24,44 @@ export default function (editor: Editor, video: string): PanelConf {
*/
function insertVideo(video: string): void {
editor.cmd.do('insertHTML', video + '<p><br></p>')
// video添加后的回调
editor.config.onlineVideoCallback(video)
}
/**
* 线
* @param video 线
*/
function checkOnlineVideo(video: string): boolean {
// 编辑器进行正常校验video 合规则使指针为true不合规为false
let flag = true
if (!videoRegex.test(video)) {
flag = false
}
// 查看开发者自定义配置的返回值
const check = editor.config.onlineVideoCheck(video)
if (check === undefined) {
if (flag === false) console.log(t('您刚才插入的视频链接未通过编辑器校验', 'validate.'))
} else if (check === true) {
// 用户通过了开发者的校验
if (flag === false) {
editor.config.customAlert(
`${t('您插入的网络视频无法识别', 'validate.')}${t(
'请替换为正确的网络视频格式',
'validate.'
)}<iframe src=...></iframe>`,
'warning'
)
} else {
return true
}
} else {
//用户未能通过开发者的校验,开发者希望我们提示这一字符串
editor.config.customAlert(check, 'error')
}
return false
}
const conf = {
@ -39,7 +82,7 @@ export default function (editor: Editor, video: string): PanelConf {
placeholder="${editor.i18next.t('如')}<iframe src=... ></iframe>"/>
</td>
<div class="w-e-button-container">
<button id="${btnOkId}" class="right">
<button type="button" id="${btnOkId}" class="right">
${editor.i18next.t('插入')}
</button>
</div>
@ -57,6 +100,8 @@ export default function (editor: Editor, video: string): PanelConf {
// 视频为空,则不插入
if (!video) return
// 对当前用户插入的内容进行判断插入为空或者返回false都停止插入
if (!checkOnlineVideo(video)) return
insertVideo(video)

View File

@ -25,6 +25,9 @@ function deleteToKeepP(editor: Editor, deleteUpEvents: Function[], deleteDownEve
$textElem.append($p)
editor.selection.createRangeByElem($p, false, true)
editor.selection.restoreSelection()
// 设置折叠后的光标位置在firebox等浏览器下
// 光标设置在end位置会自动换行
editor.selection.moveCursor($p.getNode(), 0)
}
}
deleteUpEvents.push(upFn)

View File

@ -6,36 +6,37 @@
import $, { DomElement } from './../utils/dom-core'
import { NodeListType } from './getChildrenJSON'
function getHtmlByNodeList(nodeList: NodeListType): DomElement {
function getHtmlByNodeList(
nodeList: NodeListType,
parent: Node = document.createElement('div')
): DomElement {
// 设置一个父节点存储所有子节点
let $root = $(`<div></div>`)
let root = parent
// 遍历节点JSON
nodeList.forEach(item => {
let $elem: DomElement = $('')
let elem: Text | Node | undefined
// 当为文本节点时
if (typeof item === 'string') {
$elem = $(`<span>${item}</span>`)
elem = document.createTextNode(item)
}
// 当为普通节点时
if (typeof item === 'object') {
$elem = $(`<${item.tag}></${item.tag}>`)
elem = document.createElement(item.tag)
item.attrs.forEach(attr => {
$elem.attr(attr.name, attr.value)
$(elem).attr(attr.name, attr.value)
})
// 有子节点时递归将子节点加入当前节点
if (item.children && item.children.length > 0) {
const $elemChilds = getHtmlByNodeList(item.children).children()
$elemChilds && $elem.append($elemChilds)
getHtmlByNodeList(item.children, elem.getRootNode())
}
}
$root.append($elem)
elem && root.appendChild(elem)
})
return $root
return $(root)
}
export default getHtmlByNodeList

View File

@ -528,7 +528,7 @@ class Text {
const target = e.target as HTMLElement
//获取最祖父元素
$dom = $(target).parentUntil('TABLE', target)
$dom = $(target).parentUntilEditor('TABLE', editor, target)
// 没有table范围内则返回
if (!$dom) return

View File

@ -146,11 +146,13 @@ function parseHtml(html: string, filterStyle: boolean = true, ignoreImg: boolean
resultArr.push(html)
},
characters(str: string) {
str = str.trim()
if (!str) return
if (!str) {
return
}
// 忽略的标签
if (isIgnoreTag(CUR_TAG, ignoreImg)) {
// 如果复制拿到的内容是 `<body><html>这种形式无法成功粘贴</html></body>`
if (isIgnoreTag(CUR_TAG, ignoreImg) && /^</.test(str)) {
return
}

View File

@ -9,3 +9,6 @@ export const imgRegex = /\.(gif|jpg|jpeg|png)$/i
//用于校验是否为url格式字符串
export const urlRegex = /^(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-.,@?^=%&amp;:/~+#]*[\w\-@?^=%&amp;/~+#])?/
//用于校验在线视频是否符合规范
export const videoRegex = /<iframe(([\s\S])*?)<\/iframe>/

View File

@ -573,8 +573,19 @@ export class DomElement<T extends DomElementSelector = DomElementSelector> {
/**
*
*/
getNode(): Node {
const elem = this.elems[0]
/**
* 0
* @param n
*/
getNode(n?: number): Node {
let elem: Node
if (n) {
elem = this.elems[n]
} else {
elem = this.elems[0]
}
return elem
}
@ -683,7 +694,7 @@ export class DomElement<T extends DomElementSelector = DomElementSelector> {
}
/**
* selector
* selector
* @param selector css
* @param curElem
*/
@ -707,6 +718,31 @@ export class DomElement<T extends DomElementSelector = DomElementSelector> {
return this.parentUntil(selector, parent)
}
/**
* selector ,
* @param selector css
* @param curElem
*/
parentUntilEditor(selector: string, editor: Editor, curElem?: HTMLElement): DomElement | null {
const elem = curElem || this.elems[0]
if ($(elem).equal(editor.$textContainerElem) || $(elem).equal(editor.$toolbarElem)) {
return null
}
const parent = elem.parentElement
if (parent === null) {
return null
}
if (parent.matches(selector)) {
// 找到,并返回
return $(parent)
}
// 继续查找,递归
return this.parentUntilEditor(selector, editor, parent)
}
/**
*
* @param $elem
@ -738,7 +774,7 @@ export class DomElement<T extends DomElementSelector = DomElementSelector> {
}
/**
*
* selector元素后
* @param selector css
*/
insertAfter(selector: string | DomElement): DomElement {

View File

@ -220,3 +220,20 @@ export function toArray<T>(data: T) {
export function getRandomCode() {
return Math.random().toString(36).slice(-5)
}
/**
* hex color rgb
* @param hex string
*/
export function hexToRgb(hex: string) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
if (result == null) return null
const colors = result.map(i => parseInt(i, 16))
const r = colors[1]
const g = colors[2]
const b = colors[3]
return `rgb(${r}, ${g}, ${b})`
}

View File

@ -1,42 +0,0 @@
/**
* @description video test
* @author
*/
import $ from 'jquery'
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import Video from '../../src/menus/video/index'
import { getMenuInstance } from '../fns/menus'
import Panel from '../../src/menus/menu-constructors/Panel'
let editor: Editor
let videoMenu: Video
test('video 菜单:点击弹出 panel', () => {
editor = createEditor(document, 'div1')
videoMenu = getMenuInstance(editor, Video) as Video
videoMenu.clickHandler()
expect(videoMenu.panel).not.toBeNull()
})
test('video 菜单:插入', () => {
const panel = videoMenu.panel as Panel
const panelElem = panel.$container.elems[0]
const $panelElem = $(panelElem) // jquery 对象
// panel 里的 input 和 button 元素
const $btnInsert = $panelElem.find(":button[id^='btn-ok']") // id 以 'btn-ok' 的 button
const $videoIFrame = $panelElem.find(":input[id^='input-iframe']")
// 插入链接
mockCmdFn(document)
const video =
'<iframe height="498" width="510" src="http://player.youku.com/embed/XMjcwMzc3MzM3Mg==" frameborder="0"></iframe>'
$videoIFrame.val(video)
$btnInsert.click()
// 此处触发 editor.cmd.do('insertHTML', xx),可以被 jest 成功执行,具体参考 mockCmdFn 的描述
expect(editor.$textElem.html().indexOf(video)).toBeGreaterThan(0)
})

1
test/setup/index.ts Normal file
View File

@ -0,0 +1 @@
import '@testing-library/jest-dom'

View File

@ -0,0 +1,12 @@
/**
* @description alert test
* @author raosiling
*/
import events from '../../../src/config/events'
test('customAlert 事件', () => {
window.alert = jest.fn()
events.customAlert('customAlert', 'success')
expect(window.alert).toHaveBeenCalledTimes(1)
})

View File

@ -3,7 +3,7 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import createEditor from '../../helpers/create-editor'
test('styleWithCSS 测试', () => {
const editor = createEditor(document, 'div1')

View File

@ -3,7 +3,7 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import createEditor from '../../helpers/create-editor'
test('菜单数量', () => {
const editor = createEditor(document, 'div1')

View File

@ -3,7 +3,7 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import createEditor from '../../helpers/create-editor'
import $ from 'jquery'
test('onblur 事件', () => {

View File

@ -3,8 +3,8 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
test.only('onchange 事件', done => {
mockCmdFn(document)

View File

@ -3,7 +3,7 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import createEditor from '../../helpers/create-editor'
import $ from 'jquery'
test('onfocus 事件', () => {

View File

@ -3,7 +3,7 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import createEditor from '../../helpers/create-editor'
test('z-index 测试', () => {
const editor = createEditor(document, 'div1')

View File

@ -3,8 +3,8 @@
* @author tonghan
*/
import createEditor from '../fns/create-editor'
import $ from '../../src/utils/dom-core'
import createEditor from '../../helpers/create-editor'
import $ from '../../../src/utils/dom-core'
const TEXT = '我是一行文字'
const $SPAN = $(`<span>${TEXT}</span>`)

View File

@ -3,10 +3,10 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import Editor from '../../src/editor'
import $ from '../../src/utils/dom-core'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import Editor from '../../../src/editor'
import $ from '../../../src/utils/dom-core'
let editor: Editor

View File

@ -3,7 +3,7 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import createEditor from '../../helpers/create-editor'
test('创建一个编辑器实例', () => {
const editor = createEditor(document, 'div1')

View File

@ -0,0 +1,52 @@
import createEditor from '../../helpers/create-editor'
import Editor from '../../../src/editor/index'
import disableInit from '../../../src/editor/disable'
let editor: Editor
describe('Editor disable', () => {
beforeEach(() => {
editor = createEditor(document, 'div1')
})
test('编辑器可以被禁用', () => {
const disabledObj = disableInit(editor)
disabledObj.disable()
expect(editor.$textElem.elems[0].style.display).toBe('none')
})
test('编辑器禁用后通过js修改内容change hook监听会触发', done => {
expect.assertions(1)
const changeFn = jest.fn()
editor.disable()
editor.txt.eventHooks.changeEvents.push(changeFn)
editor.txt.html(`<span>123</span>`)
setTimeout(() => {
try {
expect(changeFn).toBeCalled()
done()
} catch (err) {
done.fail(err)
}
}, 500)
})
test('编辑器禁用后可以取消禁用', () => {
const disabledObj = disableInit(editor)
disabledObj.disable()
expect(editor.$textElem.elems[0].style.display).toBe('none')
disabledObj.enable()
expect(editor.$textElem.elems[0].style.display).toBe('block')
})
})

View File

@ -0,0 +1,52 @@
import createEditor from '../../../helpers/create-editor'
import Editor from '../../../../src/editor'
import compile, {
compileType,
compileValue,
complieNodes,
compliePosition,
} from '../../../../src/editor/history/data/node/compile'
import { Compile } from '../../../../src/editor/history/data/type'
let editor: Editor
function generateCompileData(mutationList: MutationRecord[]) {
let mockData: Compile[] = []
mutationList.forEach(record => {
const item: Compile = {
type: compileType(record.type),
target: record.target,
attr: record.attributeName || '',
value: compileValue(record) || '',
oldValue: record.oldValue || '',
nodes: complieNodes(record),
position: compliePosition(record),
}
mockData.push(item)
})
return mockData
}
describe('Editor history compile', () => {
beforeEach(() => {
editor = createEditor(document, 'div1')
})
test('可以将MutationRecord生成Compile数据', done => {
expect.assertions(3)
const observer = new MutationObserver((mutationList: MutationRecord[]) => {
const compileData = compile(mutationList)
const mockData = generateCompileData(mutationList)
expect(compileData instanceof Array).toBeTruthy()
expect(compileData.length).toBe(1)
expect(compileData).toEqual(mockData)
done()
})
const $textEl = editor.$textElem.elems[0]
observer.observe($textEl, { attributes: true, childList: true, subtree: true })
editor.txt.html('<span>123</span>')
})
})

View File

@ -0,0 +1,62 @@
import createEditor from '../../../helpers/create-editor'
import Editor from '../../../../src/editor'
import compile from '../../../../src/editor/history/data/node/compile'
import { restore, revoke } from '../../../../src/editor/history/data/node/decompilation'
let editor: Editor
describe('Editor history decompile', () => {
beforeEach(() => {
editor = createEditor(document, 'div1')
})
test('可以通过revoke方法撤销编辑器设置的内容', done => {
expect.assertions(3)
const observer = new MutationObserver((mutationList: MutationRecord[]) => {
const compileData = compile(mutationList)
expect(compileData instanceof Array).toBeTruthy()
expect(compileData.length).toBe(1)
observer.disconnect()
revoke(compileData)
expect(editor.$textElem.html()).toEqual('<p><br></p>')
done()
})
const $textEl = editor.$textElem.elems[0]
observer.observe($textEl, { attributes: true, childList: true, subtree: true })
editor.txt.html('<span>123</span>')
})
test('可以通过restore方法恢复撤销的内容', done => {
expect.assertions(2)
const testHtml = '<span>123</span>'
const observer = new MutationObserver((mutationList: MutationRecord[]) => {
const compileData = compile(mutationList)
expect(compileData instanceof Array).toBeTruthy()
observer.disconnect()
revoke(compileData)
restore(compileData)
expect(editor.$textElem.elems[0]).toContainHTML(testHtml)
done()
})
const $textEl = editor.$textElem.elems[0]
observer.observe($textEl, { attributes: true, childList: true, subtree: true })
editor.txt.html(testHtml)
})
})

View File

@ -4,9 +4,9 @@
*/
import i18next from 'i18next'
import i18nextInit from '../../src/editor/init-fns/i18next-init'
import createEditor from '../fns/create-editor'
import Editor from '../../src/editor'
import i18nextInit from '../../../src/editor/init-fns/i18next-init'
import createEditor from '../../helpers/create-editor'
import Editor from '../../../src/editor'
let editor: Editor

View File

@ -3,9 +3,9 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import Selection from '../../src/editor/selection'
import $, { DomElement } from '../../src/utils/dom-core'
import createEditor from '../../helpers/create-editor'
import Selection from '../../../src/editor/selection'
import $, { DomElement } from '../../../src/utils/dom-core'
const TEXT = '我是一行文字'
const $SPAN = $(`<span>${TEXT}</span>`)

View File

@ -0,0 +1,67 @@
import createEditor from '../../helpers/create-editor'
import Editor from '../../../src/editor/index'
import { setFullScreen, setUnFullScreen } from '../../../src/editor/init-fns/set-full-screen'
import $ from 'jquery'
let editor: Editor
const FULLSCREEN_MENU_CLASS_SELECTOR = '.w-e-icon-fullscreen'
const EDIT_CONTAINER_FULLSCREEN_CLASS = 'w-e-full-screen-editor'
describe('设置全屏', () => {
beforeEach(() => {
editor = createEditor(document, 'div1')
})
test('编辑器默认初始化全屏菜单', () => {
const toolbarSelector = editor.$toolbarElem.elems[0].className
const fullMenuEl = $(`.${toolbarSelector}`).find(FULLSCREEN_MENU_CLASS_SELECTOR)
expect(fullMenuEl.length).toBe(1)
})
test('编辑器区和菜单分离的编辑器不初始化全屏菜单', () => {
const seprateModeEditor = createEditor(document, 'div1', 'div2')
const toolbarSelector = seprateModeEditor.$toolbarElem.selector as string
const fullMenuEl = $(toolbarSelector).find(FULLSCREEN_MENU_CLASS_SELECTOR)
expect(fullMenuEl.length).toBe(0)
})
test('编辑器配置 showFullScreen 为false时不初始化全屏菜单', () => {
const seprateModeEditor = createEditor(document, 'div1', '', { showFullScreen: false })
const toolbarSelector = seprateModeEditor.$toolbarElem.selector as string
const fullMenuEl = $(toolbarSelector).find(FULLSCREEN_MENU_CLASS_SELECTOR)
expect(fullMenuEl.length).toBe(0)
})
test('调用 setFullScreen 设置编辑器全屏模式', () => {
setFullScreen(editor)
const toolbarSelector = editor.$toolbarElem.elems[0].className
const $iconElem = $(`.${toolbarSelector}`).children().last().find('i')
const $editorParent = $(`.${toolbarSelector}`).parent().get(0)
const $textContainerElem = editor.$textContainerElem
expect($iconElem.get(0).className).toContain('w-e-icon-fullscreen_exit')
expect($editorParent.className).toContain(EDIT_CONTAINER_FULLSCREEN_CLASS)
expect(+$editorParent.style.zIndex).toEqual(editor.config.zIndexFullScreen)
expect($textContainerElem.elems[0].style.height).toBe('100%')
})
test('调用 setUnFullScreen 取消编辑器全屏模式', () => {
setFullScreen(editor)
setUnFullScreen(editor)
const toolbarSelector = editor.$toolbarElem.elems[0].className
const $iconElem = $(`.${toolbarSelector}`).children().last().find('i')
const $editorParent = $(`.${toolbarSelector}`).parent().get(0)
const $textContainerElem = editor.$textContainerElem
expect($iconElem.get(0).className).toContain('w-e-icon-fullscreen')
expect($editorParent.className).not.toContain(EDIT_CONTAINER_FULLSCREEN_CLASS)
expect($editorParent.style.zIndex).toBe('auto')
expect($textContainerElem.elems[0].style.height).toBe(editor.config.height + 'px')
})
})

View File

@ -3,11 +3,11 @@
* @author lkw
*/
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import BackColor from '../../src/menus/back-color'
import { getMenuInstance } from '../fns/menus'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import BackColor from '../../../src/menus/back-color'
import { getMenuInstance } from '../../helpers/menus'
let editor: Editor
let backColorMenu: BackColor

View File

@ -3,11 +3,11 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import Editor from '../../src/editor'
import Bold from '../../src/menus/bold/index'
import mockCmdFn from '../fns/command-mock'
import { getMenuInstance } from '../fns/menus'
import createEditor from '../../helpers/create-editor'
import Editor from '../../../src/editor'
import Bold from '../../../src/menus/bold/index'
import mockCmdFn from '../../helpers/command-mock'
import { getMenuInstance } from '../../helpers/menus'
let editor: Editor
let boldMenu: Bold

View File

@ -4,12 +4,12 @@
*/
import $ from 'jquery'
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import Code from '../../src/menus/code/index'
import { getMenuInstance } from '../fns/menus'
import Panel from '../../src/menus/menu-constructors/Panel'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import Code from '../../../src/menus/code/index'
import { getMenuInstance } from '../../helpers/menus'
import Panel from '../../../src/menus/menu-constructors/Panel'
import hljs from 'highlight.js'
let editor: Editor

View File

@ -3,8 +3,8 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import Editor from '../../src/wangEditor'
import createEditor from '../../helpers/create-editor'
import Editor from '../../../src/wangEditor'
const { BtnMenu, DropListMenu, PanelMenu, DropList, Panel, Tooltip } = Editor

View File

@ -2,10 +2,10 @@
* @description
* @author liuwei
*/
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import EmoticonMenu from '../../src/menus/emoticon/index'
import { getMenuInstance } from '../fns/menus'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import EmoticonMenu from '../../../src/menus/emoticon/index'
import { getMenuInstance } from '../../helpers/menus'
let editor: Editor
let emoticonMenu: EmoticonMenu
test('表情 菜单:点击弹出 panel', () => {

View File

@ -3,11 +3,11 @@
* @author lkw
*/
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import FontColor from '../../src/menus/font-color'
import { getMenuInstance } from '../fns/menus'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import FontColor from '../../../src/menus/font-color'
import { getMenuInstance } from '../../helpers/menus'
let editor: Editor
let fontColorMenu: FontColor

View File

@ -5,11 +5,11 @@
// 暂时用 customFontSize 代替 fontSize ,因此这个测试用例暂时不用,先暂存 - wangfupeng 2020.08.10
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import FontSize from '../../src/menus/font-size'
import { getMenuInstance } from '../fns/menus'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import FontSize from '../../../src/menus/font-size'
import { getMenuInstance } from '../../helpers/menus'
let editor: Editor
let fontSizeMenu: FontSize

View File

@ -3,11 +3,11 @@
* @author dyl
*/
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import FontStyle from '../../src/menus/font-style'
import { getMenuInstance } from '../fns/menus'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import FontStyle from '../../../src/menus/font-style'
import { getMenuInstance } from '../../helpers/menus'
let editor: Editor
let fontStyleMenu: FontStyle

View File

@ -3,11 +3,11 @@
* @author wangfupeng
*/
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import Head from '../../src/menus/head/index'
import { getMenuInstance } from '../fns/menus'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import Head from '../../../src/menus/head/index'
import { getMenuInstance } from '../../helpers/menus'
let editor: Editor
let headMenu: Head

View File

@ -4,12 +4,12 @@
*/
import $ from 'jquery'
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import ImgMenu from '../../src/menus/img/index'
import { getMenuInstance } from '../fns/menus'
import Panel from '../../src/menus/menu-constructors/Panel'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import ImgMenu from '../../../src/menus/img/index'
import { getMenuInstance } from '../../helpers/menus'
import Panel from '../../../src/menus/menu-constructors/Panel'
let editor: Editor
let imgMenu: ImgMenu

View File

@ -3,11 +3,11 @@
* @author tonghan
*/
import $ from '../../src/utils/dom-core'
import Indent from '../../src/menus/indent/index'
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import { getMenuInstance } from '../fns/menus'
import $ from '../../../src/utils/dom-core'
import Indent from '../../../src/menus/indent/index'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import { getMenuInstance } from '../../helpers/menus'
let editor: Editor
let indentMenu: Indent

View File

@ -3,8 +3,8 @@
* @author wangfupeng
*/
import createEditor from '../fns/create-editor'
import Editor from '../../src/editor'
import createEditor from '../../helpers/create-editor'
import Editor from '../../../src/editor'
let editor: Editor

View File

@ -3,11 +3,11 @@
* @author liuwei
*/
import createEditor from '../fns/create-editor'
import Editor from '../../src/editor'
import Italic from '../../src/menus/italic/index'
import mockCmdFn from '../fns/command-mock'
import { getMenuInstance } from '../fns/menus'
import createEditor from '../../helpers/create-editor'
import Editor from '../../../src/editor'
import Italic from '../../../src/menus/italic/index'
import mockCmdFn from '../../helpers/command-mock'
import { getMenuInstance } from '../../helpers/menus'
let editor: Editor
let italicMenu: Italic

View File

@ -3,11 +3,11 @@
* @author liuwei
*/
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import justify from '../../src/menus/justify/index'
import { getMenuInstance } from '../fns/menus'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import justify from '../../../src/menus/justify/index'
import { getMenuInstance } from '../../helpers/menus'
let editor: Editor
let justifyMenu: justify

View File

@ -3,11 +3,11 @@
* @author lichunlin
*/
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import lineHeight from '../../src/menus/lineHeight/index'
import { getMenuInstance } from '../fns/menus'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import lineHeight from '../../../src/menus/lineHeight/index'
import { getMenuInstance } from '../../helpers/menus'
let editor: Editor
let lineHeightMenu: lineHeight

View File

@ -4,12 +4,12 @@
*/
import $ from 'jquery'
import Editor from '../../src/editor'
import createEditor from '../fns/create-editor'
import mockCmdFn from '../fns/command-mock'
import Link from '../../src/menus/link/index'
import { getMenuInstance } from '../fns/menus'
import Panel from '../../src/menus/menu-constructors/Panel'
import Editor from '../../../src/editor'
import createEditor from '../../helpers/create-editor'
import mockCmdFn from '../../helpers/command-mock'
import Link from '../../../src/menus/link/index'
import { getMenuInstance } from '../../helpers/menus'
import Panel from '../../../src/menus/menu-constructors/Panel'
let editor: Editor
let linkMenu: Link

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