Skip to content

Commit 27e9005

Browse files
authored
Merge pull request #2217 from tf/gallery-indicators
Pagination and portrait images for image galleries
2 parents a5b6d4d + 01bf87b commit 27e9005

File tree

14 files changed

+400
-46
lines changed

14 files changed

+400
-46
lines changed

entry_types/scrolled/config/locales/new/image_gallery_peeks.de.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,30 @@ de:
77
displayPeeks:
88
label: "Anrisse benachbarter Bilder zeigen"
99
inline_help: "Teile der benachbarten Bilder als Hinweis auf weitere Inhalte anzeigen."
10+
displayPaginationIndicator:
11+
label: "Paginierung anzeigen"
12+
inline_help: |-
13+
Zeigt die aktuelle Bildposition und die Gesamtzahl der
14+
Bilder an, um die Navigation durch die Galerie zu
15+
erleichtern.
16+
edit_item:
17+
back: Zurück
18+
destroy: Löschen
19+
confirm_delete_link: Soll der Bildergalerie-Eintrag wirklich gelöscht werden?
20+
attributes:
21+
image:
22+
label: Bild
23+
portraitImage:
24+
inline_help: |-
25+
Wird verwendet, wenn der Browser-Viewport höher als
26+
breit ist - zum Beispiel auf Smartphones oder
27+
Tablets in Portrait-Ausrichtung. Kann als
28+
Alternative zu einem querformatigen Bild
29+
konfiguriert werden, das ansonsten zu klein
30+
dargestellt würde.
31+
label: Bild (Hochkant)
32+
tabs:
33+
item: Bildergalerie-Eintrag
34+
public:
35+
image_gallery_pagination: "Bildergalerie-Paginierung"
36+
go_to_image_gallery_item: "Gehe zu Bild %{index}"

entry_types/scrolled/config/locales/new/image_gallery_peeks.en.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,29 @@ en:
77
displayPeeks:
88
label: "Display peeks of neighboring images"
99
inline_help: "Show partial adjacent image previews as navigation cues."
10+
displayPaginationIndicator:
11+
label: "Display pagination"
12+
inline_help: |-
13+
Displays the current image position and the total
14+
number of images to make navigating the gallery
15+
easier.
16+
edit_item:
17+
back: Back
18+
destroy: Delete
19+
confirm_delete_link: Are you sure you want to delete this image gallery item?
20+
attributes:
21+
image:
22+
label: Image
23+
portraitImage:
24+
inline_help: |-
25+
Displayed when the browser viewport is taller than
26+
wide, for example on phones or tablets in portrait
27+
orientation. Can be used to provide an alternative
28+
to a landscape image that would otherwise be
29+
displayed too small.
30+
label: Image (Portrait)
31+
tabs:
32+
item: Image Gallery Item
33+
public:
34+
image_gallery_pagination: "Image gallery pagination"
35+
go_to_image_gallery_item: "Go to image %{index}"

entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js

Lines changed: 88 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import {
1212
Image,
1313
InlineFileRights,
1414
ToggleFullscreenCornerButton,
15+
PaginationIndicator,
1516
usePhonePlatform,
17+
usePortraitOrientation,
1618
contentElementWidths
1719
} from 'pageflow-scrolled/frontend';
1820

@@ -102,8 +104,12 @@ function Scroller({
102104
}, [visibleIndex, scrollerRef, controlled]);
103105

104106
function scrollBy(delta) {
107+
scrollTo(visibleIndex + delta);
108+
}
109+
110+
function scrollTo(index) {
105111
const scroller = scrollerRef.current;
106-
const child = scroller.children[visibleIndex + delta];
112+
const child = scroller.children[index];
107113

108114
if (child) {
109115
scrollerRef.current.scrollTo(child.offsetLeft - scroller.offsetLeft, 0);
@@ -139,9 +145,9 @@ function Scroller({
139145
<div className={classNames(styles.wrapper,
140146
{[styles.wide]:
141147
contentElementWidth === contentElementWidths.lg ||
142-
contentElementWidth === contentElementWidths.xl},
148+
contentElementWidth === contentElementWidths.xl},
143149
{[styles.full]:
144-
contentElementWidth === contentElementWidths.full},
150+
contentElementWidth === contentElementWidths.full},
145151
{[styles.clip]: configuration.hidePeeks},
146152
{[styles.customMargin]: customMargin})}>
147153
<div className={styles.leftButton}>
@@ -169,23 +175,70 @@ function Scroller({
169175
</Item>
170176
))}
171177
</div>
178+
{configuration.displayPaginationIndicator &&
179+
<div className={styles.paginationIndicator}>
180+
<PaginationIndicator
181+
itemCount={items.length}
182+
currentIndex={visibleIndex}
183+
scrollerRef={scrollerRef}
184+
navAriaLabelTranslationKey="pageflow_scrolled.public.image_gallery_pagination"
185+
itemAriaLabelTranslationKey="pageflow_scrolled.public.go_to_image_gallery_item"
186+
onItemClick={index => scrollTo(index)} />
187+
</div>}
172188
</div>
173189
);
174190
}
175191

176192
const Item = forwardRef(function({item, configuration, current, onClick, children}, ref) {
177-
const updateConfiguration = useContentElementConfigurationUpdate();
178-
const {shouldLoad} = useContentElementLifecycle();
179-
180-
const captions = configuration.captions || {};
181-
const caption = captions[item.id];
182-
183193
const imageFile = useFileWithInlineRights({
184194
configuration: item,
185195
collectionName: 'imageFiles',
186196
propertyName: 'image'
187197
});
188198

199+
const portraitImageFile = useFileWithInlineRights({
200+
configuration: item,
201+
collectionName: 'imageFiles',
202+
propertyName: 'portraitImage'
203+
});
204+
205+
const ItemImageComponent = portraitImageFile ? OrientationAwareItemImage : ItemImageWithCaption;
206+
207+
return (
208+
<div className={classNames(styles.item, {[styles.current]: current,
209+
[styles.placeholder]: item.placeholder})}
210+
ref={ref}>
211+
<div className={styles.figure}>
212+
<ItemImageComponent item={item}
213+
imageFile={imageFile}
214+
portraitImageFile={portraitImageFile}
215+
configuration={configuration}
216+
current={current}
217+
onClick={onClick}
218+
children={children} />
219+
</div>
220+
</div>
221+
);
222+
});
223+
224+
function OrientationAwareItemImage({imageFile, portraitImageFile, ...props}) {
225+
const portraitOrientation = usePortraitOrientation();
226+
227+
imageFile = portraitOrientation && portraitImageFile ?
228+
portraitImageFile : imageFile;
229+
return (
230+
<ItemImageWithCaption imageFile={imageFile} {...props} />
231+
);
232+
}
233+
234+
function ItemImageWithCaption({item, imageFile, configuration, current, onClick, children}) {
235+
const {shouldLoad} = useContentElementLifecycle();
236+
237+
const updateConfiguration = useContentElementConfigurationUpdate();
238+
239+
const captions = configuration.captions || {};
240+
const caption = captions[item.id];
241+
189242
const handleCaptionChange = function(caption) {
190243
updateConfiguration({
191244
captions: {
@@ -196,36 +249,30 @@ const Item = forwardRef(function({item, configuration, current, onClick, childre
196249
}
197250

198251
return (
199-
<div className={classNames(styles.item, {[styles.current]: current,
200-
[styles.placeholder]: item.placeholder})}
201-
ref={ref}>
202-
<div className={styles.figure}>
203-
<FitViewport file={imageFile}
204-
aspectRatio={imageFile ? undefined : 0.75}
205-
scale={0.8}
206-
opaque={!imageFile}>
207-
<ContentElementBox>
208-
<Figure caption={caption}
209-
variant={configuration.captionVariant}
210-
onCaptionChange={handleCaptionChange}
211-
addCaptionButtonVisible={current && !item.placeholder}
212-
addCaptionButtonPosition="inside">
213-
<FitViewport.Content>
214-
<div onClick={onClick}>
215-
<Image imageFile={imageFile} load={shouldLoad} />
216-
</div>
217-
{children}
218-
<InlineFileRights configuration={configuration}
219-
context="insideElement"
220-
items={[{file: imageFile, label: 'image'}]} />
221-
</FitViewport.Content>
222-
</Figure>
223-
</ContentElementBox>
224-
<InlineFileRights configuration={configuration}
225-
context="afterElement"
226-
items={[{file: imageFile, label: 'image'}]} />
227-
</FitViewport>
228-
</div>
229-
</div>
252+
<FitViewport file={imageFile}
253+
aspectRatio={imageFile ? undefined : 0.75}
254+
scale={0.8}
255+
opaque={!imageFile}>
256+
<ContentElementBox>
257+
<Figure caption={caption}
258+
variant={configuration.captionVariant}
259+
onCaptionChange={handleCaptionChange}
260+
addCaptionButtonVisible={current && !item.placeholder}
261+
addCaptionButtonPosition="inside">
262+
<FitViewport.Content>
263+
<div onClick={onClick}>
264+
<Image imageFile={imageFile} load={shouldLoad} />
265+
</div>
266+
{children}
267+
<InlineFileRights configuration={configuration}
268+
context="insideElement"
269+
items={[{file: imageFile, label: 'image'}]} />
270+
</FitViewport.Content>
271+
</Figure>
272+
</ContentElementBox>
273+
<InlineFileRights configuration={configuration}
274+
context="afterElement"
275+
items={[{file: imageFile, label: 'image'}]} />
276+
</FitViewport>
230277
);
231-
});
278+
}

entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.module.css

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@
5454
grid-column: 3;
5555
}
5656

57+
.paginationIndicator {
58+
grid-column: 1 / -1;
59+
align-self: end;
60+
justify-self: center;
61+
z-index: 3;
62+
margin-top: var(--theme-image-gallery-pagination-margin-top, 10px);
63+
margin-bottom: var(--theme-image-gallery-pagination-margin-bottom, 10px);
64+
}
65+
5766
.items {
5867
grid-row: 1;
5968
grid-column: 1/-1;
@@ -89,7 +98,7 @@
8998

9099
.figure {
91100
transition: transform .2s ease, filter .2s linear;
92-
transform: scale(0.9);
101+
transform: scale(var(--theme-image-gallery-item-scale, 0.9));
93102
filter: brightness(0.5);
94103
}
95104

entry_types/scrolled/package/src/contentElements/imageGallery/editor/ItemsListView.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Marionette from 'backbone.marionette';
2-
import {buttonStyles} from 'pageflow-scrolled/editor';
2+
import {editor, buttonStyles} from 'pageflow-scrolled/editor';
33
import {ListView} from 'pageflow/editor';
44
import {cssModulesUtils} from 'pageflow/ui';
55
import I18n from 'i18n-js';
@@ -28,6 +28,12 @@ export const ItemsListView = Marionette.Layout.extend({
2828
collection: this.collection,
2929
sortable: true,
3030

31+
onEdit: (model) => {
32+
editor.navigate(
33+
`/scrolled/imageGalleries/${this.options.contentElement.id}/${model.id}`,
34+
{trigger: true}
35+
)
36+
},
3137
onRemove: (model) => this.collection.remove(model)
3238
}));
3339
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {SidebarEditItemView} from './SidebarEditItemView';
2+
import {ItemsCollection} from './models/ItemsCollection';
3+
import Marionette from 'backbone.marionette';
4+
5+
export const SidebarController = Marionette.Controller.extend({
6+
initialize: function(options) {
7+
this.entry = options.entry;
8+
this.region = options.region;
9+
},
10+
11+
item: function(id, itemId) {
12+
const contentElement = this.entry.contentElements.get(id);
13+
const itemsCollection = ItemsCollection.forContentElement(contentElement, this.entry);
14+
15+
this.region.show(new SidebarEditItemView({
16+
model: itemsCollection.get(itemId),
17+
collection: itemsCollection,
18+
entry: this.entry,
19+
contentElement
20+
}));
21+
}
22+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import {ConfigurationEditorView} from 'pageflow/ui';
2+
import {editor, FileInputView} from 'pageflow/editor';
3+
import Marionette from 'backbone.marionette';
4+
import I18n from 'i18n-js';
5+
6+
export const SidebarEditItemView = Marionette.Layout.extend({
7+
template: (data) => `
8+
<a class="back">${I18n.t('pageflow_scrolled.editor.content_elements.imageGallery.edit_item.back')}</a>
9+
<a class="destroy">${I18n.t('pageflow_scrolled.editor.content_elements.imageGallery.edit_item.destroy')}</a>
10+
11+
<div class='form_container'></div>
12+
`,
13+
14+
regions: {
15+
formContainer: '.form_container',
16+
},
17+
18+
events: {
19+
'click a.back': 'goBack',
20+
'click a.destroy': 'destroyLink'
21+
},
22+
23+
onRender: function () {
24+
const options = this.options;
25+
26+
const configurationEditor = new ConfigurationEditorView({
27+
model: this.model,
28+
attributeTranslationKeyPrefixes: ['pageflow_scrolled.editor.content_elements.imageGallery.edit_item.attributes'],
29+
tabTranslationKeyPrefix: 'pageflow_scrolled.editor.content_elements.imageGallery.edit_item.tabs',
30+
});
31+
32+
configurationEditor.tab('item', function() {
33+
this.input('image', FileInputView, {
34+
collection: 'image_files',
35+
fileSelectionHandler: 'imageGalleryItem',
36+
fileSelectionHandlerOptions: {
37+
contentElementId: options.contentElement.get('id')
38+
},
39+
positioning: false
40+
});
41+
this.input('portraitImage', FileInputView, {
42+
collection: 'image_files',
43+
fileSelectionHandler: 'imageGalleryItem',
44+
fileSelectionHandlerOptions: {
45+
contentElementId: options.contentElement.get('id')
46+
},
47+
positioning: false
48+
});
49+
});
50+
51+
this.formContainer.show(configurationEditor);
52+
},
53+
54+
goBack: function() {
55+
editor.navigate(`/scrolled/content_elements/${this.options.contentElement.get('id')}`, {trigger: true});
56+
},
57+
58+
destroyLink: function () {
59+
if (window.confirm(I18n.t('pageflow_scrolled.editor.content_elements.imageGallery.edit_item.confirm_delete_link'))) {
60+
this.options.collection.remove(this.model);
61+
this.goBack();
62+
}
63+
}
64+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Marionette from 'backbone.marionette';
2+
3+
export const SidebarRouter = Marionette.AppRouter.extend({
4+
appRoutes: {
5+
'scrolled/imageGalleries/:id/:item_id': 'item'
6+
}
7+
});

0 commit comments

Comments
 (0)