Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions src/contrib/virtual-span.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
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
}

getHost (host) {
return host || (this.win && this.win.document.body)
}

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])
}
const textRange = range.toCharacterRange(host)
if (textRange.start > 0) textRange.start -= 1
if (textRange.end > 0) textRange.end -= 1
return textRange
}

getIdSelector (id) {
return `[${this.idAttribute}="${id}"]`
}

getIdAttrSelector () {
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) {
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)
}

insert (host, id, startIndex, endIndex) {
if (this.has(id, host)) {
this.remove(id, host)
}
this.insertIntoHost(host, id, startIndex, endIndex)
}

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) => {
unwrap(elem)
})
}

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) => {
if (removeCssClass) elem.classList.remove(removeCssClass)
if (addCssClass) elem.classList.add(addCssClass)
})
}

getData (contentStr) {
const $host = $(`<div>${contentStr}</div>`)
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] = Object.assign({}, position, { namespace })
}
})
return res
}

containsAny (contentStr) {
return !!$(`<div>${contentStr}</div>`).find(this.getIdAttrSelector()).length
}

applyData (contentStr, data) {
const $host = $(`<div>${contentStr}</div>`)

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 = $(`<div>${contentStr}</div>`)
const markers = $host.find(this.getIdAttrSelector())
$(markers).each((_, m) => unwrap(m))
return $host.html()
}
}

export default VirtualSpan
5 changes: 5 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down
6 changes: 5 additions & 1 deletion src/selection.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'rangy/lib/rangy-textrange'
import $ from 'jquery'

import Cursor from './cursor'
import * as content from './content'
import * as parser from './parser'
Expand Down Expand Up @@ -51,6 +51,10 @@ export default class Selection extends Cursor {
)
}

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 () {
Expand Down