This commit is contained in:
Rainer Simon 2022-07-24 11:01:02 +02:00
parent e7d7cd1948
commit 9657769e42
4 changed files with 83 additions and 59 deletions

View File

@ -11,6 +11,7 @@ export default class Selection {
constructor(target, body) { constructor(target, body) {
this.underlying = { this.underlying = {
'@context': 'http://www.w3.org/ns/anno.jsonld',
type: 'Selection', type: 'Selection',
body: body || [], body: body || [],
target target
@ -21,7 +22,7 @@ export default class Selection {
clone = opt_props => { clone = opt_props => {
// Deep-clone // Deep-clone
const cloned = new Selection(); const cloned = new Selection();
cloned.underlying = JSON.parse(JSON.stringify(this.underlying)); cloned.underlying = JSON.parse(JSON.stringify(this.underlying));
if (opt_props) if (opt_props)
cloned.underlying = { ...cloned.underlying, ...opt_props }; cloned.underlying = { ...cloned.underlying, ...opt_props };
@ -29,6 +30,10 @@ export default class Selection {
return cloned; return cloned;
} }
get context() {
return this.underlying['@context'];
}
get type() { get type() {
return this.underlying.type; return this.underlying.type;
} }
@ -54,7 +59,7 @@ export default class Selection {
return equals(this.underlying, other.underlying); return equals(this.underlying, other.underlying);
} }
} }
get bodies() { get bodies() {
return (Array.isArray(this.underlying.body)) ? return (Array.isArray(this.underlying.body)) ?
this.underlying.body : [ this.underlying.body ]; this.underlying.body : [ this.underlying.body ];
@ -77,7 +82,7 @@ export default class Selection {
return this.selector('TextQuoteSelector')?.exact; return this.selector('TextQuoteSelector')?.exact;
} }
/*******************************************/ /*******************************************/
/* Selection-specific properties & methods */ /* Selection-specific properties & methods */
/*******************************************/ /*******************************************/
@ -87,7 +92,6 @@ export default class Selection {
toAnnotation = () => { toAnnotation = () => {
const a = Object.assign({}, this.underlying, { const a = Object.assign({}, this.underlying, {
'@context': 'http://www.w3.org/ns/anno.jsonld',
'type': 'Annotation', 'type': 'Annotation',
'id': `#${uuid()}` 'id': `#${uuid()}`
}); });
@ -95,4 +99,4 @@ export default class Selection {
return new WebAnnotation(a); return new WebAnnotation(a);
} }
} }

View File

@ -42,13 +42,17 @@ export default class WebAnnotation {
return this.opts?.readOnly; return this.opts?.readOnly;
} }
/*************************************/ /*************************************/
/* Getters to forward properties of */ /* Getters to forward properties of */
/* the underlying annotation */ /* the underlying annotation */
/*************************************/ /*************************************/
get id() { get id() {
return this.underlying.id; return this.underlying.id;
}
get context() {
return this.underlying['@context'];
} }
get type() { get type() {
@ -83,8 +87,8 @@ export default class WebAnnotation {
return (Array.isArray(this.underlying.target)) ? return (Array.isArray(this.underlying.target)) ?
this.underlying.target : [ this.underlying.target ]; this.underlying.target : [ this.underlying.target ];
} }
/*****************************************/ /*****************************************/
/* Various access helpers and shorthands */ /* Various access helpers and shorthands */
/*****************************************/ /*****************************************/
@ -115,5 +119,5 @@ export default class WebAnnotation {
get end() { get end() {
return this.selector('TextPositionSelector')?.end; return this.selector('TextPositionSelector')?.end;
} }
} }

View File

@ -13,7 +13,7 @@ const bounds = elem => {
/** /**
* The popup editor component. * The popup editor component.
* *
* TODO instead of just updating the current annotation state, * TODO instead of just updating the current annotation state,
* we could create a stack of revisions, and allow going back * we could create a stack of revisions, and allow going back
* with CTRL+Z. * with CTRL+Z.
@ -38,10 +38,10 @@ export default class Editor extends Component {
const nextBounds = bounds(next.selectedElement); const nextBounds = bounds(next.selectedElement);
if (!this.props.annotation?.isEqual(next.annotation)) { if (!this.props.annotation?.isEqual(next.annotation)) {
this.setState({ this.setState({
currentAnnotation: next.annotation, currentAnnotation: next.annotation,
selectionBounds: nextBounds selectionBounds: nextBounds
}); });
} else { } else {
this.setState({ selectionBounds: nextBounds }); this.setState({ selectionBounds: nextBounds });
} }
@ -84,7 +84,7 @@ export default class Editor extends Component {
// Defaults to true // Defaults to true
const autoPosition = const autoPosition =
this.props.autoPosition === undefined ? true : this.props.autoPosition; this.props.autoPosition === undefined ? true : this.props.autoPosition;
if (window?.ResizeObserver) { if (window?.ResizeObserver) {
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
if (!this.state.dragged) if (!this.state.dragged)
@ -97,7 +97,7 @@ export default class Editor extends Component {
// Fire setPosition manually *only* for devices that don't support ResizeObserver // Fire setPosition manually *only* for devices that don't support ResizeObserver
if (!this.state.dragged) if (!this.state.dragged)
setPosition(this.props.wrapperEl, this.element.current, this.props.selectedElement, autoPosition); setPosition(this.props.wrapperEl, this.element.current, this.props.selectedElement, autoPosition);
} }
} }
/** Creator and created/modified timestamp metadata **/ /** Creator and created/modified timestamp metadata **/
@ -122,7 +122,7 @@ export default class Editor extends Component {
getCurrentAnnotation = () => getCurrentAnnotation = () =>
this.state.currentAnnotation.clone(); this.state.currentAnnotation.clone();
/** Shorthand **/ /** Shorthand **/
updateCurrentAnnotation = (diff, saveImmediately) => this.setState({ updateCurrentAnnotation = (diff, saveImmediately) => this.setState({
currentAnnotation: this.state.currentAnnotation.clone(diff) currentAnnotation: this.state.currentAnnotation.clone(diff)
}, () => { }, () => {
@ -130,15 +130,15 @@ export default class Editor extends Component {
this.onOk(); this.onOk();
}) })
onAppendBody = (body, saveImmediately) => this.updateCurrentAnnotation({ onAppendBody = (body, saveImmediately) => this.updateCurrentAnnotation({
body: [ body: [
...this.state.currentAnnotation.bodies, ...this.state.currentAnnotation.bodies,
{ ...body, ...this.creationMeta(body) } { ...body, ...this.creationMeta(body) }
] ]
}, saveImmediately); }, saveImmediately);
onUpdateBody = (previous, updated, saveImmediately) => this.updateCurrentAnnotation({ onUpdateBody = (previous, updated, saveImmediately) => this.updateCurrentAnnotation({
body: this.state.currentAnnotation.bodies.map(body => body: this.state.currentAnnotation.bodies.map(body =>
body === previous ? { ...updated, ...this.creationMeta(updated) } : body) body === previous ? { ...updated, ...this.creationMeta(updated) } : body)
}, saveImmediately); }, saveImmediately);
@ -146,7 +146,7 @@ export default class Editor extends Component {
body: this.state.currentAnnotation.bodies.filter(b => b !== body) body: this.state.currentAnnotation.bodies.filter(b => b !== body)
}, saveImmediately); }, saveImmediately);
/** /**
* For convenience: an 'append or update' shorthand. * For convenience: an 'append or update' shorthand.
*/ */
onUpsertBody = (arg1, arg2, saveImmediately) => { onUpsertBody = (arg1, arg2, saveImmediately) => {
@ -167,21 +167,21 @@ export default class Editor extends Component {
} }
} }
/** /**
* Advanced method for applying a batch of body changes * Advanced method for applying a batch of body changes
* in one go (append, remove update), optionally saving * in one go (append, remove update), optionally saving
* immediately afterwards. The argument is an array of * immediately afterwards. The argument is an array of
* diff objects with the following structure: * diff objects with the following structure:
* *
* [ * [
* { action: 'append', body: bodyToAppend }, * { action: 'append', body: bodyToAppend },
* { action: 'update', previous: prevBody, updated: updatedBody } * { action: 'update', previous: prevBody, updated: updatedBody }
* { action: 'remove', body: bodyToRemove }, * { action: 'remove', body: bodyToRemove },
* *
* // Normal upsert, previous is optional * // Normal upsert, previous is optional
* { action: 'upsert', previous: prevBody, updated: updatedBody } * { action: 'upsert', previous: prevBody, updated: updatedBody }
* *
* // Auto-upsert based on purpose * // Auto-upsert based on purpose
* { action: 'upsert', body: bodyToUpsert } * { action: 'upsert', body: bodyToUpsert }
* ] * ]
*/ */
@ -197,7 +197,7 @@ export default class Editor extends Component {
const toRemove = diffs const toRemove = diffs
.filter(d => d.action === 'remove') .filter(d => d.action === 'remove')
.map(d => d.body); .map(d => d.body);
const toAppend = [ const toAppend = [
...diffs ...diffs
.filter(d => (d.action === 'append') || (d.action === 'upsert' && d.updated && !d.previous)) .filter(d => (d.action === 'append') || (d.action === 'upsert' && d.updated && !d.previous))
@ -208,12 +208,12 @@ export default class Editor extends Component {
.map(d => d.updated) .map(d => d.updated)
]; ];
const toUpdate = [ const toUpdate = [
...diffs ...diffs
.filter(d => (d.action === 'update') || (d.action === 'upsert' && d.updated && d.previous)) .filter(d => (d.action === 'update') || (d.action === 'upsert' && d.updated && d.previous))
.map(d => ({ .map(d => ({
previous: d.previous, previous: d.previous,
updated: { ...d.updated, ...this.creationMeta(d.updated) } updated: { ...d.updated, ...this.creationMeta(d.updated) }
})), })),
...autoUpserts ...autoUpserts
@ -222,7 +222,7 @@ export default class Editor extends Component {
const updatedBodies = [ const updatedBodies = [
// Current bodies // Current bodies
...this.state.currentAnnotation.bodies ...this.state.currentAnnotation.bodies
// Remove // Remove
.filter(b => !toRemove.includes(b)) .filter(b => !toRemove.includes(b))
@ -235,7 +235,7 @@ export default class Editor extends Component {
// Append // Append
...toAppend ...toAppend
] ]
this.updateCurrentAnnotation({ body: updatedBodies }, saveImmediately); this.updateCurrentAnnotation({ body: updatedBodies }, saveImmediately);
} }
@ -246,7 +246,7 @@ export default class Editor extends Component {
*/ */
onSetProperty = (property, value) => { onSetProperty = (property, value) => {
// A list of properties the user is NOT allowed to set // A list of properties the user is NOT allowed to set
const isForbidden = [ '@context', 'id', 'type', 'body', 'target' ].includes(property); const isForbidden = [ '@context', 'id', 'type', 'body', 'target' ].includes(property);
if (isForbidden) if (isForbidden)
throw new Exception(`Cannot set ${property} - not allowed`); throw new Exception(`Cannot set ${property} - not allowed`);
@ -260,25 +260,39 @@ export default class Editor extends Component {
} }
} }
onCancel = () => /**
* Adds a URI to the context field
*/
onAddContext = uri => {
const { currentAnnotation } = this.state;
const context = Array.isArray(currentAnnotation.context) ?
currentAnnotation.context : [ currentAnnotation.context ];
if (context.indexOf(uri) < 0) {
context.push(uri);
this.updateCurrentAnnotation({ '@context': context });
}
}
onCancel = () =>
this.props.onCancel(this.props.annotation); this.props.onCancel(this.props.annotation);
onOk = () => { onOk = () => {
// Removes the state payload from all bodies // Removes the state payload from all bodies
const undraft = annotation => const undraft = annotation =>
annotation.clone({ annotation.clone({
body : annotation.bodies.map(({ draft, ...rest }) => rest) body : annotation.bodies.map(({ draft, ...rest }) => rest)
}); });
const { currentAnnotation } = this.state; const { currentAnnotation } = this.state;
// Current annotation is either a selection (if it was created from // Current annotation is either a selection (if it was created from
// scratch just now) or an annotation (if it existed already and was // scratch just now) or an annotation (if it existed already and was
// selected for editing) // selected for editing)
if (currentAnnotation.bodies.length === 0 && !this.props.allowEmpty) { if (currentAnnotation.bodies.length === 0 && !this.props.allowEmpty) {
if (currentAnnotation.isSelection) if (currentAnnotation.isSelection)
this.onCancel(); this.onCancel();
else else
this.props.onAnnotationDeleted(this.props.annotation); this.props.onAnnotationDeleted(this.props.annotation);
} else { } else {
if (currentAnnotation.isSelection) if (currentAnnotation.isSelection)
@ -288,14 +302,14 @@ export default class Editor extends Component {
} }
} }
onDelete = () => onDelete = () =>
this.props.onAnnotationDeleted(this.props.annotation); this.props.onAnnotationDeleted(this.props.annotation);
render() { render() {
const { currentAnnotation } = this.state; const { currentAnnotation } = this.state;
// Use default comment + tag widget unless host app overrides // Use default comment + tag widget unless host app overrides
const widgets = this.props.widgets ? const widgets = this.props.widgets ?
this.props.widgets.map(getWidget) : DEFAULT_WIDGETS; this.props.widgets.map(getWidget) : DEFAULT_WIDGETS;
const isReadOnlyWidget = w => w.type.disableDelete ? const isReadOnlyWidget = w => w.type.disableDelete ?
@ -305,7 +319,7 @@ export default class Editor extends Component {
env: this.props.env env: this.props.env
}) : false; }) : false;
const hasDelete = currentAnnotation && const hasDelete = currentAnnotation &&
// annotation has bodies or allowEmpty, // annotation has bodies or allowEmpty,
(currentAnnotation.bodies.length > 0 || this.props.allowEmpty) && // AND (currentAnnotation.bodies.length > 0 || this.props.allowEmpty) && // AND
!this.props.readOnly && // we are not in read-only mode AND !this.props.readOnly && // we are not in read-only mode AND
@ -313,17 +327,17 @@ export default class Editor extends Component {
!widgets.some(isReadOnlyWidget); // every widget is deletable !widgets.some(isReadOnlyWidget); // every widget is deletable
return ( return (
<Draggable <Draggable
disabled={!this.props.detachable} disabled={!this.props.detachable}
handle=".r6o-draggable" handle=".r6o-draggable"
cancel=".r6o-btn, .r6o-btn *" cancel=".r6o-btn, .r6o-btn *"
onDrag={() => this.setState({ dragged: true })}> onDrag={() => this.setState({ dragged: true })}>
<div ref={this.element} className={this.state.dragged ? 'r6o-editor dragged' : 'r6o-editor'}> <div ref={this.element} className={this.state.dragged ? 'r6o-editor dragged' : 'r6o-editor'}>
<div className="r6o-arrow" /> <div className="r6o-arrow" />
<div className="r6o-editor-inner"> <div className="r6o-editor-inner">
{widgets.map((widget, idx) => {widgets.map((widget, idx) =>
React.cloneElement(widget, { React.cloneElement(widget, {
key: `${idx}`, key: `${idx}`,
focus: idx === 0, focus: idx === 0,
annotation : currentAnnotation, annotation : currentAnnotation,
@ -335,33 +349,34 @@ export default class Editor extends Component {
onUpsertBody: this.onUpsertBody, onUpsertBody: this.onUpsertBody,
onBatchModify: this.onBatchModify, onBatchModify: this.onBatchModify,
onSetProperty: this.onSetProperty, onSetProperty: this.onSetProperty,
onSaveAndClose: this.onOk onAddContext: this.onAddContext,
onSaveAndClose: this.onOk
}) })
)} )}
{ this.props.readOnly ? ( { this.props.readOnly ? (
<div className="r6o-footer"> <div className="r6o-footer">
<button <button
className="r6o-btn" className="r6o-btn"
onClick={this.onCancel}>{i18n.t('Close')}</button> onClick={this.onCancel}>{i18n.t('Close')}</button>
</div> </div>
) : ( ) : (
<div <div
className={this.props.detachable ? "r6o-footer r6o-draggable" : "r6o-footer"}> className={this.props.detachable ? "r6o-footer r6o-draggable" : "r6o-footer"}>
{ hasDelete && ( { hasDelete && (
<button <button
className="r6o-btn left delete-annotation" className="r6o-btn left delete-annotation"
title={i18n.t('Delete')} title={i18n.t('Delete')}
onClick={this.onDelete}> onClick={this.onDelete}>
<TrashIcon width={12} /> <TrashIcon width={12} />
</button> </button>
)} )}
<button <button
className="r6o-btn outline" className="r6o-btn outline"
onClick={this.onCancel}>{i18n.t('Cancel')}</button> onClick={this.onCancel}>{i18n.t('Cancel')}</button>
<button <button
className="r6o-btn " className="r6o-btn "
onClick={this.onOk}>{i18n.t('Ok')}</button> onClick={this.onOk}>{i18n.t('Ok')}</button>
</div> </div>

View File

@ -18,7 +18,8 @@ export default class WrappedWidget extends Component {
onUpsertBody: (previous, updated, saveImmediately) => props.onUpsertBody(previous, updated, saveImmediately), onUpsertBody: (previous, updated, saveImmediately) => props.onUpsertBody(previous, updated, saveImmediately),
onRemoveBody: (body, saveImmediately) => props.onRemoveBody(body, saveImmediately), onRemoveBody: (body, saveImmediately) => props.onRemoveBody(body, saveImmediately),
onBatchModify: (diffs, saveImmediately) => props.onBatchModify(diffs, saveImmediately), onBatchModify: (diffs, saveImmediately) => props.onBatchModify(diffs, saveImmediately),
onSetProperty: (property, value) => props.onSetProperty(property, value), onSetProperty: (property, value) => props.onSetProperty(property, value),
onAddContext: uri => props.onAddContext(uri),
onSaveAndClose: () => props.onSaveAndClose() onSaveAndClose: () => props.onSaveAndClose()
}); });