From 400406a650bfcb75bec09d1936ec41e3651f1a1e Mon Sep 17 00:00:00 2001 From: Rainer Simon Date: Sat, 20 Mar 2021 14:56:32 +0100 Subject: [PATCH] Enhanced logic to determine whether annotation has delete button or not --- src/TextAnnotator.jsx | 287 ------------------- src/editor/Editor.jsx | 24 +- src/editor/widgets/comment/CommentWidget.jsx | 61 ++-- 3 files changed, 51 insertions(+), 321 deletions(-) delete mode 100644 src/TextAnnotator.jsx diff --git a/src/TextAnnotator.jsx b/src/TextAnnotator.jsx deleted file mode 100644 index aad0121..0000000 --- a/src/TextAnnotator.jsx +++ /dev/null @@ -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 && - - } - - { this.state.selectedRelation && - - } - - ); - } - -} diff --git a/src/editor/Editor.jsx b/src/editor/Editor.jsx index b4f322d..228cafb 100644 --- a/src/editor/Editor.jsx +++ b/src/editor/Editor.jsx @@ -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 const widgets = props.config.widgets ? props.config.widgets.map(getWidget) : DEFAULT_WIDGETS; - const removable = - currentAnnotation && - currentAnnotation.bodies.length > 0 && - !currentAnnotation.isSelection && - (props.isRemovable ? props.isRemovable(currentAnnotation) : true); + const isReadOnlyWidget = w => w.type.disableDelete ? + w.type.disableDelete(currentAnnotation, { + ...w.props, + readOnly:props.readOnly, + 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 (
@@ -180,8 +188,8 @@ const Editor = props => {
) : (
- { removable && ( - )} diff --git a/src/editor/widgets/comment/CommentWidget.jsx b/src/editor/widgets/comment/CommentWidget.jsx index 1fe0fdc..dd737f8 100644 --- a/src/editor/widgets/comment/CommentWidget.jsx +++ b/src/editor/widgets/comment/CommentWidget.jsx @@ -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 */ @@ -59,31 +86,6 @@ const CommentWidget = props => { const onChangeReplyPurpose = purpose => 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 ( <> { comments.map((body, idx) => @@ -91,7 +93,7 @@ const CommentWidget = props => { key={idx} env={props.env} purposeSelector={props.purposeSelector} - readOnly={isReadOnly(body)} + readOnly={isReadOnlyComment(body, props)} body={body} onUpdate={props.onUpdateBody} 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; \ No newline at end of file