From 48dc1eb1a43d4ac95dbb58a4ba9876d37b95ffc0 Mon Sep 17 00:00:00 2001 From: Lukas Buenger Date: Wed, 13 Mar 2019 18:24:52 +0100 Subject: [PATCH 1/6] feat: rangy/TextRange module added and exposed in selection --- src/selection.js | 77 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/src/selection.js b/src/selection.js index b03d693e..244da66f 100644 --- a/src/selection.js +++ b/src/selection.js @@ -1,5 +1,5 @@ -import $ from 'jquery' - +import 'rangy/lib/rangy-textrange' +var $ = require('jquery') import Cursor from './cursor' import * as content from './content' import * as parser from './parser' @@ -40,17 +40,24 @@ export default class Selection extends Cursor { } isAllSelected () { - return parser.isBeginningOfHost( - this.host, - this.range.startContainer, - this.range.startOffset - ) && parser.isTextEndOfHost( - this.host, - this.range.endContainer, - this.range.endOffset + return ( + parser.isBeginningOfHost( + this.host, + this.range.startContainer, + this.range.startOffset + ) && + parser.isTextEndOfHost( + this.host, + this.range.endContainer, + this.range.endOffset + ) ) } + getTextRange () { + return this.range.toCharacterRange(this.host) + } + // Get the ClientRects of this selection. // Use this if you want more precision than getBoundingClientRect can give. getRects () { @@ -59,9 +66,10 @@ export default class Selection extends Cursor { return this.range.nativeRange.getClientRects() } - link (href, attrs = {}) { - const $link = $(this.createElement(config.linkMarkup.name, config.linkMarkup.attribs)) + const $link = $( + this.createElement(config.linkMarkup.name, config.linkMarkup.attribs) + ) if (href) attrs.href = href $link.attr(attrs) this.forceWrap($link[0]) @@ -87,7 +95,7 @@ export default class Selection extends Cursor { // Manually add a highlight // Note: the current code does not work with newlines (LP) - highlight ({highlightId}) { + highlight ({ highlightId }) { const textBefore = this.textBefore() const currentTextContent = this.text() @@ -115,32 +123,50 @@ export default class Selection extends Cursor { } makeBold () { - const bold = this.createElement(config.boldMarkup.name, config.boldMarkup.attribs) + const bold = this.createElement( + config.boldMarkup.name, + config.boldMarkup.attribs + ) this.forceWrap(bold) } toggleBold () { - const bold = this.createElement(config.boldMarkup.name, config.boldMarkup.attribs) + const bold = this.createElement( + config.boldMarkup.name, + config.boldMarkup.attribs + ) this.toggle(bold) } giveEmphasis () { - const em = this.createElement(config.italicMarkup.name, config.italicMarkup.attribs) + const em = this.createElement( + config.italicMarkup.name, + config.italicMarkup.attribs + ) this.forceWrap(em) } toggleEmphasis () { - const em = this.createElement(config.italicMarkup.name, config.italicMarkup.attribs) + const em = this.createElement( + config.italicMarkup.name, + config.italicMarkup.attribs + ) this.toggle(em) } makeUnderline () { - const u = this.createElement(config.underlineMarkup.name, config.underlineMarkup.attribs) + const u = this.createElement( + config.underlineMarkup.name, + config.underlineMarkup.attribs + ) this.forceWrap(u) } toggleUnderline () { - const u = this.createElement(config.underlineMarkup.name, config.underlineMarkup.attribs) + const u = this.createElement( + config.underlineMarkup.name, + config.underlineMarkup.attribs + ) this.toggle(u) } @@ -157,7 +183,12 @@ export default class Selection extends Cursor { // @param {String} E.g. '«' // @param {String} E.g. '»' surround (startCharacter, endCharacter) { - this.range = content.surround(this.host, this.range, startCharacter, endCharacter) + this.range = content.surround( + this.host, + this.range, + startCharacter, + endCharacter + ) this.setSelection() } @@ -168,8 +199,10 @@ export default class Selection extends Cursor { } toggleSurround (startCharacter, endCharacter) { - if (this.containsString(startCharacter) && - this.containsString(endCharacter)) { + if ( + this.containsString(startCharacter) && + this.containsString(endCharacter) + ) { this.removeSurround(startCharacter, endCharacter) } else { this.surround(startCharacter, endCharacter) From 2de895f0bb6bc9143c652898b13e5a17eba9e8a1 Mon Sep 17 00:00:00 2001 From: Lukas Buenger Date: Wed, 13 Mar 2019 18:26:02 +0100 Subject: [PATCH 2/6] feat: introducing contrib namespace and its first package, VirtualSpan --- src/contrib/virtual-span.js | 152 ++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/contrib/virtual-span.js diff --git a/src/contrib/virtual-span.js b/src/contrib/virtual-span.js new file mode 100644 index 00000000..1232426d --- /dev/null +++ b/src/contrib/virtual-span.js @@ -0,0 +1,152 @@ +import $ from 'jquery' +import rangy from 'rangy' +import { unwrap, adoptElement } from './content' + +function defaultMarkerConstructor (id, namespace) { + const marker = $('span')[0] + return marker +} + +const defaultOptions = { + namespace: 'vspan', + idAttribute: 'data-vspan-id', + namespaceAttribute: 'data-vspan-namespace', + editableNamespace: 'vspan', + markerConstructor: defaultMarkerConstructor, + win: undefined +} + +class VirtualSpan { + constructor ({ + namespace, + idAttribute, + namespaceAttribute, + editableNamespace, + markerConstructor, + win + } = defaultOptions) { + this.namespace = namespace + this.idAttribute = idAttribute + this.namespaceAttribute = namespaceAttribute + this.editableNamespace = editableNamespace + this.markerConstructor = markerConstructor + this.win = win + } + + extractRanges (host, markers) { + const range = rangy.createRange() + if (markers.length > 1) { + range.setStartBefore(markers.first()[0]) + range.setEndAfter(markers.last()[0]) + } else { + range.selectNode(markers[0]) + } + return range.toCharacterRange(host) + } + + getIdSelector (id) { + return `[${this.idAttribute}="${id}"]` + } + + getIdAttrSelector () { + return `[${this.idAttribute}]` + } + + createMarkerNode (id) { + let marker = this.markerConstructor(id, this.namespace) + if (this.win) { + marker = adoptElement(marker, this.win.document) + } + marker.setAttribute('data-editable', this.editableNamespace) + marker.setAttribute(this.idAttribute, id) + marker.setAttribute(this.namespaceAttribute, this.namespace) + return marker + } + + insertIntoHost (host, id, startIndex, endIndex) { + const marker = this.createMarkerNode(id, this.namespace, this.win) + const range = rangy.createRange() + range.selectCharacters(host, startIndex, endIndex) + const fragment = range.extractContents() + marker.appendChild(fragment) + range.deleteContents() + range.insertNode(marker) + } + + has (host, id) { + const matches = $(host).find(this.getIdSelector(id)) + return !!matches.length + } + + insert (host, id, startIndex, endIndex) { + if (this.has(host, id)) { + this.removeVSpan(host, id) + } + this.insertIntoHost(host, id, startIndex, endIndex) + } + + remove (host, id) { + $(host) + .find(this.getIdSelector(id)) + .each((index, elem) => { + unwrap(elem) + }) + } + + update (host, id, addCssClass, removeCssClass) { + if (!this.win || !this.win.document.documentElement.classList) return + $(host) + .find(this.getIdSelector(id)) + .each((index, elem) => { + if (removeCssClass) elem.classList.remove(removeCssClass) + if (addCssClass) elem.classList.add(addCssClass) + }) + } + + getData (contentStr) { + const $host = $(`
${contentStr}
`) + const markers = $host.find(this.getIdAttrSelector()) + if (!markers.length) { + return + } + const groups = {} + markers.each((_, marker) => { + const id = $(marker).attr(this.idAttribute) + if (!groups[id]) { + groups[id] = $host.find(this.getIdSelector(id)) + } + }) + + const res = {} + Object.keys(groups).forEach(id => { + const position = this.extractRanges($host[0], groups[id]) + const namespace = groups[id].attr(this.namespaceAttribute) + if (position) { + res[id] = { + ...position, + namespace + } + } + }) + return res + } + + applyData (contentStr, data) { + const $host = $(`
${contentStr}
`) + + for (const id in data) { + const rangeData = data[id] + this.insertIntoHost($host[0], id, rangeData.start, rangeData.end) + } + return $host.html() + } + + cleanUp (contentStr) { + const $host = $(`
${contentStr}
`) + const markers = $host.find(this.getIdAttrSelector()) + $(markers).each((_, m) => unwrap(m)) + return $host.html() + } +} + +export default VirtualSpan From f8b5905bded782c31eb962294277c4516a393719 Mon Sep 17 00:00:00 2001 From: Lukas Buenger Date: Wed, 13 Mar 2019 18:28:06 +0100 Subject: [PATCH 3/6] feat: expose Editor.Contrib.VirtualSpan --- src/core.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core.js b/src/core.js index f0f27986..f2e81f26 100644 --- a/src/core.js +++ b/src/core.js @@ -13,6 +13,7 @@ import highlightSupport from './highlight-support' import Highlighting from './highlighting' import createDefaultEvents from './create-default-events' import browser from 'bowser' +import VirtualSpan from './contrib/virtual-span' /** * The Core module provides the Editable class that defines the Editable.JS @@ -402,6 +403,10 @@ Editable.parser = parser Editable.content = content Editable.browser = browser +Editable.Contrib = { + VirtualSpan +} + // Set up callback functions for several events. ;['focus', 'blur', 'flow', 'selection', 'cursor', 'newline', 'insert', 'split', 'merge', 'empty', 'change', 'switch', From d1b1a67d542dbdb7e93eb8c1944a092235713e79 Mon Sep 17 00:00:00 2001 From: Lukas Buenger Date: Fri, 15 Mar 2019 11:33:35 +0100 Subject: [PATCH 4/6] feat: methods to help with orphaned spans and mores --- src/contrib/virtual-span.js | 49 +++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/contrib/virtual-span.js b/src/contrib/virtual-span.js index 1232426d..10b42587 100644 --- a/src/contrib/virtual-span.js +++ b/src/contrib/virtual-span.js @@ -1,6 +1,6 @@ import $ from 'jquery' import rangy from 'rangy' -import { unwrap, adoptElement } from './content' +import { unwrap, adoptElement } from '../content' function defaultMarkerConstructor (id, namespace) { const marker = $('span')[0] @@ -33,6 +33,10 @@ class VirtualSpan { this.win = win } + getHost (host) { + return host || (this.win && this.win.document.body) + } + extractRanges (host, markers) { const range = rangy.createRange() if (markers.length > 1) { @@ -41,7 +45,10 @@ class VirtualSpan { } else { range.selectNode(markers[0]) } - return range.toCharacterRange(host) + const textRange = range.toCharacterRange(host) + if (textRange.start > 0) textRange.start -= 1 + if (textRange.end > 0) textRange.end -= 1 + return textRange } getIdSelector (id) { @@ -52,6 +59,14 @@ class VirtualSpan { return `[${this.idAttribute}]` } + cleanOrphaned (ids, maybeHost) { + const host = this.getHost(maybeHost) + const query = $(host).find(this.getIdAttrSelector()) + ids + .reduce((q, id) => q.not(this.getIdSelector(id)), query) + .each((_, m) => unwrap(m)) + } + createMarkerNode (id) { let marker = this.markerConstructor(id, this.namespace) if (this.win) { @@ -73,19 +88,21 @@ class VirtualSpan { range.insertNode(marker) } - has (host, id) { - const matches = $(host).find(this.getIdSelector(id)) - return !!matches.length - } - insert (host, id, startIndex, endIndex) { - if (this.has(host, id)) { - this.removeVSpan(host, id) + if (this.has(id, host)) { + this.remove(id, host) } this.insertIntoHost(host, id, startIndex, endIndex) } - remove (host, id) { + has (id, maybeHost) { + const host = this.getHost(maybeHost) + const matches = $(host).find(this.getIdSelector(id)) + return !!matches.length + } + + remove (id, maybeHost) { + const host = this.getHost(maybeHost) $(host) .find(this.getIdSelector(id)) .each((index, elem) => { @@ -93,8 +110,9 @@ class VirtualSpan { }) } - update (host, id, addCssClass, removeCssClass) { + update (id, { addCssClass, removeCssClass }, maybeHost) { if (!this.win || !this.win.document.documentElement.classList) return + const host = this.getHost(maybeHost) $(host) .find(this.getIdSelector(id)) .each((index, elem) => { @@ -122,15 +140,16 @@ class VirtualSpan { const position = this.extractRanges($host[0], groups[id]) const namespace = groups[id].attr(this.namespaceAttribute) if (position) { - res[id] = { - ...position, - namespace - } + res[id] = Object.assign({}, position, { namespace }) } }) return res } + containsAny (contentStr) { + return !!$(`
${contentStr}
`).find(this.getIdAttrSelector()).length + } + applyData (contentStr, data) { const $host = $(`
${contentStr}
`) From 3caaead2ba9479c6e943c97f7ea67098f8f14c88 Mon Sep 17 00:00:00 2001 From: Lukas Buenger Date: Fri, 15 Mar 2019 12:48:18 +0100 Subject: [PATCH 5/6] chore: reorder imports to match eslint rules --- src/selection.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/selection.js b/src/selection.js index 244da66f..2209534b 100644 --- a/src/selection.js +++ b/src/selection.js @@ -1,12 +1,11 @@ import 'rangy/lib/rangy-textrange' -var $ = require('jquery') +import $ from 'jquery' import Cursor from './cursor' import * as content from './content' import * as parser from './parser' import * as config from './config' import highlightSupport from './highlight-support' import highlightText from './highlight-text' - /** * The Selection module provides a cross-browser abstraction layer for range * and selection. From 366b7606ba575e652142ba44c0f423f042df6456 Mon Sep 17 00:00:00 2001 From: Lukas Buenger Date: Fri, 15 Mar 2019 13:26:41 +0100 Subject: [PATCH 6/6] chore: remove prettier fu --- src/selection.js | 70 +++++++++++++++--------------------------------- 1 file changed, 21 insertions(+), 49 deletions(-) diff --git a/src/selection.js b/src/selection.js index 2209534b..f142b447 100644 --- a/src/selection.js +++ b/src/selection.js @@ -6,6 +6,7 @@ import * as parser from './parser' import * as config from './config' import highlightSupport from './highlight-support' import highlightText from './highlight-text' + /** * The Selection module provides a cross-browser abstraction layer for range * and selection. @@ -39,17 +40,14 @@ export default class Selection extends Cursor { } isAllSelected () { - return ( - parser.isBeginningOfHost( - this.host, - this.range.startContainer, - this.range.startOffset - ) && - parser.isTextEndOfHost( - this.host, - this.range.endContainer, - this.range.endOffset - ) + return parser.isBeginningOfHost( + this.host, + this.range.startContainer, + this.range.startOffset + ) && parser.isTextEndOfHost( + this.host, + this.range.endContainer, + this.range.endOffset ) } @@ -65,10 +63,9 @@ export default class Selection extends Cursor { return this.range.nativeRange.getClientRects() } + link (href, attrs = {}) { - const $link = $( - this.createElement(config.linkMarkup.name, config.linkMarkup.attribs) - ) + const $link = $(this.createElement(config.linkMarkup.name, config.linkMarkup.attribs)) if (href) attrs.href = href $link.attr(attrs) this.forceWrap($link[0]) @@ -94,7 +91,7 @@ export default class Selection extends Cursor { // Manually add a highlight // Note: the current code does not work with newlines (LP) - highlight ({ highlightId }) { + highlight ({highlightId}) { const textBefore = this.textBefore() const currentTextContent = this.text() @@ -122,50 +119,32 @@ export default class Selection extends Cursor { } makeBold () { - const bold = this.createElement( - config.boldMarkup.name, - config.boldMarkup.attribs - ) + const bold = this.createElement(config.boldMarkup.name, config.boldMarkup.attribs) this.forceWrap(bold) } toggleBold () { - const bold = this.createElement( - config.boldMarkup.name, - config.boldMarkup.attribs - ) + const bold = this.createElement(config.boldMarkup.name, config.boldMarkup.attribs) this.toggle(bold) } giveEmphasis () { - const em = this.createElement( - config.italicMarkup.name, - config.italicMarkup.attribs - ) + const em = this.createElement(config.italicMarkup.name, config.italicMarkup.attribs) this.forceWrap(em) } toggleEmphasis () { - const em = this.createElement( - config.italicMarkup.name, - config.italicMarkup.attribs - ) + const em = this.createElement(config.italicMarkup.name, config.italicMarkup.attribs) this.toggle(em) } makeUnderline () { - const u = this.createElement( - config.underlineMarkup.name, - config.underlineMarkup.attribs - ) + const u = this.createElement(config.underlineMarkup.name, config.underlineMarkup.attribs) this.forceWrap(u) } toggleUnderline () { - const u = this.createElement( - config.underlineMarkup.name, - config.underlineMarkup.attribs - ) + const u = this.createElement(config.underlineMarkup.name, config.underlineMarkup.attribs) this.toggle(u) } @@ -182,12 +161,7 @@ export default class Selection extends Cursor { // @param {String} E.g. '«' // @param {String} E.g. '»' surround (startCharacter, endCharacter) { - this.range = content.surround( - this.host, - this.range, - startCharacter, - endCharacter - ) + this.range = content.surround(this.host, this.range, startCharacter, endCharacter) this.setSelection() } @@ -198,10 +172,8 @@ export default class Selection extends Cursor { } toggleSurround (startCharacter, endCharacter) { - if ( - this.containsString(startCharacter) && - this.containsString(endCharacter) - ) { + if (this.containsString(startCharacter) && + this.containsString(endCharacter)) { this.removeSurround(startCharacter, endCharacter) } else { this.surround(startCharacter, endCharacter)