diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.editorconfig.ts index 9a54e2ec..6205f6ea 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.editorconfig.ts @@ -25,6 +25,8 @@ import { Color, ContentBlockEditor, ContentBlockType } from '../../../types/cont import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, + COPYRIGHT_FIELDS, + COPYRIGHT_STATE, PADDING_SINGLE_VALUE_FIELD, TEXT_FIELD, } from '../defaults'; @@ -42,6 +44,7 @@ export const INITIAL_HET_ARCHIEF_IMAGE_TEXT_BACKGROUND_COMPONENTS_STATE = imageAlignment: 'left-inside-page', buttonLabel: '', buttonIconAlignment: 'left', + ...COPYRIGHT_STATE(), }); export const INITIAL_HET_ARCHIEF_IMAGE_TEXT_BACKGROUND_BLOCK_STATE = @@ -155,6 +158,7 @@ export const HET_ARCHIEF_IMAGE_TEXT_BACKGROUND_BLOCK_CONFIG = ( validator: undefined, }, }, + ...COPYRIGHT_FIELDS(), buttonType: { label: tText( 'admin/content-block/helpers/image-text-background/image-text-background___knop-type' diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss index 3ee27465..9b8528af 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.scss @@ -4,179 +4,227 @@ .c-block-het-archief-image-text-background { width: 100%; display: grid; - - // Add margin right by adding an extra column - // Also make the middle column a bit smaller so the image is a bit smaller than 66% width - grid-template-columns: 1fr 0.6fr 1fr 20px; + grid-template-columns: 1fr 0.6fr 1fr; gap: variables.$g-spacer-unit * 3; - min-height: 34rem; margin: 0 auto; - @media (max-width: variables.$g-bp2) { - padding-left: variables.$g-spacer-unit; - padding-right: variables.$g-spacer-unit; - } - - &.c-block-het-archief-image-text-background--left-screen { - position: initial; - - .c-block-het-archief-image-text-background__content-wrapper { - grid-row: 1; - grid-column: 3/3; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 2; - } - } - - .c-block-het-archief-image-text-background__image-wrapper { - grid-column: 1/3; - grid-row: 1; - - img { - margin: 0 auto 0 0; - } - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 1; - } - } + // The image column is absolutely positioned (see __image-wrapper) so it can't grow + // this row's height. Reserve room for the copyright's real rendered height here so + // it isn't clipped by a consuming app's `.c-content-block { overflow: hidden }`. + min-height: max(34rem, calc(var(--copyright-height, 0px) + #{variables.$g-spacer-unit})); + + &.c-block-het-archief-image-text-background--has-image { + // Also reserve the image's own 21.7rem floor (or its actual height if a + // tall/portrait source needs more, see __image below). + min-height: max( + 34rem, + calc(max(21.7rem, var(--image-height, 0px)) + var(--copyright-height, 0px) + #{variables.$g-spacer-unit}) + ); } - &.c-block-het-archief-image-text-background--right-screen { - position: initial; - - .c-block-het-archief-image-text-background__content-wrapper { - grid-row: 1; - grid-column: 1/1; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 1; - } - } - - .c-block-het-archief-image-text-background__image-wrapper { - grid-row: 1; - grid-column: 2/4; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 2; - } - - img { - margin: 0 0 0 auto; - } - } + @media (max-width: variables.$g-bp2) { + padding-left: variables.$g-spacer-unit; + padding-right: variables.$g-spacer-unit; } + &.c-block-het-archief-image-text-background--left-screen, &.c-block-het-archief-image-text-background--left-inside-page { - position: relative; - - .c-block-het-archief-image-text-background__content-wrapper { - grid-row: 1; - grid-column: 3/3; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 2; - } - } - - .c-block-het-archief-image-text-background__image-wrapper { - grid-row: 1; - grid-column: 1/3; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 1; - } - - img { - margin: 0 auto 0 0; - } - } + position: relative; + + .c-block-het-archief-image-text-background__content-wrapper { + grid-row: 1; + grid-column: 3/3; + + @media (max-width: variables.$g-bp2) { + grid-column: 1; + grid-row: 2; + } + } + + .c-block-het-archief-image-text-background__image-wrapper { + grid-column: 1/3; + grid-row: 1; + + @media (max-width: variables.$g-bp2) { + grid-column: 1; + grid-row: 1; + } + } } + &.c-block-het-archief-image-text-background--right-screen, &.c-block-het-archief-image-text-background--right-inside-page { - position: relative; - - .c-block-het-archief-image-text-background__content-wrapper { - grid-row: 1; - grid-column: 1/1; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 1; - } - } - - .c-block-het-archief-image-text-background__image-wrapper { - grid-row: 1; - grid-column: 2/4; - - @media (max-width: variables.$g-bp2) { - grid-column: 1; - grid-row: 2; - } - - &.c-image img { - margin-right: 0 !important; - margin-left: auto !important; - } - } + position: relative; + + .c-block-het-archief-image-text-background__content-wrapper { + grid-row: 1; + grid-column: 1/1; + + @media (max-width: variables.$g-bp2) { + grid-column: 1; + grid-row: 1; + } + } + + .c-block-het-archief-image-text-background__image-wrapper { + grid-row: 1; + grid-column: 2/4; + + @media (max-width: variables.$g-bp2) { + grid-column: 1; + grid-row: 2; + } + } + } + + // `.media`'s width is set from JS to match its content (see __image-wrapper), so + // pushing it — rather than the image/copyright individually — aligns the group to + // the screen/page edge. Only with an actual image: a caption-only block has + // nothing to align against and just sits at the start. + &.c-block-het-archief-image-text-background--left-screen.c-block-het-archief-image-text-background--has-image, + &.c-block-het-archief-image-text-background--left-inside-page.c-block-het-archief-image-text-background--has-image { + .c-block-het-archief-image-text-background__image-wrapper .c-block-het-archief-image-text-background__media { + @media (min-width: variables.$g-bp2) { + margin-right: auto; + margin-left: 0; + } + } + } + + &.c-block-het-archief-image-text-background--right-screen.c-block-het-archief-image-text-background--has-image, + &.c-block-het-archief-image-text-background--right-inside-page.c-block-het-archief-image-text-background--has-image { + .c-block-het-archief-image-text-background__image-wrapper .c-block-het-archief-image-text-background__media { + @media (min-width: variables.$g-bp2) { + margin-left: auto; + margin-right: 0; + } + } + + // The only ever renders at its own width, left-aligned by default. When a + // wide copyright makes `.media` wider than the image, pin the image to the end so + // it stays flush with the block's right edge regardless of how wide `.media` grows. + .c-block-het-archief-image-text-background__image-wrapper .c-block-het-archief-image-text-background__image { + @media (min-width: variables.$g-bp2) { + justify-content: flex-end; + } + } } @media (max-width: variables.$g-bp2) { - grid-template-rows: repeat(2, auto); - grid-template-columns: repeat(1, 1fr); + grid-template-rows: repeat(2, auto); + grid-template-columns: repeat(1, 1fr); } .c-block-het-archief-image-text-background__content-wrapper { - width: 100%; - height: 100%; - display: flex; - align-items: flex-start; - justify-content: center; - flex-direction: column; - padding: variables.$g-spacer-unit * 4; - z-index: 1; - - @media (max-width: variables.$g-bp2) { - padding: 0; - } - - p { - padding: unset !important; - } + width: 100%; + height: 100%; + display: flex; + align-items: flex-start; + justify-content: center; + flex-direction: column; + padding: variables.$g-spacer-unit * 4; + z-index: 1; + + @media (max-width: variables.$g-bp2) { + padding: 0; + } + + p { + padding: unset !important; + } } .c-block-het-archief-image-text-background__content, .c-block-het-archief-image-text-background__heading { - margin-bottom: variables.$g-spacer-unit !important; + margin-bottom: variables.$g-spacer-unit !important; } .c-block-het-archief-image-text-background__button { - margin-top: math.div(variables.$g-spacer-unit, 2); + margin-top: math.div(variables.$g-spacer-unit, 2); } .c-block-het-archief-image-text-background__image-wrapper { - @media (min-width: variables.$g-bp2) { - position: absolute; - inset: 0; - } - - img { - max-width: 100%; - height: 100%; - object-fit: contain; - - @media (max-width: variables.$g-bp2) { - width: 100%; - } - } + // Below $g-bp2 this stays a normal in-flow grid item, so the default + // `min-width: auto` would let `.media`'s JS-applied width (sized off the + // copyright's unwrapped width) drag this track — and the image — past the + // viewport. `min-width: 0` keeps it bound to the actual available width. + min-width: 0; + + @media (min-width: variables.$g-bp2) { + position: absolute; + inset: 0; + } + + .c-block-het-archief-image-text-background__media { + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + + // Width is set inline from JS (image's rendered width, or the caption's + // natural width if wider — see component) so both share one width and their + // left edges align. `max-width` here is just the safety net beyond that. + max-width: 100%; + } + + .c-block-het-archief-image-text-background__image { + min-width: 0; + min-height: 21.7rem; + display: flex; + align-items: center; + + // Capped, not just floored, on mobile: a tall source image should crop + // instead of growing the whole single-column block, unlike on desktop. + @media (max-width: variables.$g-bp2) { + height: 21.7rem; + overflow: hidden; + } + + img { + display: block; + max-width: 100%; + // `max-height`, not `height`, keeps the box aspect-correct so the + // width-alignment above matches the image's actual visible content. + max-height: 100%; + height: auto; + object-fit: contain; + + @media (max-width: variables.$g-bp2) { + width: 100%; + height: 100%; + } + } + } + + .c-block-het-archief-image-text-background__copyright { + flex: 0 0 auto; + margin-top: variables.$g-spacer-unit; + max-width: 100%; + + &:empty { + display: none; + } + } + + // Off-screen, single-line clone of the caption, used only to measure its + // natural (unwrapped) width in JS — see the component. `inline-block` + // shrink-wraps it; `white-space: nowrap` is what keeps it unwrapped. + .c-block-het-archief-image-text-background__copyright-measure { + position: absolute; + top: 0; + left: 0; + visibility: hidden; + white-space: nowrap; + pointer-events: none; + width: auto; + display: inline-block; + + // Override `.a-copyright-attribution`'s own `width: 100%` — otherwise this + // clone fills the column instead of shrinking to its content, breaking the + // natural-width measurement above. + .a-copyright-attribution { + width: max-content; + } + } } -} +} \ No newline at end of file diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx index cb47c6e2..0adc663a 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockHetArchiefImageTextBackground/BlockHetArchiefImageTextBackground.tsx @@ -1,7 +1,15 @@ import type { ButtonAction, ButtonType, IconName } from '@viaa/avo2-components'; -import { Button, Image } from '@viaa/avo2-components'; +import { Button } from '@viaa/avo2-components'; import clsx from 'clsx'; -import type { FunctionComponent, ReactElement } from 'react'; +import React, { + type CSSProperties, + type FunctionComponent, + type ReactElement, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import type { BackgroundAlignOption, Color, @@ -15,6 +23,7 @@ import { BlockHeading } from '../BlockHeading/BlockHeading'; import './BlockHetArchiefImageTextBackground.scss'; import type { ContentPageWidth } from '~modules/content-page/types/content-pages.types'; import { CONTENT_PAGE_WIDTH_TO_REM } from '~modules/content-page/types/content-pages.types'; +import { CopyrightAttribution } from '~shared/components/CopyrightAttribution'; export interface BlockHetArchiefImageTextBackgroundProps extends DefaultComponentProps { heading: string; @@ -25,6 +34,9 @@ export interface BlockHetArchiefImageTextBackgroundProps extends DefaultComponen image?: string; imageAlignment?: BackgroundAlignOption; imageAltText?: string; + copyrightTitle: string; + copyrightIconVisible: boolean; + copyrightText: string; buttonAction?: ButtonAction; buttonAltTitle?: string; buttonLabel: string; @@ -46,6 +58,9 @@ export const BlockHetArchiefImageTextBackground: FunctionComponent< image, imageAlignment = 'left-screen', imageAltText, + copyrightTitle, + copyrightIconVisible, + copyrightText, buttonAction, buttonAltTitle, buttonLabel, @@ -54,18 +69,101 @@ export const BlockHetArchiefImageTextBackground: FunctionComponent< buttonIconAlignment = 'left', pageWidth, }): ReactElement => { + const imgRef = useRef(null); + const copyrightRef = useRef(null); + // Hidden, unwrapped clone of the caption, used only to measure its natural + // (single-line) width in JS — see updateMediaMeasurements below. Can't be done in + // pure CSS: a flex/grid container's intrinsic-size computation uses the 's + // intrinsic pixel dimensions, not its rendered size, so `width: fit-content` on + // `.media` collapses to the full column width for any normal-sized image. + const copyrightMeasureRef = useRef(null); + // Copyright's rendered height, fed into --copyright-height (see scss). The image + // column is absolutely positioned so it can't grow the block's height itself, so + // this is what makes the block reserve enough room for a tall caption instead of + // letting it overflow into a consuming app's `.c-content-block { overflow: hidden }`. + const [copyrightHeight, setCopyrightHeight] = useState(); + // Image's own rendered width, or the caption's natural (unwrapped) width if that's + // wider — applied to `.media` below so both share exactly that width and align + // their left edges, while letting the caption grow past the image instead of + // wrapping when it would otherwise need to. + const [mediaWidth, setMediaWidth] = useState(); + // Image's rendered height, which can exceed its 21.7rem floor (see scss) for a + // tall/portrait source. Fed into --image-height for the same reason as + // copyrightHeight: without it such an image could get squeezed or overflow. + const [imageHeight, setImageHeight] = useState(); + + // Uses the image's *rendered* width, not `naturalWidth` (its raw file resolution): + // a grid/flex container's intrinsic-size computation falls back to the raw pixel + // dimensions whenever height isn't a definite value, which would always win the + // `max()` below and pin `.media` to the full column regardless of the copyright. + // Only reads it once the image has finished loading — reading mid-load measures + // ~0px, which would shrink `.media` (and the image with it) with no way to + // recover, since a later re-measure would just report that same self-inflicted size. + // biome-ignore lint/correctness/useExhaustiveDependencies: refs are stable + const updateMediaMeasurements = useCallback(() => { + const imgEl = imgRef.current; + const measureEl = copyrightMeasureRef.current; + const naturalCopyrightWidth = measureEl?.getBoundingClientRect().width ?? 0; + + if (imgEl && !imgEl.complete) { + return; + } + + const imageWidth = imgEl?.getBoundingClientRect().width ?? 0; + setMediaWidth(Math.max(imageWidth, naturalCopyrightWidth) || undefined); + setImageHeight(imgEl?.getBoundingClientRect().height || undefined); + }, []); + + // Deps aren't read in the effect body; they re-attach the observers when the + // /measuring clone are conditionally (re)mounted, e.g. in a live preview. + // biome-ignore lint/correctness/useExhaustiveDependencies: see above + useEffect(() => { + const imgEl = imgRef.current; + const measureEl = copyrightMeasureRef.current; + + const observer = new ResizeObserver(updateMediaMeasurements); + if (imgEl) { + observer.observe(imgEl); + } + if (measureEl) { + observer.observe(measureEl); + } + updateMediaMeasurements(); + + return () => observer.disconnect(); + }, [image, copyrightTitle, copyrightText, copyrightIconVisible, updateMediaMeasurements]); + + useEffect(() => { + const copyrightEl = copyrightRef.current; + if (!copyrightEl) { + return; + } + + const observer = new ResizeObserver(() => { + setCopyrightHeight(copyrightEl.getBoundingClientRect().height); + }); + observer.observe(copyrightEl); + + return () => observer.disconnect(); + }, []); + return (
- {image && ( - {imageAltText} + > +
+ {image && ( +
+ {imageAltText} +
+ )} +
+ +
+ +
+
)}
); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.editorconfig.ts index 2fab0ef4..cb6f82ca 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.editorconfig.ts @@ -10,10 +10,17 @@ import type { } from '../../../types/content-block.types'; import { ContentBlockEditor, ContentBlockType } from '../../../types/content-block.types'; -import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, FILE_FIELD, TEXT_FIELD } from '../defaults'; +import { + BLOCK_FIELD_DEFAULTS, + BLOCK_STATE_DEFAULTS, + COPYRIGHT_FIELDS, + FILE_FIELD, + TEXT_FIELD, +} from '../defaults'; export const INITIAL_IMAGE_COMPONENTS_STATE = (): ImageBlockComponentState => ({ title: '', + copyrightIconVisible: true, text: '', imageSource: '', imageAlt: '', @@ -37,13 +44,13 @@ export const IMAGE_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ components: { state: INITIAL_IMAGE_COMPONENTS_STATE(), fields: { - title: TEXT_FIELD({ - label: tText('admin/content-block/helpers/generators/image___bijschift-titel'), - validator: undefined, - }), - text: TEXT_FIELD({ - label: tText('admin/content-block/helpers/generators/image___bijschrift-beschrijving'), - validator: undefined, + ...COPYRIGHT_FIELDS({ + title: { + fieldName: 'title', + }, + text: { + fieldName: 'text', + }, }), imageSource: FILE_FIELD( tText('admin/content-block/helpers/generators/image___een-afbeelding-is-verplicht'), diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.tsx index 19d921b2..37d71e10 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockImage/BlockImage.tsx @@ -6,6 +6,7 @@ import React from 'react'; import type { AlignOption } from '~modules/content-page/types/content-block.types'; import './BlockImage.scss'; +import { CopyrightAttribution } from '~shared/components/CopyrightAttribution'; import { generateSmartLink, SmartLink } from '~shared/components/SmartLink/SmartLink'; export interface BlockImageProps extends DefaultProps { @@ -14,6 +15,7 @@ export interface BlockImageProps extends DefaultProps { imageAction?: ButtonAction; imageAlt?: string; title?: string; + copyrightIconVisible?: boolean; text?: string; width?: 'page-header' | 'full-width' | string; align?: AlignOptions; @@ -31,6 +33,7 @@ export const BlockImage: FunctionComponent = ({ imageAction, imageAlt, title = '', + copyrightIconVisible = false, text = '', width = '100%', align = 'center', @@ -95,12 +98,12 @@ export const BlockImage: FunctionComponent = ({ {/* image author attribution */} - {(!!title || !!text) && ( -
- {title &&

© {title}

} - {text &&

{text}

} -
- )} + ); }; diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.editorconfig.ts index f8fe2629..dca287c5 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.editorconfig.ts @@ -15,7 +15,14 @@ import type { } from '../../../types/content-block.types'; import { ContentBlockEditor, ContentBlockType } from '../../../types/content-block.types'; -import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, FILE_FIELD, TEXT_FIELD } from '../defaults'; +import { + BLOCK_FIELD_DEFAULTS, + BLOCK_STATE_DEFAULTS, + COPYRIGHT_FIELDS, + COPYRIGHT_STATE, + FILE_FIELD, + TEXT_FIELD, +} from '../defaults'; export const INITIAL_IMAGE_GRID_COMPONENTS_STATE = (): ImageGridBlockComponentStateFields[] => [ { @@ -26,6 +33,7 @@ export const INITIAL_IMAGE_GRID_COMPONENTS_STATE = (): ImageGridBlockComponentSt buttonType: 'primary', buttonTitle: '', action: undefined, + ...COPYRIGHT_STATE(), } as unknown as ImageGridBlockComponentStateFields, ]; @@ -64,6 +72,7 @@ export const IMAGE_GRID_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ } as FileUploadProps, } ), + ...COPYRIGHT_FIELDS(), title: TEXT_FIELD({ label: tText('admin/content-block/helpers/generators/image-grid___titel'), validator: undefined, diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.tsx index a14e5051..a2193426 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockImageGrid/BlockImageGrid.tsx @@ -9,6 +9,7 @@ import type { import { defaultRenderLinkFunction } from '~shared/helpers/routing/link'; import './BlockImageGrid.scss'; +import { CopyrightAttribution } from '~shared/components/CopyrightAttribution'; export const BlockImageGrid: FunctionComponent = ({ elements = [], @@ -48,6 +49,11 @@ export const BlockImageGrid: FunctionComponent = ({ backgroundPosition: imageItemAlignment || 'center', }} /> +
[ { @@ -22,6 +29,7 @@ export const INITIAL_USP_GRID_COMPONENTS_STATE = (): ImageGridBlockComponentStat title: '', text: '', action: undefined, + ...COPYRIGHT_STATE(), } as unknown as ImageGridBlockComponentStateFields, ]; @@ -58,6 +66,7 @@ export const USP_GRID_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ } as FileUploadProps, } ), + ...COPYRIGHT_FIELDS(), title: TEXT_FIELD({ label: tText('admin/content-block/helpers/generators/image-grid___titel'), }), diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.editorconfig.ts index 5afac4a0..3b771479 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.editorconfig.ts @@ -9,10 +9,17 @@ import type { } from '../../../types/content-block.types'; import { ContentBlockEditor, ContentBlockType } from '../../../types/content-block.types'; -import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, TEXT_FIELD } from '../defaults'; +import { + BLOCK_FIELD_DEFAULTS, + BLOCK_STATE_DEFAULTS, + COPYRIGHT_FIELDS, + COPYRIGHT_STATE, + TEXT_FIELD, +} from '../defaults'; export const INITIAL_RICH_TEXT_COMPONENTS_STATE = (): RichTextBlockComponentState => ({ content: '', + ...COPYRIGHT_STATE(), }); export const INITIAL_RICH_TEXT_BLOCK_STATE = (): DefaultContentBlockState => @@ -43,6 +50,7 @@ export const RICH_TEXT_BLOCK_CONFIG = (position = 0): ContentBlockConfig => ({ }, tText('admin/content-block/helpers/generators/rich-text___tekst-is-verplicht') ), + ...COPYRIGHT_FIELDS(), }, }, block: { diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx index ffcf1ba3..0d88565f 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichText.tsx @@ -17,9 +17,13 @@ import { defaultRenderLinkFunction } from '~shared/helpers/routing/link'; import { SanitizePreset } from '~shared/helpers/sanitize/presets'; import './BlockRichText.scss'; +import { CopyrightAttribution } from '~shared/components/CopyrightAttribution'; interface BlockRichTextElement { content: string; + copyrightTitle?: string; + copyrightIconVisible?: boolean; + copyrightText?: string; buttons?: (ButtonProps & { buttonAction: ButtonAction })[]; color?: string; } @@ -35,6 +39,9 @@ export const BlockRichText: FunctionComponent = ({ elements = [ { content: '', + copyrightTitle: '', + copyrightIconVisible: false, + copyrightText: '', }, ], maxTextWidth, @@ -69,7 +76,8 @@ export const BlockRichText: FunctionComponent = ({ }; const renderContent = (contentElem: BlockRichTextElement, columnIndex = 0) => { - const { content, color, buttons } = contentElem; + const { content, copyrightTitle, copyrightIconVisible, copyrightText, color, buttons } = + contentElem; return ( <> @@ -83,6 +91,11 @@ export const BlockRichText: FunctionComponent = ({ }} type="div" /> + {buttons && !!buttons.length && renderButtons(columnIndex, buttons)} ); diff --git a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichTextTwoColumns.editorconfig.ts b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichTextTwoColumns.editorconfig.ts index ec42a019..5442bf09 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichTextTwoColumns.editorconfig.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/BlockRichText/BlockRichTextTwoColumns.editorconfig.ts @@ -17,15 +17,23 @@ import { DEFAULT_BUTTON_PROPS, } from '../../../types/content-block.types'; -import { BLOCK_FIELD_DEFAULTS, BLOCK_STATE_DEFAULTS, TEXT_FIELD } from '../defaults'; +import { + BLOCK_FIELD_DEFAULTS, + BLOCK_STATE_DEFAULTS, + COPYRIGHT_FIELDS, + COPYRIGHT_STATE, + TEXT_FIELD, +} from '../defaults'; export const INITIAL_RICH_TEXT_TWO_COLUMNS_COMPONENTS_STATE = (): RichTextBlockComponentState[] => [ { content: '', + ...COPYRIGHT_STATE(), buttons: [], }, { content: '', + ...COPYRIGHT_STATE(), buttons: [], }, ]; @@ -60,6 +68,7 @@ export const RICH_TEXT_TWO_COLUMNS_BLOCK_CONFIG = (position = 0): ContentBlockCo }, tText('admin/content-block/helpers/generators/rich-text-two-columns___tekst-is-verplicht') ), + ...COPYRIGHT_FIELDS(), buttons: { label: tText('admin/content-block/helpers/generators/rich-text-two-columns___knop'), fields: { diff --git a/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts b/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts index bd5fe446..bd4c8b50 100644 --- a/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts +++ b/ui/src/react-admin/modules/content-page/components/blocks/defaults.ts @@ -1,4 +1,4 @@ -import type { SelectOption } from '@viaa/avo2-components'; +import type { CheckboxProps, SelectOption } from '@viaa/avo2-components'; import { GET_ALIGN_OPTIONS } from '~modules/content-page/const/get-align-options'; import { GET_BACKGROUND_COLOR_OPTIONS_ARCHIEF, @@ -16,14 +16,17 @@ import { isAvo } from '~shared/helpers/is-avo'; import { tText } from '~shared/helpers/translation-functions'; import { validateRequiredValue } from '~shared/helpers/validation.ts'; import { SpecialUserGroups } from '~shared/types/authentication.types'; -import type { - ContentBlockField, - CustomBackground, - DefaultContentBlockState, - GradientColor, - PaddingFieldState, +import { + Color, + type ContentBlockComponentsConfig, + ContentBlockEditor, + type ContentBlockField, + type CopyrightComponentState, + type CustomBackground, + type DefaultContentBlockState, + type GradientColor, + type PaddingFieldState, } from '../../types/content-block.types'; -import { Color, ContentBlockEditor } from '../../types/content-block.types'; // Block config defaults export const BLOCK_STATE_DEFAULTS = ( @@ -197,3 +200,42 @@ export const CONTENT_TYPE_AND_LABELS_INPUT = ( validator: undefined, ...propOverride, }); + +export const COPYRIGHT_FIELDS = (overrides?: { + title?: { + fieldName?: string; + overrides?: Partial; + }; + showIcon?: { + fieldName?: string; + overrides?: Partial; + }; + text?: { + fieldName?: string; + overrides?: Partial; + }; +}): ContentBlockComponentsConfig['fields'] => ({ + [overrides?.title?.fieldName || 'copyrightTitle']: TEXT_FIELD({ + label: tText('bijschrift titel'), + validator: undefined, + ...overrides?.title?.overrides, + }), + [overrides?.showIcon?.fieldName || 'copyrightIconVisible']: { + editorType: ContentBlockEditor.Checkbox, + editorProps: { + label: tText('Toon bijschrift icoon'), + } as CheckboxProps, + ...overrides?.showIcon?.overrides, + }, + [overrides?.text?.fieldName || 'copyrightText']: TEXT_FIELD({ + label: tText('bijschrift beschrijving'), + validator: undefined, + ...overrides?.text?.overrides, + }), +}); + +export const COPYRIGHT_STATE = (): CopyrightComponentState => ({ + copyrightTitle: '', + copyrightIconVisible: true, + copyrightText: '', +}); diff --git a/ui/src/react-admin/modules/content-page/types/content-block.types.ts b/ui/src/react-admin/modules/content-page/types/content-block.types.ts index b8462c62..e9133974 100644 --- a/ui/src/react-admin/modules/content-page/types/content-block.types.ts +++ b/ui/src/react-admin/modules/content-page/types/content-block.types.ts @@ -283,7 +283,14 @@ export interface HeadingBlockComponentState { align: AlignOption; } -export interface ImageBlockComponentState { +export interface CopyrightComponentState { + copyrightTitle: string; + copyrightIconVisible: boolean; + copyrightText: string; +} + +export interface ImageBlockComponentState + extends Omit { title: string; text: string; imageSource: string; @@ -298,7 +305,7 @@ export interface ImageBlockComponentState { buttonAlign?: AlignOption; } -export interface ImageGridBlockComponentStateFields { +export interface ImageGridBlockComponentStateFields extends CopyrightComponentState { source: string; title?: string; text?: string; @@ -331,7 +338,7 @@ export interface ButtonsBlockComponentState { navigate?: (buttonAction: ButtonAction) => void; } -export interface RichTextBlockComponentState { +export interface RichTextBlockComponentState extends CopyrightComponentState { content: string; buttons?: ButtonsBlockComponentState[]; } @@ -559,7 +566,7 @@ export interface AvoImageTextBackgroundBlockComponentState { buttonIconAlignment?: SimpleAlignOption; } -export interface HetArchiefImageTextBackgroundBlockComponentState { +export interface HetArchiefImageTextBackgroundBlockComponentState extends CopyrightComponentState { heading: string; headingType: HeadingTypeOption; headingSize: HeadingSizeOption; diff --git a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.scss b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.scss new file mode 100644 index 00000000..da5fab27 --- /dev/null +++ b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.scss @@ -0,0 +1,22 @@ +@use "../../styles/settings/colors" as colors; +@use "../../styles/mixins/typography" as typography; + +.a-copyright-attribution { + display: flex; + flex-direction: column; + width: 100%; + + &__annotation { + color: colors.$neutral; + @include typography.sofia-pro-body-sm(true); + font-size: 1.4rem; + line-height: 2rem; + } + + &__text { + color: colors.$color-gray-1000; + @include typography.sofia-pro-body-sm(true); + font-size: 1.4rem; + line-height: 2rem; + } +} diff --git a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx new file mode 100644 index 00000000..31bed498 --- /dev/null +++ b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/CopyrightAttribution.tsx @@ -0,0 +1,42 @@ +import type { DefaultProps } from '@viaa/avo2-components'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + +import './CopyrightAttribution.scss'; +import clsx from 'clsx'; + +export interface CopyrightAttributionProps extends DefaultProps { + title?: string; + showIcon?: boolean; + text?: string; +} + +export const CopyrightAttribution: FunctionComponent = ({ + className, + title = '', + showIcon = false, + text = '', +}) => { + if (!title && !text) { + return null; + } + + const renderTitle = () => { + if (!title && !showIcon) { + return null; + } + + return ( + + {showIcon && <>©} {title} + + ); + }; + + return ( +
+ {renderTitle()} + {text && {text}} +
+ ); +}; diff --git a/ui/src/react-admin/modules/shared/components/CopyrightAttribution/index.ts b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/index.ts new file mode 100644 index 00000000..034f4f4c --- /dev/null +++ b/ui/src/react-admin/modules/shared/components/CopyrightAttribution/index.ts @@ -0,0 +1 @@ +export * from './CopyrightAttribution.tsx';