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.
341 lines (288 loc) • 9.36 kB
JavaScript
import { annotationHelpers } from '../model'
import Command from './Command'
/**
A class for commands intended to be executed on the annotations.
See the example below to learn how to register an `AnnotationCommand`
for a strong annotation.
@class AnnotationCommand
@extends ui/Command
@example
```js
import { AnnotationCommand } from 'substance'
config.addCommand('strong', AnnotationCommand, {nodeType: 'strong'})
// Disable, when cursor is collapsed
config.addCommand('strong', AnnotationCommand, {
nodeType: 'strong'
})
```
*/
class AnnotationCommand extends Command {
constructor(...args) {
super(...args)
if (!this.config.nodeType) {
throw new Error("'nodeType' is required")
}
}
/**
Get the type of an annotation.
@returns {String} The annotation's type.
*/
getAnnotationType() {
return this.config.nodeType
}
getType() {
return this.getAnnotationType()
}
/**
Get the annotation's data.
@returns {Object} The annotation's data.
*/
getAnnotationData() {
return {}
}
/**
Checks if command couldn't be executed with current selection.
@param {Array} annos annotations
@param {Object} sel selection
@returns {Boolean} Whether or not command could be executed.
*/
isDisabled(sel, params) {
let selectionState = params.selectionState
let isBlurred = params.editorSession.isBlurred()
// TODO: Container selections should be valid if the annotation type
// is a container annotation. Currently we only allow property selections.
if (isBlurred || !sel || sel.isNull() || !sel.isAttached() || sel.isCustomSelection()||
sel.isNodeSelection() || sel.isContainerSelection() || selectionState.isInlineNodeSelection()) {
return true
}
return false
}
/*
When cursor is not collapsed tool may be displayed in context (e.g. in an
overlay)
*/
showInContext(sel/*, params*/) {
return !sel.isCollapsed()
}
/**
Checks if new annotations could be created.
There should be no annotation overlapping, selection must be not collapsed.
@param {Array} annos annotations
@param {Object} sel selection
@returns {Boolean} Whether or not annotation could be created.
*/
// When there's no existing annotation overlapping, we create a new one.
canCreate(annos, sel) {
return (annos.length === 0 && !sel.isCollapsed())
}
/**
Checks if annotations could be fused.
There should be more than one annotation overlaped by current selection.
@param {Array} annos annotations
@param {Object} sel selection
@returns {Boolean} Whether or not annotations could be fused.
*/
canFuse(annos, sel) {
// When more than one annotation overlaps with the current selection
return (annos.length >= 2 && !sel.isCollapsed())
}
/**
Checks if annotation could be deleted.
Cursor or selection must be inside an existing annotation.
@param {Array} annos annotations
@param {Object} sel selection
@returns {Boolean} Whether or not annotation could be deleted.
*/
canDelete(annos, sel) {
// When the cursor or selection is inside an existing annotation
if (annos.length !== 1) return false
let annoSel = annos[0].getSelection()
return sel.isInsideOf(annoSel)
}
/**
Checks if annotation could be expanded.
There should be overlap with only a single annotation,
selection should be also outside of this annotation.
@param {Array} annos annotations
@param {Object} sel selection
@returns {Boolean} Whether or not annotation could be expanded.
*/
canExpand(annos, sel) {
// When there's some overlap with only a single annotation we do an expand
if (annos.length !== 1) return false
let annoSel = annos[0].getSelection()
return sel.overlaps(annoSel) && !sel.isInsideOf(annoSel)
}
/**
Checks if annotation could be truncated.
There should be overlap with only a single annotation,
selection should also have boundary in common with this annotation.
@param {Array} annos annotations
@param {Object} sel selection
@returns {Boolean} Whether or not annotation could be truncated.
*/
canTruncate(annos, sel) {
if (annos.length !== 1) return false
let annoSel = annos[0].getSelection()
return (sel.isLeftAlignedWith(annoSel) || sel.isRightAlignedWith(annoSel)) &&
!sel.contains(annoSel) &&
!sel.isCollapsed()
}
/**
Gets command state object.
@param {Object} state.selection the current selection
@returns {Object} info object with command details.
*/
getCommandState(params) { // eslint-disable-line
let sel = this._getSelection(params)
// We can skip all checking if a disabled condition is met
// E.g. we don't allow toggling of property annotations when current
// selection is a container selection
if (this.isDisabled(sel, params)) {
return {
disabled: true
}
}
let annos = this._getAnnotationsForSelection(params)
let newState = {
disabled: false,
active: false,
mode: null
}
if (this.canCreate(annos, sel)) {
newState.mode = 'create'
} else if (this.canFuse(annos, sel)) {
newState.mode = 'fuse'
} else if (this.canTruncate(annos, sel)) {
newState.active = true
newState.mode = 'truncate'
} else if (this.canExpand(annos, sel)) {
newState.mode = 'expand'
} else if (this.canDelete(annos, sel)) {
newState.active = true
newState.mode = 'delete'
} else {
newState.disabled = true
}
newState.showInContext = this.showInContext(sel, params)
return newState
}
/**
Execute command and trigger transformation.
@returns {Object} info object with execution details.
*/
// Execute command and trigger transformations
execute(params) {
// Disabled the next line as I believe it is
// always passed via params already
// params.selection = this._getSelection(params)
let commandState = params.commandState
if (commandState.disabled) return false
switch(commandState.mode) {
case 'create':
return this.executeCreate(params)
case 'fuse':
return this.executeFuse(params)
case 'truncate':
return this.executeTruncate(params)
case 'expand':
return this.executeExpand(params)
case 'delete':
return this.executeDelete(params)
default:
console.warn('Command.execute(): unknown mode', commandState.mode)
return false
}
}
executeCreate(params) {
let annos = this._getAnnotationsForSelection(params)
this._checkPrecondition(params, annos, this.canCreate)
let editorSession = this._getEditorSession(params)
let annoData = this.getAnnotationData()
annoData.type = this.getAnnotationType()
let anno
editorSession.transaction((tx) => {
anno = tx.annotate(annoData)
})
return {
mode: 'create',
anno: anno
}
}
executeFuse(params) {
let annos = this._getAnnotationsForSelection(params);
this._checkPrecondition(params, annos, this.canFuse);
this._applyTransform(params, function(tx) {
annotationHelpers.fuseAnnotation(tx, annos)
})
return {
mode: 'fuse',
anno: annos[0]
}
}
executeTruncate(params) {
let annos = this._getAnnotationsForSelection(params)
let anno = annos[0]
this._checkPrecondition(params, annos, this.canTruncate)
this._applyTransform(params, function(tx) {
annotationHelpers.truncateAnnotation(tx, anno, params.selection)
})
return {
mode: 'truncate',
anno: anno
}
}
executeExpand(params) {
let annos = this._getAnnotationsForSelection(params)
let anno = annos[0]
this._checkPrecondition(params, annos, this.canExpand)
this._applyTransform(params, function(tx) {
annotationHelpers.expandAnnotation(tx, anno, params.selection)
})
return {
mode: 'expand',
anno: anno
}
}
executeDelete(params) {
let annos = this._getAnnotationsForSelection(params)
let anno = annos[0]
this._checkPrecondition(params, annos, this.canDelete)
this._applyTransform(params, function(tx) {
return tx.delete(anno.id)
})
return {
mode: 'delete',
annoId: anno.id
}
}
isAnnotationCommand() {
return true
}
_checkPrecondition(params, annos, checker) {
let sel = this._getSelection(params)
if (!checker.call(this, annos, sel)) {
throw new Error("AnnotationCommand: can't execute command for selection " + sel.toString())
}
}
_getAnnotationsForSelection(params) {
return params.selectionState.getAnnotationsForType(this.getAnnotationType())
}
/**
Apply an annotation transformation.
@returns {Object} transformed annotations.
*/
_applyTransform(params, transformFn) {
let sel = this._getSelection(params)
if (sel.isNull()) return
let editorSession = this._getEditorSession(params)
let result // to store transform result
editorSession.setSelection(sel)
editorSession.transaction(function(tx) {
let out = transformFn(tx, params)
if (out) result = out.result
})
return result
}
}
export default AnnotationCommand