UNPKG

substance

Version:

Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing system. It is developed to power our online editing platform [Substance](http://substance.io).

277 lines (266 loc) 9.43 kB
import isArray from '../util/isArray' import uuid from '../util/uuid' // A collection of methods to update annotations // -------- // // As we treat annotations as overlay of plain text we need to keep them up-to-date during editing. export default { insertedText, deletedText, transferAnnotations, expandAnnotation, fuseAnnotation, truncateAnnotation } function insertedText (doc, coordinate, length) { if (!length) return var index = doc.getIndex('annotations') var annotations = index.get(coordinate.path) for (let i = 0; i < annotations.length; i++) { const anno = annotations[i] var pos = coordinate.offset var start = anno.start.offset var end = anno.end.offset var newStart = start var newEnd = end if ((pos < start) || (pos === start)) { newStart += length } // inline nodes do not expand automatically if ((pos < end) || (pos === end && !anno.isInlineNode())) { newEnd += length } // TODO: Use coordintate ops! if (newStart !== start) { doc.set([anno.id, 'start', 'offset'], newStart) } if (newEnd !== end) { doc.set([anno.id, 'end', 'offset'], newEnd) } } // TODO: fix support for container annotations // // same for container annotation anchors // index = doc.getIndex('container-annotation-anchors'); // var anchors = index.get(coordinate.path); // forEach(anchors, function(anchor) { // var pos = coordinate.offset; // var start = anchor.offset; // var changed = false; // if ( (pos < start) || // (pos === start && !coordinate.after) ) { // start += length; // changed = true; // } // if (changed) { // let coor = (anchor.isStart?'start':'end'); // // TODO: Use coordintate ops! // doc.set([anchor.id, coor, 'offset'], start); // } // }); } function deletedText (doc, path, startOffset, endOffset) { if (startOffset === endOffset) return var index = doc.getIndex('annotations') var annotations = index.get(path) var length = endOffset - startOffset for (let i = 0; i < annotations.length; i++) { const anno = annotations[i] var pos1 = startOffset var pos2 = endOffset var start = anno.start.offset var end = anno.end.offset var newStart = start var newEnd = end if (pos2 <= start) { newStart -= length newEnd -= length doc.set([anno.id, 'start', 'offset'], newStart) doc.set([anno.id, 'end', 'offset'], newEnd) } else { if (pos1 <= start) { newStart = start - Math.min(pos2 - pos1, start - pos1) } if (pos1 <= end) { newEnd = end - Math.min(pos2 - pos1, end - pos1) } // delete the annotation if it has collapsed by this delete if (start !== end && newStart === newEnd) { doc.delete(anno.id) } else { // TODO: Use coordintate ops! if (start !== newStart) { doc.set([anno.id, 'start', 'offset'], newStart) } if (end !== newEnd) { doc.set([anno.id, 'end', 'offset'], newEnd) } } } } // TODO: fix support for container annotations // // same for container annotation anchors // index = doc.getIndex('container-annotation-anchors'); // var anchors = index.get(path); // var containerAnnoIds = []; // forEach(anchors, function(anchor) { // containerAnnoIds.push(anchor.id); // var pos1 = startOffset; // var pos2 = endOffset; // var start = anchor.offset; // var changed = false; // if (pos2 <= start) { // start -= length; // changed = true; // } else { // if (pos1 <= start) { // var newStart = start - Math.min(pos2-pos1, start-pos1); // if (start !== newStart) { // start = newStart; // changed = true; // } // } // } // if (changed) { // // TODO: Use coordintate ops! // let coor = (anchor.isStart?'start':'end'); // doc.set([anchor.id, coor, 'offset'], start); // } // }); // // check all anchors after that if they have collapsed and remove the annotation in that case // forEach(uniq(containerAnnoIds), function(id) { // var anno = doc.get(id); // var annoSel = anno.getSelection(); // if(annoSel.isCollapsed()) { // // console.log("...deleting container annotation because it has collapsed" + id); // doc.delete(id); // } // }); } // used when breaking a node to transfer annotations to the new property function transferAnnotations (doc, path, offset, newPath, newOffset) { var index = doc.getIndex('annotations') var annotations = index.get(path, offset) for (let i = 0; i < annotations.length; i++) { const a = annotations[i] var isInside = (offset > a.start.offset && offset < a.end.offset) var start = a.start.offset var end = a.end.offset // 1. if the cursor is inside an annotation it gets either split or truncated if (isInside) { // create a new annotation if the annotation is splittable if (a.canSplit()) { const newAnno = a.toJSON() newAnno.id = uuid(a.type + '_') newAnno.start.path = newPath newAnno.start.offset = newOffset newAnno.end.path = newPath newAnno.end.offset = newOffset + a.end.offset - offset doc.create(newAnno) } // in either cases truncate the first part const newStartOffset = a.start.offset const newEndOffset = offset // if after truncate the anno is empty, delete it if (newEndOffset === newStartOffset) { doc.delete(a.id) // ... otherwise update the range } else { // TODO: Use coordintate ops! if (newStartOffset !== start) { doc.set([a.id, 'start', 'offset'], newStartOffset) } if (newEndOffset !== end) { doc.set([a.id, 'end', 'offset'], newEndOffset) } } // 2. if the cursor is before an annotation then simply transfer the annotation to the new node } else if (a.start.offset >= offset) { // TODO: Use coordintate ops! // Note: we are preserving the annotation so that anything which is connected to the annotation // remains valid. doc.set([a.id, 'start', 'path'], newPath) doc.set([a.id, 'start', 'offset'], newOffset + a.start.offset - offset) doc.set([a.id, 'end', 'path'], newPath) doc.set([a.id, 'end', 'offset'], newOffset + a.end.offset - offset) } } // TODO: fix support for container annotations // // same for container annotation anchors // index = doc.getIndex('container-annotation-anchors'); // var anchors = index.get(path); // var containerAnnoIds = []; // forEach(anchors, function(anchor) { // containerAnnoIds.push(anchor.id); // var start = anchor.offset; // if (offset <= start) { // // TODO: Use coordintate ops! // let coor = anchor.isStart?'start':'end' // doc.set([anchor.id, coor, 'path'], newPath); // doc.set([anchor.id, coor, 'offset'], newOffset + anchor.offset - offset); // } // }); // // check all anchors after that if they have collapsed and remove the annotation in that case // forEach(uniq(containerAnnoIds), function(id) { // var anno = doc.get(id); // var annoSel = anno.getSelection(); // if(annoSel.isCollapsed()) { // // console.log("...deleting container annotation because it has collapsed" + id); // doc.delete(id); // } // }); } /* @param {model/Document} tx @param {model/PropertyAnnotation} args.anno annotation which should be expanded @param {model/Selection} args.selection selection to which to expand */ function truncateAnnotation (tx, anno, sel) { if (!sel || !sel._isSelection) throw new Error('Argument "selection" is required.') if (!anno || !anno.isAnnotation()) throw new Error('Argument "anno" is required and must be an annotation.') const annoSel = anno.getSelection() const newAnnoSel = annoSel.truncateWith(sel) anno._updateRange(tx, newAnnoSel) return anno } /* @param {model/Document} tx @param {model/PropertyAnnotation} args.anno annotation which should be expanded @param {model/Selection} args.selection selection to which to expand */ function expandAnnotation (tx, anno, sel) { if (!sel || !sel._isSelection) throw new Error('Argument "selection" is required.') if (!anno || !anno.isAnnotation()) throw new Error('Argument "anno" is required and must be an annotation.') const annoSel = anno.getSelection() const newAnnoSel = annoSel.expand(sel) anno._updateRange(tx, newAnnoSel) return anno } /* @param {model/Document} tx @param {model/PropertyAnnotation[]} args.annos annotations which should be fused */ function fuseAnnotation (tx, annos) { if (!isArray(annos) || annos.length < 2) { throw new Error('fuseAnnotation(): at least two annotations are necessary.') } let sel, annoType annos.forEach(function (anno, idx) { if (idx === 0) { sel = anno.getSelection() annoType = anno.type } else { if (anno.type !== annoType) { throw new Error('fuseAnnotation(): all annotations must be of the same type.') } sel = sel.expand(anno.getSelection()) } }) // expand the first and delete the others for (var i = 1; i < annos.length; i++) { tx.delete(annos[i].id) } expandAnnotation(tx, annos[0], sel) tx.setSelection(sel) }