Newer
Older
each,
deleteCharacter as deleteChar,
deleteSelection as deleteSel,
truncateAnnotation,
TOCProvider
class TrackChangesProvider extends TOCProvider {
constructor (document, config) {
super(document, config)
config.documentSession.on('didUpdate', this.handleUndoRedo, this)
// HACK -- use TOCProvider's event to capture new / deleted changes
this.on('toc:updated', this.reComputeEntries, this)
/*
HANDLERS
*/
options.selection = this.getSelection()
this.chooseHanlder(options)
chooseHanlder (options) {
if (options.status === 'add') return this.handleAdd(options)
if (options.status === 'delete') return this.handleDelete(options)
}
handleAdd (options) {
const isSelectionCollapsed = this.isSelectionCollapsed()
if (isSelectionCollapsed) return this.handleAddCollapsed(options)
if (!isSelectionCollapsed) return this.handleAddNonCollapsed(options)
}
handleAddCollapsed (options) {
const { event } = options
const notOnTrack = this.isNotOnTrackAnnotation()
const isOnAdd = this.isOnAnnotation('add')
const isOnDelete = this.isOnAnnotation('delete')
if (notOnTrack) return this.insertCharacterWithAddAnnotation(options)
if (isOnAdd) {
// annotation gets expanded automatically, unless the selection is on its left edge
const annotation = this.getAnnotationByStatus('add')
const isOnLeftEdge = this.isOnLeftEdge(annotation)
const isOnRightEdge = this.isOnRightEdge(annotation)
const isFromSameUser = this.isAnnotationFromTheSameUser(annotation)
const mode = this.getMode()
// console.log(annotation)
if (isFromSameUser) {
this.insertText(event)
if (isOnLeftEdge) this.expandAnnotationToDirection(annotation)
}
if (!isFromSameUser) {
if (isOnRightEdge) {
this.insertCharacterWithoutExpandingAnnotation(annotation, options)
} else {
this.insertText(event)
}
if (mode) this.createAdditionAnnotationOnLastChar()
}
if (isOnDelete) {
const annotation = this.getAnnotationByStatus('delete')
const isOnLeftEdge = this.isOnLeftEdge(annotation)
const isOnRightEdge = this.isOnRightEdge(annotation)
const withinAnnotation = this.isSelectionContainedWithin(annotation, true)
// if contained within the delete annotation, move it to the edge,
// insert character, set event to null so that the character does
// not get inserted twice, and handle again
if (withinAnnotation) {
this.moveCursorTo(annotation.endOffset)
this.insertCharacterWithoutExpandingAnnotation(annotation, options)
options.selection = this.getSelection()
return this.handleAdd(options)
if (isOnLeftEdge) return this.insertCharacterWithAddAnnotation(options)
if (isOnRightEdge) {
this.insertCharacterWithoutExpandingAnnotation(annotation, options)
return this.handleAdd(options)
}
handleAddNonCollapsed (options) {
let { selection } = options
const notOnTrack = this.isNotOnTrackAnnotation()
const isOnDelete = this.isOnAnnotation('delete')
if (notOnTrack) return this.deleteSelectedAndCreateAddition(options)
// delete all additions of the same user and
// shorten selection by the number of deleted characters
const shortenBy = this.deleteAllOwnAdditions(selection)
const startOffset = selection.startOffset
const endOffset = selection.endOffset - shortenBy
selection = this.updateSelection(selection, startOffset, endOffset)
options.selection = selection
if (isOnDelete) {
const annotation = this.getAnnotationByStatus('delete')
const withinAnnotation = this.isSelectionContainedWithin(annotation)
if (withinAnnotation) {
// if selection is wholly contained within a delete annotation,
// move to the end of the annotation and handle again
this.moveCursorTo(annotation.endOffset)
options.selection = this.getSelection()
return this.handleAddCollapsed(options)
}
}
// after deleting all own additions, there is still text selected
// mark it as deleted and add new addition annotation at the end
// TODO -- use selection.isCollapsed()
if (selection.endOffset > selection.startOffset) {
this.deleteOrMergeAllOwnDeletions(selection)
this.deleteSelectedAndCreateAddition(options)
return
}
// if you got here, deleting all own additions left a collapsed selection
// (ie. the whole selection was on an own addition annotation)
// since selection is collapsed, handle it as such
return this.handleAddCollapsed(options)
handleDelete (options) {
const { key, move } = options
const isSelectionCollapsed = this.isSelectionCollapsed()
options.direction = {
cursorTo: (move === 'left') ? 'start' : 'end',
key: key,
move: move
}
if (isSelectionCollapsed) return this.handleDeleteCollapsed(options)
if (!isSelectionCollapsed) return this.handleDeleteNonCollapsed(options)
handleDeleteCollapsed (options) {
const { direction } = options
const notOnTrack = this.isNotOnTrackAnnotation()
const isOnAdd = this.isOnAnnotation('add')
const isOnDelete = this.isOnAnnotation('delete')
if (notOnTrack) return this.selectCharacterAndMarkDeleted(options)
if (isOnAdd) {
const annotation = this.getAnnotationByStatus('add')
const isOnLeftEdge = this.isOnLeftEdge(annotation)
const isOnRightEdge = this.isOnRightEdge(annotation)
const isFromSameUser = this.isAnnotationFromTheSameUser(annotation)
const key = direction.key
// use this variable to throw the flow to the isOnDelete handler underneath
let pass = false
// when on own additions, simply delete the character
// unless it is on the edge: then make a deletion annotation
// TODO -- watch it for different users
if (
(isOnLeftEdge && key === 'BACKSPACE') ||
(isOnRightEdge && key === 'DELETE') ||
(!isFromSameUser && !isOnDelete)
if (mode) return this.selectCharacterAndMarkDeleted(options)
pass = true
if (!isFromSameUser && isOnDelete) pass = true
if (!pass) return this.deleteCharacter(direction.move)
}
if (isOnDelete) {
const annotation = this.getAnnotationByStatus('delete')
const isOnLeftEdge = this.isOnLeftEdge(annotation)
const isOnRightEdge = this.isOnRightEdge(annotation)
const isFromSameUser = this.isAnnotationFromTheSameUser(annotation)
const key = direction.key
let moveOnly, point
if (!isOnLeftEdge && !isOnRightEdge) {
point = annotation[direction.cursorTo + 'Offset']
moveOnly = true
} else if (isOnLeftEdge && key === 'DELETE') {
point = annotation.endOffset
moveOnly = true
} else if (isOnRightEdge && key === 'BACKSPACE') {
point = annotation.startOffset
moveOnly = true
}
if (moveOnly) return this.moveCursorTo(point)
if (isFromSameUser) {
return this.expandAnnotationToDirection(annotation, direction)
} else {
return this.selectCharacterAndMarkDeleted(options)
}
handleDeleteNonCollapsed (options) {
const { direction } = options
let { selection } = options
const notOnTrack = this.isNotOnTrackAnnotation()
const isOnDelete = this.isOnAnnotation('delete')
if (notOnTrack) return this.markSelectionAsDeleted(options)
const shortenBy = this.deleteAllOwnAdditions(selection)
const startOffset = selection.startOffset
const endOffset = selection.endOffset - shortenBy
this.updateSelection(selection, startOffset, endOffset)
// console.log('collapsed', selection.isCollapsed())
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
if (selection.isCollapsed()) return this.handleDeleteCollapsed(options)
if (isOnDelete) {
// console.log
const annotation = this.getAnnotationByStatus('delete')
const containedWithin = this.isSelectionContainedWithin(annotation)
if (containedWithin) {
const point = annotation[direction.cursorTo + 'Offset']
return this.moveCursorTo(point)
}
// const selection = this.getSelection()
// this.expandTrackAnnotation(selection, annotation)
//
// const key = direction.key
//
// let point
// if (key === 'BACKSPACE') point = selection.startOffset
// if (key === 'DELETE') point = selection.endOffset
// this.moveCursorTo(point)
}
options.selection = this.deleteOrMergeAllOwnDeletions(selection)
// console.log(this.getSelection())
// options.selection = this.getSelection()
this.markSelectionAsDeleted(options)
/*
HANDLER COMMON FUNCTIONS
*/
createAdditionAnnotationOnLastChar () {
const selection = this.setSelectionPlusOne('left')
this.createAddAnnotation(selection)
this.moveCursorTo('end')
}
deleteAllOwnAdditions (selection) {
const originalSelection = selection || this.getSelection()
let shortenBy = 0
const additions = this.getAllAnnotationsByStatus('add')
const ownAdditions = filter(additions, annotation => {
return this.isAnnotationFromTheSameUser(annotation)
})
each(ownAdditions, (annotation) => {
const selection = annotation.getSelection()
// make sure only the part of the annotation that is selected is deleted
if (annotation.startOffset < originalSelection.startOffset) {
selection.startOffset = originalSelection.startOffset
}
if (annotation.endOffset > originalSelection.endOffset) {
selection.endOffset = originalSelection.endOffset
}
shortenBy += (selection.endOffset - selection.startOffset)
this.deleteSelection(selection)
})
return shortenBy // return how much shorter the selection should now be
deleteOrMergeAllOwnDeletions (selection) {
const deletions = clone(this.getAllAnnotationsByStatus('delete'))
const ownDeletions = filter(deletions, annotation => {
return this.isAnnotationFromTheSameUser(annotation)
})
const selectionArray = [selection]
each(ownDeletions, (annotation) => {
const annotationSelection = annotation.getSelection()
const contained = selection.contains(annotationSelection)
if (!contained) {
selectionArray.push(annotationSelection)
}
this.removeTrackAnnotation(annotation)
})
selection.startOffset = minBy(selectionArray, 'startOffset').startOffset
selection.endOffset = maxBy(selectionArray, 'endOffset').endOffset
return selection
// TODO
// this.updateSelection(selection, startOffset, endOffset)
}
deleteSelectedAndCreateAddition (options) {
let { selection } = options
this.createDeleteAnnotation(selection)
this.moveCursorTo('end', selection)
// selection is now collapsed, so handle it as collapsed
this.handleAddCollapsed(options)
}
expandAnnotationToDirection (annotation, options) {
if (!options) options = {}
const move = options.move || 'left'
const cursorTo = options.cursorTo || 'end'
const selection = this.setSelectionPlusOne(move)
this.expandTrackAnnotation(selection, annotation)
this.moveCursorTo(cursorTo)
}
insertCharacterWithAddAnnotation (options) {
const { event } = options
this.insertText(event)
// TODO -- watch it with additions by other users
this.createAdditionAnnotationOnLastChar()
}
insertCharacterWithoutExpandingAnnotation (annotation, options) {
const { event } = options
let selection = options
this.insertText(event)
selection = this.setSelectionPlusOne('left')
this.truncateTrackAnnotation(selection, annotation)
this.moveCursorTo('end')
options.event = null
}
markSelectionAsDeleted (options) {
const { direction, selection } = options
this.createDeleteAnnotation(selection)
this.moveCursorTo(direction.cursorTo)
}
selectCharacterAndMarkDeleted (options) {
const { direction } = options
const selection = this.setSelectionPlusOne(direction.move)
this.createDeleteAnnotation(selection)
this.moveCursorTo(direction.cursorTo)
/*
TRANSFORMATIONS
*/
createAddAnnotation (selection) {
this.createTrackAnnotation(selection, 'add')
}
createDeleteAnnotation (selection) {
this.createTrackAnnotation(selection, 'delete')
}
// TODO -- selection could default to current selection
createTrackAnnotation (selection, status) {
const surface = this.getSurface()
const { user } = this.config
if (selection.isContainerSelection()) {
return console.warn('Cannot delete a container')
}
if (selection.isNodeSelection()) {
if (selection.isCollapsed()) return this.deleteCharacter('left')
return this.deleteSelection(selection)
}
const transformation = (tx, args) => {
const newNode = {
selection: selection,
node: {
status: status,
roles: user.roles,
username: user.username
}
}
surface.transaction(transformation, info)
deleteCharacter (direction) {
const info = { action: 'delete' }
const transformation = (tx, args) => {
args.direction = direction
return deleteChar(tx, args)
}
surface.transaction(transformation, info)
deleteSelection (selection) {
const surface = this.getSurface()
const info = { action: 'delete' }
const transformation = (tx, args) => {
args.selection = selection
return deleteSel(tx, args)
}
surface.transaction(transformation, info)
}
expandTrackAnnotation (selection, annotation) {
const info = this.getInfo()
const transformation = (tx, args) => {
args.selection = selection
args.anno = annotation
expandAnnotation(tx, args)
}
surface.transaction(transformation, info)
insertText (event) {
if (!event) return
surface.transaction(function (tx, args) {
if (surface.domSelection) surface.domSelection.clear()
return surface.insertText(tx, args)
}, { action: 'type' })
removeTrackAnnotation (annotation) {
const surface = this.getSurface()
const transformation = (tx, args) => {
args.nodeId = annotation.id
deleteNode(tx, args)
surface.transaction(transformation)
truncateTrackAnnotation (selection, annotation) {
const surface = this.getSurface()
const info = this.getInfo()
const transformation = (tx, args) => {
args.selection = selection
truncateAnnotation(tx, args)
}
surface.transaction(transformation, info)
}
/*
ACCEPT / REJECT
*/
resolve (annotation, action) {
const next = this.getNext(annotation)
const selection = annotation.getSelection()
const status = annotation.status
this.removeTrackAnnotation(annotation)
if (
(action === 'accept' && status === 'delete') ||
(action === 'reject' && status === 'add')
) {
this.deleteSelection(selection)
}
this.focus(next)
}
computeEntries () {
const doc = this.getDocument()
const nodes = doc.getNodes()
return node.type === 'track-change'
})
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
return entries
}
reComputeEntries () {
this.entries = this.computeEntries()
}
sortNodes (nodes) {
const changes = clone(nodes)
const doc = this.getDocument()
const container = doc.get('body')
const changesArray = map(changes, annotation => {
const blockId = annotation.path[0]
const blockPosition = container.getPosition(blockId)
const nodePosition = annotation.startOffset
return {
id: annotation.id,
blockPosition: blockPosition,
nodePosition: nodePosition,
node: annotation
}
})
return sortBy(changesArray, ['blockPosition', 'nodePosition'])
}
getNext (annotation) {
const entries = this.entries
if (entries.length <= 1) return
const position = findIndex(entries, change => {
return change.node.id === annotation.id
})
if (position === -1) return
const next = position + 1
if (next >= entries.length) return entries[0].node
return entries[next].node
}
focus (annotation) {
if (!annotation) return
const surface = this.getSurface()
const { controller } = this.config
controller.scrollTo(annotation.id)
const selection = annotation.getSelection()
surface.setSelection(selection)
this.moveCursorTo('start')
canAct () {
const { user } = this.config
const accepted = ['admin', 'production-editor', 'copy-editor']
return some(accepted, (role) => includes(user.roles, role))
}
getAllAnnotationsByStatus (status) {
const annotations = this.getAllExistingTrackAnnotations()
return annotationsByStatus
}
// TODO -- handle multiple delete and add annotations
getAnnotationByStatus (status) {
const annotationsForStatus = this.getAllAnnotationsByStatus(status)
return annotationsForStatus[0]
// getExistingAnnotation () {
// const documentSession = this.getDocumentSession()
// const selectionState = documentSession.getSelectionState()
// const annotations = selectionState.getAnnotationsForType('track-change')
// return annotations[0]
// }
getAllExistingTrackAnnotations () {
const documentSession = this.getDocumentSession()
const selectionState = documentSession.getSelectionState()
const annotations = selectionState.getAnnotationsForType('track-change')
return annotations
}
getAnnotationUser (annotation) {
return annotation.user.id
}
// getDistinctAnnotationsForSelection () {
// const selection = this.getSelection()
// const annotations = this.getAllExistingTrackAnnotations()
//
// console.log(selection)
// console.log(annotations)
//
// console.log(selection.getFragments())
//
// // get all annotations by status (add / delete)
// // get all pieces of free text
// }
// prevent substance from running getBoundingRectangle,
// as we will unset the selection manually
getInfo () {
return { skipSelection: true }
}
isAnnotationFromTheSameUser (annotation) {
const annotationUser = this.getAnnotationUser(annotation)
const currentUser = this.getCurrentUser()
if (annotationUser === currentUser) return true
return false
}
isNotOnTrackAnnotation () {
const annotations = this.getAllExistingTrackAnnotations()
return (annotations.length === 0)
}
// 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
}
isOnLeftEdge (annotation) {
const selection = this.getSelection()
return (selection.startOffset === annotation.startOffset)
}
isOnRightEdge (annotation) {
const selection = this.getSelection()
return (selection.endOffset === annotation.endOffset)
}
// TODO -- shouldn't run both all the time
handleUndoRedo (update, info) {
if (!info.replay) return
// console.log('update', update)
// console.log('info', info)
// this.handleUndo(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()
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)
isAnnotationContainedWithinSelection (annotation, strict) {
const selection = this.getSelection()
const annotationSelection = annotation.getSelection()
return selection.contains(annotationSelection, strict)
isSelectionCollapsed () {
const isCollapsed = selection.isCollapsed()
return isCollapsed
}
// TODO -- refactor this and isAnnotationContainedWithinSelection into one
isSelectionContainedWithin (annotation, strict) {
const selection = this.getSelection()
// console.trace()
const annotationSelection = annotation.getSelection()
return annotationSelection.contains(selection, strict)
// const leftSide = (selection.startOffset < annotation.startOffset)
// const rightSide = (selection.endOffset > annotation.endOffset)
//
// if (leftSide || rightSide) return false
// return true
moveCursorTo (point, sel) {
const selection = sel || this.getSelection()
// TODO -- use substance's selection.collapse(direction)
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)
}
setSelectionPlusOne (direction) {
const selection = this.getSelection()
if (direction === 'left') selection.startOffset -= 1
if (direction === 'right') selection.endOffset += 1
updateSelection (selection, startOffset, endOffset) {
const surface = this.getSurface()
selection.startOffset = startOffset
selection.endOffset = endOffset
surface.setSelection(selection)
return selection
}
getCommandManager () {
return this.config.commandManager
}
getCurrentUser () {
return this.config.user.id
}
getDocumentSession () {
return this.config.documentSession
getMode () {
const trackState = this.getTrackState()
return trackState.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 commandManager = this.getCommandManager()
const commandStates = commandManager.getCommandStates()
return commandStates['track-change']
}
TOCProvider.tocTypes = ['track-change']
export default TrackChangesProvider