diff --git a/entry_types/scrolled/config/locales/new/image_gallery_peeks.de.yml b/entry_types/scrolled/config/locales/new/image_gallery_peeks.de.yml index 4575a80ffa..943a623a05 100644 --- a/entry_types/scrolled/config/locales/new/image_gallery_peeks.de.yml +++ b/entry_types/scrolled/config/locales/new/image_gallery_peeks.de.yml @@ -7,3 +7,30 @@ de: displayPeeks: label: "Anrisse benachbarter Bilder zeigen" inline_help: "Teile der benachbarten Bilder als Hinweis auf weitere Inhalte anzeigen." + displayPaginationIndicator: + label: "Paginierung anzeigen" + inline_help: |- + Zeigt die aktuelle Bildposition und die Gesamtzahl der + Bilder an, um die Navigation durch die Galerie zu + erleichtern. + edit_item: + back: Zurück + destroy: Löschen + confirm_delete_link: Soll der Bildergalerie-Eintrag wirklich gelöscht werden? + attributes: + image: + label: Bild + portraitImage: + inline_help: |- + Wird verwendet, wenn der Browser-Viewport höher als + breit ist - zum Beispiel auf Smartphones oder + Tablets in Portrait-Ausrichtung. Kann als + Alternative zu einem querformatigen Bild + konfiguriert werden, das ansonsten zu klein + dargestellt würde. + label: Bild (Hochkant) + tabs: + item: Bildergalerie-Eintrag + public: + image_gallery_pagination: "Bildergalerie-Paginierung" + go_to_image_gallery_item: "Gehe zu Bild %{index}" diff --git a/entry_types/scrolled/config/locales/new/image_gallery_peeks.en.yml b/entry_types/scrolled/config/locales/new/image_gallery_peeks.en.yml index 67dab244a8..06350abaf1 100644 --- a/entry_types/scrolled/config/locales/new/image_gallery_peeks.en.yml +++ b/entry_types/scrolled/config/locales/new/image_gallery_peeks.en.yml @@ -7,3 +7,29 @@ en: displayPeeks: label: "Display peeks of neighboring images" inline_help: "Show partial adjacent image previews as navigation cues." + displayPaginationIndicator: + label: "Display pagination" + inline_help: |- + Displays the current image position and the total + number of images to make navigating the gallery + easier. + edit_item: + back: Back + destroy: Delete + confirm_delete_link: Are you sure you want to delete this image gallery item? + attributes: + image: + label: Image + portraitImage: + inline_help: |- + Displayed when the browser viewport is taller than + wide, for example on phones or tablets in portrait + orientation. Can be used to provide an alternative + to a landscape image that would otherwise be + displayed too small. + label: Image (Portrait) + tabs: + item: Image Gallery Item + public: + image_gallery_pagination: "Image gallery pagination" + go_to_image_gallery_item: "Go to image %{index}" diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js b/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js index 349d02a4d0..e9ddad542d 100644 --- a/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js @@ -12,7 +12,9 @@ import { Image, InlineFileRights, ToggleFullscreenCornerButton, + PaginationIndicator, usePhonePlatform, + usePortraitOrientation, contentElementWidths } from 'pageflow-scrolled/frontend'; @@ -102,8 +104,12 @@ function Scroller({ }, [visibleIndex, scrollerRef, controlled]); function scrollBy(delta) { + scrollTo(visibleIndex + delta); + } + + function scrollTo(index) { const scroller = scrollerRef.current; - const child = scroller.children[visibleIndex + delta]; + const child = scroller.children[index]; if (child) { scrollerRef.current.scrollTo(child.offsetLeft - scroller.offsetLeft, 0); @@ -139,9 +145,9 @@ function Scroller({
@@ -169,23 +175,70 @@ function Scroller({ ))}
+ {configuration.displayPaginationIndicator && +
+ scrollTo(index)} /> +
}
); } const Item = forwardRef(function({item, configuration, current, onClick, children}, ref) { - const updateConfiguration = useContentElementConfigurationUpdate(); - const {shouldLoad} = useContentElementLifecycle(); - - const captions = configuration.captions || {}; - const caption = captions[item.id]; - const imageFile = useFileWithInlineRights({ configuration: item, collectionName: 'imageFiles', propertyName: 'image' }); + const portraitImageFile = useFileWithInlineRights({ + configuration: item, + collectionName: 'imageFiles', + propertyName: 'portraitImage' + }); + + const ItemImageComponent = portraitImageFile ? OrientationAwareItemImage : ItemImageWithCaption; + + return ( +
+
+ +
+
+ ); +}); + +function OrientationAwareItemImage({imageFile, portraitImageFile, ...props}) { + const portraitOrientation = usePortraitOrientation(); + + imageFile = portraitOrientation && portraitImageFile ? + portraitImageFile : imageFile; + return ( + + ); +} + +function ItemImageWithCaption({item, imageFile, configuration, current, onClick, children}) { + const {shouldLoad} = useContentElementLifecycle(); + + const updateConfiguration = useContentElementConfigurationUpdate(); + + const captions = configuration.captions || {}; + const caption = captions[item.id]; + const handleCaptionChange = function(caption) { updateConfiguration({ captions: { @@ -196,36 +249,30 @@ const Item = forwardRef(function({item, configuration, current, onClick, childre } return ( -
-
- - -
- -
- -
- {children} - -
-
-
- -
-
-
+ + +
+ +
+ +
+ {children} + +
+
+
+ +
); -}); +} diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.module.css b/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.module.css index 4a819fbc1c..ac6d0654bb 100644 --- a/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.module.css +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.module.css @@ -54,6 +54,15 @@ grid-column: 3; } +.paginationIndicator { + grid-column: 1 / -1; + align-self: end; + justify-self: center; + z-index: 3; + margin-top: var(--theme-image-gallery-pagination-margin-top, 10px); + margin-bottom: var(--theme-image-gallery-pagination-margin-bottom, 10px); +} + .items { grid-row: 1; grid-column: 1/-1; @@ -89,7 +98,7 @@ .figure { transition: transform .2s ease, filter .2s linear; - transform: scale(0.9); + transform: scale(var(--theme-image-gallery-item-scale, 0.9)); filter: brightness(0.5); } diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/editor/ItemsListView.js b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/ItemsListView.js index db9f1450f1..1fe72d0bce 100644 --- a/entry_types/scrolled/package/src/contentElements/imageGallery/editor/ItemsListView.js +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/ItemsListView.js @@ -1,5 +1,5 @@ import Marionette from 'backbone.marionette'; -import {buttonStyles} from 'pageflow-scrolled/editor'; +import {editor, buttonStyles} from 'pageflow-scrolled/editor'; import {ListView} from 'pageflow/editor'; import {cssModulesUtils} from 'pageflow/ui'; import I18n from 'i18n-js'; @@ -28,6 +28,12 @@ export const ItemsListView = Marionette.Layout.extend({ collection: this.collection, sortable: true, + onEdit: (model) => { + editor.navigate( + `/scrolled/imageGalleries/${this.options.contentElement.id}/${model.id}`, + {trigger: true} + ) + }, onRemove: (model) => this.collection.remove(model) })); } diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/editor/SidebarController.js b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/SidebarController.js new file mode 100644 index 0000000000..d9194303bf --- /dev/null +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/SidebarController.js @@ -0,0 +1,22 @@ +import {SidebarEditItemView} from './SidebarEditItemView'; +import {ItemsCollection} from './models/ItemsCollection'; +import Marionette from 'backbone.marionette'; + +export const SidebarController = Marionette.Controller.extend({ + initialize: function(options) { + this.entry = options.entry; + this.region = options.region; + }, + + item: function(id, itemId) { + const contentElement = this.entry.contentElements.get(id); + const itemsCollection = ItemsCollection.forContentElement(contentElement, this.entry); + + this.region.show(new SidebarEditItemView({ + model: itemsCollection.get(itemId), + collection: itemsCollection, + entry: this.entry, + contentElement + })); + } +}); diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/editor/SidebarEditItemView.js b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/SidebarEditItemView.js new file mode 100644 index 0000000000..4ee9ce7547 --- /dev/null +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/SidebarEditItemView.js @@ -0,0 +1,64 @@ +import {ConfigurationEditorView} from 'pageflow/ui'; +import {editor, FileInputView} from 'pageflow/editor'; +import Marionette from 'backbone.marionette'; +import I18n from 'i18n-js'; + +export const SidebarEditItemView = Marionette.Layout.extend({ + template: (data) => ` + ${I18n.t('pageflow_scrolled.editor.content_elements.imageGallery.edit_item.back')} + ${I18n.t('pageflow_scrolled.editor.content_elements.imageGallery.edit_item.destroy')} + +
+ `, + + regions: { + formContainer: '.form_container', + }, + + events: { + 'click a.back': 'goBack', + 'click a.destroy': 'destroyLink' + }, + + onRender: function () { + const options = this.options; + + const configurationEditor = new ConfigurationEditorView({ + model: this.model, + attributeTranslationKeyPrefixes: ['pageflow_scrolled.editor.content_elements.imageGallery.edit_item.attributes'], + tabTranslationKeyPrefix: 'pageflow_scrolled.editor.content_elements.imageGallery.edit_item.tabs', + }); + + configurationEditor.tab('item', function() { + this.input('image', FileInputView, { + collection: 'image_files', + fileSelectionHandler: 'imageGalleryItem', + fileSelectionHandlerOptions: { + contentElementId: options.contentElement.get('id') + }, + positioning: false + }); + this.input('portraitImage', FileInputView, { + collection: 'image_files', + fileSelectionHandler: 'imageGalleryItem', + fileSelectionHandlerOptions: { + contentElementId: options.contentElement.get('id') + }, + positioning: false + }); + }); + + this.formContainer.show(configurationEditor); + }, + + goBack: function() { + editor.navigate(`/scrolled/content_elements/${this.options.contentElement.get('id')}`, {trigger: true}); + }, + + destroyLink: function () { + if (window.confirm(I18n.t('pageflow_scrolled.editor.content_elements.imageGallery.edit_item.confirm_delete_link'))) { + this.options.collection.remove(this.model); + this.goBack(); + } + } +}); diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/editor/SidebarRouter.js b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/SidebarRouter.js new file mode 100644 index 0000000000..bc209ae1ca --- /dev/null +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/SidebarRouter.js @@ -0,0 +1,7 @@ +import Marionette from 'backbone.marionette'; + +export const SidebarRouter = Marionette.AppRouter.extend({ + appRoutes: { + 'scrolled/imageGalleries/:id/:item_id': 'item' + } +}); diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/editor/index.js b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/index.js index 7dd6408426..ac638e0ce8 100644 --- a/entry_types/scrolled/package/src/contentElements/imageGallery/editor/index.js +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/index.js @@ -5,8 +5,16 @@ import {CheckBoxInputView, SeparatorView} from 'pageflow/editor'; import {ItemsListView} from './ItemsListView'; import {ItemsCollection} from './models/ItemsCollection'; +import {SidebarRouter} from './SidebarRouter'; +import {SidebarController} from './SidebarController'; + import pictogram from './pictogram.svg'; +editor.registerSideBarRouting({ + router: SidebarRouter, + controller: SidebarController +}); + editor.contentElementTypes.register('imageGallery', { pictogram, category: 'media', @@ -16,11 +24,13 @@ editor.contentElementTypes.register('imageGallery', { configurationEditor({entry, contentElement}) { this.tab('general', function() { this.view(ItemsListView, { - collection: ItemsCollection.forContentElement(this.model.parent, entry) + contentElement, + collection: ItemsCollection.forContentElement(this.model.parent, entry), }); this.input('displayPeeks', CheckBoxInputView, { storeInverted: 'hidePeeks' }); + this.input('displayPaginationIndicator', CheckBoxInputView); this.input('enableFullscreenOnDesktop', CheckBoxInputView, { disabledBinding: ['position', 'width'], disabled: () => contentElement.getWidth() === contentElementWidths.full, @@ -31,5 +41,20 @@ editor.contentElementTypes.register('imageGallery', { this.group('ContentElementCaption', {entry, disableWhenNoCaption: false}); this.group('ContentElementInlineFileRightsSettings', {entry, disableWhenNoFileRights: false}); }); - } + }, + + defaultConfig: {displayPaginationIndicator: true} +}); + +editor.registerFileSelectionHandler('imageGalleryItem', function (options) { + const contentElement = options.entry.contentElements.get(options.contentElementId); + const items = ItemsCollection.forContentElement(contentElement, options.entry) + + this.call = function(file) { + items.get(options.id).setReference(options.attributeName, file); + }; + + this.getReferer = function() { + return '/scrolled/imageGalleries/' + contentElement.id + '/' + options.id; + }; }); diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/editor/models/ItemsCollection.js b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/models/ItemsCollection.js index 3f5f82969e..e5b9a48475 100644 --- a/entry_types/scrolled/package/src/contentElements/imageGallery/editor/models/ItemsCollection.js +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/models/ItemsCollection.js @@ -11,7 +11,7 @@ export const ItemsCollection = Backbone.Collection.extend({ initialize(models, options) { this.entry = options.entry; this.contentElement = options.contentElement; - this.listenTo(this, 'add remove sort', this.updateConfiguration); + this.listenTo(this, 'add change remove sort', this.updateConfiguration); this.listenTo(this, 'remove', this.pruneCaptions); }, diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/stories.js b/entry_types/scrolled/package/src/contentElements/imageGallery/stories.js index 02d6ec41a7..67a2d43c2d 100644 --- a/entry_types/scrolled/package/src/contentElements/imageGallery/stories.js +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/stories.js @@ -34,6 +34,19 @@ storiesOfContentElement(module, { }, inlineFileRights: true, variants: [ + { + name: 'Portrait image', + configuration: { + items: [ + { + id: 1, + image: filePermaId('imageFiles', 'turtle'), + portraitImage: filePermaId('imageFiles', 'churchBefore') + } + ] + }, + viewport: 'phone' + }, { name: 'Custom scroll button size', themeOptions: { @@ -105,6 +118,12 @@ storiesOfContentElement(module, { } } } + }, + { + name: 'With pagination indicator', + configuration: { + displayPaginationIndicator: true + } } ] }); diff --git a/entry_types/scrolled/package/src/frontend/PaginationIndicator.js b/entry_types/scrolled/package/src/frontend/PaginationIndicator.js new file mode 100644 index 0000000000..44ed6f47c6 --- /dev/null +++ b/entry_types/scrolled/package/src/frontend/PaginationIndicator.js @@ -0,0 +1,67 @@ +import React, {useEffect, useRef} from 'react'; +import classNames from 'classnames'; + +import { + useI18n, + useTheme +} from 'pageflow-scrolled/frontend'; + +import styles from './PaginationIndicator.module.css'; + +export function PaginationIndicator({ + itemCount, currentIndex, + scrollerRef, + navAriaLabelTranslationKey, itemAriaLabelTranslationKey, + onItemClick +}) { + const {t} = useI18n(); + const navRef = useRef(); + const theme = useTheme(); + + const currentItemFlex = theme.options.properties.root.paginationIndicatorCurrentItemFlex || 3; + + useEffect(() => { + if (!(currentItemFlex > 1)) { + return; + } + + const timeline = new window.ScrollTimeline({ + source: scrollerRef.current, + axis: 'inline' + }); + + const animations = [...navRef.current.children].map((element, index) => { + const start = 1 / Math.max(itemCount - 1, 1) * (index - 1); + const end = 1 / Math.max(itemCount - 1, 1) * (index + 1); + + return element.animate( + [ + (start >= 0) && {flex: 1, offset: start}, + {flex: currentItemFlex}, + (end <= 1) && {flex: 1, offset: end} + ].filter(Boolean), + { + easing: 'linear', + timeline + } + ) + }); + + return () => animations.forEach(animation => animation.cancel()); + }, [currentItemFlex, scrollerRef, itemCount]) + + return ( + + ); +} diff --git a/entry_types/scrolled/package/src/frontend/PaginationIndicator.module.css b/entry_types/scrolled/package/src/frontend/PaginationIndicator.module.css new file mode 100644 index 0000000000..8f8723bcfd --- /dev/null +++ b/entry_types/scrolled/package/src/frontend/PaginationIndicator.module.css @@ -0,0 +1,34 @@ +.nav { + display: flex; + height: var(--theme-pagination-indicator-item-height, 18px); +} + +.item { + flex: 1; + position: relative; + border: 5px solid transparent; + background: transparent; + padding: 0; + display: block; + height: 100%; + box-sizing: border-box; + min-width: var(--theme-pagination-indicator-item-width, 15px); + transition: opacity .5s, transform .5s ease; + cursor: pointer; +} + +.item::after { + content: ""; + position: absolute; + inset: 0; + background-color: var(--theme-light-content-text-color, #fff); + border-radius: var(--theme-pagination-indicator-item-border-radius, 3px); + transition: filter .2s; + opacity: var(--theme-pagination-indicator-item-opacity, 0.9); + border: solid 1px var(--theme-pagination-indicator-item-border-color, rgb(0 0 0 / 0.2)); + filter: var(--theme-pagination-indicator-item-filter, drop-shadow(0 1px 4px rgb(0 0 0 / 0.8))); +} + +.current::after { + opacity: var(--theme-pagination-indicator-current-item-opacity); +} diff --git a/entry_types/scrolled/package/src/frontend/index.js b/entry_types/scrolled/package/src/frontend/index.js index 7c4d5f3c45..72ff4356aa 100644 --- a/entry_types/scrolled/package/src/frontend/index.js +++ b/entry_types/scrolled/package/src/frontend/index.js @@ -49,6 +49,7 @@ export {Panorama} from './Panorama'; export {ExpandableImage} from './ExpandableImage'; export {ToggleFullscreenCornerButton} from './ToggleFullscreenCornerButton'; export {FullscreenViewer} from './FullscreenViewer'; +export {PaginationIndicator} from './PaginationIndicator'; export * from './useOnScreen'; export * from './i18n';