@livingdocs/editable.js 3.1.7 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editable.js +1 -1
- package/dist/editable.js.map +1 -1
- package/package.json +7 -8
- package/src/block.js +6 -1
- package/src/clipboard.js +29 -21
- package/src/config.js +3 -0
- package/src/core.js +19 -11
- package/src/selection.js +3 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livingdocs/editable.js",
|
|
3
3
|
"description": "Friendly contenteditable API",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.2.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@babel/runtime": "^7.16.7",
|
|
7
7
|
"bowser": "2.11.0",
|
|
@@ -21,8 +21,6 @@
|
|
|
21
21
|
"css-loader": "^6.5.1",
|
|
22
22
|
"eslint": "^8.8.0",
|
|
23
23
|
"eslint-plugin-react": "^7.28.0",
|
|
24
|
-
"font-awesome": "^4.7.0",
|
|
25
|
-
"gh-pages": "^5.0.0",
|
|
26
24
|
"karma": "^6.3.13",
|
|
27
25
|
"karma-chrome-launcher": "^3.1.0",
|
|
28
26
|
"karma-coverage": "^2.1.0",
|
|
@@ -37,7 +35,7 @@
|
|
|
37
35
|
"react": "^18.0.0",
|
|
38
36
|
"react-dom": "^18.0.0",
|
|
39
37
|
"react-transition-group": "^4.4.2",
|
|
40
|
-
"rimraf": "^
|
|
38
|
+
"rimraf": "^5.0.0",
|
|
41
39
|
"sinon": "^15.0.0",
|
|
42
40
|
"style-loader": "^3.3.1",
|
|
43
41
|
"url-loader": "^4.1.1",
|
|
@@ -46,7 +44,8 @@
|
|
|
46
44
|
"webpack-dev-server": "^4.7.3"
|
|
47
45
|
},
|
|
48
46
|
"engines": {
|
|
49
|
-
"node": ">=12"
|
|
47
|
+
"node": ">=12",
|
|
48
|
+
"npm": ">=6"
|
|
50
49
|
},
|
|
51
50
|
"files": [
|
|
52
51
|
"src",
|
|
@@ -83,11 +82,11 @@
|
|
|
83
82
|
"isCi": true,
|
|
84
83
|
"name": "Drone",
|
|
85
84
|
"service": "drone",
|
|
86
|
-
"commit": "
|
|
87
|
-
"build": "
|
|
85
|
+
"commit": "82a67b054b04e557628b02fa4a1e9f5d605eae19",
|
|
86
|
+
"build": "1218",
|
|
88
87
|
"branch": "master",
|
|
89
88
|
"isPr": false,
|
|
90
89
|
"slug": "livingdocsIO/editable.js",
|
|
91
|
-
"date": "2023-
|
|
90
|
+
"date": "2023-09-19T06:19:31.745Z"
|
|
92
91
|
}
|
|
93
92
|
}
|
package/src/block.js
CHANGED
|
@@ -7,11 +7,12 @@ const state = {}
|
|
|
7
7
|
export const next = getSibling('nextElementSibling')
|
|
8
8
|
export const previous = getSibling('previousElementSibling')
|
|
9
9
|
|
|
10
|
-
export function init (elem, {normalize, shouldSpellcheck}) {
|
|
10
|
+
export function init (elem, {normalize, plainText, shouldSpellcheck}) {
|
|
11
11
|
setBlockId(elem)
|
|
12
12
|
|
|
13
13
|
elem.setAttribute('contenteditable', true)
|
|
14
14
|
elem.setAttribute('spellcheck', Boolean(shouldSpellcheck))
|
|
15
|
+
elem.setAttribute('data-plaintext', Boolean(plainText))
|
|
15
16
|
|
|
16
17
|
elem.classList.remove(config.editableDisabledClass)
|
|
17
18
|
elem.classList.add(config.editableClass)
|
|
@@ -23,6 +24,7 @@ export function init (elem, {normalize, shouldSpellcheck}) {
|
|
|
23
24
|
export function disable (elem) {
|
|
24
25
|
elem.removeAttribute('contenteditable')
|
|
25
26
|
elem.removeAttribute('spellcheck')
|
|
27
|
+
elem.removeAttribute('data-plaintext')
|
|
26
28
|
|
|
27
29
|
setState(elem, undefined)
|
|
28
30
|
|
|
@@ -30,6 +32,9 @@ export function disable (elem) {
|
|
|
30
32
|
elem.classList.add(config.editableDisabledClass)
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export function isPlainTextBlock (elem) {
|
|
36
|
+
return elem.getAttribute('data-plaintext') === 'true'
|
|
37
|
+
}
|
|
33
38
|
|
|
34
39
|
export function setBlockId (elem) {
|
|
35
40
|
if (!elem.hasAttribute('data-editable')) {
|
package/src/clipboard.js
CHANGED
|
@@ -2,8 +2,9 @@ import config from './config'
|
|
|
2
2
|
import * as string from './util/string'
|
|
3
3
|
import * as nodeType from './node-type'
|
|
4
4
|
import * as quotes from './quotes'
|
|
5
|
+
import {isPlainTextBlock} from './block'
|
|
5
6
|
|
|
6
|
-
let allowedElements, requiredAttributes, transformElements, blockLevelElements, replaceQuotes
|
|
7
|
+
let allowedElements, allowedPlainTextElements, requiredAttributes, transformElements, blockLevelElements, replaceQuotes
|
|
7
8
|
let splitIntoBlocks, blacklistedElements
|
|
8
9
|
const whitespaceOnly = /^\s*$/
|
|
9
10
|
const blockPlaceholder = '<!-- BLOCK -->'
|
|
@@ -13,6 +14,7 @@ updateConfig(config)
|
|
|
13
14
|
export function updateConfig (conf) {
|
|
14
15
|
const rules = conf.pastedHtmlRules
|
|
15
16
|
allowedElements = rules.allowedElements || {}
|
|
17
|
+
allowedPlainTextElements = rules.allowedPlainTextElements || {}
|
|
16
18
|
requiredAttributes = rules.requiredAttributes || {}
|
|
17
19
|
transformElements = rules.transformElements || {}
|
|
18
20
|
blacklistedElements = rules.blacklistedElements || []
|
|
@@ -25,9 +27,9 @@ export function updateConfig (conf) {
|
|
|
25
27
|
rules.splitIntoBlocks.forEach((name) => { splitIntoBlocks[name] = true })
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
export function paste (
|
|
29
|
-
const document =
|
|
30
|
-
|
|
30
|
+
export function paste (block, cursor, clipboardContent) {
|
|
31
|
+
const document = block.ownerDocument
|
|
32
|
+
block.setAttribute(config.pastingAttribute, true)
|
|
31
33
|
|
|
32
34
|
if (cursor.isSelection) {
|
|
33
35
|
cursor = cursor.deleteExactSurroundingTags()
|
|
@@ -39,9 +41,10 @@ export function paste (element, cursor, clipboardContent) {
|
|
|
39
41
|
const pasteHolder = document.createElement('div')
|
|
40
42
|
pasteHolder.innerHTML = clipboardContent
|
|
41
43
|
|
|
42
|
-
const
|
|
44
|
+
const isPlainText = isPlainTextBlock(block)
|
|
45
|
+
const blocks = parseContent(pasteHolder, {plainText: isPlainText})
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
block.removeAttribute(config.pastingAttribute)
|
|
45
48
|
return {blocks, cursor}
|
|
46
49
|
}
|
|
47
50
|
|
|
@@ -55,30 +58,35 @@ export function paste (element, cursor, clipboardContent) {
|
|
|
55
58
|
* @param {DOM node} A container where the pasted content is located.
|
|
56
59
|
* @returns {Array of Strings} An array of cleaned innerHTML like strings.
|
|
57
60
|
*/
|
|
58
|
-
export function parseContent (element) {
|
|
61
|
+
export function parseContent (element, {plainText = false} = {}) {
|
|
62
|
+
const options = {
|
|
63
|
+
allowedElements: plainText ? allowedPlainTextElements : allowedElements,
|
|
64
|
+
keepInternalRelativeLinks: plainText ? false : keepInternalRelativeLinks
|
|
65
|
+
}
|
|
66
|
+
|
|
59
67
|
// Filter pasted content
|
|
60
|
-
return filterHtmlElements(element)
|
|
68
|
+
return filterHtmlElements(element, options)
|
|
61
69
|
// Handle Blocks
|
|
62
70
|
.split(blockPlaceholder)
|
|
63
71
|
.map((entry) => string.trim(cleanWhitespace(replaceAllQuotes(entry))))
|
|
64
72
|
.filter((entry) => !whitespaceOnly.test(entry))
|
|
65
73
|
}
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
function filterHtmlElements (elem, options) {
|
|
68
76
|
return Array.from(elem.childNodes).reduce((content, child) => {
|
|
69
77
|
if (blacklistedElements.indexOf(child.nodeName.toLowerCase()) !== -1) {
|
|
70
78
|
return ''
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
// Keep internal relative links relative (on paste).
|
|
74
|
-
if (keepInternalRelativeLinks && child.nodeName === 'A' && child.href) {
|
|
82
|
+
if (options.keepInternalRelativeLinks && child.nodeName === 'A' && child.href) {
|
|
75
83
|
const stripInternalHost = child.getAttribute('href').replace(window.location.origin, '')
|
|
76
84
|
child.setAttribute('href', stripInternalHost)
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
if (child.nodeType === nodeType.elementNode) {
|
|
80
|
-
const childContent = filterHtmlElements(child)
|
|
81
|
-
return content + conditionalNodeWrap(child, childContent)
|
|
88
|
+
const childContent = filterHtmlElements(child, options)
|
|
89
|
+
return content + conditionalNodeWrap(child, childContent, options)
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
// Escape HTML characters <, > and &
|
|
@@ -87,11 +95,11 @@ export function filterHtmlElements (elem) {
|
|
|
87
95
|
}, '')
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
|
|
98
|
+
function conditionalNodeWrap (child, content, options) {
|
|
91
99
|
let nodeName = child.nodeName.toLowerCase()
|
|
92
100
|
nodeName = transformNodeName(nodeName)
|
|
93
101
|
|
|
94
|
-
if (shouldKeepNode(nodeName, child)) {
|
|
102
|
+
if (shouldKeepNode(nodeName, child, options)) {
|
|
95
103
|
const attributes = filterAttributes(nodeName, child)
|
|
96
104
|
|
|
97
105
|
if (nodeName === 'br') return `<${nodeName + attributes}>`
|
|
@@ -115,7 +123,7 @@ export function conditionalNodeWrap (child, content) {
|
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
// returns string of concatenated attributes e.g. 'target="_blank" rel="nofollow" href="/test.com"'
|
|
118
|
-
|
|
126
|
+
function filterAttributes (nodeName, node) {
|
|
119
127
|
return Array.from(node.attributes).reduce((attributes, {name, value}) => {
|
|
120
128
|
if (allowedElements[nodeName][name] && value) {
|
|
121
129
|
return `${attributes} ${name}="${value}"`
|
|
@@ -124,22 +132,22 @@ export function filterAttributes (nodeName, node) {
|
|
|
124
132
|
}, '')
|
|
125
133
|
}
|
|
126
134
|
|
|
127
|
-
|
|
135
|
+
function transformNodeName (nodeName) {
|
|
128
136
|
return transformElements[nodeName] || nodeName
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
|
|
139
|
+
function hasRequiredAttributes (nodeName, node) {
|
|
132
140
|
const requiredAttrs = requiredAttributes[nodeName]
|
|
133
141
|
if (!requiredAttrs) return true
|
|
134
142
|
|
|
135
143
|
return !requiredAttrs.some((name) => !node.getAttribute(name))
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
return allowedElements[nodeName] && hasRequiredAttributes(nodeName, node)
|
|
146
|
+
function shouldKeepNode (nodeName, node, options) {
|
|
147
|
+
return options.allowedElements[nodeName] && hasRequiredAttributes(nodeName, node)
|
|
140
148
|
}
|
|
141
149
|
|
|
142
|
-
|
|
150
|
+
function cleanWhitespace (str) {
|
|
143
151
|
return str
|
|
144
152
|
.replace(/\n/g, ' ')
|
|
145
153
|
.replace(/ {2,}/g, ' ')
|
|
@@ -149,7 +157,7 @@ export function cleanWhitespace (str) {
|
|
|
149
157
|
))
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
|
|
160
|
+
function replaceAllQuotes (str) {
|
|
153
161
|
if (replaceQuotes.quotes || replaceQuotes.singleQuotes || replaceQuotes.apostrophe) {
|
|
154
162
|
return quotes.replaceAllQuotes(str, replaceQuotes)
|
|
155
163
|
}
|
package/src/config.js
CHANGED
package/src/core.js
CHANGED
|
@@ -93,10 +93,11 @@ export class Editable {
|
|
|
93
93
|
* @param {HTMLElement|Array(HTMLElement)|String} target A HTMLElement, an
|
|
94
94
|
* array of HTMLElement or a query selector representing the target where
|
|
95
95
|
* the API should be added on.
|
|
96
|
+
* @param {normalize, plainText} options Block specific configuration
|
|
96
97
|
* @chainable
|
|
97
98
|
*/
|
|
98
|
-
add (target) {
|
|
99
|
-
this.enable(target)
|
|
99
|
+
add (target, options) {
|
|
100
|
+
this.enable(target, options)
|
|
100
101
|
// TODO check css whitespace settings
|
|
101
102
|
return this
|
|
102
103
|
}
|
|
@@ -143,19 +144,26 @@ export class Editable {
|
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
* Adds the Editable.JS API to the given target elements.
|
|
148
|
+
*
|
|
149
|
+
* @method enable
|
|
150
|
+
* @param { HTMLElement | undefined } target editable root element(s)
|
|
151
|
+
* If no param is specified all editables marked as disabled are enabled.
|
|
152
|
+
* @param {boolean} normalize normalizes target content (legacy param)
|
|
153
|
+
* @param {boolean} options.normalize normalizes target content
|
|
154
|
+
* @param {boolean} options.plainText prevents text formatting for block
|
|
155
|
+
* @chainable
|
|
156
|
+
*/
|
|
157
|
+
enable (target, options) {
|
|
158
|
+
const {
|
|
159
|
+
normalize = typeof options === 'boolean' ? options : false,
|
|
160
|
+
plainText = false
|
|
161
|
+
} = options ?? {}
|
|
154
162
|
const shouldSpellcheck = this.config.browserSpellcheck
|
|
155
163
|
const targets = domArray(target || `.${config.editableDisabledClass}`, this.win.document)
|
|
156
164
|
|
|
157
165
|
for (const element of targets) {
|
|
158
|
-
block.init(element, {normalize, shouldSpellcheck})
|
|
166
|
+
block.init(element, {normalize, plainText, shouldSpellcheck})
|
|
159
167
|
this.dispatcher.notify('init', element)
|
|
160
168
|
}
|
|
161
169
|
|
package/src/selection.js
CHANGED
|
@@ -3,6 +3,7 @@ import 'rangy/lib/rangy-textrange'
|
|
|
3
3
|
import Cursor from './cursor'
|
|
4
4
|
import * as content from './content'
|
|
5
5
|
import * as parser from './parser'
|
|
6
|
+
import * as block from './block'
|
|
6
7
|
import config from './config'
|
|
7
8
|
import highlightSupport from './highlight-support'
|
|
8
9
|
import highlightText from './highlight-text'
|
|
@@ -144,6 +145,7 @@ export default class Selection extends Cursor {
|
|
|
144
145
|
|
|
145
146
|
// toggle('<em>')
|
|
146
147
|
toggle (elem) {
|
|
148
|
+
if (block.isPlainTextBlock(this.host)) return
|
|
147
149
|
elem = this.adoptElement(elem)
|
|
148
150
|
this.range = content.toggleTag(this.host, this.range, elem)
|
|
149
151
|
this.setSelection()
|
|
@@ -307,6 +309,7 @@ export default class Selection extends Cursor {
|
|
|
307
309
|
// the same tagName is affecting the selection this tag will be
|
|
308
310
|
// remove first.
|
|
309
311
|
forceWrap (elem) {
|
|
312
|
+
if (block.isPlainTextBlock(this.host)) return
|
|
310
313
|
elem = this.adoptElement(elem)
|
|
311
314
|
this.range = content.forceWrap(this.host, this.range, elem)
|
|
312
315
|
this.setSelection()
|