Enhanced logic to determine whether annotation has delete button or not
This commit is contained in:
parent
75d5c79d13
commit
400406a650
|
@ -1,287 +0,0 @@
|
||||||
import React, { Component } from 'preact/compat';
|
|
||||||
import Editor from './editor/Editor';
|
|
||||||
import Highlighter from './highlighter/Highlighter';
|
|
||||||
import SelectionHandler from './selection/SelectionHandler';
|
|
||||||
import RelationsLayer from './relations/RelationsLayer';
|
|
||||||
import RelationEditor from './relations/editor/RelationEditor';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pulls the strings between the annotation highlight layer
|
|
||||||
* and the editor popup.
|
|
||||||
*/
|
|
||||||
export default class TextAnnotator extends Component {
|
|
||||||
|
|
||||||
state = {
|
|
||||||
selectedAnnotation: null,
|
|
||||||
selectedDOMElement: null,
|
|
||||||
selectedRelation: null,
|
|
||||||
headless: false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Shorthand **/
|
|
||||||
clearState = () => {
|
|
||||||
this.setState({
|
|
||||||
selectedAnnotation: null,
|
|
||||||
selectedDOMElement: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEscape = (evt) => {
|
|
||||||
if (evt.which === 27)
|
|
||||||
this.onCancelAnnotation();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.highlighter = new Highlighter(this.props.contentEl, this.props.config.formatter);
|
|
||||||
|
|
||||||
this.selectionHandler = new SelectionHandler(this.props.contentEl, this.highlighter, this.props.config.readOnly);
|
|
||||||
this.selectionHandler.on('select', this.handleSelect);
|
|
||||||
|
|
||||||
this.relationsLayer = new RelationsLayer(this.props.contentEl);
|
|
||||||
this.relationsLayer.readOnly = true; // Deactivate by default
|
|
||||||
|
|
||||||
this.relationsLayer.on('createRelation', this.onEditRelation);
|
|
||||||
this.relationsLayer.on('selectRelation', this.onEditRelation);
|
|
||||||
this.relationsLayer.on('cancelDrawing', this.closeRelationsEditor);
|
|
||||||
|
|
||||||
document.addEventListener('keydown', this.handleEscape);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
document.removeEventListener('keydown', this.handleEscape);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**************************/
|
|
||||||
/* Annotation CRUD events */
|
|
||||||
/**************************/
|
|
||||||
|
|
||||||
/** Selection on the text **/
|
|
||||||
handleSelect = evt => {
|
|
||||||
const { selection, element } = evt;
|
|
||||||
if (selection) {
|
|
||||||
this.setState({
|
|
||||||
selectedAnnotation: null,
|
|
||||||
selectedDOMElement: null
|
|
||||||
}, () => this.setState({
|
|
||||||
selectedAnnotation: selection,
|
|
||||||
selectedDOMElement: element
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!selection.isSelection)
|
|
||||||
this.props.onAnnotationSelected(selection.clone());
|
|
||||||
} else {
|
|
||||||
this.clearState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A convenience method that allows the external application to
|
|
||||||
* override the autogenerated Id for an annotation.
|
|
||||||
*
|
|
||||||
* Usually, the override will happen almost immediately after
|
|
||||||
* the annotation is created. But we need to be defensive and assume
|
|
||||||
* that the override might come in with considerable delay, thus
|
|
||||||
* the user might have made further edits already.
|
|
||||||
*
|
|
||||||
* A key challenge here is that there may be dependencies between
|
|
||||||
* the original annotation and relations that were created meanwhile.
|
|
||||||
*/
|
|
||||||
overrideAnnotationId = originalAnnotation => forcedId => {
|
|
||||||
const { id } = originalAnnotation;
|
|
||||||
|
|
||||||
// After the annotation update, we need to update dependencies
|
|
||||||
// on the annotation layer, if any
|
|
||||||
const updateDependentRelations = updatedAnnotation => {
|
|
||||||
// Wait until the highlighter update has come into effect
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.relationsLayer.overrideTargetAnnotation(originalAnnotation, updatedAnnotation);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// Force the editors to close first, otherwise their annotations will be orphaned
|
|
||||||
if (this.state.selectedAnnotation || this.state.selectedRelation) {
|
|
||||||
this.relationsLayer.resetDrawing();
|
|
||||||
this.setState({
|
|
||||||
selectedAnnotation: null,
|
|
||||||
selectedRelation: null
|
|
||||||
}, () => {
|
|
||||||
const updated = this.highlighter.overrideId(id, forcedId);
|
|
||||||
updateDependentRelations(updated);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const updated = this.highlighter.overrideId(id, forcedId);
|
|
||||||
updateDependentRelations(updated);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A convenience method that allows the external application to
|
|
||||||
* override the autogenerated Id for a relation.
|
|
||||||
*
|
|
||||||
* This operation is less problematic than .overrideAnnotation().
|
|
||||||
* We just need to make sure the RelationEditor is closed, so that
|
|
||||||
* the annotation doesn't become orphaned. Otherwise, there are
|
|
||||||
* no dependencies.
|
|
||||||
*/
|
|
||||||
overrideRelationId = originalId => forcedId => {
|
|
||||||
if (this.state.selectedRelation) {
|
|
||||||
this.setState({ selectedRelation: null }, () =>
|
|
||||||
this.relationsLayer.overrideId(originalId, forcedId));
|
|
||||||
} else {
|
|
||||||
this.relationsLayer.overrideId(originalId, forcedId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Common handler for annotation CREATE or UPDATE **/
|
|
||||||
onCreateOrUpdateAnnotation = method => (annotation, previous) => {
|
|
||||||
this.clearState();
|
|
||||||
|
|
||||||
this.selectionHandler.clearSelection();
|
|
||||||
this.highlighter.addOrUpdateAnnotation(annotation, previous);
|
|
||||||
|
|
||||||
// Call CREATE or UPDATE handler
|
|
||||||
if (previous)
|
|
||||||
this.props[method](annotation.clone(), previous.clone());
|
|
||||||
else
|
|
||||||
this.props[method](annotation.clone(), this.overrideAnnotationId(annotation));
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeleteAnnotation = annotation => {
|
|
||||||
// Delete connections
|
|
||||||
this.relationsLayer.destroyConnectionsFor(annotation);
|
|
||||||
|
|
||||||
this.clearState();
|
|
||||||
this.selectionHandler.clearSelection();
|
|
||||||
this.highlighter.removeAnnotation(annotation);
|
|
||||||
|
|
||||||
this.props.onAnnotationDeleted(annotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Cancel button on annotation editor **/
|
|
||||||
onCancelAnnotation = () => {
|
|
||||||
this.clearState();
|
|
||||||
this.selectionHandler.clearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/************************/
|
|
||||||
/* Relation CRUD events */
|
|
||||||
/************************/
|
|
||||||
|
|
||||||
// Shorthand
|
|
||||||
closeRelationsEditor = () => {
|
|
||||||
this.setState({ selectedRelation: null });
|
|
||||||
this.relationsLayer.resetDrawing();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Selection on the relations layer: open an existing
|
|
||||||
* or newly created connection for editing.
|
|
||||||
*/
|
|
||||||
onEditRelation = relation => {
|
|
||||||
this.setState({
|
|
||||||
selectedRelation: relation
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 'Ok' on the relation editor popup **/
|
|
||||||
onCreateOrUpdateRelation = (relation, previous) => {
|
|
||||||
this.relationsLayer.addOrUpdateRelation(relation, previous);
|
|
||||||
this.closeRelationsEditor();
|
|
||||||
|
|
||||||
// This method will always receive a 'previous' connection -
|
|
||||||
// if the previous is just an empty connection, fire 'create',
|
|
||||||
// otherwise, fire 'update'
|
|
||||||
const isNew = previous.annotation.bodies.length === 0;
|
|
||||||
|
|
||||||
if (isNew)
|
|
||||||
this.props.onAnnotationCreated(relation.annotation.clone(), this.overrideRelationId(relation.annotation.id));
|
|
||||||
else
|
|
||||||
this.props.onAnnotationUpdated(relation.annotation.clone(), previous.annotation.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 'Delete' on the relation editor popup **/
|
|
||||||
onDeleteRelation = relation => {
|
|
||||||
this.relationsLayer.removeRelation(relation);
|
|
||||||
this.closeRelationsEditor();
|
|
||||||
this.props.onAnnotationDeleted(relation.annotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************/
|
|
||||||
/* External API */
|
|
||||||
/****************/
|
|
||||||
|
|
||||||
addAnnotation = annotation => {
|
|
||||||
this.highlighter.addOrUpdateAnnotation(annotation.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
removeAnnotation = annotation => {
|
|
||||||
this.highlighter.removeAnnotation(annotation);
|
|
||||||
|
|
||||||
// If the editor is currently open on this annotation, close it
|
|
||||||
const { selectedAnnotation } = this.state;
|
|
||||||
if (selectedAnnotation && annotation.isEqual(selectedAnnotation))
|
|
||||||
this.clearState();
|
|
||||||
}
|
|
||||||
|
|
||||||
setAnnotations = annotations => {
|
|
||||||
const clones = annotations.map(a => a.clone());
|
|
||||||
this.highlighter.init(clones).then(() =>
|
|
||||||
this.relationsLayer.init(clones));
|
|
||||||
}
|
|
||||||
|
|
||||||
getAnnotations = () => {
|
|
||||||
const annotations = this.highlighter.getAllAnnotations();
|
|
||||||
const relations = this.relationsLayer.getAllRelations();
|
|
||||||
return annotations.concat(relations).map(a => a.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
setMode = mode => {
|
|
||||||
if (mode === 'RELATIONS') {
|
|
||||||
this.clearState();
|
|
||||||
|
|
||||||
this.selectionHandler.enabled = false;
|
|
||||||
|
|
||||||
this.relationsLayer.readOnly = false;
|
|
||||||
this.relationsLayer.startDrawing();
|
|
||||||
} else {
|
|
||||||
this.setState({ selectedRelation: null });
|
|
||||||
|
|
||||||
this.selectionHandler.enabled = true;
|
|
||||||
|
|
||||||
this.relationsLayer.readOnly = true;
|
|
||||||
this.relationsLayer.stopDrawing();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{ this.state.selectedAnnotation &&
|
|
||||||
<Editor
|
|
||||||
wrapperEl={this.props.wrapperEl}
|
|
||||||
annotation={this.state.selectedAnnotation}
|
|
||||||
selectedElement={this.state.selectedDOMElement}
|
|
||||||
config={this.props.config}
|
|
||||||
env={this.props.env}
|
|
||||||
isRemovable={this.props.isRemovable}
|
|
||||||
onAnnotationCreated={this.onCreateOrUpdateAnnotation('onAnnotationCreated')}
|
|
||||||
onAnnotationUpdated={this.onCreateOrUpdateAnnotation('onAnnotationUpdated')}
|
|
||||||
onAnnotationDeleted={this.onDeleteAnnotation}
|
|
||||||
onCancel={this.onCancelAnnotation} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{ this.state.selectedRelation &&
|
|
||||||
<RelationEditor
|
|
||||||
relation={this.state.selectedRelation}
|
|
||||||
onRelationCreated={this.onCreateOrUpdateRelation}
|
|
||||||
onRelationUpdated={this.onCreateOrUpdateRelation}
|
|
||||||
onRelationDeleted={this.onDeleteRelation}
|
|
||||||
onCancel={this.closeRelationsEditor}
|
|
||||||
vocabulary={this.props.relationVocabulary}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -142,17 +142,25 @@ const Editor = props => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRemove = () => props.onAnnotationDeleted(props.annotation);
|
const onDelete = () =>
|
||||||
|
props.onAnnotationDeleted(props.annotation);
|
||||||
|
|
||||||
// Use default comment + tag widget unless host app overrides
|
// Use default comment + tag widget unless host app overrides
|
||||||
const widgets = props.config.widgets ?
|
const widgets = props.config.widgets ?
|
||||||
props.config.widgets.map(getWidget) : DEFAULT_WIDGETS;
|
props.config.widgets.map(getWidget) : DEFAULT_WIDGETS;
|
||||||
|
|
||||||
const removable =
|
const isReadOnlyWidget = w => w.type.disableDelete ?
|
||||||
currentAnnotation &&
|
w.type.disableDelete(currentAnnotation, {
|
||||||
currentAnnotation.bodies.length > 0 &&
|
...w.props,
|
||||||
!currentAnnotation.isSelection &&
|
readOnly:props.readOnly,
|
||||||
(props.isRemovable ? props.isRemovable(currentAnnotation) : true);
|
env: props.env
|
||||||
|
}) : false;
|
||||||
|
|
||||||
|
const hasDelete = currentAnnotation &&
|
||||||
|
currentAnnotation.bodies.length > 0 && // annotation has bodies,
|
||||||
|
!props.readOnly && // we are not in read-only config,
|
||||||
|
!currentAnnotation.isSelection && // this is not a selection, and
|
||||||
|
!widgets.some(isReadOnlyWidget); // every widget is deletable
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={element} className="r6o-editor">
|
<div ref={element} className="r6o-editor">
|
||||||
|
@ -180,8 +188,8 @@ const Editor = props => {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="r6o-footer">
|
<div className="r6o-footer">
|
||||||
{ removable && (
|
{ hasDelete && (
|
||||||
<button className="r6o-btn highlight left outline" onClick={onRemove}>
|
<button className="r6o-btn highlight left outline" onClick={onDelete}>
|
||||||
{i18n.t('Remove')}
|
{i18n.t('Remove')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -19,6 +19,33 @@ const isComment = (body, matchAllPurposes) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
/* A comment should be read-only if:
|
||||||
|
/* - the global read-only flag is set
|
||||||
|
/* - the current rule is 'MINE_ONLY' and the creator ID differs
|
||||||
|
/* The 'editable' config flag overrides the global setting, if any
|
||||||
|
*/
|
||||||
|
const isReadOnlyComment = (body, props) => {
|
||||||
|
if (props.editable === true)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (props.editable === false)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (props.editable === 'MINE_ONLY') {
|
||||||
|
// The original creator of the body
|
||||||
|
const creator = body.creator?.id;
|
||||||
|
|
||||||
|
// The current user
|
||||||
|
const me = props.env.user?.id;
|
||||||
|
|
||||||
|
return me !== creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global setting as last possible option
|
||||||
|
return props.readOnly;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The draft reply is a comment body with a 'draft' flag
|
* The draft reply is a comment body with a 'draft' flag
|
||||||
*/
|
*/
|
||||||
|
@ -59,31 +86,6 @@ const CommentWidget = props => {
|
||||||
const onChangeReplyPurpose = purpose =>
|
const onChangeReplyPurpose = purpose =>
|
||||||
props.onUpdateBody(draftReply, { ...draftReply, purpose: purpose.value });
|
props.onUpdateBody(draftReply, { ...draftReply, purpose: purpose.value });
|
||||||
|
|
||||||
// A comment should be read-only if:
|
|
||||||
// - the global read-only flag is set
|
|
||||||
// - the current rule is 'MINE_ONLY' and the creator ID differs
|
|
||||||
// The 'editable' config flag overrides the global setting, if any
|
|
||||||
const isReadOnly = body => {
|
|
||||||
if (props.editable === true)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (props.editable === false)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
if (props.editable === 'MINE_ONLY') {
|
|
||||||
// The original creator of the body
|
|
||||||
const creator = body.creator?.id;
|
|
||||||
|
|
||||||
// The current user
|
|
||||||
const me = props.env.user?.id;
|
|
||||||
|
|
||||||
return me !== creator;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global setting as last possible option
|
|
||||||
return props.readOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ comments.map((body, idx) =>
|
{ comments.map((body, idx) =>
|
||||||
|
@ -91,7 +93,7 @@ const CommentWidget = props => {
|
||||||
key={idx}
|
key={idx}
|
||||||
env={props.env}
|
env={props.env}
|
||||||
purposeSelector={props.purposeSelector}
|
purposeSelector={props.purposeSelector}
|
||||||
readOnly={isReadOnly(body)}
|
readOnly={isReadOnlyComment(body, props)}
|
||||||
body={body}
|
body={body}
|
||||||
onUpdate={props.onUpdateBody}
|
onUpdate={props.onUpdateBody}
|
||||||
onDelete={props.onRemoveBody}
|
onDelete={props.onRemoveBody}
|
||||||
|
@ -122,4 +124,11 @@ const CommentWidget = props => {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CommentWidget.disableDelete = (annotation, props) => {
|
||||||
|
const commentBodies =
|
||||||
|
annotation.bodies.filter(body => isComment(body, props.purposeSelector));
|
||||||
|
|
||||||
|
return commentBodies.some(comment => isReadOnlyComment(comment, props));
|
||||||
|
}
|
||||||
|
|
||||||
export default CommentWidget;
|
export default CommentWidget;
|
Loading…
Reference in New Issue