diff --git a/app/assets/stylesheets/pageflow/editor/background_positioning.scss b/app/assets/stylesheets/pageflow/editor/background_positioning.scss index 0a8cfdb8e7..4239d585b3 100644 --- a/app/assets/stylesheets/pageflow/editor/background_positioning.scss +++ b/app/assets/stylesheets/pageflow/editor/background_positioning.scss @@ -18,6 +18,11 @@ margin: 35px; } + h3 { + border-bottom: solid 1px var(--ui-on-surface-color-lightest); + margin: 40px 0; + } + .previews { overflow: scroll; overflow-x: auto; @@ -27,6 +32,7 @@ > div { height: 100%; white-space: nowrap; + text-align: center; } } diff --git a/entry_types/scrolled/config/locales/new/ext-link-options.de.yml b/entry_types/scrolled/config/locales/new/ext-link-options.de.yml new file mode 100644 index 0000000000..44a0cde1a1 --- /dev/null +++ b/entry_types/scrolled/config/locales/new/ext-link-options.de.yml @@ -0,0 +1,63 @@ +de: + pageflow: + external_links_options: + feature_name: "External Links Optionen" + pageflow_scrolled: + editor: + content_elements: + externalLinkList: + attributes: + thumbnailAspectRatio: + label: "Thumbnail Seitenverhältnis" + inline_help: |- + Bestimme die Proportion der Thumbnails. Wird der Text + neben dem Bild gezeigt, kann längerer Text dazu + führen, dass ein hochformatigeres Seitenverähltnis + gewählt wird, damit das Thumbnail die gesamte Höhe des + Links überdeckt. + values: + narrow: Landscape (4:3) + portrait: Portrait (3:4) + square: Quadratisch (1:1) + wide: Landscape (16:9) + original: Original + textPosition: + label: "Textposition" + inline_help: |- + Anordnung von Bild und Text innerhalb von Links. + values: + below: "Unter dem Bild" + right: "Rechts neben dem Bild" + title: "Nur Bild" + thumbnailSize: + label: "Thumbnail-Breite" + inline_help: |- + Anteil der Thumbnail-Breite im Verhältnis zur + Gesamtbreite des Links. + values: + small: "Klein" + medium: "Mittel" + large: "Groß" + linkWidth: + label: "Link-Breite" + inline_help: |- + Wähle eine schmalere Breite, um mehr Links + nebeneinander anzuordnen. + linkAlignment: + label: "Link-Ausrichtung" + inline_help: |- + Bestimmt die Ausrichtung in Zeilen, die nicht + vollständig mit Links gefüllt sind. + values: + spaceEvenly: Gleichmäßig verteilt + left: Links + right: Rechts + center: Mittig + textSize: + label: "Schriftgröße" + inline_help: | + Legt die Schriftgröße für Titel und Beschreibung der Links fest. + values: + small: Klein + medium: Mittel + large: Groß diff --git a/entry_types/scrolled/config/locales/new/ext-link-options.en.yml b/entry_types/scrolled/config/locales/new/ext-link-options.en.yml new file mode 100644 index 0000000000..4861aec48b --- /dev/null +++ b/entry_types/scrolled/config/locales/new/ext-link-options.en.yml @@ -0,0 +1,62 @@ +en: + pageflow: + external_links_options: + feature_name: "External Links Options" + pageflow_scrolled: + editor: + content_elements: + externalLinkList: + attributes: + thumbnailAspectRatio: + label: "Thumbnail aspect ratio" + inline_help: |- + Determine the proportions of the thumbnails. If the + text is displayed next to the image, longer text may + result in a more portrait-like aspect ratio so that + the thumbnail covers the entire height of the link. + values: + narrow: Landscape (4:3) + portrait: Portrait (3:4) + square: Square (1:1) + wide: Landscape (16:9) + original: Original + textPosition: + label: "Text position" + inline_help: |- + Arrangement of image and text within links. + values: + below: "Below image" + right: "Right from image" + title: "Image only" + thumbnailSize: + label: "Thumbnail width" + inline_help: |- + Proportion of the thumbnail width relative to the + overall width of the link. + values: + small: "Small" + medium: "Medium" + large: "Large" + linkWidth: + label: "Link width" + inline_help: |- + Choose a narrower width to arrange more links + side by side. + linkAlignment: + label: "Link alignment" + inline_help: |- + Determines the alignment in rows that are not + completely filled with links. + values: + spaceEvenly: Space evenly + left: Left + right: Right + center: Center + textSize: + label: "Text size" + inline_help: |- + Defines the font size for the title and description of the links. + values: + small: Small + medium: Medium + large: Large diff --git a/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb b/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb index 5b7aecd6c2..6a97ffc091 100644 --- a/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb +++ b/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb @@ -42,6 +42,7 @@ def configure(config) c.features.register('frontend_v2') c.features.register('scrolled_entry_fragment_caching') c.features.register('backdrop_content_elements') + c.features.register('external_links_options') c.additional_frontend_seed_data.register( 'frontendVersion', diff --git a/entry_types/scrolled/package/spec/contentElements/externalLinkList/frontend/ExternalLink-spec.js b/entry_types/scrolled/package/spec/contentElements/externalLinkList/frontend/ExternalLink-spec.js index f6e864171d..7010177809 100644 --- a/entry_types/scrolled/package/spec/contentElements/externalLinkList/frontend/ExternalLink-spec.js +++ b/entry_types/scrolled/package/spec/contentElements/externalLinkList/frontend/ExternalLink-spec.js @@ -29,4 +29,86 @@ describe('ExternalLink', () => { expect(getByRole('link')).toHaveAttribute('href', 'https://example.com'); }); + + it('renders linkThumbnailLarge image by default', () => { + const {getByRole} = renderInEntry( + , + { + seed: { + imageFiles: [{permaId: 5}], + imageFileUrlTemplates: { + linkThumbnailLarge: ':id_partition/linkThumbnailLarge/image.jpg' + }, + } + } + ) + + expect(getByRole('img')).toHaveAttribute('src', expect.stringContaining('linkThumbnailLarge')); + expect(getByRole('img').parentNode).not.toHaveAttribute('style'); + }); + + it('uses medium image as thumbnail with alternative aspect ratio', () => { + const {getByRole} = renderInEntry( + , + { + seed: { + imageFiles: [{permaId: 5}], + imageFileUrlTemplates: { + linkThumbnailLarge: ':id_partition/linkThumbnailLarge/image.jpg', + medium: ':id_partition/medium/image.jpg' + }, + } + } + ) + + expect(getByRole('img')).toHaveAttribute('src', expect.stringContaining('medium')); + expect(getByRole('img').parentNode).toHaveStyle('paddingTop: 100%'); + }); + + it('uses medium image as thumbnail with alternative crop position', () => { + const {getByRole} = renderInEntry( + , + { + seed: { + imageFiles: [{permaId: 5}], + imageFileUrlTemplates: { + linkThumbnailLarge: ':id_partition/linkThumbnailLarge/image.jpg', + medium: ':id_partition/medium/image.jpg' + }, + } + } + ) + + expect(getByRole('img')).toHaveAttribute('src', expect.stringContaining('medium')); + expect(getByRole('img')).toHaveStyle('object-position: 100% 20%'); + }); + + it('uses medium image as thumbnail with original aspect ratio', () => { + const {getByRole} = renderInEntry( + , + { + seed: { + imageFiles: [{permaId: 5, width: 1000, height: 500}], + imageFileUrlTemplates: { + linkThumbnailLarge: ':id_partition/linkThumbnailLarge/image.jpg', + medium: ':id_partition/medium/image.jpg' + }, + } + } + ) + + expect(getByRole('img')).toHaveAttribute('src', expect.stringContaining('medium')); + expect(getByRole('img').parentNode).toHaveStyle('paddingTop: 50%'); + }); }); diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/SidebarEditLinkView.js b/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/SidebarEditLinkView.js index bccc958679..182a9ae2cb 100644 --- a/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/SidebarEditLinkView.js +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/SidebarEditLinkView.js @@ -4,6 +4,13 @@ import {InlineFileRightsMenuItem} from 'pageflow-scrolled/editor'; import Marionette from 'backbone.marionette'; import I18n from 'i18n-js'; +const previewAspectRatios = { + wide: 16 / 9, + narrow: 4 / 3, + square: 1, + portrait: 3 / 4 +}; + export const SidebarEditLinkView = Marionette.Layout.extend({ template: (data) => ` ${I18n.t('pageflow_scrolled.editor.content_elements.externalLinkList.back')} @@ -27,6 +34,8 @@ export const SidebarEditLinkView = Marionette.Layout.extend({ tabTranslationKeyPrefix: 'pageflow_scrolled.editor.content_elements.externalLinkList.tabs' }); var self = this; + var thumbnailAspectRatio = this.options.contentElement.configuration.get('thumbnailAspectRatio'); + configurationEditor.tab('edit_link', function () { this.input('url', TextInputView, { required: true @@ -38,13 +47,18 @@ export const SidebarEditLinkView = Marionette.Layout.extend({ fileSelectionHandlerOptions: { contentElementId: self.options.contentElement.get('id') }, - positioning: false, + positioning: !!previewAspectRatios[thumbnailAspectRatio], + positioningOptions: { + preview: previewAspectRatios[thumbnailAspectRatio] + }, dropDownMenuItems: [InlineFileRightsMenuItem] }); this.input('title', TextInputView, { required: true }); - this.input('description', TextInputView); + this.input('description', TextInputView, { + maxLength: 10000 + }); }); this.formContainer.show(configurationEditor); }, diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/index.js b/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/index.js index 70f3447cef..31975968b5 100644 --- a/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/index.js +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/index.js @@ -1,9 +1,12 @@ import {editor} from 'pageflow-scrolled/editor'; +import {features} from 'pageflow/frontend'; +import {SelectInputView, SliderInputView, SeparatorView} from 'pageflow/ui'; import {SidebarRouter} from './SidebarRouter'; import {SidebarController} from './SidebarController'; import {SidebarListView} from './SidebarListView'; import {ExternalLinkCollection} from './models/ExternalLinkCollection'; +import {maxLinkWidth} from '../linkWidths'; import pictogram from './pictogram.svg'; @@ -18,16 +21,57 @@ editor.registerSideBarRouting({ editor.contentElementTypes.register('externalLinkList', { pictogram, category: 'links', - supportedPositions: ['inline'], + supportedPositions: ['inline', 'standAlone'], + supportedWidthRange: ['m', 'xl'], - configurationEditor({entry}) { + configurationEditor({entry, contentElement}) { this.tab('general', function() { - this.group('ContentElementVariant', {entry}); + const layout = contentElement.section.configuration.get('layout'); + + if (!features.isEnabled('external_links_options')) { + this.group('ContentElementVariant', {entry}); + } this.view(SidebarListView, { contentElement: this.model.parent, collection: ExternalLinkCollection.forContentElement(this.model.parent, entry) }); + + if (features.isEnabled('external_links_options')) { + this.input('textPosition', SelectInputView, { + values: ['below', 'right', 'title'] + }); + this.group('ContentElementVariant', {entry}); + this.view(SeparatorView); + this.group('ContentElementPosition'); + this.view(SeparatorView); + this.input('linkWidth', SliderInputView, { + displayText: value => [ + 'XS', 'S', 'M', 'L', 'XL', 'XXL' + ][value + 2], + saveOnSlide: true, + minValue: -2, + maxValueBinding: ['width', 'textPosition'], + maxValue: ([width, textPosition]) => maxLinkWidth({width, layout, textPosition}), + defaultValue: -1 + }); + this.input('linkAlignment', SelectInputView, { + values: ['spaceEvenly', 'left', 'right', 'center'], + visibleBinding: 'textPosition', + visible: textPosition => textPosition !== 'right' + }); + this.input('thumbnailSize', SelectInputView, { + values: ['small', 'medium', 'large'], + visibleBinding: 'textPosition', + visibleBindingValue: 'right' + }); + this.input('thumbnailAspectRatio', SelectInputView, { + values: ['wide', 'narrow', 'square', 'portrait', 'original'] + }); + this.input('textSize', SelectInputView, { + values: ['small', 'medium', 'large'] + }); + } }); } }); diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/models/ExternalLinkModel.js b/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/models/ExternalLinkModel.js index 675a8ab78d..362b7ee4a1 100644 --- a/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/models/ExternalLinkModel.js +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/models/ExternalLinkModel.js @@ -14,4 +14,20 @@ export const ExternalLinkModel = Backbone.Model.extend({ title: function() { return this.get('title'); }, + + getFilePosition: function(attribute, coord) { + const cropPosition = this.get('thumbnailCropPosition'); + return cropPosition ? cropPosition[coord] : 50; + }, + + setFilePosition: function(attribute, coord, value) { + this.set('thumbnailCropPosition', { + ...this.get('thumbnailCropPosition'), + [coord]: value + }); + }, + + setFilePositions: function(attribute, x, y) { + this.set('thumbnailCropPosition', {x, y}); + }, }); diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLink.js b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLink.js index 07b6acbd83..11a7bd7009 100644 --- a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLink.js +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLink.js @@ -2,16 +2,16 @@ import React, {useState} from 'react'; import classNames from 'classnames'; import styles from './ExternalLink.module.css'; import { - Image, InlineFileRights, useFileWithInlineRights, useI18n, useContentElementEditorState } from 'pageflow-scrolled/frontend'; +import {Thumbnail} from './Thumbnail'; + export function ExternalLink(props) { const [hideTooltip, setHideTooltip] = useState(true); - var {layout} = props.sectionProps; const {t} = useI18n({locale: 'ui'}); const {isEditable, isSelected} = useContentElementEditorState(); const thumbnailImageFile = useFileWithInlineRights({ @@ -55,22 +55,26 @@ export function ExternalLink(props) { } return ( - -
- - +
+ + +
@@ -84,10 +88,6 @@ export function ExternalLink(props) { ); } -ExternalLink.defaultProps = { - sectionProps: {} -}; - function ensureAbsolute(url) { if (url.match(/^(https?:)?\/\//)) { return url; diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLink.module.css b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLink.module.css index 9d2b46f1f2..24f34e4d14 100644 --- a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLink.module.css +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLink.module.css @@ -9,12 +9,9 @@ display: none; } -.link_item { +.item { display: flex; - flex-direction: column; - width: 45%; vertical-align: top; - margin: 2% auto; text-decoration: none; transition: transform 0.3s; border-radius: var(--theme-content-element-box-border-radius); @@ -22,24 +19,36 @@ will-change: transform; } -.link_item.layout_center { - width: 29%; +.textPosition-below { + flex-direction: column; +} + +.textPosition-title { + composes: textPosition-below; } -.link_item:hover { +.textPosition-below:hover { transform: scale(var(--theme-external-links-card-hover-scale, 1.05)); } -.link_item:hover .link_title { +.textPosition-right:hover { + transform: scale(var(--theme-external-links-card-hover-scale, 1.02)); +} + +.item:hover .link_title { text-decoration: underline; } -.link_thumbnail { - width: auto; - background-repeat: no-repeat; - background-size: cover; - padding-top: 56.25%; - position: relative; +.textPosition-right .thumbnail { + width: 33%; +} + +.textPosition-right.thumbnailSize-medium .thumbnail { + width: 50%; +} + +.textPosition-right.thumbnailSize-large .thumbnail { + width: 66%; } .background { @@ -52,17 +61,25 @@ padding-right: min(var(--padding-inline), 5px); } -.details { +.textPosition-title .background { + display: none; +} + +.textPosition-below .details { padding-top: 15px; padding-bottom: 20px; padding-left: calc(var(--padding-inline) - 5px); padding-right: calc(var(--padding-inline) - 5px); } -.details:first-child { +.textPosition-below .details:first-child { padding-top: 20px; } +.textPosition-right .details { + padding: 10px 10px 10px 15px; +} + .invert > .background { background-color: var(--theme-external-links-card-surface-color, darkContentSurfaceColor); color: var(--theme-external-links-card-text-color, lightContentTextColor); @@ -74,23 +91,35 @@ width: 100%; white-space: normal; line-height: 1.3em; - margin-bottom: 0; } .link_title { composes: typography-externalLinkTitle from global; font-weight: bold; - margin: 0 0 20px; + margin: 0 0 1rem; } .link_desc { composes: typography-externalLinkDescription from global; + margin: 0; +} + +.textSize-medium { + font-size: 18px; +} + +.textSize-large { + font-size: 20px; } @media screen and breakpoint-sm { - .link_title { + .textSize-small .link_title { font-size: 1.2em; } + + .textSize-medium .link_title { + font-size: 1.1em; + } } .tooltip { @@ -113,18 +142,3 @@ color: #fff; text-decoration: underline; } - -@media only screen and (max-width: 600px) { - .link_item.layout_center { - width: 45%; - } -} - -@media only screen and (max-width: 350px) { - .link_item { - width: 85%; - } - .link_item.layout_center { - width: 85%; - } -} diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLinkList.js b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLinkList.js index d42d591e51..9b5aa39475 100644 --- a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLinkList.js +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLinkList.js @@ -1,26 +1,70 @@ import React from 'react'; import classNames from 'classnames'; -import {useContentElementLifecycle, useDarkBackground} from 'pageflow-scrolled/frontend'; +import { + useContentElementLifecycle, + useDarkBackground, + contentElementWidthName +} from 'pageflow-scrolled/frontend'; import {ExternalLink} from './ExternalLink'; import styles from './ExternalLinkList.module.css'; +import textPositionBelowStyles from './textPositons/below.module.css'; +import textPositionRightStyles from './textPositons/right.module.css'; + +const linkWidths = value => [ + 'xs', + 's', + 'm', + 'l', + 'xl', + 'xxl' +][value + 2]; + export function ExternalLinkList(props) { const linkList = props.configuration.links || []; const {shouldLoad} = useContentElementLifecycle(); const darkBackground = useDarkBackground(); + const layout = props.sectionProps.layout === 'centerRagged' ? + 'center' : + props.sectionProps.layout; + + const linkWidth = linkWidths( + ('linkWidth' in props.configuration) ? props.configuration.linkWidth : -1 + ); + + const textPosition = props.configuration.textPosition || 'below'; + const textPositionStyles = textPosition === 'right' ? + textPositionRightStyles : + textPositionBelowStyles; + return ( -
- {linkList.map((link, index) => - - )} +
+
+ {linkList.map((link, index) => + + )} +
); } diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLinkList.module.css b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLinkList.module.css index fc3a5e5ad2..e81d580556 100644 --- a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLinkList.module.css +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/ExternalLinkList.module.css @@ -1,20 +1,14 @@ @value contentColorScope from "pageflow-scrolled/values/colors.module.css"; -.ext_links_container{ +.container { + container-type: inline-size; +} + +.list { composes: scope-externalLinks from global; composes: contentColorScope; - display: flex; - flex-wrap: wrap; - border-collapse: separate; - border-spacing: 10px; +} + +.textPosition-below { min-height: 240px; - width: auto; - height: auto; - pointer-events: auto; - position: relative; - -webkit-transition: opacity 0.5s; - -moz-transition: opacity 0.5s; - transition: opacity 0.5s; - transition-timing-function: cubic-bezier(0.1, 0.57, 0.1, 1); - transition-duration: 0ms; } diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/Thumbnail.js b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/Thumbnail.js new file mode 100644 index 0000000000..1cbf067ddc --- /dev/null +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/Thumbnail.js @@ -0,0 +1,40 @@ +import React from 'react'; + +import {Image} from 'pageflow-scrolled/frontend'; + +import styles from './Thumbnail.module.css'; + +const aspectRatioPaddings = { + narrow: '75%', + square: '100%', + portrait: '133%' +}; + +export function Thumbnail({imageFile, aspectRatio, cropPosition, load, children}) { + imageFile = { + ...imageFile, + cropPosition + }; + + const aspectRatioPadding = getAspectRatioPadding(aspectRatio, imageFile); + + return ( +
+ + {children} +
+ ); +} + +function getAspectRatioPadding(aspectRatio, imageFile) { + if (aspectRatio === 'original' && imageFile) { + return `${imageFile.height / imageFile.width * 100}%`; + } + else { + return aspectRatioPaddings[aspectRatio]; + } +} diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/Thumbnail.module.css b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/Thumbnail.module.css new file mode 100644 index 0000000000..08baa540cb --- /dev/null +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/Thumbnail.module.css @@ -0,0 +1,7 @@ +.thumbnail { + width: auto; + padding-top: 56.25%; + position: relative; + height: 100%; + box-sizing: border-box;; +} diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/textPositons/below.module.css b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/textPositons/below.module.css new file mode 100644 index 0000000000..d47ac92269 --- /dev/null +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/textPositons/below.module.css @@ -0,0 +1,112 @@ +.list { + --columns: 1; + --gap: 1rem; + display: flex; + flex-wrap: wrap; + column-gap: var(--gap); + justify-content: space-evenly; +} + +.linkAlignment-left { + justify-content: flex-start; +} + +.linkAlignment-right { + justify-content: flex-end; +} + +.linkAlignment-center { + justify-content: center; +} + +.list > a { + margin: 2% 0; + width: calc((100% - var(--gap) * (var(--columns) - 1)) / var(--columns)); +} + +@container (min-width: 320px) { + .linkWidth-xs, + .linkWidth-s { + --columns: 2 + } +} + +@container (min-width: 500px) { + .linkWidth-s, + .linkWidth-m, + .linkWidth-l, + .linkWidth-xl { + --gap: 1.5rem; + } + + .linkWidth-xs { + --columns: 3; + } +} + +@container (max-width: 699px) { + .linkWidth-m > a { + max-width: 300px; + } +} + +@container (min-width: 700px) { + .linkWidth-s, + .linkWidth-m, + .linkWidth-l, + .linkWidth-xl { + --gap: 2rem; + } + + .linkWidth-xs { + --columns: 4; + } + + .linkWidth-s { + --columns: 3; + } + + .linkWidth-m { + --columns: 2; + } +} + +@container (min-width: 950px) { + .linkWidth-xs { + --columns: 5; + } + + .linkWidth-s { + --columns: 4; + } + + .linkWidth-m { + --columns: 3; + } + + .linkWidth-l { + --columns: 2; + } +} + +@container (min-width: 1000px) { + .linkWidth-xs { + --columns: 6; + } + + .linkWidth-s { + --columns: 5; + } + + .linkWidth-m { + --columns: 4; + } + + .linkWidth-l { + --columns: 3; + } + + .linkWidth-xl { + --columns: 2; + } +} diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/textPositons/right.module.css b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/textPositons/right.module.css new file mode 100644 index 0000000000..3c69a9a1cc --- /dev/null +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/frontend/textPositons/right.module.css @@ -0,0 +1,88 @@ +.list { + display: grid; + grid-template-columns: repeat( + var(--columns, 1), + minmax(0px, var(--column-max-width, 1fr)) + ); + column-gap: 1rem; + justify-content: center; +} + +.list > a { + width: 100%; + margin: 2% 0; +} + +@container (max-width: 700px) { + .linkWidth-xs { + --column-max-width: 350px; + } +} + +.linkWidth-s { + --column-max-width: 500px; +} + +.linkWidth-l { + --column-max-width: 950px; +} + +.linkWidth-s.width-xl.layout-left { + justify-content: start; +} + +.linkWidth-s.width-xl.layout-right { + justify-content: end; +} + +@container (min-width: 501px) { + .linkWidth-xs { + --columns: 2; + } + + @container (max-width: 749px) { + .linkWidth-xs.layout-right:has(> :nth-child(2n + 1):last-child) > :last-child { + grid-column-start: 2; + } + } +} + +@container (min-width: 750px) { + .linkWidth-xs { + --columns: 3; + } + + .linkWidth-xs.layout-right:has(> :nth-child(3n + 1):last-child) > :last-child, + .linkWidth-xs.layout-right:has(> :nth-child(3n + 2):last-child) > :last-child { + grid-column-start: 3; + } + + .linkWidth-xs.layout-right:has(> :nth-child(3n + 2):last-child) > :nth-last-child(2) { + grid-column-start: 2; + } + + .linkWidth-m { + --columns: 2; + } + + .linkWidth-m.layout-right:has(> :nth-child(2n + 1):last-child) > :last-child { + grid-column-start: 2; + } + + @container (max-width: 1000px) { + .linkWidth-m.layout-center { + --columns: 1; + --column-max-width: 700px; + } + } +} + +@container (min-width: 950px) { + .linkWidth-s { + --columns: 2; + } + + .linkWidth-s.layout-right:has(> :nth-child(2n + 1):last-child) > :last-child { + grid-column-start: 2; + } +} diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/linkWidths.js b/entry_types/scrolled/package/src/contentElements/externalLinkList/linkWidths.js new file mode 100644 index 0000000000..c9298146ae --- /dev/null +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/linkWidths.js @@ -0,0 +1,45 @@ +import {contentElementWidths} from 'pageflow-scrolled/frontend'; + +export const linkWidths = { + xs: -2, + sm: -1, + md: 0, + lg: 1, + xl: 2, + xxl: 3 +} + +export function maxLinkWidth({layout, textPosition, width}) { + if (layout === 'center' || layout === 'centerRagged') { + if (textPosition === 'right') { + return { + [contentElementWidths.md]: linkWidths.md, + [contentElementWidths.lg]: linkWidths.lg, + [contentElementWidths.xl]: linkWidths.xl, + }[width]; + } + else { + return { + [contentElementWidths.md]: linkWidths.lg, + [contentElementWidths.lg]: linkWidths.xl, + [contentElementWidths.xl]: linkWidths.xxl, + }[width]; + } + } + else { + if (textPosition === 'right') { + return { + [contentElementWidths.md]: linkWidths.sm, + [contentElementWidths.lg]: linkWidths.md, + [contentElementWidths.xl]: linkWidths.xl, + }[width]; + } + else { + return { + [contentElementWidths.md]: linkWidths.lg, + [contentElementWidths.lg]: linkWidths.lg, + [contentElementWidths.xl]: linkWidths.xxl, + }[width]; + } + } +} diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/stories.js b/entry_types/scrolled/package/src/contentElements/externalLinkList/stories.js index 3f510de980..9fb27a0f9b 100644 --- a/entry_types/scrolled/package/src/contentElements/externalLinkList/stories.js +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/stories.js @@ -1,5 +1,22 @@ -import '../frontend'; -import {storiesOfContentElement, filePermaId} from 'pageflow-scrolled/spec/support/stories'; +import React from 'react'; + +import { + Entry, RootProviders, + contentElementWidths +} from 'pageflow-scrolled/frontend'; + +import { + linkWidths, + maxLinkWidth +} from './linkWidths'; + +import { + exampleHeading, + filePermaId, + normalizeAndMergeFixture, + storiesOfContentElement +} from 'pageflow-scrolled/spec/support/stories'; +import {storiesOf} from '@storybook/react'; storiesOfContentElement(module, { typeName: 'externalLinkList', @@ -11,7 +28,8 @@ storiesOfContentElement(module, { url: 'https://www.pageflow.io/', thumbnail: filePermaId('imageFiles', 'turtle'), description: 'This is description', - open_in_new_tab: '0' + open_in_new_tab: '0', + thumbnailCropPosition: {x: 0, y: 50} }, { id: '2', @@ -40,6 +58,25 @@ storiesOfContentElement(module, { ] }, variants: [ + { + name: 'With thumbnail aspect ratio', + configuration: { + thumbnailAspectRatio: 'square', + } + }, + { + name: 'With thumbnail size', + configuration: { + textPosition: 'right', + thumbnailSize: 'large', + } + }, + { + name: 'With text size', + configuration: { + textSize: 'large', + } + }, { name: 'With inverted content colors', configuration: { @@ -80,3 +117,122 @@ storiesOfContentElement(module, { ], inlineFileRights: true }); + +[['below', 6], ['right', 3]].forEach(([textPosition, linkCount]) => + storiesOf(`Content Elements/externalLinkList`, module) + .add( + `Text Position - ${textPosition}`, + () => ( + + + + ) + ) +); + +function exampleSeed({textPosition, linkCount}) { + const sectionConfiguration = { + transition: 'scroll' + }; + + return normalizeAndMergeFixture({ + sections: [ + { + id: 1, + configuration: { + ...sectionConfiguration, + layout: 'left', + backdrop: { + color: '#cad2c5' + }, + } + }, + { + id: 2, + configuration: { + ...sectionConfiguration, + layout: 'center', + backdrop: { + color: '#84a98c' + }, + } + }, + { + id: 3, + configuration: { + ...sectionConfiguration, + layout: 'right', + backdrop: { + color: '#52796f' + }, + } + } + ], + contentElements: [ + ...exampleContentElements(1, 'left', textPosition, linkCount), + ...exampleContentElements(2, 'center', textPosition, linkCount), + ...exampleContentElements(3, 'right', textPosition, linkCount), + ] + }); +} + +function linkCount({layout, textPosition, width, linkWidth}) { + if (textPosition === 'right') { + return 3; + } + else { + return range( + linkWidths.xs, + maxLinkWidth({width, layout, textPosition}) + ).reverse().indexOf(linkWidth) + 1; + } +} + +function exampleContentElements(sectionId, layout, textPosition) { + return [ + exampleHeading({sectionId, text: `Layout ${layout}`}), + ...([ + contentElementWidths.md, + contentElementWidths.lg, + contentElementWidths.xl + ].flatMap(width => + range( + linkWidths.xs, + maxLinkWidth({width, layout, textPosition}) + ).map(linkWidth => ( + { + sectionId, + typeName: 'externalLinkList', + configuration: { + textPosition, + width, + linkWidth, + links: links({ + count: linkCount({layout, textPosition, width, linkWidth}) + }) + } + } + )) + )) + ]; +} + +function range(start, end) { + const result = []; + for (let i = start; i <= end; i++) { + result.push(i); + } + return result; +} + +function links({count}) { + return Array.from({length: count}, (_, index) => ( + { + id: `${index + 1}`, + title: `Link ${index + 1}`, + url: 'https://www.pageflow.io/', + thumbnail: filePermaId('imageFiles', 'turtle'), + description: 'This is the description' + } + )); +} diff --git a/entry_types/scrolled/package/src/frontend/__stories__/appearance-stories.js b/entry_types/scrolled/package/src/frontend/__stories__/appearance-stories.js index 524f0f5401..b231814f5b 100644 --- a/entry_types/scrolled/package/src/frontend/__stories__/appearance-stories.js +++ b/entry_types/scrolled/package/src/frontend/__stories__/appearance-stories.js @@ -288,6 +288,6 @@ function exampleSeed( exampleTextBlock({sectionId}), exampleTextBlock({sectionId}), ]).flat() - ];s + ]; } } diff --git a/package/spec/editor/views/inputs/FileInputView-spec.js b/package/spec/editor/views/inputs/FileInputView-spec.js index cb866410be..2000a935f5 100644 --- a/package/spec/editor/views/inputs/FileInputView-spec.js +++ b/package/spec/editor/views/inputs/FileInputView-spec.js @@ -1,4 +1,4 @@ -import {Configuration, FileInputView} from 'pageflow/editor'; +import {Configuration, FileInputView, BackgroundPositioningView} from 'pageflow/editor'; import Backbone from 'backbone'; import * as support from '$support'; @@ -32,6 +32,37 @@ describe('FileInputView', () => { expect(dropDownButton.menuItemNames()).toEqual(expect.arrayContaining(['edit_background_positioning'])); }); + it('can pass additional options to positioning dialog', () => { + var fixture = support.factories.videoFileWithTextTrackFiles({ + videoFileAttributes: {perma_id: 5, file_name: 'video.mp4', state: 'encoded'} + }); + var model = new Configuration({ + file_id: 5, + }); + jest.spyOn(BackgroundPositioningView, 'open'); + + var fileInputView = new FileInputView({ + collection: fixture.videoFiles, + model: model, + propertyName: 'file_id', + positioning: true, + positioningOptions: { + previewAspectRatio: 0.5625 + } + }); + + fileInputView.render(); + var dropDownButton = DropDownButton.find(fileInputView); + dropDownButton.selectMenuItemByName('edit_background_positioning'); + + expect(BackgroundPositioningView.open).toHaveBeenCalledWith( + expect.objectContaining({ + propertyName: 'file_id', + previewAspectRatio: 0.5625 + }) + ); + }); + it('can render additional drop down menu item', () => { const fixture = support.factories.imageFilesFixture({ imageFileAttributes: {perma_id: 5} diff --git a/package/src/editor/views/BackgroundPositioningView.js b/package/src/editor/views/BackgroundPositioningView.js index 48cb5b1dca..35480fdf10 100644 --- a/package/src/editor/views/BackgroundPositioningView.js +++ b/package/src/editor/views/BackgroundPositioningView.js @@ -60,15 +60,18 @@ export const BackgroundPositioningView = Marionette.ItemView.extend({ createPreviews: function() { var view = this; - - _.each(view.previews, function(ratio, name) { + var previews = this.options.preview ? + {preview: this.options.preview} : + this.previews; + _.each(previews, function(ratio, name) { view.ui.previews.append(view.subview(new BackgroundPositioningPreviewView({ model: view.transientModel, propertyName: view.options.propertyName, filesCollection: view.options.filesCollection, ratio: ratio, maxSize: 200, - label: I18n.t('pageflow.editor.templates.background_positioning.previews.' + name) + label: I18n.t('pageflow.editor.templates.background_positioning.previews.' + name, + {defaultValue: ''}) })).el); }); } diff --git a/package/src/editor/views/inputs/FileInputView.js b/package/src/editor/views/inputs/FileInputView.js index 68df139983..855b15846a 100644 --- a/package/src/editor/views/inputs/FileInputView.js +++ b/package/src/editor/views/inputs/FileInputView.js @@ -136,7 +136,8 @@ export const FileInputView = Marionette.ItemView.extend({ }, { inputModel: this.model, propertyName: this.options.propertyName, - filesCollection: this.options.collection + filesCollection: this.options.collection, + positioningOptions: this.options.positioningOptions })); } @@ -221,7 +222,8 @@ FileInputView.EditBackgroundPositioningMenuItem = Backbone.Model.extend({ BackgroundPositioningView.open({ model: this.options.inputModel, propertyName: this.options.propertyName, - filesCollection: this.options.filesCollection + filesCollection: this.options.filesCollection, + ...this.options.positioningOptions }); } }); diff --git a/package/src/ui/views/inputs/SliderInputView.js b/package/src/ui/views/inputs/SliderInputView.js index 21ca0c2e88..8ae9bd3f41 100644 --- a/package/src/ui/views/inputs/SliderInputView.js +++ b/package/src/ui/views/inputs/SliderInputView.js @@ -54,7 +54,7 @@ export const SliderInputView = Marionette.ItemView.extend({ }); this.setupAttributeBinding('minValue', value => this.updateSliderOption('min', value || 0)); - this.setupAttributeBinding('maxValue', value => this.updateSliderOption('max', value || 100)); + this.setupAttributeBinding('maxValue', value => this.updateSliderOption('max', value !== undefined ? value : 100)); this.load(); this.listenTo(this.model, 'change:' + this.options.propertyName, this.load);