|
- import { h, Component, ComponentChild, createRef } from 'preact';
- import { asArray } from 'web-annotation-utils';
- import type { BodyChoice, TextualBody, WebAnnotation } from 'web-annotation-utils';
- import classes from './AnnotationBody.module.scss';
-
- interface AnnotationBodyProps {
- body: WebAnnotation['body'];
- bodyValue: WebAnnotation['bodyValue'];
- editable?: boolean;
- onChange?: (newBody: WebAnnotation['body']) => void;
- onBlur?: () => void;
- inFocus?: boolean;
- }
-
- interface AnnotationBodyState {}
-
- export class AnnotationBody extends Component<
- AnnotationBodyProps,
- AnnotationBodyState
- > {
- editorElement = createRef<HTMLDivElement>();
-
- componentDidMount() {
- if (this.props.inFocus) {
- this.editorElement.current?.focus({ preventScroll: true });
- }
- }
-
- componentDidUpdate(previousProps: Readonly<AnnotationBodyProps>) {
- if (this.props.inFocus && !previousProps.inFocus) {
- this.editorElement.current?.focus();
- }
- }
-
- render({ body, bodyValue, editable }: AnnotationBodyProps) {
- // An annotation either contains a `bodyValue` (simply a string), or an actual `body`.
- if (bodyValue !== undefined) return this.renderBodyValue(bodyValue);
- const result = this.renderBody(body);
- if (result === null && editable) {
- // For an empty but editable body, render an empty text field.
- return this.renderBodyValue('');
- } else {
- return result;
- }
- }
-
- renderBodyValue(bodyValue: string) {
- // A bodyValue is defined as equivalent to a TextualBody containing this value.
- return this.renderTextualBody({
- type: 'TextualBody',
- value: bodyValue,
- format: 'text/plain',
- });
- }
-
- renderTextualBody(body: TextualBody) {
- // TODO use other available information: textDirection, format, …?
- return (
- <div
- ref={this.editorElement}
- class={classes.annotationBodyText}
- contentEditable={this.props.editable}
- spellcheck={false}
- onInput={(e) =>
- this.props.onChange?.({
- ...body,
- value: (e.target as HTMLParagraphElement).textContent!,
- })
- }
- onBlur={() => this.props.onBlur?.()}
- >
- {body.value}
- </div>
- );
- }
-
- renderBody(body: WebAnnotation['body']): ComponentChild {
- if (!body) {
- return null;
- }
-
- // A body can take many forms. Handle each as well as we can.
-
- // Firstly, it could be a string, identifying the body resource.
- if (typeof body === 'string') {
- // We assume the body is the URL of the body content.
- // TODO Handle the case where this string instead refers to a JSON-LD node (e.g. a SpecificResource or Choice).
- return this.renderIframe(body);
- }
-
- // There can be multiple bodies (according to the spec, each body is equally applicable to each target).
- if (Array.isArray(body)) {
- // We simply concatenate the bodies. Perhaps not the clearest/prettiest, but simple.
- return body.map((actualBody) => this.renderBody(actualBody));
- }
-
- // TextualBody, a body consisting of a simple text value.
- if ('type' in body && body.type === 'TextualBody') {
- return this.renderTextualBody(body as TextualBody);
- }
-
- if ('type' in body && body.type === 'Choice') {
- const bodyOptions = (body as BodyChoice).items;
- if (bodyOptions.length === 0) return null;
- // The default option is listed first; take that.
- return this.renderBody(bodyOptions[0]);
- }
-
- if ('source' in body) {
- // The body is a Specific Resource.
- // TODO Try render exactly that part of the resource indicated by body.selector.
- return this.renderIframe(body.source);
- }
-
- // The body is an External Web Resource. Depending on its type, render an appropriate element.
-
- if (
- asArray(body.format).every((item) => item.startsWith('image/')) ||
- asArray(body.type).every((item) => item === 'Image')
- ) {
- return this.renderImage(body.id);
- }
-
- if (
- asArray(body.format).every((item) => item.startsWith('audio/')) ||
- asArray(body.type).every((item) => item === 'Sound')
- ) {
- return this.renderAudio(body.id);
- }
-
- if (
- asArray(body.format).every((item) => item.startsWith('video/')) ||
- asArray(body.type).every((item) => item === 'Video')
- ) {
- return this.renderAudio(body.id);
- }
-
- return this.renderIframe(body.id);
- }
-
- renderImage(bodyUrl: string) {
- return <img src={bodyUrl}></img>;
- }
-
- renderAudio(bodyUrl: string) {
- return (
- <audio class={classes.annotationBodyAudio} controls src={bodyUrl}></audio>
- );
- }
-
- renderVideo(bodyUrl: string) {
- return <video class={classes.annotationBodyVideo} src={bodyUrl}></video>;
- }
-
- renderIframe(bodyUrl: string) {
- return (
- <iframe
- class={classes.annotationBodyIframe}
- sandbox=""
- src={bodyUrl}
- ></iframe>
- );
- }
- }
|