UNPKG

labo-components

Version:
351 lines (299 loc) 11 kB
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, };