import { filter, find, keys, last } from 'lodash' import { createAnnotation, expandAnnotation, truncateAnnotation } from 'substance' class TrackChangesProvider { constructor (config) { this.config = config this.config.documentSession.on('didUpdate', this.handleUndoRedo, this) } handleTransaction (options) { this.handleAdd(options) this.handleDelete(options) } handleAdd (options) { const { event, status } = options if (status !== 'add') return const isSelectionCollapsed = this.isSelectionCollapsed() const mode = this.getMode() let selection if (isSelectionCollapsed) { this.insertText(event) if (mode) return selection = this.setSelectionPlusOne('left') this.createAddAnnotation(selection) this.moveCursorTo('end') return } else { selection = this.getSelection() this.createDeleteAnnotation(selection) this.moveCursorTo('end') this.insertText(event) selection = this.setSelectionPlusOne('left') this.createAddAnnotation(selection) this.separateAnnotations() this.moveCursorTo('end') return } } handleDelete (options) { const { move, status } = options if (status !== 'delete') return const mode = this.getMode() const isSelectionCollapsed = this.isSelectionCollapsed() let selection const direction = { cursorTo: (move === 'left') ? 'start' : 'end', move: move } if (isSelectionCollapsed) { if (!mode) { selection = this.setSelectionPlusOne(direction.move) this.createDeleteAnnotation(selection) this.moveCursorTo(direction.cursorTo) return } if (mode === 'delete') { selection = this.setSelectionPlusOne(direction.move) const annotation = this.getAnnotationByStatus('delete') this.expandTrackAnnotation(selection, annotation) this.moveCursorTo(direction.cursorTo) return } } else { selection = this.getSelection() this.createDeleteAnnotation(selection) this.moveCursorTo(direction.cursorTo) return } } createAddAnnotation (selection) { this.createTrackAnnotation(selection, 'add') } createDeleteAnnotation (selection) { this.createTrackAnnotation(selection, 'delete') } createTrackAnnotation (selection, status) { const surface = this.getSurface() surface.transaction((tx, args) => { const newNode = { selection: selection, node: { status: status, type: 'track-change' } } createAnnotation(tx, newNode) }) } expandTrackAnnotation (selection, annotation) { const surface = this.getSurface() surface.transaction((tx, args) => { args.selection = selection args.anno = annotation expandAnnotation(tx, args) }) } separateAnnotations () { const surface = this.getSurface() const addAnnotation = this.getAnnotationByStatus('add') const deleteAnnotation = this.getAnnotationByStatus('delete') surface.transaction((tx, args) => { args.anno = deleteAnnotation args.selection = addAnnotation.getSelection() truncateAnnotation(tx, args) }) } insertText (event) { const surface = this.getSurface() surface.transaction(function (tx, args) { if (surface.domSelection) surface.domSelection.clear() args.text = event.data return surface.insertText(tx, args) }, { action: 'type' }) } /* ANNOTATION HELPERS */ // is on left edge // is on right edge // is annotation from the same user // TODO -- handle multiple delete and add annotations getAnnotationByStatus (status) { const annotations = this.getAllExistingTrackAnnotations() console.log('annos', annotations) const annos = filter(annotations, (annotation) => { return annotation.status === status }) return annos[0] } getExistingAnnotation () { const documentSession = this.getDocumentSession() const selectionState = documentSession.getSelectionState() const annotations = selectionState.getAnnotationsForType('track-change') console.log(annotations) return annotations[0] } getAllExistingTrackAnnotations () { const documentSession = this.getDocumentSession() const selectionState = documentSession.getSelectionState() const annotations = selectionState.getAnnotationsForType('track-change') return annotations } // returns whether the selection is on an add / delete tracked change isOnAnnotation (status) { const annotations = this.getAllExistingTrackAnnotations() const annotation = find(annotations, (annotation) => { return annotation.status === status }) if (!annotation) return false return true } /** HISTORY HANDLERS */ // TODO -- shouldn't run both all the time handleUndoRedo (update, info) { if (!info.replay) return // console.log(update) // console.log(info) this.handleUndo(update) // this.handleRedo(update) } handleUndo (update) { const deleted = update.change.deleted const deletedLength = keys(deleted).length // console.log(keys(deleted)) if (deletedLength === 0) return if (deletedLength > 1) { return console.warn('FIXME: Multiple operations in track changes replay!') } const deletedOp = deleted[keys(deleted)[0]] if (!deletedOp.type === 'track-change') return const documentSession = this.getDocumentSession() documentSession.undo() } handleRedo () { const documentSession = this.getDocumentSession() const undoneChanges = documentSession.undoneChanges const lastChange = last(undoneChanges) const op = last(lastChange.ops) const isTrack = op.path[0].split('-').slice(0, -1).join('-') === 'track-change' console.log(isTrack) } /* SELECTION HANDLERS */ // get delete direction // is part of the selection outside the annotation // move cursor to setSelectionPlusOne (direction) { const selection = this.getSelection() if (direction === 'left') selection.startOffset -= 1 if (direction === 'right') selection.endOffset += 1 return selection } moveCursorTo (point) { const selection = this.getSelection() const surface = this.getSurface() if (point === 'start') { selection.endOffset = selection.startOffset } else if (point === 'end') { selection.startOffset = selection.endOffset } else { selection.startOffset = point selection.endOffset = point } surface.setSelection(selection) } clearSelection (args, status) { const selection = args.selection if ( status === 'add' || (status === 'delete' && args.deleteCollapsed) ) { selection.startOffset = selection.endOffset } else if (status === 'delete') { selection.endOffset = selection.startOffset } return selection } createSelection (args) { const selection = args.selection selection.startOffset -= 1 return selection } isSelectionCollapsed () { const selection = this.getSelection() const isCollapsed = selection.isCollapsed() return isCollapsed } /* GETTERS */ getCommandManager () { return this.config.commandManager } getCommandStates () { const commandManager = this.getCommandManager() return commandManager.getCommandStates() } getDocumentSession () { return this.config.documentSession } getMode () { const state = this.getTrackState() return state.mode } getSelection () { const surface = this.getSurface() return surface.getSelection() } getSurface () { const surfaceManager = this.config.surfaceManager const id = this.config.containerId return surfaceManager.getSurface(id) } getTrackState () { const commandStates = this.getCommandStates() return commandStates['track-change'] } } export default TrackChangesProvider