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 &&
+
}
);
}
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 (
+
+ {Array(itemCount).fill().map((_, index) =>
+ onItemClick(index)} />
+ )}
+
+ );
+}
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';