diff --git a/src/components/AnnotationManifestsAccordion.js b/src/components/AnnotationManifestsAccordion.js index 5287697ca3fccc3b0f913aba26ffcd31693de0e3..95cb2f11e3b6d0068c055259380a269b1b21f48f 100644 --- a/src/components/AnnotationManifestsAccordion.js +++ b/src/components/AnnotationManifestsAccordion.js @@ -24,16 +24,6 @@ export class AnnotationManifestsAccordion extends Component { this.handleOpenManifestSideToSide = this.handleOpenManifestSideToSide.bind(this); this.handleOpenAccordion = this.handleOpenAccordion.bind(this); - /** Search manifest directly in content. We consider all the links with #manifest at the end are manifest */ - function searchManifestInContent(text) { - if (text == null) { - return null; - } - return text.match( - /((http|https)\:\/\/[a-z0-9\/:%_+.,#?!@&=-]+)#manifest/gi, - ); - } - /** Search if the annotation is a manifest. URL must be resolvable for the annotation. So the manifest url is added at the end of the id */ function searchManifestInID(id) { const match = id.match( @@ -45,17 +35,13 @@ export class AnnotationManifestsAccordion extends Component { const { annotation } = this.props; - /** Merge array even if some are null) */ - const concat = (...arrays) => [].concat(...arrays.filter(Array.isArray)); - - annotation.manifests = concat(searchManifestInContent(annotation.content), searchManifestInID(annotation.id)); + annotation.manifests = searchManifestInID(annotation.id); if (annotation.manifests) { annotation.manifests = annotation.manifests.map(id => ({ id })); } else { annotation.manifests = []; } - annotation.manifests = removeDuplicates(annotation.manifests); this.state = { annotation }; } diff --git a/src/components/CanvasAnnotations.js b/src/components/CanvasAnnotations.js index 2dadbf89a820e0c3b1e74c3e15b2584b5a8d2465..a45baead1ec5a21b26bcbd5ad2049a0454039bf8 100644 --- a/src/components/CanvasAnnotations.js +++ b/src/components/CanvasAnnotations.js @@ -6,9 +6,15 @@ import MenuList from '@material-ui/core/MenuList'; import MenuItem from '@material-ui/core/MenuItem'; import ListItemText from '@material-ui/core/ListItemText'; import Typography from '@material-ui/core/Typography'; +import SearchIcon from '@material-ui/icons/SearchSharp'; +import InputBase from '@material-ui/core/InputBase'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import TextField from '@material-ui/core/TextField'; import SanitizedHtml from '../containers/SanitizedHtml'; import { ScrollTo } from './ScrollTo'; import AnnotationManifestsAccordion from '../containers/AnnotationManifestsAccordion'; +import { filterAnnotation } from '../helper/utils'; +import { MiradorMenuButton } from './MiradorMenuButton'; /** * CanvasAnnotations ~ @@ -23,6 +29,9 @@ export class CanvasAnnotations extends Component { this.handleClick = this.handleClick.bind(this); this.handleAnnotationHover = this.handleAnnotationHover.bind(this); this.handleAnnotationBlur = this.handleAnnotationBlur.bind(this); + this.handleAnnotationSearch = this.handleAnnotationSearch.bind(this); + + this.state = { }; } /** @@ -54,21 +63,51 @@ export class CanvasAnnotations extends Component { hoverAnnotation(windowId, []); } + /** */ + handleAnnotationSearch(event) { + this.setState({ inputSearch: event.target.value }); + } + /** * Returns the rendered component */ render() { const { - annotations, autoScroll, classes, index, label, selectedAnnotationId, t, totalSize, + autoScroll, classes, index, label, selectedAnnotationId, t, totalSize, listContainerComponent, htmlSanitizationRuleSet, hoveredAnnotationIds, containerRef, } = this.props; + + let { annotations } = this.props; + if (annotations.length === 0) return null; + + const { inputSearch } = this.state; + + if (inputSearch != undefined && inputSearch !== '') { + annotations = filterAnnotation(annotations, inputSearch); + } + + const annotationCount = annotations.length; + return ( <> - <Typography className={classes.sectionHeading} variant="overline"> - {t('annotationCanvasLabel', { context: `${index + 1}/${totalSize}`, label })} - </Typography> + <div className={classes.headerAnnotationPanel}> + <TextField + label={t('searchPlaceholderAnnotation')} + onChange={this.handleAnnotationSearch} + className={classes.searchAnnotationsTextfield} + InputProps={{ + endAdornment: ( + <div className={classes.endAdornment}> + <MiradorMenuButton aria-label={t('searchAnnotationTooltip')} type="submit"> + <SearchIcon /> + </MiradorMenuButton> + </div> + ), + }} + /> + </div> <MenuList autoFocusItem variant="selectedMenu"> { annotations.map(annotation => ( @@ -117,7 +156,18 @@ export class CanvasAnnotations extends Component { </ScrollTo> )) } + {annotations.length == 0 + && ( + <MenuItem> + <Typography> + {t('noAnnotationFound')} + </Typography> + </MenuItem> + )} </MenuList> + <div className={classes.footerAnnotationPanel}> + <Typography component="p" variant="subtitle2">{t('showingNumAnnotations', { count: annotationCount, number: annotationCount })}</Typography> + </div> </> ); } diff --git a/src/components/WindowSideBarAnnotationsPanel.js b/src/components/WindowSideBarAnnotationsPanel.js index f5182e6443dcfc8430aba05f808200e6ac83f25b..0a4436e7af3c3db885b44aa4cd3fa3a8d1fe52f9 100644 --- a/src/components/WindowSideBarAnnotationsPanel.js +++ b/src/components/WindowSideBarAnnotationsPanel.js @@ -1,6 +1,5 @@ import { createRef, Component } from 'react'; import PropTypes from 'prop-types'; -import Typography from '@material-ui/core/Typography'; import AnnotationSettings from '../containers/AnnotationSettings'; import CanvasAnnotations from '../containers/CanvasAnnotations'; import CompanionWindow from '../containers/CompanionWindow'; @@ -34,10 +33,6 @@ export class WindowSideBarAnnotationsPanel extends Component { otherRef={this.containerRef} titleControls={<AnnotationSettings windowId={windowId} />} > - <div className={classes.section}> - <Typography component="p" variant="subtitle2">{t('showingNumAnnotations', { count: annotationCount, number: annotationCount })}</Typography> - </div> - {canvasIds.map((canvasId, index) => ( <CanvasAnnotations canvasId={canvasId} diff --git a/src/containers/CanvasAnnotations.js b/src/containers/CanvasAnnotations.js index eba5b3bbd61cb9812ffe1971e85742cc676b2079..b607523b4c476da0604f95a325181dcdf7c6435f 100644 --- a/src/containers/CanvasAnnotations.js +++ b/src/containers/CanvasAnnotations.js @@ -1,7 +1,7 @@ import { compose } from 'redux'; import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; -import { withStyles } from '@material-ui/core/styles'; +import { alpha, withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; import * as actions from '../state/actions'; import { @@ -69,16 +69,39 @@ const styles = theme => ({ marginRight: theme.spacing(0.5), marginTop: theme.spacing(1), }, - hovered: {}, - manifestLabel: { - fontSize: '10px', + endAdornment: { + position: 'absolute', + right: 0, }, - sectionHeading: { + footerAnnotationPanel: { + background: theme.palette.background.paper, + borderTop: `.5px solid ${theme.palette.section_divider}`, + bottom: 0, + paddingBottom: theme.spacing(1), paddingLeft: theme.spacing(2), paddingRight: theme.spacing(1), paddingTop: theme.spacing(2), + position: 'sticky', + }, + grow: { + flexGrow: 1, + }, + headerAnnotationPanel: { + background: theme.palette.background.paper, + borderBottom: `.5px solid ${theme.palette.section_divider}`, + marginBottom: theme.spacing(1), + padding: theme.spacing(0, 1, 1, 1), + position: 'sticky', + top: 0, + zIndex: 10, + }, + hovered: {}, + manifestLabel: { + fontSize: '10px', + }, + searchAnnotationsTextfield: { + width: '100%', }, - }); const enhance = compose( diff --git a/src/helper/utils.js b/src/helper/utils.js index 3caff377e08d242ceeeca28a33e2ee1e9cee0acd..a39f2b31a29cbd5399f4a018b3375263c2fa1a84 100644 --- a/src/helper/utils.js +++ b/src/helper/utils.js @@ -1,5 +1,8 @@ /** - * Remove duplicate elements in array - * + * Filter annotation with a query string. Search in ID and value * */ -export const removeDuplicates = (arr) => [...new Map(arr.map(v => [v.id, v])).values()]; +export const filterAnnotation = (annotations, query) => annotations.filter((annotation) => { + // eslint-disable-next-line max-len + const queryLowered = query.toLowerCase(); + return annotation.id.toLowerCase().includes(queryLowered) || annotation.content.toLowerCase().includes(queryLowered); +}); diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 6c724e130bb841305c6f0c1b974dd769c52cec73..43e937a3040bb7cf3ff7e8e42e179083e4217449 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -96,6 +96,7 @@ "multipartCollection": "Multipart Collection", "nextCanvas": "Next item", "noItemSelected": "No item selected", + "noAnnotationFound": "No annotation found", "numItems_one": "{{number}} item", "numItems_other": "{{number}} items", "off": "Off", @@ -118,6 +119,7 @@ "right": "Right", "rights": "License", "scroll": "Scroll", + "searchAnnotationTooltip": "Filter annotations", "searchInputLabel": "search terms", "searchNextResult": "Next result", "searchNoResults": "No results found", @@ -125,6 +127,7 @@ "searchResultsRemaining": "{{numLeft}} remaining", "searchSubmitAria": "Submit search", "searchTitle": "Search", + "searchPlaceholderAnnotation" : "Filter...", "selectWorkspaceMenu": "Select workspace type", "showingNumAnnotations_one": "Showing {{number}} annotation", "showingNumAnnotations_other": "Showing {{number}} annotations", diff --git a/src/locales/fr/translation.json b/src/locales/fr/translation.json index 853cfbc8bf57aca027582e710fde3477672d780c..7ff8bc8d4b56c0ed0d72a654a177236fe65bf27b 100644 --- a/src/locales/fr/translation.json +++ b/src/locales/fr/translation.json @@ -92,6 +92,7 @@ "moveCompanionWindowToRight": "Déplacer à droite", "nextCanvas": "Suivant", "noItemSelected": "Aucun élément sélectionné", + "noAnnotationFound": "Aucune annotation trouvé", "numItems_one": "{{number}} image", "numItems_other": "{{number}} images", "off": "aucun", @@ -114,9 +115,11 @@ "right": "Droite", "rights": "Licence", "scroll": "Défilement horizontal", + "searchAnnotationTooltip": "Filtrer les annotations", "searchInputLabel": "Chercher un mot", "searchNextResult": "Résultat suivant", "searchNoResults": "Aucun résultat trouvé", + "searchPlaceholderAnnotation" : "Filtrer...", "searchPreviousResult": "Résultat précédent", "searchSubmitAria": "Lancer la recherche", "searchTitle": "Rechercher",