labo-components
Version:
351 lines (299 loc) • 11 kB
JSX
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import AnnotationAPI from '../../api/AnnotationAPI';
import IDUtil from '../../util/IDUtil'; // for generating unique CSS classnames for this component
import SessionStorageHandler from '../../util/SessionStorageHandler';
import { AnnotationEvents } from './AnnotationClient';
import PopupHeader from './PopupHeader';
import {
ANNOTATION_TARGET,
CLASSIFICATION,
METADATA,
LINK,
COMMENT,
} from '../../util/AnnotationConstants';
import AnnotationFilter from './annotationColumn/AnnotationFilter';
import {
renderPerType,
getAnnotationsPerType,
getAnnotationTargetTitle,
} from './AnnotationHelpers';
import { ResourceViewerContext } from './ResourceViewerContext';
import Strings from './_Strings';
import debounce from 'debounce';
/*
AnnotationPopup shows annotations for the given fragment
This component should react to changes to:
- selected project
- selected annotation target (active media object or parts of that media object or pieces of selected text later on)
The user is required for the annotation API
Supplies the ResourceViewerContext with all current annotation data
This component utilizes components from the annotation column (for re-use and consistency)
*/
const SESSION_STORAGE_ANNOTATION_POPUP_LEFT = "bg__annotation-popup-left";
const SESSION_STORAGE_ANNOTATION_POPUP_TOP = "bg__annotation-popup-top";
// Amount of height to add to the popup for each annotation type
const ANNOTATION_TYPE_HEIGHT = {
[CLASSIFICATION]: 250,
[COMMENT]: 400,
[LINK]: 250,
[METADATA]: 400,
};
export default class AnnotationPopup extends React.Component {
static contextType = ResourceViewerContext;
constructor(props) {
super(props);
// Size
this.width = Math.max(300, Math.min(window.innerWidth / 3, 400));
this.maxHeight = 500;
// Position from session storage, default to screen center
const left = SessionStorageHandler.getInt(
SESSION_STORAGE_ANNOTATION_POPUP_LEFT,
window.innerWidth / 2 - this.width / 2
);
const top = SessionStorageHandler.getInt(
SESSION_STORAGE_ANNOTATION_POPUP_TOP,
window.innerHeight / 2 - this.maxHeight / 2
);
// Initial state
this.state = {
isBookmarked: false,
left,
top,
height: this.maxHeight,
};
// Variables to store drag changes independent of state
this._left = left;
this._top = top;
// Debounce storeposition function to limit calls to sessionStorage
this.storePosition = debounce(this.storePosition, 200);
this.annotationEvents = [
AnnotationEvents.ON_EDIT,
AnnotationEvents.ON_SET_ANNOTATION,
AnnotationEvents.ON_SET_SELECTION,
AnnotationEvents.ON_SAVE,
AnnotationEvents.ON_DELETE,
AnnotationEvents.ON_CHANGE_TARGET,
];
this._lastActiveAnnotationTypes = null;
}
// Calculate popup height, dependent on active annotation types
// Update top/left, limit to always keep the popup on screen
updateDimensions(maxHeight = 500) {
let height = 30;
this.context.activeAnnotationTypes.forEach((annotationType) => {
if (annotationType in ANNOTATION_TYPE_HEIGHT) {
height += ANNOTATION_TYPE_HEIGHT[annotationType];
}
});
this.setState(
{
height: Math.max(100, Math.min(maxHeight, height)),
},
() => {
// update position
this.setState({
top: this.limitTop(this.state.top),
left: this.limitLeft(this.state.left),
});
}
);
}
/* ----------------------------- LIFECYCLE ------------------------ */
componentDidMount() {
// Bind to annotation updates
this.annotationEvents.forEach((event) => {
this.context.annotationClient.events.bind(event, this.update);
});
// Bind to keypress
document.addEventListener("keydown", this.onKeyDown);
// Calculutate popup height
this.updateDimensions();
}
componentWillUnmount() {
// Unbind annotation updates
this.annotationEvents.forEach((event) => {
this.context.annotationClient.events.unbind(event, this.update);
});
// Unbind keypress
document.removeEventListener("keydown", this.onKeyDown);
}
componentDidUpdate() {
// When active annotation types changed; update popup height
if (
this.context.activeAnnotationTypes !=
this._lastActiveAnnotationTypes
) {
this.updateDimensions();
this._lastActiveAnnotationTypes = this.context.activeAnnotationTypes;
}
}
update = () => {
this.forceUpdate();
};
/* ----------------------------- KEYPRESS ------------------------ */
onKeyDown = (e) => {
if (e.key == "Escape") {
this.props.onClose();
}
};
/* ----------------------------- DRAGGING ------------------------ */
onDrag = (dx, dy) => {
this._left = this.limitLeft(this._left - dx);
this._top = this.limitTop(this._top - dy);
this.setState({
left: this._left,
top: this._top,
});
this.storePosition();
};
storePosition = () => {
SessionStorageHandler.set(
SESSION_STORAGE_ANNOTATION_POPUP_TOP,
this._top
);
SessionStorageHandler.set(
SESSION_STORAGE_ANNOTATION_POPUP_LEFT,
this._left
);
};
// keep popup on screen
limitLeft(left) {
return left
? Math.min(window.innerWidth - this.width, Math.max(0, left))
: 0;
}
// keep popup on screen
limitTop(top, mediaSuiteHeaderHeight = 95) {
return top
? Math.min(
window.innerHeight - this.state.height,
Math.max(mediaSuiteHeaderHeight, top)
)
: mediaSuiteHeaderHeight;
}
/* ----------------------------- BOOKMARK HELPER ------------------------ */
isBookmarked(userId, project) {
const filter = {
"user.keyword": userId,
motivation: "bookmarking",
};
if (project) {
filter["project"] = project.id;
}
AnnotationAPI.getFilteredAnnotations(
// load all "bookmark group annotations" of current user project
userId,
filter,
null, //not_filters
() => {
this.setState({ isBookmarked: true });
}
);
}
/* ----------------------------- RENDER FUNCTIONS ------------------------ */
renderWarningList = (activeProject, isBookmarked, showBookmarkSelector) => {
const warnings = [];
// active project required warning
!activeProject &&
warnings.push(
<div className="warning" key="project">
<i className="fas fa-info-circle" />
{Strings.ANNOTATIONS_FIRST_SELECT_PROJECT}
</div>
);
// bookmark required warning, only when active project is available
// because else the bookmark link won't work properly
if (activeProject && !isBookmarked) {
const parts = Strings.ANNOTATIONS_FIRST_BOOKMARK.split("%ACTION%");
if (parts.length < 2) {
console.error(
"ANNOTATIONS_FIRST_BOOKMARK requires: aaa %ACTION% bbb"
);
} else {
warnings.push(
<div className="warning" key="bookmark">
<i className="fa fa-info-circle" />
{parts[0]}
<span className="action" onClick={showBookmarkSelector}>
bookmark
</span>
{parts[1]}
</div>
);
}
}
return warnings;
};
render() {
// active annotation required
if (!this.context.annotationClient.activeAnnotation) {
return null;
}
// get active project
const activeProject = this.context.activeProject;
const isBookmarked = activeProject && this.state.isBookmarked;
// Check if the resource has been bookmarked
if (activeProject && !isBookmarked) {
this.isBookmarked(this.context.user.id, activeProject);
}
// Warnings that can be shown before showing the annotation list
const warnings = this.renderWarningList(
activeProject,
isBookmarked,
this.props.showBookmarkSelector
);
// types
const types =
warnings.length > 0
? null
: renderPerType(
getAnnotationsPerType(
this.context.activeAnnotationTypes,
this.context.annotationClient.activeAnnotation
),
ANNOTATION_TARGET.SEGMENT,
this.context.annotationClient.activeAnnotation,
this.context.annotationClient,
this.context.activeAnnotationTypes,
true // show forms by default
);
// title
const title = getAnnotationTargetTitle(
this.context.annotationClient.activeAnnotation
);
// filter
const annotationFilter =
warnings.length > 0 ? null : <AnnotationFilter />;
return (
<div
className={classNames(IDUtil.cssClassName("annotation-popup"), {
active: this.props.active,
inactive: !this.props.active,
})}
style={{
left: this.state.left,
top: this.state.top,
width: this.width,
height: this.state.height,
}}
>
<PopupHeader
onClose={this.props.onClose}
title={title}
onDrag={this.onDrag}
></PopupHeader>
<div key="column-content" className="column-content">
{warnings}
{types}
{annotationFilter}
</div>
</div>
);
}
}
AnnotationPopup.propTypes = {
showBookmarkSelector: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};