Implement functions `getPlainText` and `setPlainText`
This commit is contained in:
parent
fce4c7910a
commit
5cc3665c47
|
@ -1,14 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { getPlainText, setPlainText } from './utils/domUtils.js'
|
||||||
import Icon from 'svelte-awesome'
|
import Icon from 'svelte-awesome'
|
||||||
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'
|
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { SEARCH_PROPERTY, SEARCH_VALUE } from './search'
|
import { SEARCH_PROPERTY, SEARCH_VALUE } from './search'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import debounce from 'lodash/debounce'
|
import debounce from 'lodash/debounce'
|
||||||
import { isUrl, stringConvert, valueType } from './utils/typeUtils'
|
import { isUrl, stringConvert, valueType } from './utils/typeUtils'
|
||||||
import { escapeHTML } from './utils/stringUtils.js'
|
|
||||||
import { updateProps } from './utils/updateProps.js'
|
import { updateProps } from './utils/updateProps.js'
|
||||||
import { unescapeHTML } from './utils/stringUtils'
|
|
||||||
import { getInnerText } from './utils/domUtils'
|
|
||||||
import { compileJSONPointer } from './utils/jsonPointer'
|
import { compileJSONPointer } from './utils/jsonPointer'
|
||||||
|
|
||||||
export let key = undefined // only applicable for object properties
|
export let key = undefined // only applicable for object properties
|
||||||
|
@ -52,12 +50,10 @@
|
||||||
? limited ? value.slice(0, limit) : value
|
? limited ? value.slice(0, limit) : value
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
$: escapedKey = escapeHTML(key, escapeUnicode)
|
|
||||||
$: escapedValue = escapeHTML(value, escapeUnicode)
|
|
||||||
$: valueIsUrl = isUrl(value)
|
$: valueIsUrl = isUrl(value)
|
||||||
|
|
||||||
$: keyClass = classnames('key', {
|
$: keyClass = classnames('key', {
|
||||||
empty: escapedKey.length === 0,
|
empty: key === '',
|
||||||
search: searchResult
|
search: searchResult
|
||||||
? !!searchResult[SEARCH_PROPERTY]
|
? !!searchResult[SEARCH_PROPERTY]
|
||||||
: false
|
: false
|
||||||
|
@ -67,20 +63,20 @@
|
||||||
$: valueClass = getValueClass(value, searchResult)
|
$: valueClass = getValueClass(value, searchResult)
|
||||||
|
|
||||||
$: if (domKey) {
|
$: if (domKey) {
|
||||||
if (document.activeElement !== domKey || escapedKey === '') {
|
if (document.activeElement !== domKey || key === '') {
|
||||||
// synchronize the innerText of the editable div with the escaped value,
|
// synchronize the innerText of the editable div with the escaped value,
|
||||||
// but only when the domValue does not have focus else we will ruin
|
// but only when the domValue does not have focus else we will ruin
|
||||||
// the cursor position.
|
// the cursor position.
|
||||||
domKey.innerText = escapedKey
|
setPlainText(domKey, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (domValue) {
|
$: if (domValue) {
|
||||||
if (document.activeElement !== domValue || escapedValue === '') {
|
if (document.activeElement !== domValue || value === '') {
|
||||||
// synchronize the innerText of the editable div with the escaped value,
|
// synchronize the innerText of the editable div with the escaped value,
|
||||||
// but only when the domValue does not have focus else we will ruin
|
// but only when the domValue does not have focus else we will ruin
|
||||||
// the cursor position.
|
// the cursor position.
|
||||||
domValue.innerText = escapedValue
|
setPlainText(domValue, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +85,7 @@
|
||||||
|
|
||||||
return classnames('value', type, {
|
return classnames('value', type, {
|
||||||
url: isUrl(value),
|
url: isUrl(value),
|
||||||
empty: escapedValue.length === 0,
|
empty: typeof value === 'string' && value.length === 0,
|
||||||
search: searchResult
|
search: searchResult
|
||||||
? !!searchResult[SEARCH_VALUE]
|
? !!searchResult[SEARCH_VALUE]
|
||||||
: false
|
: false
|
||||||
|
@ -101,7 +97,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateKey () {
|
function updateKey () {
|
||||||
const newKey = unescapeHTML(getInnerText(domKey))
|
const newKey = getPlainText(domKey)
|
||||||
|
|
||||||
// TODO: replace the onChangeKey callback with gobally managed JSONNode id's,
|
// TODO: replace the onChangeKey callback with gobally managed JSONNode id's,
|
||||||
// which are kept in sync with the json itself using JSONPatch
|
// which are kept in sync with the json itself using JSONPatch
|
||||||
|
@ -125,12 +121,12 @@
|
||||||
updateKeyDebounced.flush()
|
updateKeyDebounced.flush()
|
||||||
|
|
||||||
// make sure differences in escaped text like with new lines is updated
|
// make sure differences in escaped text like with new lines is updated
|
||||||
domKey.innerText = escapedValue
|
setPlainText(domKey, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the value from the DOM
|
// get the value from the DOM
|
||||||
function getValue () {
|
function getValue () {
|
||||||
const valueText = unescapeHTML(getInnerText(domValue))
|
const valueText = getPlainText(domValue)
|
||||||
return stringConvert(valueText) // TODO: implement support for type "string"
|
return stringConvert(valueText) // TODO: implement support for type "string"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,7 +155,7 @@
|
||||||
debouncedUpdateValue.flush()
|
debouncedUpdateValue.flush()
|
||||||
|
|
||||||
// make sure differences in escaped text like with new lines is updated
|
// make sure differences in escaped text like with new lines is updated
|
||||||
domValue.innerText = escapedValue
|
setPlainText(domValue, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleValueClick (event) {
|
function handleValueClick (event) {
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
|
|
||||||
$font-family: "dejavu sans mono", "droid sans mono", consolas, monaco, "lucida console", "courier new", courier, monospace, sans-serif;
|
$font-family: consolas, monaco, "lucida console", "courier new", "dejavu sans mono", "droid sans mono", courier, monospace, sans-serif;
|
||||||
$font-size: 10pt;
|
$font-size: 10pt;
|
||||||
$font-size-small: 8pt;
|
$font-size-small: 8pt;
|
||||||
$font-family-menu: arial, "sans-serif";
|
$font-family-menu: arial, "sans-serif";
|
||||||
$font-size-icon: 16px;
|
$font-size-icon: 16px;
|
||||||
|
|
||||||
|
|
||||||
$white: #fff;
|
$white: #fff;
|
||||||
$black: #1A1A1A;
|
$black: #1A1A1A;
|
||||||
$contentsMinHeight: 150px;
|
$contentsMinHeight: 150px;
|
||||||
|
|
|
@ -1,27 +1,148 @@
|
||||||
|
// TODO: write unit tests for getPlainText and setPlainText
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the plain text from an HTML element
|
||||||
|
* @param {Element} element An HTML DOM element like a DIV
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export function getPlainText(element) {
|
||||||
|
return unescapeHTML(traverseInnerText(element))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set plain text in an HTML element
|
||||||
|
* @param {Element} element An HTML DOM element like a DIV
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
export function setPlainText(element, text) {
|
||||||
|
element.innerHTML = escapeHTML(text)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* escape a text, such that it can be displayed safely in an HTML element
|
||||||
|
* @param {string} text
|
||||||
|
* @param {boolean} [escapeUnicode=false]
|
||||||
|
* @return {string} escapedText
|
||||||
|
*/
|
||||||
|
export function escapeHTML (text, escapeUnicode = false) {
|
||||||
|
if (typeof text !== 'string') {
|
||||||
|
return String(text)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let htmlEscaped = String(text)
|
||||||
|
if (escapeUnicode === true) {
|
||||||
|
// FIXME: should not unescape the just created non-breaking spaces \u00A0 ?
|
||||||
|
htmlEscaped = escapeUnicodeChars(htmlEscaped)
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlEscaped = htmlEscaped
|
||||||
|
.replace(/ {2}/g, ' \u00A0') // replace double space with an nbsp and space
|
||||||
|
.replace(/^ /, '\u00A0') // space at start
|
||||||
|
.replace(/ $/, '\u00A0') // space at end
|
||||||
|
|
||||||
|
const json = JSON.stringify(htmlEscaped)
|
||||||
|
return json.substring(1, json.length - 1) // remove enclosing double quotes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape unicode characters.
|
||||||
|
* For example input '\u2661' (length 1) will output '\\u2661' (length 5).
|
||||||
|
* @param {string} text
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
export function escapeUnicodeChars (text) {
|
||||||
|
// see https://www.wikiwand.com/en/UTF-16
|
||||||
|
// note: we leave surrogate pairs as two individual chars,
|
||||||
|
// as JSON doesn't interpret them as a single unicode char.
|
||||||
|
return text.replace(/[\u007F-\uFFFF]/g, function(c) {
|
||||||
|
return '\\u'+('0000' + c.charCodeAt(0).toString(16)).slice(-4)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unescape a string.
|
||||||
|
* @param {string} escapedText
|
||||||
|
* @return {string} text
|
||||||
|
*/
|
||||||
|
export function unescapeHTML (escapedText) {
|
||||||
|
const json = '"' + escapeJSON(escapedText) + '"'
|
||||||
|
const htmlEscaped = JSON.parse(json) // TODO: replace with a JSON.parse which does do linting and give an informative error
|
||||||
|
|
||||||
|
return htmlEscaped.replace(/\u00A0/g, ' ') // nbsp character
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* escape a text to make it a valid JSON string. The method will:
|
||||||
|
* - replace unescaped double quotes with '\"'
|
||||||
|
* - replace unescaped backslash with '\\'
|
||||||
|
* - replace returns with '\n'
|
||||||
|
* @param {string} text
|
||||||
|
* @return {string} escapedText
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
export function escapeJSON (text) {
|
||||||
|
// TODO: replace with some smart regex (only when a new solution is faster!)
|
||||||
|
let escaped = ''
|
||||||
|
let i = 0
|
||||||
|
while (i < text.length) {
|
||||||
|
let c = text.charAt(i)
|
||||||
|
if (c === '\n') {
|
||||||
|
escaped += '\\n'
|
||||||
|
}
|
||||||
|
else if (c === '\\') {
|
||||||
|
escaped += c
|
||||||
|
i++
|
||||||
|
|
||||||
|
c = text.charAt(i)
|
||||||
|
if (c === '' || '"\\/bfnrtu'.indexOf(c) === -1) {
|
||||||
|
escaped += '\\' // no valid escape character
|
||||||
|
}
|
||||||
|
escaped += c
|
||||||
|
}
|
||||||
|
else if (c === '"') {
|
||||||
|
escaped += '\\"'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
escaped += c
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return escaped
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the inner text of an HTML element (for example a div element)
|
* Get the inner text of an HTML element (for example a div element)
|
||||||
* @param {Element} element
|
* @param {Element} element
|
||||||
* @param {Object} [buffer]
|
* @param {Object} [buffer]
|
||||||
* @return {String} innerText
|
* @return {string} innerText
|
||||||
*/
|
*/
|
||||||
export function getInnerText (element, buffer) {
|
export function traverseInnerText (element, buffer) {
|
||||||
if (buffer === undefined) {
|
const first = (buffer === undefined)
|
||||||
|
if (first) {
|
||||||
buffer = {
|
buffer = {
|
||||||
'text': '',
|
_text: '',
|
||||||
'flush': function () {
|
flush: function () {
|
||||||
const text = this.text
|
const text = this._text
|
||||||
this.text = ''
|
this._text = ''
|
||||||
return text
|
return text
|
||||||
},
|
},
|
||||||
'set': function (text) {
|
set: function (text) {
|
||||||
this.text = text
|
this._text = text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// text node
|
// text node
|
||||||
if (element.nodeValue) {
|
if (element.nodeValue) {
|
||||||
return buffer.flush() + element.nodeValue
|
// remove return characters and the whitespace surrounding return characters
|
||||||
|
const trimmedValue = element.nodeValue.replace(/\s*\n\s*/g, '')
|
||||||
|
if (trimmedValue !== '') {
|
||||||
|
return buffer.flush() + trimmedValue
|
||||||
|
} else {
|
||||||
|
// ignore empty text
|
||||||
|
return ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// divs or other HTML elements
|
// divs or other HTML elements
|
||||||
|
@ -36,18 +157,18 @@ export function getInnerText (element, buffer) {
|
||||||
const prevChild = childNodes[i - 1]
|
const prevChild = childNodes[i - 1]
|
||||||
const prevName = prevChild ? prevChild.nodeName : undefined
|
const prevName = prevChild ? prevChild.nodeName : undefined
|
||||||
if (prevName && prevName !== 'DIV' && prevName !== 'P' && prevName !== 'BR') {
|
if (prevName && prevName !== 'DIV' && prevName !== 'P' && prevName !== 'BR') {
|
||||||
|
if (innerText !== '') {
|
||||||
innerText += '\n'
|
innerText += '\n'
|
||||||
|
}
|
||||||
buffer.flush()
|
buffer.flush()
|
||||||
}
|
}
|
||||||
innerText += getInnerText(child, buffer)
|
innerText += traverseInnerText(child, buffer)
|
||||||
buffer.set('\n')
|
buffer.set('\n')
|
||||||
}
|
} else if (child.nodeName === 'BR') {
|
||||||
else if (child.nodeName === 'BR') {
|
|
||||||
innerText += buffer.flush()
|
innerText += buffer.flush()
|
||||||
buffer.set('\n')
|
buffer.set('\n')
|
||||||
}
|
} else {
|
||||||
else {
|
innerText += traverseInnerText(child, buffer)
|
||||||
innerText += getInnerText(child, buffer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,5 +177,4 @@ export function getInnerText (element, buffer) {
|
||||||
|
|
||||||
// br or unknown
|
// br or unknown
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { escapeHTML, unescapeHTML } from './domUtils.js'
|
||||||
|
import { expect } from './testUtils.js' // FIXME: replace jest with mocha tests, or move to jest
|
||||||
|
|
||||||
|
const test = it // TODO: replace jest with mocha tests, or move to jest
|
||||||
|
|
||||||
|
test('escapeHTML', () => {
|
||||||
|
expect(escapeHTML(' hello ')).toEqual('\u00A0\u00A0 hello \u00A0')
|
||||||
|
expect(escapeHTML('\u00A0 hello')).toEqual('\u00A0 hello')
|
||||||
|
expect(escapeHTML('hello\nworld')).toEqual('hello\\nworld')
|
||||||
|
|
||||||
|
// TODO: test escapeHTML more thoroughly
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unescapeHTML', () => {
|
||||||
|
expect(unescapeHTML(' \u00A0 hello \u00A0')).toEqual(' hello ')
|
||||||
|
expect(unescapeHTML('\u00A0 hello')).toEqual(' hello')
|
||||||
|
|
||||||
|
expect(unescapeHTML('hello\\nworld')).toEqual('hello\nworld')
|
||||||
|
|
||||||
|
// TODO: test unescapeHTML more thoroughly
|
||||||
|
})
|
|
@ -1,96 +1,3 @@
|
||||||
/**
|
|
||||||
* escape a text, such that it can be displayed safely in an HTML element
|
|
||||||
* @param {String} text
|
|
||||||
* @param {boolean} [escapeUnicode=false]
|
|
||||||
* @return {String} escapedText
|
|
||||||
*/
|
|
||||||
export function escapeHTML (text, escapeUnicode = false) {
|
|
||||||
if (typeof text !== 'string') {
|
|
||||||
return String(text)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let htmlEscaped = String(text)
|
|
||||||
if (escapeUnicode === true) {
|
|
||||||
// FIXME: should not unescape the just created non-breaking spaces \u00A0 ?
|
|
||||||
htmlEscaped = escapeUnicodeChars(htmlEscaped)
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlEscaped = htmlEscaped
|
|
||||||
.replace(/ {2}/g, ' \u00A0') // replace double space with an nbsp and space
|
|
||||||
.replace(/^ /, '\u00A0') // space at start
|
|
||||||
.replace(/ $/, '\u00A0') // space at end
|
|
||||||
|
|
||||||
const json = JSON.stringify(htmlEscaped)
|
|
||||||
return json.substring(1, json.length - 1) // remove enclosing double quotes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape unicode characters.
|
|
||||||
* For example input '\u2661' (length 1) will output '\\u2661' (length 5).
|
|
||||||
* @param {string} text
|
|
||||||
* @return {string}
|
|
||||||
*/
|
|
||||||
export function escapeUnicodeChars (text) {
|
|
||||||
// see https://www.wikiwand.com/en/UTF-16
|
|
||||||
// note: we leave surrogate pairs as two individual chars,
|
|
||||||
// as JSON doesn't interpret them as a single unicode char.
|
|
||||||
return text.replace(/[\u007F-\uFFFF]/g, function(c) {
|
|
||||||
return '\\u'+('0000' + c.charCodeAt(0).toString(16)).slice(-4)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* unescape a string.
|
|
||||||
* @param {String} escapedText
|
|
||||||
* @return {String} text
|
|
||||||
*/
|
|
||||||
export function unescapeHTML (escapedText) {
|
|
||||||
const json = '"' + escapeJSON(escapedText) + '"'
|
|
||||||
const htmlEscaped = JSON.parse(json) // TODO: replace with a JSON.parse which does do linting and give an informative error
|
|
||||||
|
|
||||||
return htmlEscaped.replace(/\u00A0/g, ' ') // nbsp character
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* escape a text to make it a valid JSON string. The method will:
|
|
||||||
* - replace unescaped double quotes with '\"'
|
|
||||||
* - replace unescaped backslash with '\\'
|
|
||||||
* - replace returns with '\n'
|
|
||||||
* @param {String} text
|
|
||||||
* @return {String} escapedText
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
export function escapeJSON (text) {
|
|
||||||
// TODO: replace with some smart regex (only when a new solution is faster!)
|
|
||||||
let escaped = ''
|
|
||||||
let i = 0
|
|
||||||
while (i < text.length) {
|
|
||||||
let c = text.charAt(i)
|
|
||||||
if (c === '\n') {
|
|
||||||
escaped += '\\n'
|
|
||||||
}
|
|
||||||
else if (c === '\\') {
|
|
||||||
escaped += c
|
|
||||||
i++
|
|
||||||
|
|
||||||
c = text.charAt(i)
|
|
||||||
if (c === '' || '"\\/bfnrtu'.indexOf(c) === -1) {
|
|
||||||
escaped += '\\' // no valid escape character
|
|
||||||
}
|
|
||||||
escaped += c
|
|
||||||
}
|
|
||||||
else if (c === '"') {
|
|
||||||
escaped += '\\"'
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
escaped += c
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
return escaped
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a unique name. Suffix the name with ' (copy)', '(copy 2)', etc
|
* Find a unique name. Suffix the name with ' (copy)', '(copy 2)', etc
|
||||||
|
|
|
@ -1,25 +1,13 @@
|
||||||
import { compareStrings, duplicateInText, escapeHTML, findUniqueName, toCapital, unescapeHTML } from './stringUtils.js'
|
import {
|
||||||
|
compareStrings,
|
||||||
|
duplicateInText,
|
||||||
|
findUniqueName,
|
||||||
|
toCapital
|
||||||
|
} from './stringUtils.js'
|
||||||
import { expect } from './testUtils.js' // FIXME: replace jest with mocha tests, or move to jest
|
import { expect } from './testUtils.js' // FIXME: replace jest with mocha tests, or move to jest
|
||||||
|
|
||||||
const test = it // TODO: replace jest with mocha tests, or move to jest
|
const test = it // TODO: replace jest with mocha tests, or move to jest
|
||||||
|
|
||||||
test('escapeHTML', () => {
|
|
||||||
expect(escapeHTML(' hello ')).toEqual('\u00A0\u00A0 hello \u00A0')
|
|
||||||
expect(escapeHTML('\u00A0 hello')).toEqual('\u00A0 hello')
|
|
||||||
expect(escapeHTML('hello\nworld')).toEqual('hello\\nworld')
|
|
||||||
|
|
||||||
// TODO: test escapeHTML more thoroughly
|
|
||||||
})
|
|
||||||
|
|
||||||
test('unescapeHTML', () => {
|
|
||||||
expect(unescapeHTML(' \u00A0 hello \u00A0')).toEqual(' hello ')
|
|
||||||
expect(unescapeHTML('\u00A0 hello')).toEqual(' hello')
|
|
||||||
|
|
||||||
expect(unescapeHTML('hello\\nworld')).toEqual('hello\nworld')
|
|
||||||
|
|
||||||
// TODO: test unescapeHTML more thoroughly
|
|
||||||
})
|
|
||||||
|
|
||||||
test('findUniqueName', () => {
|
test('findUniqueName', () => {
|
||||||
expect(findUniqueName('other', {'a': true, 'b': true, 'c': true})).toEqual('other')
|
expect(findUniqueName('other', {'a': true, 'b': true, 'c': true})).toEqual('other')
|
||||||
expect(findUniqueName('b', {'a': true, 'b': true, 'c': true})).toEqual('b (copy)')
|
expect(findUniqueName('b', {'a': true, 'b': true, 'c': true})).toEqual('b (copy)')
|
||||||
|
|
Loading…
Reference in New Issue