Implement functions `getPlainText` and `setPlainText`
This commit is contained in:
parent
fce4c7910a
commit
5cc3665c47
|
@ -1,14 +1,12 @@
|
|||
<script>
|
||||
import { getPlainText, setPlainText } from './utils/domUtils.js'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'
|
||||
import { SEARCH_PROPERTY, SEARCH_VALUE } from './search'
|
||||
import classnames from 'classnames'
|
||||
import debounce from 'lodash/debounce'
|
||||
import { isUrl, stringConvert, valueType } from './utils/typeUtils'
|
||||
import { escapeHTML } from './utils/stringUtils.js'
|
||||
import { updateProps } from './utils/updateProps.js'
|
||||
import { unescapeHTML } from './utils/stringUtils'
|
||||
import { getInnerText } from './utils/domUtils'
|
||||
import { compileJSONPointer } from './utils/jsonPointer'
|
||||
|
||||
export let key = undefined // only applicable for object properties
|
||||
|
@ -52,12 +50,10 @@
|
|||
? limited ? value.slice(0, limit) : value
|
||||
: undefined
|
||||
|
||||
$: escapedKey = escapeHTML(key, escapeUnicode)
|
||||
$: escapedValue = escapeHTML(value, escapeUnicode)
|
||||
$: valueIsUrl = isUrl(value)
|
||||
|
||||
$: keyClass = classnames('key', {
|
||||
empty: escapedKey.length === 0,
|
||||
empty: key === '',
|
||||
search: searchResult
|
||||
? !!searchResult[SEARCH_PROPERTY]
|
||||
: false
|
||||
|
@ -67,20 +63,20 @@
|
|||
$: valueClass = getValueClass(value, searchResult)
|
||||
|
||||
$: if (domKey) {
|
||||
if (document.activeElement !== domKey || escapedKey === '') {
|
||||
if (document.activeElement !== domKey || key === '') {
|
||||
// synchronize the innerText of the editable div with the escaped value,
|
||||
// but only when the domValue does not have focus else we will ruin
|
||||
// the cursor position.
|
||||
domKey.innerText = escapedKey
|
||||
setPlainText(domKey, key)
|
||||
}
|
||||
}
|
||||
|
||||
$: if (domValue) {
|
||||
if (document.activeElement !== domValue || escapedValue === '') {
|
||||
if (document.activeElement !== domValue || 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
|
||||
// the cursor position.
|
||||
domValue.innerText = escapedValue
|
||||
setPlainText(domValue, value)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,7 +85,7 @@
|
|||
|
||||
return classnames('value', type, {
|
||||
url: isUrl(value),
|
||||
empty: escapedValue.length === 0,
|
||||
empty: typeof value === 'string' && value.length === 0,
|
||||
search: searchResult
|
||||
? !!searchResult[SEARCH_VALUE]
|
||||
: false
|
||||
|
@ -101,7 +97,7 @@
|
|||
}
|
||||
|
||||
function updateKey () {
|
||||
const newKey = unescapeHTML(getInnerText(domKey))
|
||||
const newKey = getPlainText(domKey)
|
||||
|
||||
// TODO: replace the onChangeKey callback with gobally managed JSONNode id's,
|
||||
// which are kept in sync with the json itself using JSONPatch
|
||||
|
@ -125,12 +121,12 @@
|
|||
updateKeyDebounced.flush()
|
||||
|
||||
// make sure differences in escaped text like with new lines is updated
|
||||
domKey.innerText = escapedValue
|
||||
setPlainText(domKey, key)
|
||||
}
|
||||
|
||||
// get the value from the DOM
|
||||
function getValue () {
|
||||
const valueText = unescapeHTML(getInnerText(domValue))
|
||||
const valueText = getPlainText(domValue)
|
||||
return stringConvert(valueText) // TODO: implement support for type "string"
|
||||
}
|
||||
|
||||
|
@ -159,7 +155,7 @@
|
|||
debouncedUpdateValue.flush()
|
||||
|
||||
// make sure differences in escaped text like with new lines is updated
|
||||
domValue.innerText = escapedValue
|
||||
setPlainText(domValue, value)
|
||||
}
|
||||
|
||||
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-small: 8pt;
|
||||
$font-family-menu: arial, "sans-serif";
|
||||
$font-size-icon: 16px;
|
||||
|
||||
|
||||
$white: #fff;
|
||||
$black: #1A1A1A;
|
||||
$contentsMinHeight: 150px;
|
||||
|
|
|
@ -1,60 +1,180 @@
|
|||
// 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)
|
||||
* @param {Element} element
|
||||
* @param {Object} [buffer]
|
||||
* @return {String} innerText
|
||||
* @return {string} innerText
|
||||
*/
|
||||
export function getInnerText (element, buffer) {
|
||||
if (buffer === undefined) {
|
||||
buffer = {
|
||||
'text': '',
|
||||
'flush': function () {
|
||||
const text = this.text
|
||||
this.text = ''
|
||||
return text
|
||||
},
|
||||
'set': function (text) {
|
||||
this.text = text
|
||||
}
|
||||
export function traverseInnerText (element, buffer) {
|
||||
const first = (buffer === undefined)
|
||||
if (first) {
|
||||
buffer = {
|
||||
_text: '',
|
||||
flush: function () {
|
||||
const text = this._text
|
||||
this._text = ''
|
||||
return text
|
||||
},
|
||||
set: function (text) {
|
||||
this._text = text
|
||||
}
|
||||
}
|
||||
|
||||
// text node
|
||||
if (element.nodeValue) {
|
||||
return buffer.flush() + element.nodeValue
|
||||
}
|
||||
|
||||
// divs or other HTML elements
|
||||
if (element.hasChildNodes()) {
|
||||
const childNodes = element.childNodes
|
||||
let innerText = ''
|
||||
|
||||
for (let i = 0, iMax = childNodes.length; i < iMax; i++) {
|
||||
const child = childNodes[i]
|
||||
|
||||
if (child.nodeName === 'DIV' || child.nodeName === 'P') {
|
||||
const prevChild = childNodes[i - 1]
|
||||
const prevName = prevChild ? prevChild.nodeName : undefined
|
||||
if (prevName && prevName !== 'DIV' && prevName !== 'P' && prevName !== 'BR') {
|
||||
innerText += '\n'
|
||||
buffer.flush()
|
||||
}
|
||||
innerText += getInnerText(child, buffer)
|
||||
buffer.set('\n')
|
||||
}
|
||||
else if (child.nodeName === 'BR') {
|
||||
innerText += buffer.flush()
|
||||
buffer.set('\n')
|
||||
}
|
||||
else {
|
||||
innerText += getInnerText(child, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
return innerText
|
||||
}
|
||||
|
||||
// br or unknown
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
// text node
|
||||
if (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
|
||||
if (element.hasChildNodes()) {
|
||||
const childNodes = element.childNodes
|
||||
let innerText = ''
|
||||
|
||||
for (let i = 0, iMax = childNodes.length; i < iMax; i++) {
|
||||
const child = childNodes[i]
|
||||
|
||||
if (child.nodeName === 'DIV' || child.nodeName === 'P') {
|
||||
const prevChild = childNodes[i - 1]
|
||||
const prevName = prevChild ? prevChild.nodeName : undefined
|
||||
if (prevName && prevName !== 'DIV' && prevName !== 'P' && prevName !== 'BR') {
|
||||
if (innerText !== '') {
|
||||
innerText += '\n'
|
||||
}
|
||||
buffer.flush()
|
||||
}
|
||||
innerText += traverseInnerText(child, buffer)
|
||||
buffer.set('\n')
|
||||
} else if (child.nodeName === 'BR') {
|
||||
innerText += buffer.flush()
|
||||
buffer.set('\n')
|
||||
} else {
|
||||
innerText += traverseInnerText(child, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
return innerText
|
||||
}
|
||||
|
||||
// br or unknown
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
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', () => {
|
||||
expect(findUniqueName('other', {'a': true, 'b': true, 'c': true})).toEqual('other')
|
||||
expect(findUniqueName('b', {'a': true, 'b': true, 'c': true})).toEqual('b (copy)')
|
||||
|
|
Loading…
Reference in New Issue