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 systems.

252 lines (240 loc) 8.15 kB
import { isArray, last, forEach, uuid } from '../util' import Document from './Document' import documentHelpers from './documentHelpers' import { setCursor } from './selectionHelpers' /** Pastes clipboard content at the current selection @param {Object} args object with `selection` and `doc` for Substance content or `text` for external HTML content @return {Object} with updated `selection` */ function paste(tx, args) { let sel = tx.selection if (!sel || sel.isNull() || sel.isCustomSelection()) { throw new Error("Can not paste, without selection or a custom selection.") } args = args || {} args.text = args.text || '' let pasteDoc = args.doc // TODO: is there a better way to detect that this paste is happening within a // container? let inContainer = Boolean(sel.containerId) // when we are in a container, we interpret line-breaks // and create a document with multiple paragraphs // in a PropertyEditor we paste the text as is if (!pasteDoc && !inContainer) { tx.insertText(args.text) return } if (!pasteDoc) { pasteDoc = _convertPlainTextToDocument(tx, args) } if (!sel.isCollapsed()) { tx.deleteSelection() } let snippet = pasteDoc.get(Document.SNIPPET_ID) if (snippet.getLength() > 0) { let first = snippet.getChildAt(0) if (first.isText()) { _pasteAnnotatedText(tx, pasteDoc) // now we remove the first node from the snippet, // so that we can call _pasteDocument for the remaining // content snippet.hideAt(0) } // if still nodes left > 0 if (snippet.getLength() > 0) { _pasteDocument(tx, pasteDoc) } } return args } /* Splits plain text by lines and puts them into paragraphs. */ function _convertPlainTextToDocument(tx, args) { let lines = args.text.split(/\s*\n\s*\n/) let pasteDoc = tx.getDocument().newInstance() let defaultTextType = pasteDoc.getSchema().getDefaultTextType() let container = pasteDoc.create({ type: 'container', id: Document.SNIPPET_ID, nodes: [] }) let node if (lines.length === 1) { node = pasteDoc.create({ id: Document.TEXT_SNIPPET_ID, type: defaultTextType, content: lines[0] }) container.show(node.id) } else { for (let i = 0; i < lines.length; i++) { node = pasteDoc.create({ id: uuid(defaultTextType), type: defaultTextType, content: lines[i] }) container.show(node.id); } } return pasteDoc } function _pasteAnnotatedText(tx, copy) { let sel = tx.selection let nodes = copy.get(Document.SNIPPET_ID).nodes let textPath = [nodes[0], 'content'] let text = copy.get(textPath) let annotations = copy.getIndex('annotations').get(textPath) // insert plain text let path = sel.start.path let offset = sel.start.offset tx.insertText(text) // copy annotations forEach(annotations, function(anno) { let data = anno.toJSON() data.start.path = path.slice(0) data.start.offset += offset data.end.offset += offset // create a new uuid if a node with the same id exists already if (tx.get(data.id)) data.id = uuid(data.type) tx.create(data) }) } function _pasteDocument(tx, pasteDoc) { let sel = tx.selection let containerId = sel.containerId let container = tx.get(containerId) let insertPos if (sel.isPropertySelection()) { let startPath = sel.start.path let nodeId = sel.start.getNodeId() let startPos = container.getPosition(nodeId, 'strict') let text = tx.get(startPath) // Break, unless we are at the last character of a node, // then we can simply insert after the node if (text.length === 0) { insertPos = startPos container.hide(nodeId) documentHelpers.deleteNode(tx, tx.get(nodeId)) } else if ( text.length === sel.start.offset ) { insertPos = startPos + 1 } else { tx.break() insertPos = startPos + 1 } } else if (sel.isNodeSelection()) { let nodePos = container.getPosition(sel.getNodeId(), 'strict') if (sel.isBefore()) { insertPos = nodePos } else if (sel.isAfter()) { insertPos = nodePos+1 } else { throw new Error('Illegal state: the selection should be collapsed.') } } // transfer nodes from content document let nodeIds = pasteDoc.get(Document.SNIPPET_ID).nodes let insertedNodes = [] let visited = {} for (let i = 0; i < nodeIds.length; i++) { let node = pasteDoc.get(nodeIds[i]) // Note: this will on the one hand make sure // node ids are changed to avoid collisions in // the target doc // Plus, it uses reflection to create owned nodes recursively, // and to transfer attached annotations. let newId = _transferWithDisambiguatedIds(node.getDocument(), tx, node.id, visited) // get the node in the targetDocument node = tx.get(newId) container.showAt(insertPos++, newId) insertedNodes.push(node) } if (insertedNodes.length > 0) { let lastNode = last(insertedNodes) setCursor(tx, lastNode, containerId, 'after') } } // We need to disambiguate ids if the target document // contains a node with the same id. // Unfortunately, this can be difficult in some cases, // e.g. other nodes that have a reference to the re-named node // We only fix annotations for now. function _transferWithDisambiguatedIds(sourceDoc, targetDoc, id, visited) { if (visited[id]) throw new Error('FIXME: dont call me twice') const node = sourceDoc.get(id, 'strict') let oldId = node.id let newId if (targetDoc.contains(node.id)) { // change the node id newId = uuid(node.type) node.id = newId } visited[id] = node.id const annotationIndex = sourceDoc.getIndex('annotations') const nodeSchema = node.getSchema() // collect annotations so that we can create them in the target doc afterwards let annos = [] // now we iterate all properties of the node schema, // to see if there are owned references, which need to be created recursively, // and if there are text properties, where annotations could be attached to for (let key in nodeSchema) { if (key === 'id' || key === 'type' || !nodeSchema.hasOwnProperty(key)) continue const prop = nodeSchema[key] const name = prop.name // Look for references to owned children and create recursively if ((prop.isReference() && prop.isOwned()) || (prop.type === 'file')) { // NOTE: we need to recurse directly here, so that we can // update renamed references let val = node[prop.name] if (prop.isArray()) { _transferArrayOfReferences(sourceDoc, targetDoc, val, visited) } else { let id = val if (!visited[id]) { node[name] = _transferWithDisambiguatedIds(sourceDoc, targetDoc, id, visited) } } } // Look for text properties and create annotations in the target doc accordingly else if (prop.isText()) { // This is really difficult in general // as we don't know where to look for. // TODO: ATM we only look for annotations. // We should also consider anchors / container-annotations // Probably we need a different approach, may let _annos = annotationIndex.get([oldId, prop.name]) for (let i = 0; i < _annos.length; i++) { let anno = _annos[i] if (anno.start.path[0] === oldId) { anno.start.path[0] = newId } if (anno.end.path[0] === oldId) { anno.end.path[0] = newId } annos.push(anno) } } } targetDoc.create(node) for (let i = 0; i < annos.length; i++) { _transferWithDisambiguatedIds(sourceDoc, targetDoc, annos[i].id, visited) } return node.id } function _transferArrayOfReferences(sourceDoc, targetDoc, arr, visited) { for (let i = 0; i < arr.length; i++) { let val = arr[i] // multi-dimensional if (isArray(val)) { _transferArrayOfReferences(sourceDoc, targetDoc, val, visited) } else { let id = val if (id && !visited[id]) { arr[i] = _transferWithDisambiguatedIds(sourceDoc, targetDoc, id, visited) } } } } export default paste