frontend/text.js (129 lines of code) (raw):
const { OBJECT_ID } = require('./constants')
const { isObject } = require('../src/common')
class Text {
constructor (text) {
if (typeof text === 'string') {
const elems = [...text].map(value => ({value}))
return instantiateText(undefined, elems) // eslint-disable-line
} else if (Array.isArray(text)) {
const elems = text.map(value => ({value}))
return instantiateText(undefined, elems) // eslint-disable-line
} else if (text === undefined) {
return instantiateText(undefined, []) // eslint-disable-line
} else {
throw new TypeError(`Unsupported initial value for Text: ${text}`)
}
}
get length () {
return this.elems.length
}
get (index) {
const value = this.elems[index].value
if (this.context && isObject(value)) {
const objectId = value[OBJECT_ID]
const path = this.path.concat([{key: index, objectId}])
return this.context.instantiateObject(path, objectId)
} else {
return value
}
}
getElemId (index) {
return this.elems[index].elemId
}
/**
* Iterates over the text elements character by character, including any
* inline objects.
*/
[Symbol.iterator] () {
let elems = this.elems, index = -1
return {
next () {
index += 1
if (index < elems.length) {
return {done: false, value: elems[index].value}
} else {
return {done: true}
}
}
}
}
/**
* Returns the content of the Text object as a simple string, ignoring any
* non-character elements.
*/
toString() {
// Concatting to a string is faster than creating an array and then
// .join()ing for small (<100KB) arrays.
// https://jsperf.com/join-vs-loop-w-type-test
let str = ''
for (const elem of this.elems) {
if (typeof elem.value === 'string') str += elem.value
}
return str
}
/**
* Returns the content of the Text object as a sequence of strings,
* interleaved with non-character elements.
*
* For example, the value ['a', 'b', {x: 3}, 'c', 'd'] has spans:
* => ['ab', {x: 3}, 'cd']
*/
toSpans() {
let spans = []
let chars = ''
for (const elem of this.elems) {
if (typeof elem.value === 'string') {
chars += elem.value
} else {
if (chars.length > 0) {
spans.push(chars)
chars = ''
}
spans.push(elem.value)
}
}
if (chars.length > 0) {
spans.push(chars)
}
return spans
}
/**
* Returns the content of the Text object as a simple string, so that the
* JSON serialization of an Automerge document represents text nicely.
*/
toJSON() {
return this.toString()
}
/**
* Returns a writeable instance of this object. This instance is returned when
* the text object is accessed within a change callback. `context` is the
* proxy context that keeps track of the mutations.
*/
getWriteable(context, path) {
if (!this[OBJECT_ID]) {
throw new RangeError('getWriteable() requires the objectId to be set')
}
const instance = instantiateText(this[OBJECT_ID], this.elems)
instance.context = context
instance.path = path
return instance
}
/**
* Updates the list item at position `index` to a new value `value`.
*/
set (index, value) {
if (this.context) {
this.context.setListIndex(this.path, index, value)
} else if (!this[OBJECT_ID]) {
this.elems[index].value = value
} else {
throw new TypeError('Automerge.Text object cannot be modified outside of a change block')
}
return this
}
/**
* Inserts new list items `values` starting at position `index`.
*/
insertAt(index, ...values) {
if (this.context) {
this.context.splice(this.path, index, 0, values)
} else if (!this[OBJECT_ID]) {
this.elems.splice(index, 0, ...values.map(value => ({value})))
} else {
throw new TypeError('Automerge.Text object cannot be modified outside of a change block')
}
return this
}
/**
* Deletes `numDelete` list items starting at position `index`.
* if `numDelete` is not given, one item is deleted.
*/
deleteAt(index, numDelete = 1) {
if (this.context) {
this.context.splice(this.path, index, numDelete, [])
} else if (!this[OBJECT_ID]) {
this.elems.splice(index, numDelete)
} else {
throw new TypeError('Automerge.Text object cannot be modified outside of a change block')
}
return this
}
}
// Read-only methods that can delegate to the JavaScript built-in array
for (let method of ['concat', 'every', 'filter', 'find', 'findIndex', 'forEach', 'includes',
'indexOf', 'join', 'lastIndexOf', 'map', 'reduce', 'reduceRight',
'slice', 'some', 'toLocaleString']) {
Text.prototype[method] = function (...args) {
const array = [...this]
return array[method](...args)
}
}
function instantiateText(objectId, elems) {
const instance = Object.create(Text.prototype)
instance[OBJECT_ID] = objectId
instance.elems = elems
return instance
}
module.exports = { Text, instantiateText }