├── .gitignore
├── src
├── modules
│ ├── indent
│ │ └── index.js
│ ├── outdent
│ │ └── index.js
│ ├── align
│ │ ├── tab.html
│ │ ├── index.js
│ │ └── tab.js
│ ├── ol
│ │ └── index.js
│ ├── ul
│ │ └── index.js
│ ├── image
│ │ ├── tab.html
│ │ ├── index.js
│ │ └── tab.js
│ ├── italic
│ │ └── index.js
│ ├── bold
│ │ └── index.js
│ ├── underline
│ │ └── index.js
│ ├── font
│ │ ├── index.js
│ │ ├── tab.html
│ │ ├── config.js
│ │ ├── style.styl
│ │ └── tab.js
│ ├── full-screen
│ │ └── index.js
│ ├── linethrough
│ │ └── index.js
│ ├── fore-color
│ │ ├── tab.html
│ │ ├── index.js
│ │ ├── tab.js
│ │ └── style.styl
│ ├── quote
│ │ └── index.js
│ ├── todo
│ │ └── index.js
│ ├── icourt-todo
│ │ └── index.js
│ └── index.js
├── constant-config.js
├── range-handler
│ ├── index.js
│ ├── README.md
│ ├── instance.js
│ ├── assist-methods.js
│ └── handle-methods.js
├── commands
│ ├── keydown.js
│ ├── enter.js
│ ├── justifyLeft.js
│ ├── justifyCenter.js
│ ├── justifyRight.js
│ ├── fontSize.js
│ ├── paste.js
│ ├── bold.js
│ ├── italic.js
│ ├── underline.js
│ ├── strikeThrough.js
│ ├── delete.js
│ ├── insertImage.js
│ ├── todo.js
│ ├── quote.js
│ └── index.js
├── shortcut
│ └── index.js
├── util
│ ├── mixin.js
│ ├── index.js
│ └── polyfill-ie.js
├── module-inspect
│ ├── load-module-inspect-exclude-rules.js
│ └── index.js
├── i18n
│ ├── zh-cn.js
│ └── en-us.js
├── editor
│ ├── editor.html
│ ├── drag-pic.js
│ ├── style
│ │ ├── reset.styl
│ │ └── main.styl
│ └── editor.js
└── index.js
├── LICENSE
├── package.json
├── webpack.config.js
├── example
├── base
│ └── index.html
└── editModule
│ └── index.html
├── dist
└── index.html
├── README_CN.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | # idea config
4 | .idea
5 | *.iml
--------------------------------------------------------------------------------
/src/modules/indent/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'indent',
3 | icon: 'fa fa-indent',
4 | type: 'fn',
5 | handler: function (rh) {
6 | rh.editor.execCommand('indent')
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/modules/outdent/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'indent',
3 | icon: 'fa fa-outdent',
4 | type: 'fn',
5 | handler: function (rh) {
6 | rh.editor.execCommand('outdent')
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/modules/align/tab.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/constant-config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | MAX_INDENT_LEVEL: 10,
3 | INDENT_WIDTH_PERCENT: 8,
4 | INDENT_STYLE_NAME: 'marginLeft',
5 | OUTDENT_STYLE_NAME: 'marginRight',
6 | ROW_TAG: 'p',
7 | ROW_TAG_UPPERCASE: 'P'
8 | }
9 |
--------------------------------------------------------------------------------
/src/range-handler/index.js:
--------------------------------------------------------------------------------
1 | import Instance from './instance'
2 | import hMethods from './handle-methods'
3 | import aMethods from './assist-methods'
4 | Object.assign(Instance.prototype, hMethods, aMethods)
5 | export default Instance
6 |
--------------------------------------------------------------------------------
/src/modules/ol/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'ol',
3 | icon: 'fa fa-list-ol',
4 | handler: function (rh) {
5 | rh.editor.execCommand('insertOrderedList')
6 | },
7 | inspect (add) {
8 | add('tag', 'OL')
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/ul/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'ul',
3 | icon: 'fa fa-list-ul',
4 | handler: function (rh) {
5 | rh.editor.execCommand('insertUnorderedList')
6 | },
7 | inspect (add) {
8 | add('tag', 'UL')
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/image/tab.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/modules/align/index.js:
--------------------------------------------------------------------------------
1 | import tab from './tab'
2 |
3 | export default {
4 | name: 'align',
5 | icon: 'iui-icon iui-icon-align',
6 | tab,
7 | inspect (add) {
8 | add('style', {
9 | 'textAlign': ['center', 'right']
10 | })
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/italic/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'italic',
3 | icon: 'fa fa-italic',
4 | handler (rh) {
5 | rh.editor.execCommand('italic')
6 | },
7 | inspect (add) {
8 | add('tag', 'I').add('style', {'fontStyle': 'italic'})
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/bold/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'bold',
3 | icon: 'fa fa-bold',
4 | handler: function (rh) {
5 | rh.editor.execCommand('bold')
6 | },
7 | inspect (add) {
8 | add('tag', 'STRONG').add('tag', 'B').add('style',{'font-weight': 'bold'})
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/underline/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'underline',
3 | icon: 'fa fa-underline',
4 | handler (rh) {
5 | rh.editor.execCommand('underline')
6 | },
7 | inspect (add) {
8 | add('tag', 'U').add('style', {'text-decoration-line': 'underline'})
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/font/index.js:
--------------------------------------------------------------------------------
1 | import tab from './tab'
2 | import config from './config'
3 |
4 | export default {
5 | name: 'font',
6 | tab,
7 | config,
8 | inspect (add) {
9 | add('style', {
10 | fontSize: ['xx-large', 'x-large', 'large', 'medium', 'small']
11 | })
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/modules/full-screen/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * toggle full screen mode
3 | * Created by peak on 16/8/18.
4 | */
5 | export default {
6 | name: 'full-screen',
7 | icon: 'fa fa-arrows-alt',
8 | i18n: 'full screen',
9 | handler(rh) {
10 | rh.editor.toggleFullScreen()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/linethrough/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'linethrough',
3 | icon: 'fa fa-strikethrough',
4 | handler: function (rh) {
5 | rh.editor.execCommand('strikeThrough')
6 | },
7 | inspect (add) {
8 | add('tag', 'STRIKE').add('style', {'text-decoration-line': 'line-through'})
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/font/tab.html:
--------------------------------------------------------------------------------
1 |
2 | {{choosed.name}}
3 |
4 |
7 |
8 |
--------------------------------------------------------------------------------
/src/modules/font/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // through font tag
3 | 'xx-large': {
4 | fontSize: 6,
5 | name: '标题'
6 | },
7 | 'x-large': {
8 | fontSize: 5,
9 | name: '副标题'
10 | },
11 | 'large': {
12 | fontSize: 4,
13 | name: '小标题'
14 | },
15 | 'medium': {
16 | fontSize: 3,
17 | name: '正文'
18 | },
19 | default: 'medium'
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/src/modules/fore-color/tab.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
--------------------------------------------------------------------------------
/src/commands/keydown.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 | import constant from '../constant-config'
3 |
4 | export default function (rh, e) {
5 | let node = rh.range.commonAncestorContainer
6 | if (node.nodeType === Node.TEXT_NODE) {
7 |
8 | // to keep text wrap by a row
9 | if (node.parentNode === rh.editZone()) {
10 | commands.formatBlock(rh, constant.ROW_TAG)
11 | return
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/modules/image/index.js:
--------------------------------------------------------------------------------
1 | import tab from './tab'
2 |
3 | export default {
4 | name: 'image',
5 | i18n: 'image',
6 | canUploadSameImage: true,
7 | imgOccupyNewRow: false,
8 | maxSize: 5120 * 1024,
9 | compress: {
10 | // max width
11 | width: 1600,
12 | // max height
13 | height: 1600,
14 | // cpmpress quality 0 - 1
15 | quality: 0.8
16 | },
17 | tab,
18 | inspect (add) {
19 | add('tag', 'img')
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/modules/quote/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'quote',
3 | icon: 'fa fa-quote-right',
4 | show: true,
5 | mounted (editor) {
6 | editor.execCommand('initQuote')
7 | },
8 | handler: function (rh, module) {
9 | let isInQuote = rh.editor.activeModules.includes(module.name)
10 | rh.editor.execCommand('quote', isInQuote)
11 | },
12 | inspect (add) {
13 | add('attribute', {
14 | 'data-editor-quote': ''
15 | })
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/modules/todo/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'todo',
3 | icon: 'fa fa-check-square',
4 | exclude: 'ALL_BUT_MYSELF',
5 | mounted (editor) {
6 | editor.execCommand('initTodo')
7 | },
8 | handler (rh) {
9 | rh.editor.execCommand('todo', {
10 | insertAfter: rh.range.commonAncestorContainer,
11 | placeholder: '待办事项'
12 | })
13 | },
14 | inspect (add) {
15 | add('attribute', {
16 | 'data-editor-todo': ''
17 | })
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/icourt-todo/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | name: 'itodo',
3 | icon: 'iui-icon iui-icon-checked-line',
4 | exclude: 'ALL_BUT_MYSELF',
5 | mounted (editor) {
6 | editor.execCommand('initiTodo')
7 | },
8 | handler (rh) {
9 | rh.editor.execCommand('itodo', {
10 | insertAfter: rh.range.commonAncestorContainer,
11 | placeholder: '待办事项'
12 | })
13 | },
14 | inspect (add) {
15 | add('attribute', {
16 | 'data-editor-todo': ''
17 | })
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/shortcut/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | outdent: {
3 | keyCode: 9,
4 | shiftKey: true,
5 | handler (editor, e) {
6 | e.preventDefault()
7 | editor.execCommand('outdent')
8 | }
9 | },
10 | indent: {
11 | keyCode: 9,
12 | handler (editor, e) {
13 | e.preventDefault()
14 | editor.execCommand('indent')
15 | }
16 | },
17 | delete: {
18 | keyCode: 8,
19 | handler (editor, e) {
20 | editor.execCommand('delete', e, true)
21 | }
22 | },
23 | enter: {
24 | keyCode: 13,
25 | handler (editor, e) {
26 | editor.execCommand('enter', e, true)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/modules/image/tab.js:
--------------------------------------------------------------------------------
1 | import lrz from 'lrz'
2 | import template from './tab.html'
3 |
4 | export default {
5 | template,
6 | data() {
7 | return {
8 | name: 'tab-image',
9 | curModule: null
10 | }
11 | },
12 | methods: {
13 | pick() {
14 | if (this.forbidden) return
15 | this.$refs.file.click()
16 | },
17 | process(e) {
18 | const file = this.$refs.file.files[0]
19 | this.$parent.execCommand('insertImage', file)
20 | if (this.curModule.canUploadSameImage) {
21 | e.target.value = ''
22 | }
23 | }
24 | },
25 | mounted () {
26 | this.curModule = this.$parent.modulesMap['image']
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/modules/fore-color/index.js:
--------------------------------------------------------------------------------
1 | import tab from './tab'
2 |
3 | const foreColorConfig = {
4 | name: 'foreColor',
5 | // color bust be lowercase
6 | colors: ['#000000', '#000033', '#000066', '#000099', '#003300', '#003333', '#003366',
7 | '#003399', '#006600', '#006633', '#009900', '#330000', '#330033', '#330066',
8 | '#333300', '#333366', '#660000', '#660033', '#663300', '#666600', '#666633',
9 | '#666666', '#666699', '#990000', '#990033', '#9900cc', '#996600', '#ffcc00',
10 | '#ffcccc', '#ffcc99', '#ffff00', '#fa8919', '#ed6c00', '#ccffff', '#ccff99', '#ffffff'],
11 | default: '#000000',
12 | tab,
13 | inspect (add) {
14 | add('attribute', {
15 | color: foreColorConfig.colors
16 | })
17 | }
18 | }
19 |
20 | export default foreColorConfig
--------------------------------------------------------------------------------
/src/util/mixin.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by peak on 2017/2/24.
3 | */
4 | /**
5 | * shadow clone
6 | *
7 | * @param source source object
8 | * @param ext extended object
9 | */
10 | export default function mixin(source = {}, ext = {}) {
11 | Object.keys(ext).forEach((k) => {
12 | // for data function
13 | if (k === 'data') {
14 | const dataSrc = source[k]
15 | const dataDesc = ext[k]
16 | if (typeof dataDesc === 'function') {
17 | if (typeof dataSrc !== 'function') {
18 | source[k] = dataDesc
19 | } else {
20 | source[k] = () => mixin(dataSrc(), dataDesc())
21 | }
22 | }
23 | } else {
24 | source[k] = ext[k]
25 | }
26 | })
27 | return source
28 | }
--------------------------------------------------------------------------------
/src/modules/index.js:
--------------------------------------------------------------------------------
1 | import align from './align/index'
2 | import font from './font/index'
3 | import fullScreen from './full-screen/index'
4 | import image from './image/index'
5 | import bold from './bold'
6 | import italic from './italic'
7 | import underline from './underline'
8 | import todo from './todo'
9 | import quote from './quote'
10 | import indent from './indent'
11 | import outdent from './outdent'
12 | import ul from './ul'
13 | import ol from './ol'
14 | import linethrough from './linethrough'
15 | import itodo from './icourt-todo'
16 | import foreColor from './fore-color'
17 |
18 |
19 | /**
20 | * build-in moduls
21 | */
22 | export default [
23 | font,
24 | align,
25 | image,
26 | fullScreen,
27 | bold,
28 | italic,
29 | underline,
30 | todo,
31 | quote,
32 | indent,
33 | outdent,
34 | ul,
35 | ol,
36 | linethrough,
37 | itodo,
38 | foreColor
39 | ]
40 |
--------------------------------------------------------------------------------
/src/util/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * shadow clone
3 | * @param source source object
4 | * @param ext extended object
5 | */
6 | export const mixin = (source = {}, ext = {}) => {
7 | Object.keys(ext).forEach((k) => {
8 | // for data function
9 | if (k === 'data') {
10 | const dataSrc = source[k]
11 | const dataDesc = ext[k]
12 | if (typeof dataDesc === 'function') {
13 | if (typeof dataSrc !== 'function') {
14 | source[k] = dataDesc
15 | } else {
16 | source[k] = () => mixin(dataSrc(), dataDesc())
17 | }
18 | }
19 | } else {
20 | source[k] = ext[k]
21 | }
22 | })
23 | return source
24 | }
25 |
26 | export const isObj = data => {
27 | return Object.prototype.toString.call(data).slice(-7, -1) === 'Object'
28 | }
29 |
--------------------------------------------------------------------------------
/src/module-inspect/load-module-inspect-exclude-rules.js:
--------------------------------------------------------------------------------
1 | /*
2 | * load rule keywords of style inspect
3 | **/
4 |
5 | export default function (curModule, modules) {
6 | let result = []
7 | let curExclude = curModule.exclude
8 | if (Array.isArray(curExclude)) return curExclude
9 | if (typeof curExclude === 'string') {
10 | let moduleNameList = []
11 | modules.forEach(m => {
12 | if (m.name) {
13 | moduleNameList.push(m.name)
14 | }
15 | })
16 | moduleNameList = Array.from(new Set(moduleNameList))
17 | switch (curExclude) {
18 | // exclude all modules
19 | case 'ALL':
20 | result = moduleNameList
21 | break
22 | // exclude all modules but current module
23 | case 'ALL_BUT_MYSELF':
24 | result = moduleNameList
25 | result.splice(result.indexOf(curModule.name), 1)
26 | break
27 | }
28 | }
29 | return result
30 | }
31 |
--------------------------------------------------------------------------------
/src/modules/fore-color/tab.js:
--------------------------------------------------------------------------------
1 | import template from './tab.html'
2 | import './style.styl'
3 |
4 | export default {
5 | template,
6 | watch: {
7 | activeItem (n) {
8 | n = n || this.curModule.default
9 | this.choosed = n
10 | }
11 | },
12 | data () {
13 | return {
14 | colors: [],
15 | curModule: null,
16 | choosed: {},
17 | showList: false
18 | }
19 | },
20 | methods: {
21 | showListFn () {
22 | if (this.$refs.tab.classList.contains('forbidden')) return
23 | this.showList = true
24 | },
25 | changeColor (color) {
26 | this.choosed = color
27 | this.$parent.execCommand('foreColor', color)
28 | this.showList = false
29 | }
30 | },
31 | mounted () {
32 | this.curModule = this.$parent.modulesMap['foreColor']
33 | this.colors = this.curModule.colors
34 | this.choosed = this.curModule.default
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/modules/fore-color/style.styl:
--------------------------------------------------------------------------------
1 | .fore-color-tab
2 | position: relative
3 | font-size: 10px
4 | height: 18px
5 | line-height: 18px
6 | width: 14px
7 | text-align: center
8 | padding: 4px 4px 4px 6px
9 | text-align: left
10 | &:hover
11 | background: #fff
12 | padding: 3px 3px 3px 5px
13 | border: 1px #bcbcbc solid
14 | &.forbidden
15 | cursor: not-allowed
16 | opacity: .3
17 | .icon
18 | position: relative
19 | top: -3px
20 | .choosed
21 | position: relative
22 | left: -.6px
23 | bottom: 3px
24 | width: 12px
25 | height: 4px
26 | .dropdown
27 | padding: 5px
28 | width: 180px
29 | z-index: 1
30 | position: absolute
31 | background: #fff
32 | left: -1px
33 | top: 25px
34 | border: 1px #bcbcbc solid
35 | overflow: hidden
36 | li
37 | margin: 2px
38 | float: left
39 | width: 15px
40 | height: 15px
41 | cursor: pointer
42 |
--------------------------------------------------------------------------------
/src/commands/enter.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 |
3 | export default function (rh, e) {
4 | let node = rh.range.commonAncestorContainer
5 | if (rh.range.collapsed) {
6 |
7 | // rewrite li enter logic
8 | if (rh.findSpecialAncestor(node, 'li') && rh.isEmptyNode(node)) {
9 | e.preventDefault()
10 | let ulOrOl = rh.findSpecialAncestor(node, 'ul') || rh.findSpecialAncestor(node, 'ol')
11 | if (ulOrOl.nodeName === 'UL') {
12 | commands['insertUnorderedList'](rh, e)
13 | }
14 | if (ulOrOl.nodeName === 'OL') {
15 | commands['insertOrderedList'](rh, e)
16 | }
17 | }
18 | }
19 | afterEnter(rh, e)
20 | }
21 |
22 | function afterEnter(rh, e) {
23 | setTimeout(function () {
24 | let node = rh.getSelection().baseNode
25 | let row = rh.getRow(node)
26 | // clear new row's indent
27 | if (row) {
28 | row.style.marginLeft = ''
29 | row.style.marginRight = ''
30 | }
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/src/modules/font/style.styl:
--------------------------------------------------------------------------------
1 | .font-tab
2 | position: relative
3 | font-size: 10px
4 | height: 18px
5 | line-height: 18px
6 | width: 52px
7 | text-align: center
8 | padding: 4px 4px 4px 6px
9 | text-align: left
10 | &:hover
11 | background: #fff
12 | padding: 3px 3px 3px 5px
13 | border: 1px #bcbcbc solid
14 | &.forbidden
15 | cursor: not-allowed
16 | opacity: .3
17 | .icon
18 | float: right
19 | padding: 3px
20 | .dropdown
21 | z-index: 1
22 | position: absolute
23 | background: #fff
24 | left: -1px
25 | top: 25px
26 | border: 1px #bcbcbc solid
27 | border-top: none
28 | overflow: hidden
29 | li
30 | box-sizing: border-box
31 | padding-left: 4px
32 | font-size: 12px
33 | width: 60px
34 | height: 30px
35 | line-height: 30px
36 | background: #fff
37 | text-align: left
38 | cursor: pointer
39 | &:hover
40 | background: #f0f0f0
41 |
--------------------------------------------------------------------------------
/src/commands/justifyLeft.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 |
3 | export default function (rh, arg) {
4 | let texts = rh.getAllTextNodesInRange()
5 | if (!texts.length) {
6 | let s = rh.getSelection()
7 | if (s.baseNode && s.baseNode.nodeType === Node.TEXT_NODE) {
8 | texts.push(s.baseNode)
9 | } else {
10 | document.execCommand('insertHTML', false, '')
11 | if (s.baseNode && s.baseNode.nodeType === Node.TEXT_NODE) {
12 | texts.push(s.baseNode)
13 | }
14 | }
15 | }
16 | texts.forEach(text => {
17 | let curRow = rh.getRow(text)
18 | if (!curRow) {
19 | let newRow = rh.newRow()
20 | newRow.innerText = text.nodeValue
21 | let nextSibling = text.nextSibling
22 | text.parentNode.replaceChild(newRow, text)
23 | if (nextSibling && nextSibling.nodeName === 'BR') {
24 | nextSibling.parentNode.removeChild(nextSibling)
25 | }
26 | }
27 | document.execCommand('justifyLeft', false)
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/src/commands/justifyCenter.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 |
3 | export default function (rh, arg) {
4 | let texts = rh.getAllTextNodesInRange()
5 | if (!texts.length) {
6 | let s = rh.getSelection()
7 | if (s.baseNode && s.baseNode.nodeType === Node.TEXT_NODE) {
8 | texts.push(s.baseNode)
9 | } else {
10 | document.execCommand('insertHTML', false, '')
11 | if (s.baseNode && s.baseNode.nodeType === Node.TEXT_NODE) {
12 | texts.push(s.baseNode)
13 | }
14 | }
15 | }
16 | texts.forEach(text => {
17 | let curRow = rh.getRow(text)
18 | if (!curRow) {
19 | let newRow = rh.newRow()
20 | newRow.innerText = text.nodeValue
21 | let nextSibling = text.nextSibling
22 | text.parentNode.replaceChild(newRow, text)
23 | if (nextSibling && nextSibling.nodeName === 'BR') {
24 | nextSibling.parentNode.removeChild(nextSibling)
25 | }
26 | }
27 | document.execCommand('justifyCenter', false)
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/src/commands/justifyRight.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 |
3 | export default function (rh, arg) {
4 | let texts = rh.getAllTextNodesInRange()
5 | if (!texts.length) {
6 | let s = rh.getSelection()
7 | if (s.baseNode && s.baseNode.nodeType === Node.TEXT_NODE) {
8 | texts.push(s.baseNode)
9 | } else {
10 | document.execCommand('insertHTML', false, '')
11 | if (s.baseNode && s.baseNode.nodeType === Node.TEXT_NODE) {
12 | texts.push(s.baseNode)
13 | }
14 | }
15 | }
16 | texts.forEach(text => {
17 | let curRow = rh.getRow(text)
18 | if (!curRow) {
19 | let newRow = rh.newRow()
20 | newRow.innerText = text.nodeValue
21 | let nextSibling = text.nextSibling
22 | text.parentNode.replaceChild(newRow, text)
23 | if (nextSibling && nextSibling.nodeName === 'BR') {
24 | nextSibling.parentNode.removeChild(nextSibling)
25 | }
26 | }
27 | document.execCommand('justifyRight', false)
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/src/range-handler/README.md:
--------------------------------------------------------------------------------
1 | ### document.execCommand()指令测试结果
2 |
3 | command | IE11 | chrome | firefox
4 | ----: | :---: | :---: | :-----
5 | justifyLeft | Y | Y | Y
6 | justifyCenter | Y | Y | Y
7 | justifyRight | Y | Y | Y
8 | foreColor | Y | Y | Y
9 | backColor | Y | Y | Y
10 | removeFormat | Y | Y | Y
11 | fontName | Y | Y | Y
12 | fontSize | Y | Y | Y
13 | formatBlock | Y | N | N
14 | insertHorizontalRule | Y | Y | Y
15 | insertImage | Y | Y | Y
16 | createLink | Y | Y | Y
17 | insertOrderedList | Y | Y | Y
18 | insertUnorderedList | Y | Y | Y
19 | insertHTML | N | Y | Y
20 | bold | Y | Y | Y
21 | italic | Y | Y | Y
22 | underline | Y | Y | Y
23 | strikeThrough | Y | Y | Y
24 | subscript | Y | Y | Y
25 | superscript | Y | Y | Y
26 | undo | Y | Y | Y
27 | unlink | Y | Y | Y
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/commands/fontSize.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 |
3 | export default function (rh, arg) {
4 | if (rh.range.collapsed) {
5 | let s =rh.getSelection()
6 | let node = s.baseNode
7 | let row = rh.getRow(node)
8 | if (row) {
9 | // delete current span element to keep line-height run correct
10 | if (rh.isEmptyNode(node) && node.parentNode.nodeName === 'SPAN') {
11 | document.execCommand('delete', false)
12 | }
13 | commands.insertHTML(rh, '')
14 | const range = document.createRange()
15 | range.setStart(s.focusNode, s.anchorOffset - 1)
16 | range.setEnd(s.focusNode, s.focusOffset)
17 | s.removeAllRanges()
18 | s.addRange(range)
19 | document.execCommand('styleWithCSS', false, true)
20 | document.execCommand('fontSize', false, arg)
21 | s.collapse(s.focusNode, 1)
22 | return
23 | }
24 | } else {
25 | document.execCommand('styleWithCSS', false, true)
26 | document.execCommand('fontSize', false, arg)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 beta_su
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/commands/paste.js:
--------------------------------------------------------------------------------
1 | export default function (rh, e) {
2 | e.preventDefault()
3 | let text = null
4 |
5 | if(window.clipboardData && clipboardData.setData) {
6 | // IE
7 | text = window.clipboardData.getData('text')
8 | } else {
9 | text = (e.originalEvent || e).clipboardData.getData('text/plain')
10 | }
11 | if (document.body.createTextRange) {
12 | if (document.selection) {
13 | textRange = document.selection.createRange()
14 | } else if (window.getSelection) {
15 | sel = window.getSelection()
16 | var range = sel.getRangeAt(0)
17 |
18 | // 创建临时元素,使得TextRange可以移动到正确的位置
19 | var tempEl = document.createElement("span")
20 | tempEl.innerHTML = "FEFF;"
21 | range.deleteContents()
22 | range.insertNode(tempEl)
23 | textRange = document.body.createTextRange()
24 | textRange.moveToElementText(tempEl)
25 | tempEl.parentNode.removeChild(tempEl)
26 | }
27 | textRange.text = text
28 | textRange.collapse(false)
29 | textRange.select()
30 | } else {
31 | // Chrome之类浏览器
32 | document.execCommand("insertText", false, text)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/i18n/zh-cn.js:
--------------------------------------------------------------------------------
1 | export default {
2 | align: '对齐方式',
3 | image: '图片',
4 | list: '列表',
5 | link: '链接',
6 | unlink: '去除链接',
7 | table: '表格',
8 | font: '文字',
9 | 'full screen': '全屏',
10 | text: '排版',
11 | eraser: '格式清除',
12 | info: '关于',
13 | color: '颜色',
14 | 'please enter a url': '请输入地址',
15 | 'create link': '创建链接',
16 | bold: '加粗',
17 | italic: '倾斜',
18 | underline: '下划线',
19 | 'strike through': '删除线',
20 | subscript: '上标',
21 | superscript: '下标',
22 | heading: '标题',
23 | 'font name': '字体',
24 | 'font size': '文字大小',
25 | 'left justify': '左对齐',
26 | 'center justify': '居中',
27 | 'right justify': '右对齐',
28 | 'ordered list': '有序列表',
29 | 'unordered list': '无序列表',
30 | 'fore color': '前景色',
31 | 'background color': '背景色',
32 | 'row count': '行数',
33 | 'column count': '列数',
34 | save: '确定',
35 | upload: '上传',
36 | progress: '进度',
37 | unknown: '未知',
38 | 'please wait': '请稍等',
39 | error: '错误',
40 | abort: '中断',
41 | reset: '重置',
42 | hr: '分隔线',
43 | undo: '撤消',
44 | 'line height': '行高',
45 | 'exceed size limit': '超出大小限制'
46 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-vue-editor",
3 | "version": "1.0.11",
4 | "description": "a rich text editor for VUE 2.x",
5 | "main": "dist/my-vue-editor.js",
6 | "scripts": {
7 | "build": "webpack --config webpack.config.js",
8 | "start": "webpack-dev-server --open"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/BetaSu/my-vue-editor.git"
13 | },
14 | "keywords": [
15 | "vue",
16 | "editor"
17 | ],
18 | "author": "betaSu",
19 | "license": "ISC",
20 | "bugs": {
21 | "url": "https://github.com/BetaSu/my-vue-editor/issues"
22 | },
23 | "homepage": "https://github.com/BetaSu/my-vue-editor#readme",
24 | "devDependencies": {
25 | "babel-core": "^6.26.0",
26 | "babel-loader": "^7.1.2",
27 | "babel-plugin-transform-runtime": "^6.23.0",
28 | "babel-preset-es2015": "^6.24.1",
29 | "clean-webpack-plugin": "^0.1.16",
30 | "css-loader": "^0.28.7",
31 | "html-loader": "^0.5.1",
32 | "html-webpack-plugin": "^2.30.1",
33 | "lrz": "^4.9.40",
34 | "style-loader": "^0.19.0",
35 | "stylus": "^0.54.5",
36 | "stylus-loader": "^3.0.1",
37 | "webpack": "^3.9.1",
38 | "webpack-dev-server": "^2.7.1"
39 | },
40 | "dependencies": {
41 | "font-awesome": "^4.7.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/editor/editor.html:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/modules/font/tab.js:
--------------------------------------------------------------------------------
1 | import template from './tab.html'
2 | import './style.styl'
3 |
4 | export default {
5 | template,
6 | watch: {
7 | activeItem (n) {
8 | let val = this.fontAttrMap[n]
9 | if (val) {
10 | this.choosed = val
11 | } else {
12 | this.choosed = this.fontAttrMap[this.fontAttrMap['default']]
13 | }
14 | }
15 | },
16 | data () {
17 | return {
18 | fontAttrMap: {},
19 | curModule: null,
20 | choosed: {},
21 | showList: false
22 | }
23 | },
24 | methods: {
25 | showListFn () {
26 | if (this.$refs.tab.classList.contains('forbidden')) return
27 | this.showList = true
28 | },
29 | changeAttr (val) {
30 | this.choosed = val
31 | // this.$parent.execCommand('lineHeight', val.lineHeight)
32 | this.$parent.execCommand('fontSize', val.fontSize)
33 | this.showList = false
34 | },
35 | setFontName (name) {
36 | this.$parent.execCommand('fontName', name)
37 | },
38 | setHeading (heading) {
39 | this.$parent.execCommand('formatBlock', heading)
40 | }
41 | },
42 | mounted () {
43 | this.curModule = this.$parent.modulesMap['font']
44 | this.fontAttrMap = this.curModule.config
45 | this.choosed = this.fontAttrMap[this.fontAttrMap['default']]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path =require('path')
2 | const HtmlWebpackPlugin =require('html-webpack-plugin')
3 | const CleanWebpackPlugin = require('clean-webpack-plugin');
4 | const webpack = require('webpack')
5 |
6 | module.exports = {
7 | entry: './src/index.js',
8 | output: {
9 | path: path.resolve(__dirname, 'dist'),
10 | filename: 'my-vue-editor.js',
11 | libraryTarget: 'umd',
12 | library: 'myVueEditor',
13 | umdNamedDefine: true
14 | },
15 | devtool: 'inline-source-map',
16 | devServer: {
17 | contentBase: './dist',
18 | hot: true
19 | },
20 | plugins: [
21 | new CleanWebpackPlugin(['dist']),
22 | new HtmlWebpackPlugin({
23 | title: 'dev',
24 | filename: 'index.html',
25 | template: './example/base/index.html',
26 | inject: 'head'
27 | }),
28 | new webpack.HotModuleReplacementPlugin()
29 | ],
30 | module: {
31 | loaders: [
32 | {
33 | test: /\.js$/,
34 | loader: 'babel-loader?presets=es2015',
35 | exclude: /node_modules/
36 | },
37 | {
38 | test: /\.html$/,
39 | loader: 'html-loader?exportAsEs6Default',
40 | exclude: /node_modules/
41 | },
42 | {
43 | test: /\.(styl|css)$/,
44 | use:[ 'style-loader','css-loader','stylus-loader'],
45 | exclude: /node_modules/
46 | },
47 | ]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/i18n/en-us.js:
--------------------------------------------------------------------------------
1 | export default {
2 | align: 'align',
3 | image: 'image',
4 | list: 'list',
5 | link: 'link',
6 | unlink: 'unlink',
7 | table: 'table',
8 | font: 'font',
9 | 'full screen': 'full screen',
10 | text: 'text',
11 | eraser: 'remove format',
12 | info: 'info',
13 | color: 'color',
14 | 'please enter a url': 'please enter a url',
15 | 'create link': 'create link',
16 | bold: 'bold',
17 | italic: 'italic',
18 | underline: 'underline',
19 | 'strike through': 'strike through',
20 | subscript: 'subscript',
21 | superscript: 'superscript',
22 | heading: 'heading',
23 | 'font name': 'font name',
24 | 'font size': 'font size',
25 | 'left justify': 'left justify',
26 | 'center justify': 'center justify',
27 | 'right justify': 'right justify',
28 | 'ordered list': 'ordered list',
29 | 'unordered list': 'unordered list',
30 | 'fore color': 'fore color',
31 | 'background color': 'background color',
32 | 'row count': 'row count',
33 | 'column count': 'column count',
34 | save: 'save',
35 | upload: 'upload',
36 | progress: 'progress',
37 | unknown: 'unknown',
38 | 'please wait': 'please wait',
39 | error: 'error',
40 | abort: 'abort',
41 | reset: 'reset',
42 | hr: 'horizontal rule',
43 | undo: 'undo',
44 | 'line height': 'line height',
45 | 'exceed size limit': 'exceed size limit'
46 | }
--------------------------------------------------------------------------------
/src/modules/align/tab.js:
--------------------------------------------------------------------------------
1 | import template from './tab.html'
2 | export default {
3 | template,
4 | watch: {
5 | activeItem (n) {
6 | n = n || 'left'
7 | let map = {
8 | 'left': 2,
9 | 'center': 0,
10 | 'right': 1
11 | }
12 | let index = map[n]
13 | let options = Object.keys(this.alignMap)
14 | let key = options[index]
15 | this.choosed = {
16 | icon: 'align-' + n,
17 | index,
18 | key,
19 | type: this.alignMap[key]
20 | }
21 | }
22 | },
23 | data () {
24 | return {
25 | alignMap: {
26 | '居中': 'justifyCenter',
27 | '居右': 'justifyRight',
28 | '居左': 'justifyLeft'
29 | },
30 | choosed: {}
31 | }
32 | },
33 | methods: {
34 | setAlign (index) {
35 | let options = Object.keys(this.alignMap)
36 | let key = options[index]
37 | this.$parent.execCommand(this.alignMap[key])
38 | this.$parent.saveCurrentRange()
39 | this.$parent.moduleInspect()
40 | },
41 | changeAlign () {
42 | if (this.forbidden) return
43 | let pre_index = !isNaN(this.choosed.index) ? this.choosed.index : -1
44 | let len = Object.keys(this.alignMap).length
45 | let target_index
46 | if (pre_index + 1 === len) {
47 | target_index = 0
48 | } else {
49 | target_index = ++pre_index
50 | }
51 | this.setAlign(target_index)
52 | }
53 | }
54 | }
55 |
56 |
57 |
--------------------------------------------------------------------------------
/example/base/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
21 |
22 |
23 |
24 |
25 |
26 |
58 |
59 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
10 |
21 |
22 |
23 |
24 |
25 |
26 |
58 |
59 |
--------------------------------------------------------------------------------
/example/editModule/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
20 |
21 |
22 |
23 |
24 |
25 |
61 |
62 |
--------------------------------------------------------------------------------
/src/commands/bold.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 |
3 | export default function (rh, arg) {
4 | let s = rh.getSelection()
5 | if (!s.isCollapsed) {
6 | document.execCommand('styleWithCSS', false, false)
7 | document.execCommand('bold', false, arg)
8 | return
9 | } else {
10 | let node = s.focusNode
11 | let row = rh.getRow(node)
12 |
13 | // the outermost bold tag
14 | let bold = rh.findSpecialAncestor(node, 'strong') || rh.findSpecialAncestor(node, 'b') || rh.findSpecialAncestorByStyle(node, {
15 | 'fontWeight': 'bold'
16 | })
17 | let existStyle = rh.findExistTagTillBorder(node, ['STRIKE', 'I', 'U'], row)
18 | let fontSize = rh.findSpecialAncestorStyle(node, 'fontSize', true, row)
19 | if (!bold) {
20 | existStyle.push('B')
21 | }
22 | if (existStyle.length) {
23 | let newDOM = rh.createNestDOMThroughList(existStyle)
24 | let v = rh.newRow()
25 | if (fontSize) {
26 | let span = document.createElement('span')
27 | span.style.fontSize = fontSize
28 | v.appendChild(span)
29 | span.appendChild(newDOM.dom)
30 | } else {
31 | v.appendChild(newDOM.dom)
32 | }
33 | commands.insertHTML(rh, v.innerHTML)
34 | let deepestNode = document.getElementById(newDOM.deepestId)
35 | s.collapse(deepestNode, 1)
36 | deepestNode.removeAttribute('id')
37 | } else {
38 | let newText = document.createElement('span')
39 | newText.style.fontSize = fontSize
40 | newText.innerHTML = ''
41 | rh.insertAfter(newText, bold)
42 | s.collapse(newText, 1)
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/commands/italic.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 |
3 | export default function (rh, arg) {
4 | let s = rh.getSelection()
5 | if (!s.isCollapsed) {
6 | document.execCommand('styleWithCSS', false, false)
7 | document.execCommand('italic', false, arg)
8 | return
9 | } else {
10 | let node = s.focusNode
11 | let row = rh.getRow(node)
12 |
13 | // the outermost italic tag
14 | let italic = rh.findSpecialAncestor(node, 'i', false, row) || rh.findSpecialAncestorByStyle(node, {
15 | 'fontStyle': 'italic'
16 | }, false, row)
17 | let existStyle = rh.findExistTagTillBorder(node, ['STRIKE', 'U', 'B', 'STRONG'], row)
18 | let fontSize = rh.findSpecialAncestorStyle(node, 'fontSize', true, row)
19 | // is in a italic
20 | if (!italic) {
21 | existStyle.push('I')
22 | }
23 | if (existStyle.length) {
24 | let newDOM = rh.createNestDOMThroughList(existStyle)
25 | let v = rh.newRow()
26 | if (fontSize) {
27 | let span = document.createElement('span')
28 | span.style.fontSize = fontSize
29 | v.appendChild(span)
30 | span.appendChild(newDOM.dom)
31 | } else {
32 | v.appendChild(newDOM.dom)
33 | }
34 | commands.insertHTML(rh, v.innerHTML)
35 | let deepestNode = document.getElementById(newDOM.deepestId)
36 | s.collapse(deepestNode, 1)
37 | deepestNode.removeAttribute('id')
38 | } else {
39 | let newText = document.createElement('span')
40 | newText.style.fontSize = fontSize
41 | newText.innerHTML = ''
42 | rh.insertAfter(newText, italic)
43 | s.collapse(newText, 1)
44 | }
45 | }
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/src/commands/underline.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 |
3 | export default function (rh, arg) {
4 | let s = rh.getSelection()
5 | if (!s.isCollapsed) {
6 | document.execCommand('styleWithCSS', false, false)
7 | document.execCommand('underline', false, arg)
8 | return
9 | } else {
10 | let node = s.focusNode
11 | let row = rh.getRow(node)
12 | let nodeCtn = node.innerText || node.nodeValue
13 |
14 | // the outermost underline tag
15 | let underline = rh.findSpecialAncestor(node, 'u', false, row) || rh.findSpecialAncestorByStyle(node, {
16 | 'textDecorationLine': 'underline'
17 | }, false, row)
18 | let existStyle = rh.findExistTagTillBorder(node, ['STRIKE', 'I', 'B', 'STRONG'], row)
19 | let fontSize = rh.findSpecialAncestorStyle(node, 'fontSize', true, row)
20 | if (!underline) {
21 | existStyle.push('U')
22 | }
23 | if (existStyle.length) {
24 | let newDOM = rh.createNestDOMThroughList(existStyle)
25 | let v = rh.newRow()
26 | if (fontSize) {
27 | let span = document.createElement('span')
28 | span.style.fontSize = fontSize
29 | v.appendChild(span)
30 | span.appendChild(newDOM.dom)
31 | } else {
32 | v.appendChild(newDOM.dom)
33 | }
34 | commands.insertHTML(rh, v.innerHTML)
35 | let deepestNode = document.getElementById(newDOM.deepestId)
36 | s.collapse(deepestNode, 1)
37 | deepestNode.removeAttribute('id')
38 | } else {
39 | let newText = document.createElement('span')
40 | newText.style.fontSize = fontSize
41 | newText.innerHTML = ''
42 | rh.insertAfter(newText, underline)
43 | s.collapse(newText, 1)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/commands/strikeThrough.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 |
3 | export default function (rh, arg) {
4 | let s = rh.getSelection()
5 | if (!s.isCollapsed) {
6 | document.execCommand('styleWithCSS', false, false)
7 | document.execCommand('strikeThrough', false, arg)
8 | return
9 | } else {
10 | let node = s.focusNode
11 | let row = rh.getRow(node)
12 | let nodeCtn = node.innerText || node.nodeValue
13 |
14 | // the outermost strikeThrough tag
15 | let strikeThrough = rh.findSpecialAncestor(node, 'STRIKE', false, row) || rh.findSpecialAncestorByStyle(node, {
16 | 'textDecorationLine': 'line-through'
17 | }, false, row)
18 | let existStyle = rh.findExistTagTillBorder(node, ['U', 'I', 'B', 'STRONG'], row)
19 | let fontSize = rh.findSpecialAncestorStyle(node, 'fontSize', true, row)
20 | if (!strikeThrough) {
21 | existStyle.push('STRIKE')
22 | }
23 | if (existStyle.length) {
24 | let newDOM = rh.createNestDOMThroughList(existStyle)
25 | let v = rh.newRow()
26 | if (fontSize) {
27 | let span = document.createElement('span')
28 | span.style.fontSize = fontSize
29 | v.appendChild(span)
30 | span.appendChild(newDOM.dom)
31 | } else {
32 | v.appendChild(newDOM.dom)
33 | }
34 | commands.insertHTML(rh, v.innerHTML)
35 | let deepestNode = document.getElementById(newDOM.deepestId)
36 | s.collapse(deepestNode, 1)
37 | deepestNode.removeAttribute('id')
38 | } else {
39 | let newText = document.createElement('span')
40 | newText.style.fontSize = fontSize
41 | newText.innerHTML = ''
42 | rh.insertAfter(newText, strikeThrough)
43 | s.collapse(newText, 1)
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/editor/drag-pic.js:
--------------------------------------------------------------------------------
1 | /*
2 | * directive drag and drop pic
3 | **/
4 |
5 | export default {
6 | bind (el, binding, vnode) {
7 | let editor = vnode.context
8 | let onDragOver = e => {
9 | e.preventDefault()
10 | const selection = window.getSelection ? window.getSelection() : document.getSelection()
11 | try {
12 | selection.collapse(e.target, 1)
13 | } catch (e) {
14 | selection.collapse(e.target, 0)
15 | }
16 | editor.saveCurrentRange()
17 | editor.moduleInspect()
18 | }
19 | let onDragLeave = e => {
20 | e.preventDefault()
21 | }
22 |
23 | let onDrop = e => {
24 | const selection = window.getSelection ? window.getSelection() : document.getSelection()
25 | if (e.dataTransfer && e.dataTransfer.files) {
26 | e.preventDefault()
27 | console.log(e.target)
28 | let files = e.dataTransfer.files
29 | for (let i = 0; i< files.length; i ++) {
30 | let curFile = files[i]
31 | if (curFile.size && curFile.type.includes('image')) {
32 | binding.value(curFile)
33 | }
34 | }
35 | }
36 | }
37 |
38 | // el.addEventListener('dragenter', onDragEnter, false)
39 | el.addEventListener('dragover', onDragOver, false)
40 | el.addEventListener('dragleave', onDragLeave, false)
41 | el.addEventListener('drop', onDrop, false)
42 |
43 | el.__dragOverHandler = onDragOver
44 | el.__dragLeaveHandler = onDragLeave
45 | el.__dropHandler = onDrop
46 | },
47 | unbind (el, binding) {
48 | el.removeEventListener('dragover', el.__dragOverHandler)
49 | el.removeEventListener('dragleave', el.__dragLeaveHandler)
50 | el.removeEventListener('drop', el.__dropHandler)
51 | delete el.__dragOverHandler
52 | delete el.__dragLeaveHandler
53 | delete el.__dropHandler
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/range-handler/instance.js:
--------------------------------------------------------------------------------
1 | import commands from '../commands'
2 |
3 | // for IE 11
4 | if (!Text.prototype.contains) {
5 | Text.prototype.contains = function contains(otherNode) {
6 | return this === otherNode
7 | }
8 | }
9 |
10 | export default class RangeHandler {
11 | /**
12 | * build range handler
13 | * @param {Range} range
14 | * @param {editor} current editor
15 | */
16 | constructor(range, editor) {
17 | if (!range || !(range instanceof Range)) {
18 | throw new TypeError('cant\'t resolve range')
19 | }
20 | this.range = range
21 | this.editor = editor
22 | }
23 |
24 | /**
25 | * execute edit command
26 | * @param {String} command
27 | * @param arg
28 | */
29 | execCommand(command, arg) {
30 | RangeHandler.beforeList.forEach(fn => {
31 | fn.call(this, command, this, arg)
32 | })
33 | const existCommand = commands[command]
34 | const customCommand = this.editor.commands ? this.editor.commands[command] : null
35 | if (existCommand) {
36 | existCommand(this, arg)
37 | }
38 | else if (customCommand) {
39 | customCommand(this, arg)
40 | } else {
41 | document.execCommand(command, false, arg)
42 | }
43 | RangeHandler.afterList.forEach(fn => {
44 | fn.call(this, command, this, arg)
45 | })
46 | }
47 |
48 | /*
49 | * run fn before exec command
50 | **/
51 | before (fn) {
52 | if (typeof fn === 'function') {
53 | RangeHandler.beforeList.push(fn)
54 | }
55 | }
56 |
57 | clearBeforeList () {
58 | RangeHandler.beforeList = []
59 | }
60 |
61 | /*
62 | * run fn after exec command
63 | **/
64 | after (fn) {
65 | if (typeof fn === 'function') {
66 | RangeHandler.afterList.push(fn)
67 | }
68 | }
69 |
70 | clearAfterList () {
71 | RangeHandler.afterList = []
72 | }
73 | }
74 |
75 | RangeHandler.beforeList = []
76 | RangeHandler.afterList = []
77 |
--------------------------------------------------------------------------------
/src/editor/style/reset.styl:
--------------------------------------------------------------------------------
1 | /*
2 | html5doctor.com Reset Stylesheet
3 | v1.4.1
4 | 2010-03-01
5 | Author: Richard Clark - http://richclarkdesign.com
6 | */
7 |
8 | html, body, div, span, object, iframe,
9 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
10 | abbr, address, cite, code,
11 | del, dfn, em, img, ins, kbd, q, samp,
12 | small, strong, sub, sup, var,
13 | b, i,
14 | dl, dt, dd, ol, ul, li,
15 | fieldset, form, label, legend,
16 | table, caption, tbody, tfoot, thead, tr, th, td,
17 | article, aside, canvas, details, figcaption, figure,
18 | footer, header, hgroup, menu, nav, section, summary,
19 | time, mark, audio, video {
20 | margin:0;
21 | padding:0;
22 | border:0;
23 | outline:0;
24 | font-size:100%;
25 | vertical-align:baseline;
26 | background:transparent;
27 | }
28 |
29 | body {
30 | line-height:1;
31 | }
32 |
33 | :focus {
34 | outline: 1;
35 | }
36 |
37 | article,aside,canvas,details,figcaption,figure,
38 | footer,header,hgroup,menu,nav,section,summary {
39 | display:block;
40 | }
41 |
42 | nav ul {
43 | list-style:none;
44 | }
45 |
46 | blockquote, q {
47 | quotes:none;
48 | }
49 |
50 | blockquote:before, blockquote:after,
51 | q:before, q:after {
52 | content:'';
53 | content:none;
54 | }
55 |
56 | a {
57 | margin:0;
58 | padding:0;
59 | border:0;
60 | font-size:100%;
61 | vertical-align:baseline;
62 | background:transparent;
63 | }
64 |
65 | ins {
66 | background-color:#ff9;
67 | color:#000;
68 | text-decoration:none;
69 | }
70 |
71 | mark {
72 | background-color:#ff9;
73 | color:#000;
74 | font-style:italic;
75 | font-weight:bold;
76 | }
77 |
78 | del {
79 | text-decoration: line-through;
80 | }
81 |
82 | abbr[title], dfn[title] {
83 | border-bottom:1px dotted #000;
84 | cursor:help;
85 | }
86 |
87 | table {
88 | border-collapse:collapse;
89 | border-spacing:0;
90 | }
91 |
92 | hr {
93 | display:block;
94 | height:1px;
95 | border:0;
96 | border-top:1px solid #cccccc;
97 | margin:1em 0;
98 | padding:0;
99 | }
100 |
101 | input, select {
102 | vertical-align:middle;
103 | }
--------------------------------------------------------------------------------
/src/util/polyfill-ie.js:
--------------------------------------------------------------------------------
1 | export default () => {
2 | // https://tc39.github.io/ecma262/#sec-array.prototype.includes
3 | if (!Array.prototype.includes) {
4 | Object.defineProperty(Array.prototype, 'includes', {
5 | value(searchElement, fromIndex) {
6 | // 1. Let O be ? ToObject(this value).
7 | if (this == null) {
8 | throw new TypeError('"this" is null or not defined')
9 | }
10 |
11 | const o = Object(this)
12 |
13 | // 2. Let len be ? ToLength(? Get(O, "length")).
14 | const len = o.length >>> 0
15 |
16 | // 3. If len is 0, return false.
17 | if (len === 0) {
18 | return false
19 | }
20 |
21 | // 4. Let n be ? ToInteger(fromIndex).
22 | // (If fromIndex is undefined, this step produces the value 0.)
23 | const n = fromIndex | 0
24 |
25 | // 5. If n ≥ 0, then
26 | // a. Let k be n.
27 | // 6. Else n < 0,
28 | // a. Let k be len + n.
29 | // b. If k < 0, let k be 0.
30 | let k = Math.max(n >= 0 ? n : len - Math.abs(n), 0)
31 |
32 | // 7. Repeat, while k < len
33 | while (k < len) {
34 | // a. Let elementK be the result of ? Get(O, ! ToString(k)).
35 | // b. If SameValueZero(searchElement, elementK) is true, return true.
36 | // c. Increase k by 1.
37 | // NOTE: === provides the correct "SameValueZero" comparison needed here.
38 | if (o[k] === searchElement) {
39 | return true
40 | }
41 | k++
42 | }
43 |
44 | // 8. Return false
45 | return false
46 | }
47 | })
48 | }
49 | // text.contains()
50 | if (!Text.prototype.contains) {
51 | Text.prototype.contains = function contains(node) {
52 | return this === node
53 | }
54 | }
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/src/range-handler/assist-methods.js:
--------------------------------------------------------------------------------
1 | import constant from '../constant-config'
2 | const methods = {
3 | /*
4 | * func create a new row element
5 | * @param {obj} data
6 | * tag {str}
7 | * contenteditable {boolean} default: true
8 | * id {boolean} whether need a id default: false
9 | * br {boolean} whether need a br element after current row element default: false
10 | * @return {node}
11 | **/
12 | newRow (data = {}) {
13 | const row = document.createElement(data.tag || constant.ROW_TAG)
14 | if (data.id) {
15 | row.dataset.editorRow = methods.createRandomId('row')
16 | }
17 | if (data.br) {
18 | const br = document.createElement('br')
19 | row.appendChild(br)
20 | }
21 | row.setAttribute('contenteditable', data.contenteditable !== false)
22 | return row
23 | },
24 | // get selection
25 | getSelection () {
26 | return window.getSelection ? window.getSelection() : document.getSelection()
27 | },
28 | /*
29 | * func insert a element after target element
30 | * @param newElement {node}
31 | * @param targetElement {node}
32 | **/
33 | insertAfter (newElement, targetElement) {
34 | var parent = targetElement.parentNode
35 | if (parent.lastChild === targetElement) {
36 | parent.appendChild(newElement)
37 | } else {
38 | parent.insertBefore(newElement, targetElement.nextSibling)
39 | }
40 | },
41 | /*
42 | * func create a random id
43 | * @param preffix {str} id's preffix
44 | * @return id
45 | **/
46 | createRandomId (preffix) {
47 | return `${preffix || 'id'}-${Math.random() * 1000 + new Date().getTime()}`
48 | },
49 | /*
50 | * return edit zone element
51 | **/
52 | editZone () {
53 | if (methods.editZone_cache) {
54 | return methods.editZone_cache
55 | }
56 | methods.editZone_cache = document.querySelector('[data-editor="content"]')
57 | return methods.editZone_cache
58 | },
59 | /*
60 | * set range at target node
61 | **/
62 | setRangeAt (node, checkAll) {
63 | const range = document.createRange()
64 | if (checkAll) {
65 | range.setStart(node, 0)
66 | let end = node.childNodes.length ? node.childNodes.length : (node.length ? node.length : 0)
67 | range.setEnd(node, end)
68 | } else {
69 | range.setStart(node, 0)
70 | range.setEnd(node, 0)
71 | }
72 | let selection = methods.getSelection()
73 | selection.removeAllRanges()
74 | selection.addRange(range)
75 | },
76 | getRange () {
77 | let s = methods.getSelection()
78 | if (s.rangeCount) {
79 | return s.getRangeAt(0)
80 | }
81 | return
82 | }
83 | }
84 |
85 | export default methods
86 |
--------------------------------------------------------------------------------
/src/editor/style/main.styl:
--------------------------------------------------------------------------------
1 | @import "reset.styl"
2 |
3 | .my-vue-editor
4 | display: block
5 | border: 1px solid #d1d1d1
6 | padding: 0
7 | img
8 | max-width: 98%
9 | .toolbar
10 | height: auto
11 | border-bottom: 1px solid #d1d1d1
12 | background: #f8f8f8
13 | padding: 6px 8px
14 | white-space: normal
15 | .tabs
16 | &:after
17 | content: ''
18 | display: block
19 | clear: both
20 | .tab
21 | margin: 0 2px
22 | float: left
23 | .icon
24 | vertical-align: sub
25 | .btn
26 | display: inline-block
27 | list-style: none
28 | height: 18px
29 | line-height: 18px
30 | padding: 4px 6px
31 | border: 0
32 | position: relative
33 | font: normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif
34 | color: #000
35 | text-align: center
36 | white-space: nowrap
37 | text-decoration: none
38 | &:hover
39 | padding: 3px 5px
40 | background: #f0f0f0
41 | border: 1px #bcbcbc solid
42 | &.highLight
43 | padding: 3px 5px
44 | background: #e5e5e5
45 | border: 1px #bcbcbc solid
46 | &.forbidden
47 | cursor: not-allowed
48 | opacity: .3
49 | .content
50 | box-sizing: border-box
51 | width: 100%
52 | clear both
53 | outline: none
54 | padding: 20px
55 | font-size: 16px
56 | line-height: 24px
57 | word-wrap: break-word
58 | blockquote
59 | min-width: 38px
60 | [data-editor-quote]
61 | div:first-child
62 | margin-top: -21px
63 | div
64 | margin-left: 2px
65 | outline: none
66 | color: rgb(115, 115, 115)
67 | font-size: 14px
68 | margin: 14px 0 14px 35px
69 | &::before
70 | position: relative
71 | top: 2px
72 | left: 0
73 | margin: 0px 4px 0 -24px
74 | color: #b7b7b7
75 | font-size: 20px
76 | content: "\f10e"
77 | display: inline-block
78 | font: normal normal normal 14px/1 FontAwesome
79 | text-rendering: auto
80 | -webkit-font-smoothing: antialiased
81 | transform: rotateY(180deg)
82 | ul
83 | ul
84 | margin-left: 10%
85 | li
86 | margin-left: 5.5%
87 | [data-editor-todo]
88 | display: inline-block
89 | width: 90%
90 | label
91 | cursor: pointer
92 | position: relative
93 | top: 3.5px
94 | left: 2.5px
95 | font-size: 16px
96 | color: #cdc9c5
97 | input[type=text]
98 | border: none
99 | outline: none
100 | width: 90%
101 | font-size: 14px
--------------------------------------------------------------------------------
/src/commands/delete.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 | import constant from '../constant-config'
3 |
4 | export default function (rh, e) {
5 | // restore first row
6 | let s = rh.getSelection()
7 | let node = s.baseNode
8 | let value = node.nodeValue || node.innerText
9 | // console.log('delete', node, e)
10 | let curRange = rh.getRange() || rh.range
11 |
12 | // cancel list when li is empty
13 | if ((rh.findSpecialAncestor(node, 'li')) && rh.range.startOffset === 0) {
14 | e.preventDefault()
15 | let ulOrOl = rh.findSpecialAncestor(node, 'ul') || rh.findSpecialAncestor(node, 'ol')
16 | if (ulOrOl.nodeName === 'UL') {
17 | commands['insertUnorderedList'](rh, e)
18 | }
19 | if (ulOrOl.nodeName === 'OL') {
20 | commands['insertOrderedList'](rh, e)
21 | }
22 | return
23 | }
24 | let row = rh.getRow(node)
25 |
26 | // node is edit zone
27 | if (!row) {
28 | e.preventDefault()
29 | return afterDelete(rh)
30 | }
31 |
32 | // empty row
33 | if (rh.range.collapsed && ((node === row && rh.range.startOffset === 0) || (row.innerHTML.replace(/
/g, '') === '' && rh.range.startOffset === 1))) {
34 | let firstRow = rh.editZone().firstElementChild
35 |
36 | // first row cancel outdent
37 | if (firstRow === row) {
38 | commands.outdent(rh, null)
39 | e.preventDefault()
40 | return
41 | }
42 | }
43 |
44 | // row has content, cursor is at at start of the node, do outdent
45 | if (rh.range.collapsed && value && rh.range.startOffset === 0 && (node === row.fistElementChild || node === row.firstChild)) {
46 | let outdentResult = commands.outdent(rh, null)
47 | if (outdentResult === 'NO_NEED_OUTDENT') {
48 | return
49 | }
50 | e.preventDefault()
51 | return
52 | }
53 |
54 | // empty row
55 | if (row.innerHTML.replace(/
/g, '') === '') {
56 | // get previous row with content
57 | let preRow = rh.getPreviousRow(row)
58 |
59 | // cursor focus on previous row's input if previous row is todo
60 | if (preRow && preRow.dataset && preRow.dataset.editorTodo) {
61 | row.parentNode.removeChild(row)
62 | let input = preRow.querySelector('input[type="text"]')
63 | if (input) {
64 | e.preventDefault()
65 | input.focus()
66 | }
67 | e.preventDefault()
68 | return
69 | }
70 | }
71 | e.preventDefault()
72 | return afterDelete(rh)
73 | }
74 |
75 | // handle after delete
76 | function afterDelete(rh) {
77 | let deleteInterval = window.setInterval(function () {
78 | let s = rh.getSelection()
79 | let focusNode = s.focusNode
80 | let ctn = typeof focusNode.innerText === 'string' ? focusNode.innerText : focusNode.nodeValue
81 | if (typeof ctn === 'string' && /\u200B/.test(ctn) && ctn.replace(/\u200B/g, '') === '') {
82 | document.execCommand('delete', false)
83 | } else {
84 | document.execCommand('delete', false)
85 | window.clearInterval(deleteInterval)
86 | }
87 | })
88 |
89 | // if edit zone is empty, create a row
90 | if (rh.isEmptyNode(rh.editZone()) && !rh.getRows().length) {
91 | let row = rh.newRow({br: true})
92 | rh.editZone().appendChild(row)
93 | rh.getSelection().collapse(row, 1)
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/commands/insertImage.js:
--------------------------------------------------------------------------------
1 | import lrz from 'lrz'
2 |
3 | const insertImage = function (rh, arg) {
4 | // forbidden logic
5 | let forbidden = false
6 | let actives = rh.editor.activeModules
7 | actives.forEach(name => {
8 | let module = rh.editor.modulesMap[name]
9 | if (module && module.exclude && module.exclude.includes('image')) {
10 | forbidden = true
11 | }
12 | })
13 | if (forbidden) return
14 |
15 | let returnData = {
16 | origin: arg
17 | }
18 | let editor = rh.editor
19 | let config = editor.modulesMap['image']
20 | if (!config) {
21 | throw new Error('image config load fail')
22 | }
23 | if (arg instanceof File) {
24 | handleFile(arg)
25 | }
26 | if (typeof arg === 'string') {
27 | document.execCommand('insertImage', false, arg)
28 | }
29 |
30 | function handleFile(file) {
31 | if (config.compress) {
32 | config.compress.fieldName = config.fieldName || 'image'
33 | lrz(file, config.compress).then(rst => {
34 | if (rst.file.size > config.maxSize) {
35 | Object.assign(returnData, {
36 | status: 'exceed size limit, after compress',
37 | statusCode: 3
38 | }, rst)
39 | editor.$emit('imageUpload', returnData)
40 | } else {
41 | Object.assign(returnData, rst)
42 | let imgId = rh.createRandomId('img')
43 | insertBase64(returnData.base64, imgId)
44 | }
45 | }).catch(err => {
46 | Object.assign(returnData, {
47 | status: err,
48 | statusCode: 2
49 | })
50 | editor.$emit('imageUpload', returnData)
51 | })
52 | } else {
53 | if (file.size > config.maxSize) {
54 | editor.$emit('imageUpload', Object.assign(returnData, {
55 | status: 'exceed size limit, without compress',
56 | statusCode: 3
57 | }))
58 | } else {
59 | let formData = new FormData()
60 | formData.append(config.fieldName, file, file.name)
61 | returnData.formData = formData
62 | const reader = new FileReader()
63 | reader.onload = (e) => {
64 | let imgId = rh.createRandomId('img')
65 | insertBase64(e.target.result, imgId)
66 | }
67 | reader.readAsDataURL(file)
68 | }
69 | }
70 | }
71 |
72 | function insertBase64 (base64, id) {
73 | let dom = `
`
74 | if (config.imgOccupyNewRow) {
75 | let node = rh.range.commonAncestorContainer
76 | let curRow = rh.forceGetRow(node)
77 | if (curRow) {
78 | let sibling = curRow.nextElementSibling
79 | let newRow = rh.newRow({contenteditable: false})
80 | newRow.innerHTML = dom
81 | rh.insertAfter(newRow, curRow)
82 | if (!sibling) {
83 | sibling = rh.newRow({br: true})
84 | rh.insertAfter(sibling, newRow)
85 | }
86 | try {
87 | rh.getSelection().collapse(sibling, 1)
88 | } catch (e) {
89 | rh.getSelection().collapse(sibling, 0)
90 | }
91 | }
92 | } else {
93 | editor.execCommand('insertHTML', dom, true)
94 | }
95 | editor.saveCurrentRange()
96 | editor.$emit('imageUpload', Object.assign(returnData, {
97 | status: 'everything fine',
98 | statusCode: 2,
99 | base64,
100 | replaceSrcAfterUploadFinish: replaceImg(id),
101 | deleteImgWhenUploadFail: deleteImg(id)
102 | }))
103 | }
104 |
105 | // replace image's base64 src to url after upload finish
106 | function replaceImg (id) {
107 | return function (src) {
108 | let target = document.querySelector(`img[data-editor-img='${id}']`)
109 | if (target) {
110 | target.setAttribute('src', src)
111 | target.removeAttribute('data-editor-img')
112 | editor.$emit('change', editor.$refs.content.innerHTML)
113 | }
114 | }
115 | }
116 |
117 | // delete image after upload fail
118 | function deleteImg (id) {
119 | return function () {
120 | let target = document.querySelector(`img[data-editor-img='${id}']`)
121 | if (target) {
122 | target.parentNode.removeChild(target)
123 | editor.$emit('change', editor.$refs.content.innerHTML)
124 | }
125 | }
126 | }
127 | }
128 |
129 | export default insertImage
130 |
--------------------------------------------------------------------------------
/src/commands/todo.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 | import constant from '../constant-config'
3 |
4 | const t = {
5 | 'todo' (rh, data) {
6 | let row = rh.newRow({
7 | br: true
8 | })
9 | let curRow = rh.getRow(rh.range.commonAncestorContainer)
10 |
11 | // a empty row without row element, create a row element
12 | if (!curRow) {
13 | let v = rh.newRow()
14 | let newRow = rh.newRow({br: true})
15 | v.appendChild(newRow)
16 | commands.insertHTML(rh, newRow.outerHTML)
17 | let s = rh.getSelection()
18 | curRow = s.focusNode
19 | }
20 |
21 | // insert todo after this row
22 | let afterWhich = rh.getRow(data.insertAfter)
23 |
24 | // is afterWhich is a empty row, just insert todo at current row
25 | if (afterWhich && rh.isEmptyRow(afterWhich)) {
26 | afterWhich = null
27 | }
28 | if (afterWhich) {
29 | let targetIndex
30 | let startIndex
31 | let list = afterWhich.parentNode.childNodes
32 | list.forEach((child, index) => {
33 | if (child === afterWhich) {
34 | startIndex = index
35 | if (startIndex === list.length - 1) {
36 | targetIndex = list.length
37 | }
38 | return
39 | }
40 | if (startIndex !== undefined && targetIndex === undefined) {
41 | if (child && child.getAttribute('data-editor-todo')) {
42 | targetIndex = index
43 | }
44 | }
45 | })
46 | targetIndex = targetIndex === undefined ? startIndex + 1 : targetIndex
47 | afterWhich.parentNode.insertBefore(row, list[targetIndex])
48 | rh.getSelection().collapse(row, 0)
49 | } else {
50 |
51 | // insert todo at current row if it is empty
52 | if (rh.isEmptyRow(curRow)) {
53 | rh.collapseAtRow(curRow)
54 | row = curRow
55 | } else {
56 | rh.range.commonAncestorContainer.appendChild(row, 0)
57 | rh.getSelection().collapse(row, 0)
58 | }
59 | }
60 | let todoId = rh.createRandomId('todo')
61 | commands['insertHTML'](rh, `<${constant.ROW_TAG} data-editor-todo=${todoId} contenteditable="false">${constant.ROW_TAG}>
`)
62 | document.querySelector(`[data-editor-todo='${todoId}'] input[type=text]`).focus()
63 | row.parentNode.removeChild(row)
64 | t['initTodo'](rh, data)
65 | },
66 | // init todo logic
67 | 'initTodo' (rh, data) {
68 | const checkboxs = document.querySelectorAll('[data-editor-todo]')
69 | checkboxs.forEach((c, index) => {
70 | const btn = c.querySelector('[type=checkbox]')
71 | const ctn = c.querySelector('[type=text]')
72 | if (c.init) return
73 | ctnCheckedLogic()
74 |
75 | function ctnCheckedLogic() {
76 | ctn.value = ctn.value || ctn.getAttribute('data-editor-value')
77 | ctn.setAttribute('data-editor-value', ctn.value)
78 | if (btn.checked) {
79 | ctn.style.textDecoration = 'line-through'
80 | btn.setAttribute('checked', '')
81 | } else {
82 | ctn.style.textDecoration = 'none'
83 | btn.removeAttribute('checked')
84 | }
85 |
86 | }
87 |
88 | btn.onchange = e => {
89 | ctnCheckedLogic()
90 | if (rh.editor && rh.editor.$refs && rh.editor.$refs.content) {
91 | rh.editor.$emit('change', rh.editor.$refs.content.innerHTML)
92 | }
93 | }
94 | ctn.oninput = e => {
95 | ctn.setAttribute('data-editor-value', e.target.value)
96 | }
97 | ctn.onkeydown = ctn.onkeydown || (e => {
98 | if (![13, 8].includes(e.keyCode)) return
99 | let row = c.nextSibling
100 | if (e.keyCode === 13) {
101 | if (ctn.value === '') {
102 | e.preventDefault()
103 | return deleteTodo()
104 | }
105 | t['todo'](rh, {
106 | insertAfter: c,
107 | placeholder: data.placeholder
108 | })
109 | } else if (e.keyCode === 8) {
110 | if (ctn.value === '') {
111 | e.preventDefault()
112 | e.stopPropagation()
113 | deleteTodo()
114 | }
115 | }
116 |
117 | function deleteTodo() {
118 | let newRow = rh.newRow({br: true})
119 | c.parentNode.replaceChild(newRow, c)
120 | rh.getSelection().collapse(newRow, 1)
121 | }
122 | })
123 | c.init = true
124 | })
125 | }
126 | }
127 |
128 | export default t
129 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import polyfill from './util/polyfill-ie'
2 | import buildInModules from './modules/index'
3 | import buildInShortcut from './shortcut'
4 | import constantConfig from './constant-config'
5 | import editor from './editor/editor'
6 | import buildInCommands from './commands'
7 | import initExcludeRule from './module-inspect/load-module-inspect-exclude-rules'
8 | import Inspector from './module-inspect'
9 | import i18nZhCn from './i18n/zh-cn'
10 | import i18nEnUs from './i18n/en-us'
11 | import {
12 | mixin,
13 | isObj
14 | } from './util'
15 |
16 | polyfill()
17 |
18 | class Editor {
19 |
20 | constructor(options = {}) {
21 | let modules = [...buildInModules]
22 | let reservedNames = {}
23 | modules.forEach(m => {
24 | if (m.name) {
25 | reservedNames[m.name] = true
26 | }
27 | })
28 | const components = {}
29 | const modulesMap = {}
30 |
31 | // extended modules
32 | if (Array.isArray(options.extendModules)) {
33 | options.extendModules.forEach(module => {
34 | if (module.name && !reservedNames[module.name]) {
35 | modules.push(module)
36 | } else {
37 | throw new Error('extended module must have a name and should not be the same as buildIn module')
38 | }
39 | })
40 | }
41 |
42 | // modules in use
43 | if (Array.isArray(options.modules)) {
44 | let m = []
45 | options.modules.forEach(name => {
46 | if (typeof name !== 'string') {
47 | throw new Error('modules\'s item must be string')
48 | }
49 | modules.forEach(module => {
50 | if (module.name === name) {
51 | m.push(module)
52 | }
53 | })
54 | })
55 | modules = m
56 | }
57 |
58 | modules.forEach(module => {
59 | // config
60 | let curConfig = options[module.name]
61 | let moduleConfig = module
62 | if (isObj(curConfig) && isObj(moduleConfig)) {
63 | Object.assign(moduleConfig, curConfig)
64 | }
65 |
66 | module.moduleInspectResult = null
67 | module.forbidden = null
68 | if (typeof module.inspect === 'function') {
69 | let inspector = new Inspector(module.name)
70 | module.inspect(inspector.add.bind(inspector))
71 | } else {
72 | module.type = 'fn'
73 | }
74 | module.exclude = initExcludeRule(module, modules)
75 |
76 | if (module.tab) {
77 | module.tab.module = module
78 |
79 | // add activeItem prop
80 | module.tab.props = module.tab.props ? Object.assign(module.tab.props, {activeItem: [String, Boolean], forbidden: Boolean}) : {activeItem: [String, Boolean], forbidden: Boolean}
81 | module.tabName = `tab-${module.name}`
82 | components[module.tabName] = module.tab
83 | }
84 | if (options.icons && options.icons[module.name]) {
85 | module.icon = options.icons[module.name]
86 | }
87 | module.hasTab = !!module.tab
88 |
89 | // prevent vue sync
90 | delete module.tab
91 |
92 | modulesMap[module.name] = module
93 | })
94 |
95 | // i18n
96 | const i18n = {'zh-cn': i18nZhCn, 'en-us': i18nEnUs}
97 | const customI18n = options.i18n || {}
98 | Object.keys(customI18n).forEach((key) => {
99 | i18n[key] = i18n[key] ? Object.assign(i18n[key], customI18n[key]) : customI18n[key]
100 | })
101 | const language = options.language || 'en-us'
102 | const locale = i18n[language]
103 |
104 | // shortcut
105 | options.shortcut = Object.assign(buildInShortcut, options.shortcut || {})
106 | const shortcut = {}
107 | Object.keys(options.shortcut).forEach(key => {
108 | let item = options.shortcut[key]
109 | let keyCode = item.keyCode
110 | shortcut[keyCode] = shortcut[keyCode] || []
111 | shortcut[keyCode].push(item)
112 | item.name = key
113 | })
114 |
115 | // merge commands
116 | if (isObj(options.commands)) {
117 | Object.assign(buildInCommands, options.commands)
118 | }
119 |
120 | // spellcheck
121 | const spellcheck = options.spellcheck || false
122 |
123 | const compo = mixin(editor, {
124 | data () {
125 | return {modules, locale, shortcut, modulesMap, spellcheck, constantConfig}
126 | },
127 | components
128 | })
129 | Object.assign(this, compo)
130 | }
131 |
132 | /**
133 | * global install
134 | * @param Vue
135 | * @param options
136 | */
137 | static install(Vue, options = {}) {
138 | Vue.component(options.name || 'my-vue-editor', new Editor(options))
139 | }
140 | }
141 |
142 | export default Editor
143 |
144 | // to change Babel6 export's result
145 | module.exports = Editor
146 |
147 |
--------------------------------------------------------------------------------
/src/module-inspect/index.js:
--------------------------------------------------------------------------------
1 | import { isObj } from '../util'
2 | import RH from '../range-handler'
3 | /*
4 | * Inspect and highlight module
5 | **/
6 | class Inspector {
7 | constructor (moduleName) {
8 | this.moduleName = moduleName
9 | }
10 |
11 | add (type, param) {
12 | let moduleName = this.moduleName
13 | if (typeof moduleName !== 'string') {
14 | throw new Error('moduleName must be string')
15 | }
16 | switch (type) {
17 | case 'tag':
18 | if (typeof param !== 'string') {
19 | throw new Error('inspector for tag can only receive a string param which stand for tag name')
20 | }
21 | Inspector.tagMap[param.toUpperCase()] = moduleName
22 | break
23 | case 'style':
24 | if (!isObj(param)) {
25 | throw new Error('inspector for style can only receive a object param')
26 | }
27 | Inspector.styles[moduleName] = param
28 | break
29 | case 'attribute':
30 | if (!isObj(param)) {
31 | throw new Error('inspector for attribute can only receive a object param')
32 | }
33 | Inspector.attributes[moduleName] = param
34 | break
35 | default:
36 | throw new Error('unknown inspector type')
37 | }
38 | return this
39 | }
40 |
41 | inspect_tag (node) {
42 | let result = []
43 | while (node && node !== RH.prototype.editZone()) {
44 | let inspectResult = Inspector.tagMap[node.nodeName]
45 | if (inspectResult && !result.includes(inspectResult)) {
46 | result.push(inspectResult)
47 | }
48 | node = node.parentNode
49 | }
50 | return result
51 | }
52 |
53 | inspect_style (node) {
54 | let result = []
55 | while (node && node !== RH.prototype.editZone()) {
56 | if (!node.style) break
57 | Object.keys(Inspector.styles).forEach(moduleName => {
58 | let moduleStyle = Inspector.styles[moduleName]
59 | Object.keys(moduleStyle).forEach(item => {
60 | let curValue = moduleStyle[item]
61 | if (typeof curValue === 'string' && node.style[item] === curValue) {
62 | if (!result.includes(moduleName)) {
63 | result.push(moduleName)
64 | Inspector.activeItems[moduleName] = curValue
65 | }
66 | }
67 | if (Array.isArray(curValue)) {
68 | curValue.forEach(val => {
69 | if (node.style[item] === val) {
70 | if (!result.includes(moduleName)) {
71 | result.push(moduleName)
72 | Inspector.activeItems[moduleName] = val
73 | }
74 | }
75 | })
76 | }
77 | })
78 | })
79 | node = node.parentNode
80 | }
81 | return result
82 | }
83 |
84 | inspect_attribute (node) {
85 | let result = []
86 | while (node && node !== RH.prototype.editZone()) {
87 | if (!node.getAttribute) break
88 | Object.keys(Inspector.attributes).forEach(moduleName => {
89 | let moduleAttr = Inspector.attributes[moduleName]
90 | Object.keys(moduleAttr).forEach(item => {
91 | let value = moduleAttr[item]
92 | let nodeVal = node.getAttribute(item)
93 | if (Array.isArray(value)) {
94 | value.forEach(val => {
95 | if (nodeVal === val) {
96 | if (!result.includes(moduleName)) {
97 | result.push(moduleName)
98 | Inspector.activeItems[moduleName] = val
99 | }
100 | }
101 | })
102 | }
103 | if (typeof value === 'string' && nodeVal === value || nodeVal !== null) {
104 | if (!result.includes(moduleName)) {
105 | result.push(moduleName)
106 | Inspector.activeItems[moduleName] = value
107 | }
108 | }
109 | })
110 | })
111 | node = node.parentNode
112 | }
113 | return result
114 | }
115 | }
116 |
117 | Inspector.tagMap = {}
118 | Inspector.styles = {}
119 | Inspector.attributes = {}
120 | Inspector.activeItems = {}
121 |
122 | Inspector.run = (type, nodeList) => {
123 | let fn = Inspector.prototype['inspect_' + type]
124 | let result = []
125 | if (typeof fn === 'function' && Array.isArray(nodeList)) {
126 | nodeList.forEach(node => {
127 | result.push(fn(node))
128 | })
129 | }
130 | return result
131 | }
132 |
133 | Inspector.removeDuplate = function (list) {
134 | // merge same module inspect result
135 | let sameStyleMap = {}
136 | list.forEach(m => {
137 | if (typeof m === 'string') {
138 | sameStyleMap[m] = sameStyleMap[m] ? sameStyleMap[m] + 1 : 1
139 | }
140 | if (Array.isArray(m)) {
141 | m = Array.from(new Set(m))
142 | m.forEach(am => {
143 | sameStyleMap[am] = sameStyleMap[am] ? sameStyleMap[am] + 1 : 1
144 | })
145 | }
146 | })
147 | let mergedStyle = []
148 | Object.keys(sameStyleMap).forEach(m => {
149 | if (sameStyleMap[m] === list.length) {
150 | mergedStyle.push(m)
151 | }
152 | })
153 | return mergedStyle
154 | }
155 |
156 |
157 | export default Inspector
158 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | # my-vue-editor
2 | [](https://github.com/BetaSu/my-vue-editor)
3 | [](https://github.com/BetaSu/my-vue-editor)
4 | [](https://github.com/BetaSu/my-vue-editor)
5 |
6 | 基于Vue2.x的富文本编辑器
7 | For English
8 | ## Demo
9 | 点击这里查看demo
10 | 更多demo请参考example目录
11 | ## 简介
12 | 我们的编辑器基于vue-html5-editor二次开发。感谢其作者PeakTai提供了一个简洁的富文本编辑器插件,我们在其基础上重写了原生的方法,并扩展了功能。
13 |
14 | > 注意:项目依赖`font-awesome`,请自行安装
15 |
16 | ## 安装
17 | ```javascript
18 | npm install my-vue-editor
19 | ```
20 | 作为插件引入
21 | ```javascript
22 | import Vue from 'vue'
23 | import myVueEditor from 'my-vue-editor'
24 | Vue.use(myVueEditor, options)
25 | ```
26 | 全局引入
27 | ```html
28 |
29 |
30 | ```
31 | 通过全局变量myVueEditor安装
32 | ```javascript
33 | Vue.use(myVueEditor, options)
34 | ```
35 | 注意:项目依赖 `font-awesome`,如需使用内置图标请手动安装 `font-awesome`。
36 |
37 | 使用
38 | ```html
39 |
40 | ```
41 | ## 配置
42 |
43 | | 配置项 | 参数类型 | 说明 |
44 | | ------------- |:-------------:| :-----|
45 | | name | String | 自定义组件名,默认为my-vue-editor |
46 | | modules | Array | 需要使用的模块 |
47 | | icons | Object | 覆盖指定模块的Icon |
48 | | commands | Object | 自定义指令 |
49 | | shortcut | Object | 自定义快捷键 |
50 | | extendModules | Array | 自定义模块 |
51 | | 任何内置模块名 | Object | 覆盖对应内置模块的属性 |
52 | ### 一个例子
53 | ```javascript
54 | Vue.use(myVueEditor, {
55 | // 覆盖内置模块的图标
56 | icons: {
57 | image: 'iui-icon iui-icon-pic',
58 | indent: 'iui-icon iui-icon-insert'
59 | },
60 | // 使用的模块
61 | modules: [
62 | 'font',
63 | 'bold',
64 | 'italic',
65 | 'underline',
66 | 'linethrough',
67 | 'ul',
68 | 'indent',
69 | 'align',
70 | 'image',
71 | 'quote',
72 | 'todo',
73 | // 这是一个自定义的模块
74 | 'customSave'
75 | ],
76 | // 覆盖image模块的相关配置
77 | image: {
78 | maxSize: 5120 * 1024,
79 | imgOccupyNewRow: true,
80 | compress: {
81 | width: 1600,
82 | height: 1600,
83 | quality: 0.8
84 | }
85 | },
86 | // 覆盖font模块的相关配置
87 | font: {
88 | config: {
89 | 'xx-large': {
90 | fontSize: 6,
91 | name: '大号'
92 | },
93 | 'medium': {
94 | fontSize: 3,
95 | name: '中号'
96 | },
97 | 'small': {
98 | fontSize: 2,
99 | name: '小号'
100 | },
101 | default: 'medium'
102 | },
103 | // 将font模块的模块检测机制修改为通过style的模块检测
104 | inspect (add) {
105 | add('style', {
106 | fontSize: ['xx-large', 'x-large', 'large', 'medium', 'small']
107 | }).add('tag', 'font')
108 | }
109 | },
110 | // 覆盖ul模块的相关配置
111 | ul: {
112 | // 当ul模块被检测到时,使除了他自己以外其他所有有模块检测机制的模块被禁用
113 | exclude: 'ALL_BUT_MYSELF',
114 | // 当ul模块被点击时,执行如下方法
115 | handler (rh) {
116 | console.log('i am ul!')
117 | rh.editor.execCommand('insertUnorderedList')
118 | }
119 | },
120 | // 当quote模块被检测到时,使image todo ul 这3个模块禁用
121 | quote: {
122 | exclude: ['image', 'todo', 'ul']
123 | },
124 | // 自定义一个名为getAllTexts的指令,执行时会打印出当前range对象下的所有文本节点
125 | commands: {
126 | getAllTexts (rh, arg) {
127 | console.log(rh.getAllTextNodeInRange())
128 | }
129 | },
130 | shortcut: {
131 | // 自定义一个保存快捷键,当按下command + s 时,执行save函数
132 | saveNote: {
133 | metaKey: true,
134 | keyCode: 83,
135 | handler (editor) {
136 | save()
137 | }
138 | }
139 | },
140 | // 自定义一个模块,当点击该模块图标时会弹出一个窗口
141 | extendModules: [{
142 | name: 'smile',
143 | icon: 'iui iui-icon-smile'
144 | handler (rh) {
145 | alert('smile~~')
146 | }
147 | }]
148 | })
149 | ```
150 |
151 | ## 事件
152 | | 事件名 | 说明 |
153 | | ------------- |:-------------|
154 | | change | 当编辑器内容变化时触发,参数为最新内容 |
155 | | imageUpload | 上传图片时触发,参数包括图片相应数据,
replaceSrcAfterUploadFinish函数(用于当上传成功时将img的src属性由base64格式替换为服务器返回的url)
deleteImgWhenUploadFail函数(用于当上传失败时候调用删除当前图片)|
156 | ## 修改内置模块
157 | 在配置项中添加以内置模块名(所有内置模块及他们的配置项请在源码src/modules目录下查看)为key的参数,将覆盖内置模块的原有属性
158 | ### 以image模块为例
159 | ```javascript
160 | Vue.use(myVueEditor, {
161 | image: {
162 | // 修改image模块的图标
163 | icon: 'iui-pic',
164 | // 覆盖原有的压缩参数,使图片上传时不进行压缩
165 | compress: null,
166 | // 不能重复上传同一张图片
167 | canUploadSameImage: false
168 | }
169 | })
170 | ```
171 | ## 自定义模块
172 | 通过extendModules配置项扩展模块
173 | 我们提供一些通用的模块配置项
174 |
175 | | 配置项 | 类型 | 说明 |
176 | | ------------- |:-------------|:------------|
177 | | name | String | 模块的名称|
178 | | icon | String | 模块图标的className,默认使用fontAwesome图标|
179 | | exclude | String Array | 当模块被检测到时,需要禁用的模块
值为'ALL'表示禁用所有模块,包括自己
值为'ALL_BUT_MYSELF'表示禁用除自己以外的所有模块
值为Array时,传入需要禁用的模块名|
180 | | inspect | Function | 模块检测,当光标处在列表中时,列表模块高亮,即列表模块被检测到,这是通过其UL标签作为检测依据
函数的第一个参数为add方法,通过调用add方法来增加模块的检测依据,当有多个检测依据时请链式调用add
add方法第一个参数标示通过什么途径检测,可选'tag' 'style' 'attribute'
当参数1为'tag'时,参数2请传入一个标签名字符串
当参数1为'style'时,参数2为以styleName为key,styleValue为value的对象。注意styleName请使用驼峰形式(如:fontSize),当styleValue有多个时请使用Array的形式
当参数1为'attribute'时,参数2为以attribute名为key,attribute值为value的对象,注意若希望值为任何值都满足时传入''(如:add('attribute', {'data-todo': ''}))|
181 | | handler | Function | 点击模块时执行的操作
参数1为range-handler实例,通过实例可以取得当前编辑器的Vue实例以及操作range的方法
我们并不推荐直接在handler中操作range,而是应该将range操作封装为command,在handler中通过编辑器实例下的execCommand方法调用command|
182 | | tab | Vue instance | 常规的模块使用handler来处理点击,如果你希望实现UI(font模块的下拉列表)或者逻辑更复杂(align模块的左中右切换)的模块,请使用tab|
183 | | init | Function |对应编辑器组件生命周期的created,参数1为编辑器实例
注意这时execCommand方法还不能使用|
184 | | mounted | Function | 对应编辑器组件生命周期的mounted|
185 | | updated | Function | 对应编辑器组件生命周期的updated|
186 | | destroyed | Function | 对应编辑器组件生命周期的beforeDestroy|
187 |
--------------------------------------------------------------------------------
/src/commands/quote.js:
--------------------------------------------------------------------------------
1 | import commands from './index'
2 |
3 | const q = {
4 | // only set contenteditable:false in parent node can child node trigger keydown listener
5 | 'quote' (rh, isInQuote) {
6 | let node = rh.range.commonAncestorContainer
7 | if (isInQuote) {
8 | node = node.nodeType === Node.TEXT_NODE ? node.parentNode : node
9 | let quote = rh.findSpecialAncestor(node, '[data-editor-quote]')
10 | if (quote) {
11 | let texts = rh.getDescendantTextNodes(quote)
12 | let quoteRows = []
13 | let rows = Array.from(quote.querySelector('[data-editor-quote-block').children)
14 | texts.forEach(text => {
15 | // find row in current quote row
16 | // let row = rh.findSpecialAncestor(text, constant.ROW_TAG, false, quote)
17 | let row
18 | rows.forEach(curRow => {
19 | if (curRow.contains(text)) {
20 | row = curRow
21 | }
22 | })
23 | if (!quoteRows.includes(row)) {
24 | quoteRows.push(row)
25 | }
26 | })
27 | quoteRows.forEach((qr, index) => {
28 | if (index === 0) {
29 | quote.parentNode.replaceChild(qr, quote)
30 | } else {
31 | rh.insertAfter(qr, quoteRows[index - 1])
32 | }
33 | })
34 | let s = rh.getSelection()
35 | if (quoteRows.length) {
36 | const range = document.createRange()
37 | range.setStart(quoteRows[0], 0)
38 | range.setEnd(quoteRows[quoteRows.length - 1], 1)
39 | s.removeAllRanges()
40 | s.addRange(range)
41 | } else {
42 | // it's a empty quote
43 | let newRow = rh.newRow({br: true})
44 | quote.parentNode.replaceChild(newRow, quote)
45 | s.collapse(newRow, 1)
46 | }
47 | }
48 | return
49 | }
50 | const texts = rh.getAllTextNodesInRange()
51 | let curRow = rh.getRow(node)
52 |
53 | // is at a empty row without row element, then create a row
54 | // or texts has no common parent row
55 | if (!curRow && !texts.length) {
56 | let v = rh.newRow()
57 | let newRow = rh.newRow({br: true})
58 | v.appendChild(newRow)
59 | commands.insertHTML(rh, newRow.outerHTML)
60 | let s = rh.getSelection()
61 | texts.push(s.focusNode)
62 | }
63 | if (!texts.length) {
64 | texts.push(curRow)
65 | }
66 |
67 | let container = rh.newRow()
68 | let quote = document.createElement('section')
69 | let quoteBlock = rh.newRow({tag: 'div'})
70 | quoteBlock.setAttribute('data-editor-quote-block', rh.createRandomId('quoteblock'))
71 | quote.appendChild(quoteBlock)
72 | let id = rh.createRandomId('quote')
73 | quote.setAttribute('data-editor-quote', id)
74 | quote.setAttribute('contenteditable', 'false')
75 | let quoteRows = []
76 | texts.forEach((text, index) => {
77 | let curRow = rh.getRow(text)
78 |
79 | // create a row for text without row
80 | if (!curRow && text.nodeValue) {
81 | curRow = rh.newRow()
82 | curRow.appendChild(text)
83 | }
84 | if (curRow && !quoteRows.includes(curRow)) {
85 | quoteRows.push(curRow)
86 | }
87 | })
88 | let anchorRow
89 | quoteRows.forEach((qr, index) => {
90 | if (index !== quoteRows.length - 1) {
91 | quoteBlock.appendChild(qr)
92 | return
93 | }
94 | quoteBlock.appendChild(qr.cloneNode(true))
95 | anchorRow = qr
96 | })
97 |
98 | if (anchorRow.parentNode) {
99 | anchorRow.parentNode.replaceChild(quote, anchorRow)
100 | } else {
101 | // current row is created and has no parent
102 | let v = rh.newRow()
103 | v.appendChild(quote)
104 | rh.range.deleteContents()
105 | commands['insertHTML'](rh, v.innerHTML)
106 | }
107 | const curQuote = document.querySelector(`[data-editor-quote='${id}']`)
108 | if (!curQuote.lastElementChild) return
109 | rh.getSelection().collapse(curQuote.lastElementChild, curQuote.lastElementChild.innerText ? 1 : 0)
110 | },
111 | 'initQuote' (rh, arg) {
112 | document.addEventListener('keydown', e => {
113 | let quote = rh.findSpecialAncestor(e.target, '[data-editor-quote]')
114 | if (quote) {
115 | let s = rh.getSelection()
116 | let node = s.anchorNode
117 | let ctn = node.innerText || node.nodeValue
118 | if (e.keyCode === 13) {
119 | if (ctn.replace('\n', '') === '') {
120 | e.preventDefault()
121 | let newRow = rh.newRow({br: true})
122 | rh.insertAfter(newRow, quote)
123 | if (node.parentNode.children.length > 1) {
124 | node.parentNode.removeChild(node)
125 | }
126 | rh.getSelection().collapse(newRow, 0)
127 | return
128 | }
129 | }
130 | if (e.keyCode === 8) {
131 |
132 | // cursor may at row or at quote block , so there are two judgement conditions
133 | if (s.isCollapsed && (s.focusOffset === 0 || (node.contains(s.baseNode) && (rh.isEmptyNode(s.baseNode)) && s.focusOffset === 1))) {
134 | let rows = Array.from(quote.querySelector('[data-editor-quote-block]').children)
135 |
136 | // empty quote
137 | if (!rows.length) {
138 | e.preventDefault()
139 | let newRow = rh.newRow({br: true})
140 | quote.parentNode.replaceChild(newRow, quote)
141 | rh.getSelection().collapse(newRow, 1)
142 | return
143 | }
144 |
145 | rows.forEach((row, index) => {
146 |
147 | // row and node has father-child relationship
148 | if ((row === node || row.contains(node) || node.contains(row)) && index === 0) {
149 |
150 | // only have one empty row in quote,then delete the quote
151 | if (rows.length === 1 && rh.isEmptyNode(row)) {
152 | e.preventDefault()
153 | let newRow = rh.newRow({br: true})
154 | quote.parentNode.replaceChild(newRow, quote)
155 | rh.getSelection().collapse(newRow, 1)
156 | return
157 | } else {
158 |
159 | // first row have content and previous element exist, then move cursor to previous element
160 | let preRow = rh.getPreviousRow(quote)
161 | if (preRow) {
162 | e.preventDefault()
163 |
164 | // previous row is a quote
165 | if (preRow.getAttribute('data-editor-quote')) {
166 | let lastEle = Array.from(preRow.querySelector('[data-editor-quote-block]').children).pop()
167 | try {
168 | rh.getSelection().collapse(lastEle, 1)
169 | } catch (e) {
170 | rh.getSelection().collapse(lastEle, 0)
171 | }
172 | return
173 | }
174 |
175 | // previous row is a todo
176 | if (preRow.getAttribute('data-editor-todo')) {
177 | let input = preRow.querySelector('[type="text"]')
178 | if (input) {
179 | e.preventDefault()
180 | input.focus()
181 | }
182 | return
183 | }
184 |
185 | // previous row is a row
186 | try {
187 | rh.getSelection().collapse(preRow, 1)
188 | } catch (e) {
189 | rh.getSelection().collapse(preRow, 0)
190 | }
191 | return
192 | }
193 | }
194 | }
195 | })
196 | }
197 | }
198 | }
199 | })
200 | }
201 | }
202 |
203 | export default q
204 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # my-vue-editor
2 | [](https://github.com/BetaSu/my-vue-editor)
3 | [](https://github.com/BetaSu/my-vue-editor)
4 | [](https://github.com/BetaSu/my-vue-editor)
5 |
6 | A rich text editor based on Vue2.x
7 | 中文文档
8 | ## Demo
9 | click here to see demo
10 | More demo please refer to the example directory
11 | ## Introduction
12 | Our editor is based on vue-html5-editor secondary development. Thanks to its author PeakTai for providing a concise rich text editor plug-in, on the basis of which we have rewritten the native method and extended the functionality.
13 | ## Install
14 | ```javascript
15 | npm install my-vue-editor
16 | ```
17 | Introduced as a plug-in
18 | ```javascript
19 | import Vue from 'vue'
20 | import myVueEditor from 'my-vue-editor'
21 | Vue.use(myVueEditor, options)
22 | ```
23 | Global introduction
24 | ```html
25 |
26 |
27 | ```
28 | Installed by the global variable myVueEditor
29 | ```javascript
30 | Vue.use(myVueEditor, options)
31 | ```
32 | Use
33 | ```html
34 |
35 | ```
36 | ## Configuration
37 |
38 | | Items | Type | Description |
39 | | ------------- |:-------------:|:-----|
40 | | name | String | Custom component name, the default is my-vue-editor |
41 | | modules | Array | Modules need to use |
42 | | icons | Object | Covering the specified module's icon |
43 | | commands | Object | Custom command |
44 | | shortcut | Object | Custom shortcut |
45 | | extendModules | Array | Custom module |
46 | | Any built-in module name | Object | Overwrite the properties of the corresponding built-in module |
47 | ### example
48 | ```javascript
49 | Vue.use(myVueEditor, {
50 | // Overlay built-in module's icon
51 | icons: {
52 | image: 'iui-icon iui-icon-pic',
53 | indent: 'iui-icon iui-icon-insert'
54 | },
55 | // Modules in use
56 | modules: [
57 | 'font',
58 | 'bold',
59 | 'italic',
60 | 'underline',
61 | 'linethrough',
62 | 'ul',
63 | 'indent',
64 | 'align',
65 | 'image',
66 | 'quote',
67 | 'todo',
68 | // This is a custom module
69 | 'customSave'
70 | ],
71 | // Overlay image module's configuration
72 | image: {
73 | maxSize: 5120 * 1024,
74 | imgOccupyNewRow: true,
75 | compress: {
76 | width: 1600,
77 | height: 1600,
78 | quality: 0.8
79 | }
80 | },
81 | // Overlay font module's configuration
82 | font: {
83 | config: {
84 | 'xx-large': {
85 | fontSize: 6,
86 | name: 'H1'
87 | },
88 | 'medium': {
89 | fontSize: 3,
90 | name: 'H2'
91 | },
92 | 'small': {
93 | fontSize: 2,
94 | name: 'H3'
95 | },
96 | default: 'medium'
97 | },
98 | // Modify the font module's module inspect mechanism to inspect via style and tag name
99 | inspect (add) {
100 | add('style', {
101 | fontSize: ['xx-large', 'x-large', 'large', 'medium', 'small']
102 | }).add('tag', 'font')
103 | }
104 | },
105 | // Overlay ul module's configuration
106 | ul: {
107 | // When the ul module is inspected, disabled all but itself
108 | exclude: 'ALL_BUT_MYSELF',
109 | // When the ul module is clicked, execute the following method
110 | handler (rh) {
111 | console.log('i am ul!')
112 | rh.editor.execCommand('insertUnorderedList')
113 | }
114 | },
115 | // When the ul module is inspected, disabled image, todo and ul module
116 | quote: {
117 | exclude: ['image', 'todo', 'ul']
118 | },
119 | // Customize an command named getAllTexts that prints out all the text nodes under the current range object
120 | commands: {
121 | getAllTexts (rh, arg) {
122 | console.log(rh.getAllTextNodeInRange())
123 | }
124 | },
125 | shortcut: {
126 | // Custom a shortcut key, when you press the command + s, execute the save function
127 | saveNote: {
128 | metaKey: true,
129 | keyCode: 83,
130 | handler (editor) {
131 | save()
132 | }
133 | }
134 | },
135 | // Customize a module, a alert pops up when you click on the module icon
136 | extendModules: [{
137 | name: 'smile',
138 | icon: 'iui iui-icon-smile'
139 | handler (rh) {
140 | alert('smile~~')
141 | }
142 | }]
143 | })
144 | ```
145 |
146 | ## Event
147 | | Event name | Description |
148 | | ------------- |:-------------|
149 | | change | Trigger when editor content changes, parameter is up-to-date content data |
150 | | imageUpload | Trigger when uploading images, parameters include the corresponding data of the image,
replaceSrcAfterUploadFinish:Used to replace the src attribute of img from the base64 format to the url returned by the server when the upload is successful)
deleteImgWhenUploadFail:Used to delete the current picture when the upload fails|
151 | ## Modify the built-in module
152 | Add a parameter with a built-in module's name as it's key, will cover the built-in module's original properties(See all built-in modules and their configuration items in src/modules directory)
153 | ### Take the image module as an example
154 | ```javascript
155 | Vue.user(myVueEditor, {
156 | image: {
157 | // Modify the image module's icon
158 | icon: 'iui-pic',
159 | // Cover the original compression parameters, so that the image is not compressed when uploaded
160 | compress: null,
161 | // Can not upload the same image repeatedly
162 | canUploadSameImage: false
163 | }
164 | })
165 | ```
166 | ## Cutom module
167 | Extend the module with the extendModules configuration item
168 | We provide some common module configuration items
169 |
170 | | Item | Type | Description |
171 | | ------------- |:-------------|:------------|
172 | | name | String | module's name|
173 | | icon | String | module icon's className,The fontAwesome icon is used by default|
174 | | exclude | String Array | disabled modules When current module is inspected
When value is 'ALL' means disable all modules include current module
When value is'ALL_BUT_MYSELF', means disabled all modules but current module
When value is type of Array, Input module name to be disabled|
175 | | inspect | Function | module inspect,When the cursor is in the list, the list module is highlighted, that is, the list module is inspected, which is based on its UL label as a test basis
The first argument to the function is a method named add, which called to add the module's inspection basis. When there are multiple inspection bases, please call chaining
The first parameter of the add method indicates what path to test. The optional is 'tag' 'style' and 'attribute'
When parameter 1 is 'tag', parameter 2 passes in a tag name string
When parameter 1 is 'style', parameter 2 is an object with styleName as key and styleValue as value. Note styleName use the hump form(ex:fontSize),When there are multiple styleValue please use the form of Array
When parameter 1 is 'attribute', parameter 2 is an object whose key is attribute name and attribute value is value, and note that if any value is desired, pass in ''(ex:add('attribute', {'data-todo': ''}))|
176 | | handler | Function | What to do when you click the module
Parameter 1 is range-handler instance, through which can you get the Vue instance of the current editor and the method to operate range
We do not recommend operating range directly in the handler, but should encapsulate the range action as a command, calling the command via the execCommand method under the editor instance in the handler|
177 | | tab | Vue instance | Conventional modules use handlers for handling clicks, and if you want to implement UI (drop-down list of font modules) or more complex logic (left-to-right switching of align modules), use tab|
178 | | init | Function |Corresponds to the editor component life cycle created, the parameter 1 is the editor instance. Note that the execCommand method can not be used at this time|
179 | | mounted | Function | Corresponds to the editor component life cycle mounted, the parameter 1 is the editor instance.|
180 | | updated | Function | Corresponds to the editor component life cycle updated, the parameter 1 is the editor instance.|
181 | | destroyed | Function | Corresponds to the editor component life cycle beforeDestroy, the parameter 1 is the editor instance.|
182 |
--------------------------------------------------------------------------------
/src/editor/editor.js:
--------------------------------------------------------------------------------
1 | import RH from '../range-handler'
2 | import './style/main.styl'
3 | import template from './editor.html'
4 | import dragPic from './drag-pic'
5 | import Inspector from '../module-inspect'
6 |
7 | export default {
8 | template,
9 | props: {
10 | content: {
11 | type: String,
12 | required: true,
13 | default: ''
14 | },
15 | height: {
16 | type: Number,
17 | default: 300,
18 | validator(val){
19 | return val >= 100
20 | }
21 | },
22 | zIndex: {
23 | type: Number,
24 | default: 1000
25 | },
26 | autoHeight: {
27 | type: Boolean,
28 | default: true
29 | }
30 | },
31 | directives: {
32 | dragPic
33 | },
34 | data(){
35 | return {
36 | modules: {},
37 | activeModules: [],
38 | allActiveModules: [],
39 | fullScreen: false
40 | }
41 | },
42 | watch: {
43 | content(val) {
44 | const content = this.$refs.content.innerHTML
45 | if (val !== content) {
46 | this.$refs.content.innerHTML = val
47 | }
48 | },
49 | fullScreen(val){
50 | const component = this
51 | if (val) {
52 | component.parentEl = component.$el.parentNode
53 | component.nextEl = component.$el.nextSibling
54 | document.body.appendChild(component.$el)
55 | return
56 | }
57 | if (component.nextEl) {
58 | component.parentEl.insertBefore(component.$el, component.nextEl)
59 | return
60 | }
61 | component.parentEl.appendChild(component.$el)
62 | }
63 | },
64 | computed: {
65 | contentStyle(){
66 | const style = {}
67 | if (this.fullScreen) {
68 | style.height = `${window.innerHeight - this.$refs.toolbar.clientHeight - 1}px`
69 | return style
70 | }
71 | if (!this.autoHeight) {
72 | style.height = `${this.height}px`
73 | return style
74 | }
75 | style['min-height'] = `${this.height}px`
76 | return style
77 | }
78 | },
79 | methods: {
80 | getCurActiveModuleItem () {
81 | return Inspector.activeItems
82 | },
83 | clearActiveModuleItem () {
84 | Inspector.activeItems = {}
85 | },
86 | handleDragPic (file) {
87 | if ((this.modulesMap['image'] && this.modulesMap['image'].drag !== false) || !this.modulesMap['image']) {
88 | this.saveCurrentRange()
89 | this.execCommand('insertImage', file)
90 | }
91 | },
92 | toggleFullScreen(){
93 | this.fullScreen = !this.fullScreen
94 | },
95 | enableFullScreen(){
96 | this.fullScreen = true
97 | },
98 | exitFullScreen(){
99 | this.fullScreen = false
100 | },
101 | focus(){
102 | this.$refs.content.focus()
103 | },
104 | blur(){
105 | this.$refs.content.blur()
106 | },
107 | execCommand(command, arg, execOnly){
108 | if (!execOnly) {
109 | this.restoreSelection()
110 | }
111 | if (this.range) {
112 | new RH(this.range, this).execCommand(command, arg)
113 | }
114 | this.$emit('change', this.$refs.content.innerHTML)
115 | },
116 | saveCurrentRange(){
117 | const selection = window.getSelection ? window.getSelection() : document.getSelection()
118 | const content = this.$refs.content
119 | if (!selection.rangeCount || !content) {
120 | return
121 | }
122 | for (let i = 0; i < selection.rangeCount; i++) {
123 | const range = selection.getRangeAt(0)
124 | let start = range.startContainer
125 | let end = range.endContainer
126 | // for IE11 : node.contains(textNode) always return false
127 | start = start.nodeType === Node.TEXT_NODE ? start.parentNode : start
128 | end = end.nodeType === Node.TEXT_NODE ? end.parentNode : end
129 | if (content.contains(start) && content.contains(end)) {
130 | this.range = range
131 | break
132 | }
133 | }
134 | },
135 | restoreSelection(){
136 | const selection = window.getSelection ? window.getSelection() : document.getSelection()
137 | selection.removeAllRanges()
138 | if (this.range) {
139 | selection.addRange(this.range)
140 | } else {
141 | const content = this.$refs.content
142 | const row = RH.prototype.newRow({br: true})
143 | const range = document.createRange()
144 | content.appendChild(row)
145 | range.setStart(row, 0)
146 | range.setEnd(row, 0)
147 | selection.addRange(range)
148 | this.range = range
149 | }
150 | },
151 | activeModule(module){
152 | if (module.forbidden) return
153 | if (typeof module.handler === 'function') {
154 | module.handler(new RH(this.range, this), module)
155 | this.$nextTick(() => {
156 | this.saveCurrentRange()
157 | this.moduleInspect()
158 | })
159 | return
160 | }
161 | },
162 | moduleInspect () {
163 | if (this.range) {
164 | this.clearActiveModuleItem()
165 | this.activeModules = []
166 | this.allActiveModules = []
167 | let rh = new RH(this.range, this)
168 | let texts = rh.getAllTextNodesInRange()
169 | if (texts.length === 0 && this.range.collapsed) {
170 | texts.push(this.range.commonAncestorContainer)
171 | }
172 | // texts duplicate removal
173 | let textAftetDR = []
174 | texts.forEach(text => {
175 | if (text.nodeType === Node.TEXT_NODE && text.parentNode !== rh.editZone()) {
176 | text = text.parentNode
177 | }
178 | if (!textAftetDR.includes(text)) {
179 | textAftetDR.push(text)
180 | }
181 | })
182 |
183 | let tagResult = Inspector.run('tag', textAftetDR)
184 | let tagResultRD = Inspector.removeDuplate(tagResult)
185 |
186 | let styleResult = Inspector.run('style', textAftetDR)
187 | let styleResultRD = Inspector.removeDuplate(styleResult)
188 |
189 | let attributeResult = Inspector.run('attribute', textAftetDR)
190 | let attributeResultRD = Inspector.removeDuplate(attributeResult)
191 |
192 | this.allActiveModules = tagResult.concat(styleResult, attributeResult)
193 | this.activeModules = Array.from(new Set(tagResultRD.concat(styleResultRD, attributeResultRD)))
194 |
195 | // reset
196 | this.modules.forEach(module => {
197 | module.forbidden = false
198 | module.moduleInspectResult = false
199 | })
200 |
201 | // handle forbidden logic
202 | if (this.allActiveModules.length) {
203 | let excludeList = []
204 | this.allActiveModules.forEach(m => {
205 | if (Array.isArray(m)) {
206 | m.forEach(moduleName => {
207 | let curModule = this.modulesMap[moduleName]
208 | excludeList = excludeList.concat(curModule.exclude)
209 | })
210 | }
211 | })
212 | excludeList = Array.from(new Set(excludeList))
213 | excludeList.forEach(exc => {
214 | let excModule = this.modulesMap[exc]
215 | if (excModule && excModule.type !== 'fn') {
216 | excModule.forbidden = true
217 | }
218 | })
219 | }
220 |
221 | // handle highlight logic
222 | if (this.activeModules.length) {
223 | this.modules.forEach(module => {
224 | module.moduleInspectResult = false
225 | let moduleName = module.name
226 | if (this.activeModules.includes(moduleName)) {
227 | module.moduleInspectResult = true
228 | let curModuleActiveItem = this.getCurActiveModuleItem()[moduleName]
229 | if (typeof curModuleActiveItem === 'string') {
230 | module.moduleInspectResult = curModuleActiveItem || 'ALL'
231 | }
232 | }
233 | })
234 | }
235 | }
236 | }
237 | },
238 | created(){
239 | this.modules.forEach((module) => {
240 | if (typeof module.init === 'function') {
241 | module.init(this)
242 | }
243 | })
244 | },
245 | mounted(){
246 | const content = this.$refs.content
247 | const toolbar = this.$refs.toolbar
248 | content.innerHTML = this.content
249 | // add eventListener at document to handle selection
250 | document.addEventListener('mouseup', e => {
251 | this.saveCurrentRange()
252 | this.moduleInspect()
253 | }, false)
254 | // toolbar.addEventListener('mousedown', this.saveCurrentRange, false)
255 | content.addEventListener('keyup', e => {
256 | this.$emit('change', content.innerHTML)
257 | this.saveCurrentRange()
258 | this.moduleInspect()
259 | }, false)
260 | content.addEventListener('mouseout', e => {
261 | this.saveCurrentRange()
262 | }, false)
263 | content.addEventListener('paste', e => {
264 | this.execCommand('paste', e, true)
265 | let common = this.range.commonAncestorContainer
266 | if (common) {
267 | if (common.nodeType === Node.TEXT_NODE) {
268 | common = common.parentNode
269 | }
270 | if (common.scrollIntoView) {
271 | common.scrollIntoView(false)
272 | }
273 | }
274 | })
275 | this.touchHandler = (e) => {
276 | if (content.contains(e.target)) {
277 | this.saveCurrentRange()
278 | this.moduleInspect()
279 | }
280 | }
281 | window.addEventListener('touchend', this.touchHandler, false)
282 |
283 | // before exec command
284 | // let text be a row
285 | RH.prototype.before((command, rh, arg) => {
286 | let node = rh.range.commonAncestorContainer
287 | // handle editor with no content
288 | if (rh.isEmptyNode(node) && node === rh.editZone()) {
289 | let firstChild = node.firstElementChild
290 | if (firstChild && firstChild.nodeName === 'BR') {
291 | node.removeChild(firstChild)
292 | }
293 | let newRow = rh.newRow({br: true})
294 | node.appendChild(newRow)
295 | rh.getSelection().collapse(newRow, 1)
296 | return
297 | }
298 | let texts = rh.getAllTextNodesInRange()
299 | texts.forEach(text => {
300 | if (!rh.isEmptyNode(text)) {
301 | rh.textToRow(text)
302 | }
303 | })
304 | if (texts.length) {
305 | rh.editor.saveCurrentRange()
306 | }
307 | })
308 |
309 | // handle shortcut
310 | content.addEventListener('keydown', e => {
311 | this.execCommand('keydown', e, true)
312 | let item = this.shortcut[e.keyCode]
313 | if (item && item.length) {
314 | item.forEach(s => {
315 | if (e.keyCode === s.keyCode && e.altKey === !!s.altKey && e.ctrlKey === !!s.ctrlKey && e.metaKey === !!s.metaKey && e.shiftKey === !!s.shiftKey) {
316 | if (typeof s.handler === 'function') {
317 | this.saveCurrentRange()
318 | s.handler(this, e)
319 | }
320 | }
321 | })
322 | }
323 | }, false)
324 |
325 | this.$nextTick(() => {
326 | this.modules.forEach((module) => {
327 | if (typeof module.mounted === 'function') {
328 | module.mounted(this)
329 | }
330 | })
331 | })
332 | },
333 | updated(){
334 | this.modules.forEach((module) => {
335 | if (typeof module.updated === 'function') {
336 | module.updated(this)
337 | }
338 | })
339 | },
340 | beforeDestroy(){
341 | window.removeEventListener('touchend', this.touchHandler)
342 | this.modules.forEach((module) => {
343 | if (typeof module.destroyed === 'function') {
344 | module.destroyed(this)
345 | }
346 | })
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/src/commands/index.js:
--------------------------------------------------------------------------------
1 | import insertImage from './insertImage'
2 | import fontSize from './fontSize'
3 | import paste from './paste'
4 | import enter from './enter'
5 | import underline from './underline'
6 | import strikeThrough from './strikeThrough'
7 | import italic from './italic'
8 | import bold from './bold'
9 | import quote from './quote'
10 | import todo from './todo'
11 | import keydown from './keydown'
12 | import deleteModule from './delete'
13 | import justifyRight from './justifyRight'
14 | import justifyLeft from './justifyLeft'
15 | import justifyCenter from './justifyCenter'
16 | import {isObj} from '../util'
17 | import constant from '../constant-config'
18 |
19 | const commands = {
20 | /*
21 | * add a style attribute in range(have bug)
22 | * @param {obj} arg include
23 | * key: style name
24 | * value: style value
25 | **/
26 | addStyle (rh, arg) {
27 | function doAdd(node) {
28 | Object.keys(arg).forEach(styleName => {
29 | node.style[styleName] = arg[styleName]
30 | })
31 | }
32 |
33 | if (!isObj(arg)) return
34 | const textNodes = rh.getAllTextNodesInRange()
35 | if (!textNodes.length) {
36 | if (rh.range.collapsed) {
37 | let node = rh.range.commonAncestorContainer
38 | if (node.nodeType === Node.ELEMENT_NODE) {
39 | doAdd(node)
40 | return
41 | }
42 | }
43 | }
44 | if (rh.range.collapsed && textNodes.length === 1) {
45 | let node = textNodes[0].parentNode
46 | if (node) {
47 | if (node === rh.editZone()) {
48 | let newRow = rh.newRow({tag: 'p'})
49 | newRow.innerText = textNodes[0].nodeValue
50 | node.replaceChild(newRow, textNodes[0])
51 | doAdd(newRow)
52 | return
53 | }
54 | doAdd(node)
55 | return
56 | }
57 | }
58 | if (textNodes.length === 1 && textNodes[0] === rh.range.startContainer
59 | && textNodes[0] === rh.range.endContainer) {
60 | const textNode = textNodes[0]
61 | if (rh.range.startOffset === 0
62 | && rh.range.endOffset === textNode.textContent.length) {
63 | if (textNode.parentNode.childNodes.length === 1
64 | && rh.isInlineElement(textNode.parentNode)) {
65 | doAdd(textNode.parentNode)
66 | return
67 | }
68 | const span = document.createElement('span')
69 | doAdd(span)
70 | textNode.parentNode.insertBefore(span, textNode)
71 | span.appendChild(textNode)
72 | return
73 | }
74 | const span = document.createElement('span')
75 | span.innerText = textNode.textContent.substring(
76 | rh.range.startOffset, rh.range.endOffset)
77 | doAdd(span)
78 | const frontPart = document.createTextNode(
79 | textNode.textContent.substring(0, rh.range.startOffset))
80 | textNode.parentNode.insertBefore(frontPart, textNode)
81 | textNode.parentNode.insertBefore(span, textNode)
82 | textNode.textContent = textNode.textContent.substring(rh.range.endOffset)
83 | rh.range.setStart(span, 0)
84 | rh.range.setEnd(span, 1)
85 | return
86 | }
87 |
88 | textNodes.forEach((textNode) => {
89 | if (textNode === rh.range.startContainer) {
90 | if (rh.range.startOffset === 0) {
91 | if (textNode.parentNode.childNodes.length === 1
92 | && rh.isInlineElement(textNode.parentNode)) {
93 | doAdd(textNode.parentNode)
94 | } else {
95 | const span = document.createElement('span')
96 | doAdd(span)
97 | textNode.parentNode.insertBefore(span, textNode)
98 | span.appendChild(textNode)
99 | }
100 | return
101 | }
102 | const span = document.createElement('span')
103 | textNode.textContent = textNode.textContent.substring(
104 | 0, rh.range.startOffset)
105 | doAdd(span)
106 | textNode.parentNode.insertBefore(span, textNode)
107 | rh.range.setStart(textNode, 0)
108 | return
109 | }
110 | if (textNode === rh.range.endContainer) {
111 | if (rh.range.endOffset === textNode.textContent.length) {
112 | if (textNode.parentNode.childNodes.length === 1
113 | && rh.isInlineElement(textNode.parentNode)) {
114 | doAdd(textNode.parentNode)
115 | } else {
116 | const span = document.createElement('span')
117 | doAdd(span)
118 | textNode.parentNode.insertBefore(span, textNode)
119 | span.appendChild(textNode)
120 | }
121 | return
122 | }
123 | const span = document.createElement('span')
124 | textNode.textContent = textNode.textContent.substring(rh.range.endOffset)
125 | doAdd(span)
126 | textNode.parentNode.insertBefore(span, textNode)
127 | span.appendChild(textNode)
128 | rh.range.setStart(textNode, textNode.textContent.length)
129 | return
130 | }
131 | if (textNode.parentNode.childNodes.length === 1
132 | && rh.isInlineElement(textNode.parentNode)) {
133 | doAdd(textNode.parentNode)
134 | return
135 | }
136 |
137 | const span = document.createElement('span')
138 | doAdd(span)
139 | textNode.parentNode.insertBefore(span, textNode)
140 | span.appendChild(textNode)
141 | })
142 | return
143 | },
144 | 'formatBlock' (rh, arg) {
145 | if (document.execCommand('formatBlock', false, arg)) {
146 | return
147 | }
148 | // hack
149 | const element = document.createElement(arg)
150 | rh.range.surroundContents(element)
151 | return
152 | },
153 | 'lineHeight' (rh, arg) {
154 | const textNodes = rh.getAllTextNodesInRange()
155 | textNodes.forEach((textNode) => {
156 | const parentBlock = rh.getParentBlockNode(textNode)
157 | if (parentBlock) {
158 | parentBlock.style.lineHeight = arg
159 | }
160 | })
161 | return
162 | },
163 | 'insertHTML' (rh, arg) {
164 | if (document.execCommand('insertHTML', false, arg)) {
165 | return
166 | }
167 | commands['forceInsertHTML'](rh, arg)
168 | },
169 | /*
170 | * insertHTML would insert DOM as row's child
171 | * forceInsertHTML would insert DOM as anchorNode of range
172 | **/
173 | 'forceInsertHTML' (rh, arg) {
174 | let v = rh.newRow()
175 | let s = rh.getSelection()
176 | v.innerHTML = arg
177 | if (v.hasChildNodes()) {
178 | for (let i = 0; i < v.childNodes.length; i++) {
179 | let curNode = v.childNodes[i]
180 | rh.range.deleteContents()
181 | rh.range.insertNode(curNode)
182 | s.collapse(curNode, 1)
183 | }
184 | }
185 | return
186 | },
187 | 'indent' (rh, arg) {
188 | let nodeList = []
189 | if (rh.range.collapsed) {
190 | weighting(rh.range.commonAncestorContainer)
191 | } else {
192 | let texts = rh.getAllTextNodesInRange()
193 | texts.forEach(text => {
194 | weighting(text)
195 | })
196 | }
197 |
198 | nodeList.forEach(node => {
199 | // cancel todo indent
200 | if (node.getAttribute('data-editor-todo')) {
201 | return
202 | }
203 | doIndent(node.nodeName, node)
204 | })
205 |
206 | function weighting(text) {
207 | let node = rh.findSpecialAncestor(text, 'li') || rh.findSpecialAncestor(text, constant.ROW_TAG)
208 | if (node && !nodeList.includes(node)) {
209 | nodeList.push(node)
210 | }
211 | }
212 |
213 | function doIndent(type, node) {
214 | switch (type) {
215 | case 'LI':
216 | let curLevel = rh.howManyNestAncestorSameTag(node, 'UL') || rh.howManyNestAncestorSameTag(node, 'OL')
217 | if (curLevel >= constant.MAX_INDENT_LEVEL) break
218 | document.execCommand('indent', false, arg)
219 | break
220 | case constant.ROW_TAG_UPPERCASE:
221 | let curPercent = node.style[constant.INDENT_STYLE_NAME] || '0'
222 | curPercent = Number(curPercent.replace('%', ''))
223 | node.style[constant.INDENT_STYLE_NAME] = ''
224 | node.style[constant.OUTDENT_STYLE_NAME] = ''
225 | if (curPercent / constant.INDENT_WIDTH_PERCENT >= constant.MAX_INDENT_LEVEL) {
226 | node.style[constant.INDENT_STYLE_NAME] = curPercent + '%'
227 | return
228 | }
229 | node.style[constant.INDENT_STYLE_NAME] = curPercent + constant.INDENT_WIDTH_PERCENT + '%'
230 | }
231 | }
232 | },
233 | 'outdent' (rh, arg) {
234 | let nodeList = []
235 | if (rh.range.collapsed) {
236 | weighting(rh.range.commonAncestorContainer)
237 | } else {
238 | let texts = rh.getAllTextNodesInRange()
239 | texts.forEach(text => {
240 | weighting(text)
241 | })
242 | }
243 |
244 | let outdentResult
245 | nodeList.forEach(node => {
246 | outdentResult = doOutdent(node.nodeName, node)
247 | })
248 | return outdentResult
249 |
250 | function weighting(text) {
251 | let node = rh.findSpecialAncestor(text, 'li') || rh.findSpecialAncestor(text, constant.ROW_TAG)
252 | if (node && !nodeList.includes(node)) {
253 | nodeList.push(node)
254 | }
255 | }
256 |
257 | function doOutdent(type, node) {
258 | switch (type) {
259 | case 'LI':
260 | document.execCommand('outdent', false, arg)
261 | break
262 | case constant.ROW_TAG_UPPERCASE:
263 | let curPercent = node.style[constant.INDENT_STYLE_NAME] || '0'
264 | curPercent = Number(curPercent.replace('%', ''))
265 | if (curPercent === 0) return 'NO_NEED_OUTDENT'
266 | node.style[constant.INDENT_STYLE_NAME] = ''
267 | node.style[constant.OUTDENT_STYLE_NAME] = ''
268 | let targetIndent = curPercent - constant.INDENT_WIDTH_PERCENT
269 | if (targetIndent < 0) {
270 | node.style[constant.INDENT_STYLE_NAME] = ''
271 | } else {
272 | node.style[constant.INDENT_STYLE_NAME] = targetIndent + '%'
273 | }
274 | }
275 | }
276 | },
277 | 'insertUnorderedList' (rh, arg) {
278 | // do not insert ul into a row
279 | document.execCommand('insertUnorderedList', false, null)
280 | let startNode = rh.getSelection().anchorNode
281 | let row = rh.getRow(startNode)
282 | let s = rh.getSelection()
283 |
284 | // startNode is edit zone
285 | if (!row) return
286 |
287 | row = rh.createWrapperForInline(row, constant.ROW_TAG)
288 |
289 | if (row) {
290 | // let ul be a row
291 | let maybeIsUl = row.firstElementChild
292 | if (maybeIsUl && maybeIsUl.nodeName === 'UL' && row.nodeName !== 'UL') {
293 | row.parentNode.replaceChild(maybeIsUl, row)
294 | row = maybeIsUl
295 | }
296 |
297 | // remove br
298 | if (row.nextSibling && row.nextSibling.nodeName === 'BR') {
299 | row.nextSibling.parentNode.removeChild(row.nextSibling)
300 | }
301 |
302 | // special treatment for ul>li, to let module inspect run
303 | // if ul and ol is bind into a module's tab, this should be change
304 | if (s.isCollapsed && !rh.editor.modulesMap['ul'].moduleInspectResult) {
305 | commands['insertHTML'](rh, '')
306 | }
307 | return
308 | } else {
309 | let startNode = rh.getSelection().anchorNode
310 | if (startNode === rh.editZone()) {
311 | row = rh.newRow({br: true})
312 | commands['insertHTML'](rh, row.outerHTML)
313 | }
314 | }
315 | },
316 | 'insertOrderedList' (rh, arg) {
317 | // do not insert ul into a row
318 | document.execCommand('insertOrderedList', false, null)
319 | let s = rh.getSelection()
320 | let startNode = rh.getSelection().anchorNode
321 | let row = rh.getRow(startNode)
322 |
323 | // startNode is edit zone
324 | if (!row) return
325 |
326 | row = rh.createWrapperForInline(row, constant.ROW_TAG)
327 |
328 | if (row) {
329 | // let ul be a row
330 | let maybeIsUl = row.firstElementChild
331 | if (maybeIsUl && maybeIsUl.nodeName === 'OL' && row.nodeName !== 'OL') {
332 | row.parentNode.replaceChild(maybeIsUl, row)
333 | row = maybeIsUl
334 | }
335 |
336 | // remove br
337 | if (row.nextSibling && row.nextSibling.nodeName === 'BR') {
338 | row.nextSibling.parentNode.removeChild(row.nextSibling)
339 | }
340 |
341 | // special treatment for ul>li, to let module inspect run
342 | // if ul and ol is bind into a module's tab, this should be change
343 | if (s.isCollapsed && !rh.editor.modulesMap['ol'].moduleInspectResult) {
344 | commands['insertHTML'](rh, '')
345 | }
346 | return
347 | } else {
348 | let startNode = rh.getSelection().anchorNode
349 | if (startNode === rh.editZone()) {
350 | row = rh.newRow({br: true})
351 | commands['insertHTML'](rh, row.outerHTML)
352 | }
353 | }
354 | }
355 | }
356 | commands.insertImage = insertImage
357 | commands.fontSize = fontSize
358 | commands.delete = deleteModule
359 | commands.paste = paste
360 | commands.enter = enter
361 | commands.keydown = keydown
362 | commands.underline = underline
363 | commands.strikeThrough = strikeThrough
364 | commands.bold = bold
365 | commands.italic = italic
366 | commands.justifyLeft = justifyLeft
367 | commands.justifyCenter = justifyCenter
368 | commands.justifyRight = justifyRight
369 | Object.assign(commands, quote, todo)
370 |
371 | export default commands
372 |
--------------------------------------------------------------------------------
/src/range-handler/handle-methods.js:
--------------------------------------------------------------------------------
1 | import am from './assist-methods'
2 | import constant from '../constant-config'
3 |
4 | const m = {
5 | /**
6 | * func add every elements of extArr to sourceArr.
7 | * @param sourceArr
8 | * @param extArr
9 | */
10 | mergeArray (sourceArr, extArr) {
11 | // note: Array.prototype.push.apply(arr1,arr2) is unreliable
12 | extArr.forEach((el) => {
13 | sourceArr.push(el)
14 | })
15 | },
16 | /**
17 | * find all the descendant text nodes of a element
18 | * @param ancestor
19 | */
20 | getDescendantTextNodes (ancestor) {
21 | if (ancestor.nodeType === Node.TEXT_NODE) {
22 | return [ancestor]
23 | }
24 | const textNodes = []
25 | if (!ancestor.hasChildNodes()) {
26 | return textNodes
27 | }
28 | const childNodes = ancestor.childNodes
29 | for (let i = 0; i < childNodes.length; i++) {
30 | const node = childNodes[i]
31 | if (node.nodeType === Node.TEXT_NODE) {
32 | textNodes.push(node)
33 | } else if (node.nodeType === Node.ELEMENT_NODE) {
34 | m.mergeArray(textNodes, m.getDescendantTextNodes(node))
35 | }
36 | }
37 | return textNodes
38 | },
39 | /**
40 | * func find all the descendant text nodes of an ancestor element that before the specify end element,
41 | * the ancestor element must contains the end element.
42 | * @param ancestor
43 | * @param endEl
44 | */
45 | getBeforeEndDescendantTextNodes (ancestor, endEl) {
46 | const textNodes = []
47 | let endIndex = 0
48 | for (let i = 0; i < ancestor.childNodes.length; i++) {
49 | if (ancestor.childNodes[i].contains(endEl)) {
50 | endIndex = i
51 | break
52 | }
53 | }
54 |
55 | for (let i = 0; i <= endIndex; i++) {
56 | const node = ancestor.childNodes[i]
57 | if (node === endEl) {
58 | m.mergeArray(textNodes, m.getDescendantTextNodes(node))
59 | } else if (i === endIndex) {
60 | if (node.nodeType === Node.TEXT_NODE) {
61 | textNodes.push(node)
62 | } else if (node.nodeType === Node.ELEMENT_NODE) {
63 | m.mergeArray(textNodes, m.getBeforeEndDescendantTextNodes(node, endEl))
64 | }
65 | } else if (node.nodeType === Node.TEXT_NODE) {
66 | textNodes.push(node)
67 | } else if (node.nodeType === Node.ELEMENT_NODE) {
68 | m.mergeArray(textNodes, m.getDescendantTextNodes(node))
69 | }
70 | }
71 | return textNodes
72 | },
73 | /**
74 | * func find all the descendant text nodes of an ancestor element that after the specify start element,
75 | * the ancestor element must contains the start element.
76 | * @param ancestor
77 | * @param startEl
78 | */
79 | getAfterStartDescendantTextNodes (ancestor, startEl) {
80 | const textNodes = []
81 | let startIndex = 0
82 | for (let i = 0; i < ancestor.childNodes.length; i++) {
83 | if (ancestor.childNodes[i].contains(startEl)) {
84 | startIndex = i
85 | break
86 | }
87 | }
88 |
89 | for (let i = startIndex; i < ancestor.childNodes.length; i++) {
90 | const node = ancestor.childNodes[i]
91 | if (node === startEl) {
92 | m.mergeArray(textNodes, m.getDescendantTextNodes(node))
93 | } else if (i === startIndex) {
94 | if (node.nodeType === Node.TEXT_NODE) {
95 | textNodes.push(node)
96 | } else if (node.nodeType === Node.ELEMENT_NODE) {
97 | m.mergeArray(textNodes,
98 | m.getAfterStartDescendantTextNodes(node, startEl))
99 | }
100 | } else if (node.nodeType === Node.TEXT_NODE) {
101 | textNodes.push(node)
102 | } else if (node.nodeType === Node.ELEMENT_NODE) {
103 | m.mergeArray(textNodes,
104 | m.getDescendantTextNodes(node))
105 | }
106 | }
107 | return textNodes
108 | },
109 | /**
110 | * func get the closest parent block node of a text node.
111 | * @param node
112 | * @return {Node}
113 | */
114 | getParentBlockNode (node) {
115 | const blockNodeNames = ['DIV', 'P', 'SECTION', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
116 | 'OL', 'UL', 'LI', 'TR', 'TD', 'TH', 'TBODY', 'THEAD', 'TABLE', 'ARTICLE', 'HEADER', 'FOOTER', 'BLOCKQUOTE']
117 | let container = node
118 | while (container) {
119 | if (blockNodeNames.includes(container.nodeName)) {
120 | break
121 | }
122 | container = container.parentNode
123 | }
124 | return container
125 | },
126 | isInlineElement (node) {
127 | const inlineNodeNames = ['A', 'ABBR', 'ACRONYM', 'B', 'CITE', 'CODE', 'EM', 'I',
128 | 'FONT', 'IMG', 'S', 'SMALL', 'SPAN', 'STRIKE', 'STRONG', 'U', 'SUB', 'SUP']
129 | return inlineNodeNames.includes(node.nodeName)
130 | },
131 | isInlineOrText (node) {
132 | let isInline = m.isInlineElement(node)
133 | let isText = node.nodeType === Node.TEXT_NODE
134 | return isInline || isText
135 | },
136 | /*
137 | * find all specify nodes in an ancestor through search opinions(unique attributes)
138 | * @param node
139 | * @param {obj}
140 | * must have key 'tagName'
141 | * @return {arr}
142 | **/
143 | getAllSpecifyNode (ancestor, searchOpinion) {
144 | const targetTagName = searchOpinion.tagName
145 | delete searchOpinion.tagName
146 | const tags = ancestor.querySelectorAll(targetTagName)
147 | const result = []
148 | tags.forEach(tag => {
149 | const opinionKeys = Object.keys(searchOpinion)
150 | let pass = true
151 | opinionKeys.forEach(opinion => {
152 | var a = tag.getAttribute(opinion)
153 | if (tag.getAttribute(opinion) !== searchOpinion[opinion]) {
154 | pass = false
155 | }
156 | })
157 | if (pass) {
158 | result.push(tag)
159 | }
160 | })
161 | return result
162 | },
163 | /*
164 | * func find the number of nesting ancestor which has same node name
165 | * @param {node} current node
166 | * @param {str} ancestor's tag name
167 | * @return {num} number
168 | **/
169 | howManyNestAncestorSameTag (node, ancestorNodeName) {
170 | let num = 0
171 | ancestorNodeName = ancestorNodeName.toUpperCase()
172 | while (node && (node !== am.editZone())) {
173 | if (node.nodeName === ancestorNodeName) {
174 | num++
175 | }
176 | node = node.parentNode
177 | }
178 | return num
179 | },
180 |
181 | /*
182 | * find an ancestor element through selector
183 | * @param {node} start at node
184 | * @param {str} ancestor element's selector
185 | * @param {boolean} either return first eligible element or last eligible element
186 | * default: true
187 | * @param {node} searching stop at the border element
188 | * default: editor's content zone
189 | * @return target ancestor element
190 | **/
191 | findSpecialAncestor (node, selector, firstOne = true, border) {
192 | let result
193 | let contentZone = am.editZone()
194 | border = border || contentZone
195 | while (node && (firstOne ? !result : true) && (node !== border)) {
196 | if (!border || !border.contains(node)) return
197 | let ancestors = Array.from(node.parentNode.querySelectorAll(selector))
198 | if (ancestors.length) {
199 | if (ancestors.includes(node)) {
200 | result = node
201 | }
202 | node = node.parentNode
203 | } else {
204 | node = node.parentNode
205 | }
206 | }
207 | return result
208 | },
209 | /*
210 | * find target style
211 | **/
212 | findSpecialAncestorStyle (node, styleName, firstOne = true, border) {
213 | let result
214 | let contentZone = am.editZone()
215 | border = border || contentZone
216 | while (node && (firstOne ? !result : true) && (node !== border)) {
217 | if (!border || !border.contains(node)) return
218 | if (node.style && node.style[styleName]) {
219 | result = node.style[styleName]
220 | }
221 | node = node.parentNode
222 | }
223 | return result
224 | },
225 | /*
226 | * find an ancestor element through style name and style value
227 | * @param style {obj} styleName: styleValue
228 | * @return
229 | **/
230 | findSpecialAncestorByStyle (node, style, firstOne = true, border) {
231 | let result
232 | let contentZone = am.editZone()
233 | border = border || contentZone
234 | while (node && (firstOne ? !result : true) && (node !== border)) {
235 | if (!border || !border.contains(node)) return
236 | let parent = node.parentNode
237 | let isTarget = true
238 | Object.keys(style).forEach(styleName => {
239 | if (style[styleName] !== parent.style[styleName]) {
240 | isTarget = false
241 | }
242 | })
243 | if (isTarget) {
244 | result = parent
245 | node = parent
246 | } else {
247 | node = parent
248 | }
249 | }
250 | return result
251 | },
252 |
253 | getNodeNum (ancestor, nodeName) {
254 | return ancestor.querySelectorAll(nodeName).length
255 | },
256 | /**
257 | * find all the text nodes in range
258 | */
259 | getAllTextNodesInRange() {
260 | const startContainer = this.range.startContainer
261 | const endContainer = this.range.endContainer
262 | const rootEl = this.range.commonAncestorContainer
263 | const textNodes = []
264 |
265 | if (startContainer === endContainer) {
266 | if (startContainer.nodeType === Node.TEXT_NODE) {
267 | return [startContainer]
268 | }
269 | const childNodes = startContainer.childNodes
270 | for (let i = this.range.startOffset; i < this.range.endOffset; i++) {
271 | m.mergeArray(textNodes, m.getDescendantTextNodes(childNodes[i]))
272 | }
273 | return textNodes
274 | }
275 |
276 | let startIndex = 0
277 | let endIndex = 0
278 | for (let i = 0; i < rootEl.childNodes.length; i++) {
279 | const node = rootEl.childNodes[i]
280 | if (node.contains(startContainer)) {
281 | startIndex = i
282 | }
283 | if (node.contains(endContainer)) {
284 | endIndex = i
285 | }
286 | }
287 |
288 | for (let i = startIndex; i <= endIndex; i++) {
289 | const node = rootEl.childNodes[i]
290 | if (i === startIndex) {
291 | if (node.nodeType === Node.TEXT_NODE) {
292 | textNodes.push(node)
293 | } else if (node.nodeType === Node.ELEMENT_NODE) {
294 | m.mergeArray(textNodes, m.getAfterStartDescendantTextNodes(node, startContainer))
295 | }
296 | } else if (i === endIndex) {
297 | if (node.nodeType === Node.TEXT_NODE) {
298 | textNodes.push(node)
299 | } else if (node.nodeType === Node.ELEMENT_NODE) {
300 | m.mergeArray(textNodes, m.getBeforeEndDescendantTextNodes(node, endContainer))
301 | }
302 | } else if (node.nodeType === Node.TEXT_NODE) {
303 | textNodes.push(node)
304 | } else if (node.nodeType === Node.ELEMENT_NODE) {
305 | m.mergeArray(textNodes, m.getDescendantTextNodes(node))
306 | }
307 | }
308 | return textNodes
309 | },
310 | /*
311 | * get the row which contains target element
312 | * @param {node} target element
313 | * @return {node} row
314 | **/
315 | getRow (node) {
316 | let rows = Array.from(am.editZone().children)
317 | let result
318 | rows.forEach(row => {
319 | if (row.contains(node) || row === node) {
320 | result = row
321 | }
322 | })
323 | return result
324 | },
325 | textToRow (node) {
326 | if (node.parentNode === am.editZone() && node.nodeType === Node.TEXT_NODE) {
327 | document.execCommand('formatBlock', false, constant.ROW_TAG_UPPERCASE)
328 | }
329 | return node
330 | },
331 | /*
332 | * get row, if there's not, create one
333 | **/
334 | forceGetRow (node) {
335 | node = m.textToRow(node)
336 | return m.getRow(node)
337 | },
338 | /*
339 | * return all rows
340 | **/
341 | getRows () {
342 | return Array.from(am.editZone().children)
343 | },
344 | /*
345 | * whether current node is a row
346 | **/
347 | isRow (node) {
348 | let rows = Array.from(am.editZone().children)
349 | return rows.includes(node)
350 | },
351 | /*
352 | * create a wrapper for inline element in same row
353 | **/
354 | createWrapperForInline (node, wrapperNodeName, seperateByBr = true) {
355 | if (!m.isInlineOrText(node)) return node
356 | let elements = [node]
357 | searchLeft()
358 | searchRight()
359 | let newRow = document.createElement(wrapperNodeName)
360 | elements.forEach((ele, index) => {
361 | if (index !== elements.length - 1) {
362 | newRow.appendChild(ele)
363 | return
364 | }
365 | let lastOne = ele.cloneNode(true)
366 | newRow.appendChild(lastOne)
367 | ele.parentNode.replaceChild(newRow, ele)
368 | })
369 |
370 | if (seperateByBr) {
371 | handlerBr(newRow.previousSibling, true)
372 | handlerBr(newRow.nextSibling, false)
373 | }
374 | return newRow
375 |
376 | function handlerBr(node, direction) {
377 | if (node && node.nodeName === 'BR') {
378 | let nextDir = direction ? 'previousSibling' : 'nextSibling'
379 | let targetNode = node[nextDir]
380 | if (!targetNode) return
381 | if (targetNode.nodeName === 'BR') {
382 | return handlerBr(targetNode, direction)
383 | }
384 | m.createWrapperForInline(targetNode, wrapperNodeName, seperateByBr)
385 | }
386 | }
387 | function searchLeft() {
388 | while (elements[0].previousSibling && m.isInlineOrText(elements[0].previousSibling)) {
389 | elements.unshift(elements[0].previousSibling)
390 | }
391 | }
392 | function searchRight() {
393 | while (elements[elements.length - 1].nextSibling && m.isInlineOrText(elements[elements.length - 1].nextSibling)) {
394 | elements.push(elements[elements.length - 1].nextSibling)
395 | }
396 | }
397 | },
398 | /*
399 | * get node's previous row which has content
400 | **/
401 | getPreviousRow (node) {
402 | let row = m.getRow(node)
403 | let preRow
404 | let rows = m.getRows()
405 | let rowIndex = null
406 | rows.forEach((curRow, index) => {
407 | if (curRow === row) {
408 | rowIndex = index
409 | }
410 | if (rowIndex === null) {
411 | if (curRow.innerHTML !== '') {
412 | preRow = curRow
413 | }
414 | }
415 | })
416 | return preRow
417 | },
418 | /*
419 | * whether target row is empty
420 | **/
421 | isEmptyRow (node) {
422 | let row = m.isRow(node) ? node : m.getRow(node)
423 | if (row.getAttribute) {
424 | if (typeof row.getAttribute('data-editor-todo') === 'string' || typeof row.getAttribute('data-editor-quote') === 'string') {
425 | return false
426 | }
427 | }
428 | return row.innerText.replace('\n', '').replace(/\u200B/g, '') === ''
429 | },
430 | /*
431 | * whether target node is empty
432 | **/
433 | isEmptyNode (node) {
434 | let ctn = typeof node.innerText === 'string' ? node.innerText : node.nodeValue
435 | if (typeof ctn !== 'string') return
436 | return ctn.replace('\n', '').replace(/\u200B/g, '') === ''
437 | },
438 | /*
439 | * try to collapse at target row
440 | **/
441 | collapseAtRow (node) {
442 | let row = m.isRow(node) ? node : m.getRow(node)
443 | let s = am.getSelection()
444 | try {
445 | s.collapse(row, 1)
446 | } catch (e) {
447 | s.collapse(row, 0)
448 | }
449 | },
450 | /*
451 | * searching nested ancestors till border to find the specified tagName
452 | * @param {node} from which node
453 | * @param {arr} includes tag names of target tag
454 | * @param {node} search border
455 | * @return {arr}
456 | **/
457 | findExistTagTillBorder (node, tagNamelist, border) {
458 | let result = []
459 | let contentZone = am.editZone()
460 | border = border || contentZone
461 | while (node && node !== border) {
462 | if (!border || !border.contains(node)) return
463 | let nodeName = node.nodeName
464 | if (nodeName && tagNamelist.includes(nodeName)) {
465 | if (!result.includes(nodeName)) {
466 | result.push(nodeName)
467 | }
468 | }
469 | node = node.parentNode
470 | }
471 | return result
472 | },
473 | /*
474 | * return a nested DOM data through a tag name list
475 | **/
476 | createNestDOMThroughList (list) {
477 | let result = {
478 | dom: null,
479 | deepestId: null,
480 | deepest: null
481 | }
482 | list.forEach((tag, index) => {
483 | let ele = document.createElement(tag)
484 | result[index] = ele
485 | let parent = result[index - 1]
486 | if (parent) {
487 | parent.appendChild(ele)
488 | }
489 | if (index === list.length - 1) {
490 | result.deepest = ele
491 | result.deepestId = am.createRandomId('deepest')
492 | result.dom = result['0']
493 | ele.id = result.deepestId
494 | ele.innerHTML = ''
495 | }
496 | })
497 | return result
498 | }
499 | }
500 |
501 | export default m
502 |
--------------------------------------------------------------------------------