Skip to content

Commit

Permalink
LWS-83: Show image attribution and usage policy (#1078)
Browse files Browse the repository at this point in the history
* Show image attribution and usage policy
* Replace resourcePopover with more generic popover action
* Display notice on (attributed) cropped images

---------

Co-authored-by: Olov Ylinenpää <[email protected]>
  • Loading branch information
johanbissemattsson and olovy authored Aug 5, 2024
1 parent bdb9eea commit fd9b386
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<script lang="ts">
import type { ResourceData } from '$lib/types/resourceData';
import { onMount } from 'svelte';
import { computePosition, offset, shift, inline, flip, arrow } from '@floating-ui/dom';
import DecoratedData from '$lib/components/DecoratedData.svelte';
import type { ResourceData } from '$lib/types/resourceData';
export let title: string | undefined = undefined;
export let resourceData: ResourceData | undefined = undefined;
export let referenceElement: HTMLElement;
export let data: ResourceData;
export let onMouseOver: (event: MouseEvent) => void;
export let onMouseLeave: (event: MouseEvent) => void;
export let onFocus: (event: FocusEvent) => void;
Expand Down Expand Up @@ -62,8 +63,8 @@

<!--
@component
Renders a popover with decorated data.
Note that `ResourcePopover.svelte` isn't intended to be used directly in page templates – use the `use:resourcePopover` action instead (see `$lib/actions/resourcePopover`).
Renders a popover.
Note that `Popover.svelte` isn't intended to be used directly in page templates – use the `use:popover` instead (see `$lib/actions/popover`).
-->
<div
class="absolute left-0 top-0 z-50 max-w-sm rounded-md border border-primary/16 bg-cards text-sm shadow-xl"
Expand All @@ -75,7 +76,11 @@
on:blur={onBlur}
>
<div class="p-2">
<DecoratedData {data} block allowPopovers={false} />
{#if resourceData}
<DecoratedData data={resourceData} block allowPopovers={false} />
{:else if title}
{title}
{/if}
</div>
<div class="absolute" bind:this={arrowElement}>
<svg
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import type { Action } from 'svelte/action';
import Popover from './Popover.svelte';
import type { LocaleCode } from '$lib/i18n/locales';
import ResourcePopover from './ResourcePopover.svelte';
import type { ResourceData } from '$lib/types/resourceData';

/** Tests to do
/**
* Svelte action used for showing either a generic title or decorated data for a resource (by supplying the resource id or resource data).
* This action can be made much more flexible with Svelte 5 when we will be able to set slots when creating component instances (see: https://github.com/sveltejs/svelte/issues/2588)
*
* Tests to do:
* - [] Attaches popover when user hovers over trigger node (after delay)
* - [] Removes popover when user stops hovering over trigger node (after delay)
* - [] Removes popover when user blurs trigger node
Expand All @@ -14,17 +18,20 @@ import type { ResourceData } from '$lib/types/resourceData';
* - [] Closes popover immediately when the URL changes
*/

export const resourcePopover: Action<
export const popover: Action<
HTMLElement,
{ id?: string; data?: ResourceData[]; lang: LocaleCode }
> = (node: HTMLElement, options) => {
{
title?: string;
resource?: { id: string; lang: LocaleCode } | { data: ResourceData[] };
}
> = (node: HTMLElement, { title = undefined, resource = undefined }) => {
const FETCH_DELAY = 250;
const ATTACH_DELAY = 500;
const REMOVE_DELAY = 200;
const container = document.getElementById('floating-elements-container') || document.body; // See https://atfzl.com/articles/don-t-attach-tooltips-to-document-body

let attached = false;
let floatingElement: ResourcePopover | null = null;
let floatingElement: Popover | null = null;
let cancelAttach: (reason?: unknown) => void;
let cancelRemove: (reason?: unknown) => void;
let cancelFetch: (reason?: unknown) => void;
Expand All @@ -39,25 +46,28 @@ export const resourcePopover: Action<
cancelAttach?.(); // cancel earlier promises to ensure popovers doesn't appear after navigating
cancelRemove?.();
cancelFetch?.();
if ((options.id || options.data) && !attached) {
const [decoratedData] = await Promise.all([
getDecoratedData(),
if (!attached) {
const [resourceData] = await Promise.all([
getResourceData(),
new Promise((resolve, reject) => {
cancelAttach = reject; // allows promise rejection from outside the promise constructor scope
setTimeout(resolve, ATTACH_DELAY);
})
]);
floatingElement = new ResourcePopover({

floatingElement = new Popover({
target: container,
props: {
referenceElement: node,
data: decoratedData,
title,
resourceData,
onMouseOver: startFloatingElementInteraction,
onFocus: startFloatingElementInteraction,
onMouseLeave: endFloatingElementInteraction,
onBlur: endFloatingElementInteraction
}
});

attached = true;
}
// eslint-disable-next-line no-empty
Expand Down Expand Up @@ -93,20 +103,21 @@ export const resourcePopover: Action<
removePopover();
}

async function getDecoratedData() {
async function getResourceData() {
await new Promise((resolve, reject) => {
cancelFetch = reject;
setTimeout(resolve, FETCH_DELAY);
});
try {
let resource;
if (options.data) {
resource = options.data;
} else if (options.id) {
const resourceRes = await fetch(`/api/${options.lang}/${options.id.split('/').pop()}`);
resource = await resourceRes.json();
if (resource) {
if ('id' in resource) {
const resourceRes = await fetch(`/api/${resource.lang}/${resource.id.split('/').pop()}`);
return await resourceRes.json();
}
if ('data' in resource) {
return resource.data;
}
}
return resource;
} catch (error) {
console.error(error);
cancelAttach?.();
Expand All @@ -124,4 +135,4 @@ export const resourcePopover: Action<
};
};

export default resourcePopover;
export default popover;
18 changes: 11 additions & 7 deletions lxl-web/src/lib/components/DecoratedData.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { ResourceData } from '$lib/types/resourceData';
import { ShowLabelsOptions } from '$lib/types/decoratedData';
import { page } from '$app/stores';
import resourcePopover from '$lib/actions/resourcePopover';
import popover from '$lib/actions/popover';
import { hasStyle, getStyle, getResourceId, getPropertyValue } from '$lib/utils/resourceData';
import { relativizeUrl } from '$lib/utils/http';
import { getSupportedLocale } from '$lib/i18n/locales';
Expand Down Expand Up @@ -58,13 +58,15 @@
}
/* Conditionally add popover action so it's only added when needed */
function conditionalResourcePopover(node: HTMLElement, data: ResourceData) {
function conditionalPopover(node: HTMLElement, data: ResourceData) {
if (allowPopovers && ((depth > 1 && hasStyle(data, 'link')) || hasStyle(data, 'definition'))) {
const id = getResourceId(data);
if (id) {
return resourcePopover(node, {
id,
lang: getSupportedLocale($page.params.lang)
return popover(node, {
resource: {
id,
lang: getSupportedLocale($page.params.lang)
}
});
}
}
Expand Down Expand Up @@ -170,7 +172,7 @@
target={hasStyle(data, 'ext-link') ? '_blank' : null}
data-type={data['@type']}
class={getStyleClasses(data)}
use:conditionalResourcePopover={data}
use:conditionalPopover={data}
>
<svelte:self
data={data['_display']}
Expand All @@ -184,7 +186,9 @@
/>
{#if remainder && Array.isArray(remainder)}
<span
use:resourcePopover={{ data: remainder, lang: getSupportedLocale($page.params.lang) }}
use:popover={{
resource: { data: remainder, lang: getSupportedLocale($page.params.lang) }
}}
class="remainder">+ {remainder.length}</span
>
{/if}
Expand Down
97 changes: 76 additions & 21 deletions lxl-web/src/lib/components/ResourceImage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import getTypeIcon from '$lib/utils/getTypeIcon';
import { bestSize } from '$lib/utils/auxd';
import { first } from '$lib/utils/xl';
import { page } from '$app/stores';
import { popover } from '$lib/actions/popover';
import InfoIcon from '~icons/bi/info-circle';
export let images: Image[];
export let alt: string | undefined;
Expand All @@ -16,41 +19,86 @@
$: image = first(images);
$: thumb = bestSize(image, thumbnailTargetWidth);
$: full = bestSize(image, Width.FULL);
$: thumb = (image && bestSize(image, thumbnailTargetWidth)) || undefined;
$: full = (image && bestSize(image, Width.FULL)) || undefined;
</script>

{#if image}
{#if linkToFull}
<a href={full.url} target="_blank" class="contents object-[inherit]">
{#if image && thumb}
<figure class="table aspect-square max-h-40 overflow-hidden">
{#if linkToFull && full}
<a href={full.url} target="_blank" class="object-[inherit]">
<img
{alt}
{loading}
src={thumb.url}
width={thumb.widthx}
height={thumb.heightPx}
class="object-contain object-[inherit]"
class:object-cover={geometry === 'circle'}
class:rounded-full={geometry === 'circle'}
/>
</a>
{:else}
<img
{alt}
{loading}
src={thumb.url}
width={thumb.widthx}
height={thumb.heightPx}
class="object-contain object-cover object-[inherit]"
class:object-cover={geometry === 'circle'}
class="object-contain object-[inherit]"
class:rounded-full={geometry === 'circle'}
/>
</a>
{:else}
<img
{alt}
{loading}
src={thumb.url}
width={thumb.widthx}
height={thumb.heightPx}
class="object-contain object-[inherit]"
class:rounded-full={geometry === 'circle'}
/>
{/if}
{/if}
{#if image?.usageAndAccessPolicy}
<figcaption
class="mt-1 table-caption caption-bottom overflow-hidden text-[10px] text-tertiary"
class:text-center={geometry === 'circle'}
>
{#if image.attribution}
<span class="oveflow-hidden mr-1 text-ellipsis whitespace-nowrap">
<span class="mr-0.5">©</span>
{#if image.attribution.link}
<a href={image.attribution.link} target="_blank" class="ext-link">
{image.attribution.name}
</a>
{:else}
{image.attribution.name}
{/if}
</span>
<!-- This could be based on if attribution required by license.
For now, display if there is any attribution info available -->
{#if geometry === 'circle'}
{$page.data.t('general.cropped')}
{/if}
{/if}
<span
class="overflow-hidden text-ellipsis whitespace-nowrap"
use:popover={{ title: image?.usageAndAccessPolicy.title }}
>
<InfoIcon style="display: inline; font-size: 13px" />
<span class="ml-0.5">
{#if image.usageAndAccessPolicy.link}
<a href={image.usageAndAccessPolicy.link} target="_blank" class="ext-link">
{#if image.usageAndAccessPolicy.identifier}
{image.usageAndAccessPolicy.identifier}
{:else}
{$page.data.t('general.usagePolicy')}
{/if}
</a>
{:else}
{$page.data.t('general.usagePolicy')}
{/if}
</span>
</span>
</figcaption>
{/if}
</figure>
{:else if showPlaceholder}
<div class="flex items-center justify-center">
<div class="flex items-center justify-center object-[inherit]">
<img
src={placeholder}
alt=""
class="h-20 w-20 object-cover"
class="h-20 w-20 object-cover object-[inherit]"
class:rounded-sm={geometry !== 'circle'}
class:rounded-full={geometry === 'circle'}
/>
Expand All @@ -59,3 +107,10 @@
{/if}
</div>
{/if}

<style lang="postcss">
.ext-link::after {
content: '\2009↗';
@apply align-[10%] text-icon;
}
</style>
4 changes: 3 additions & 1 deletion lxl-web/src/lib/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ export default {
apply: 'Apply',
from: 'From',
to: 'To',
year: 'Year'
year: 'Year',
usagePolicy: 'License terms',
cropped: 'Cropped'
},
holdings: {
availableAt: 'Available at',
Expand Down
4 changes: 3 additions & 1 deletion lxl-web/src/lib/i18n/locales/sv.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ export default {
apply: 'Tillämpa',
from: 'Från',
to: 'Till',
year: 'Årtal'
year: 'Årtal',
usagePolicy: 'Licensvillkor',
cropped: 'Beskuren'
},
holdings: {
availableAt: 'Finns på',
Expand Down
17 changes: 17 additions & 0 deletions lxl-web/src/lib/types/auxd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ export interface KbvImageObject {
width?: SizePx;
height?: SizePx;
thumbnail?: KbvImageObject[];
publisher?: unknown; // do we know the shape of the publisher beforehand? (e.g. { '@id': string } | { name: string, '@type': string, exactMatch: { '@id': string }}?);
usageAndAccessPolicy?: {
'@id': string;
'@type': string;
titleByLang: { [key: string]: string };
meta: unknown;
}[]; // and can we be sure of the the shape of usageAndAccessPolicy?
}

export enum Width {
Expand All @@ -32,6 +39,16 @@ export interface SecureImageResolution extends ImageResolution {
export interface Image {
sizes: ImageResolution[]; // always ordered smallest to largest
recordId: string;
attribution?: {
name: string;
link?: string;
};
attributionLink?: string;
usageAndAccessPolicy: {
title: string;
identifier?: string;
link?: string;
};
}

export interface SecureImage extends Image {
Expand Down
5 changes: 5 additions & 0 deletions lxl-web/src/lib/types/xl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export enum Base {
StructuredValue = 'StructuredValue'
}

// https://github.com/libris/definitions/blob/develop/source/vocab/concepts.ttl
export enum Concepts {
exactMatch = 'exactMatch'
}

// https://github.com/libris/definitions/blob/develop/source/vocab/platform.ttl
export enum Platform {
integral = 'integral',
Expand Down
Loading

0 comments on commit fd9b386

Please sign in to comment.