Skip to content

Commit 01bf87b

Browse files
committed
Add support for portrait images in image gallery
REDMINE-20988
1 parent 12ae6ed commit 01bf87b

File tree

10 files changed

+242
-41
lines changed

10 files changed

+242
-41
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@ de:
1313
Zeigt die aktuelle Bildposition und die Gesamtzahl der
1414
Bilder an, um die Navigation durch die Galerie zu
1515
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
1634
public:
1735
image_gallery_pagination: "Bildergalerie-Paginierung"
1836
go_to_image_gallery_item: "Gehe zu Bild %{index}"

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ en:
1313
Displays the current image position and the total
1414
number of images to make navigating the gallery
1515
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
1633
public:
1734
image_gallery_pagination: "Image gallery pagination"
1835
go_to_image_gallery_item: "Go to image %{index}"

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

Lines changed: 70 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
ToggleFullscreenCornerButton,
1515
PaginationIndicator,
1616
usePhonePlatform,
17+
usePortraitOrientation,
1718
contentElementWidths
1819
} from 'pageflow-scrolled/frontend';
1920

@@ -189,18 +190,55 @@ function Scroller({
189190
}
190191

191192
const Item = forwardRef(function({item, configuration, current, onClick, children}, ref) {
192-
const updateConfiguration = useContentElementConfigurationUpdate();
193-
const {shouldLoad} = useContentElementLifecycle();
194-
195-
const captions = configuration.captions || {};
196-
const caption = captions[item.id];
197-
198193
const imageFile = useFileWithInlineRights({
199194
configuration: item,
200195
collectionName: 'imageFiles',
201196
propertyName: 'image'
202197
});
203198

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+
204242
const handleCaptionChange = function(caption) {
205243
updateConfiguration({
206244
captions: {
@@ -211,36 +249,30 @@ const Item = forwardRef(function({item, configuration, current, onClick, childre
211249
}
212250

213251
return (
214-
<div className={classNames(styles.item, {[styles.current]: current,
215-
[styles.placeholder]: item.placeholder})}
216-
ref={ref}>
217-
<div className={styles.figure}>
218-
<FitViewport file={imageFile}
219-
aspectRatio={imageFile ? undefined : 0.75}
220-
scale={0.8}
221-
opaque={!imageFile}>
222-
<ContentElementBox>
223-
<Figure caption={caption}
224-
variant={configuration.captionVariant}
225-
onCaptionChange={handleCaptionChange}
226-
addCaptionButtonVisible={current && !item.placeholder}
227-
addCaptionButtonPosition="inside">
228-
<FitViewport.Content>
229-
<div onClick={onClick}>
230-
<Image imageFile={imageFile} load={shouldLoad} />
231-
</div>
232-
{children}
233-
<InlineFileRights configuration={configuration}
234-
context="insideElement"
235-
items={[{file: imageFile, label: 'image'}]} />
236-
</FitViewport.Content>
237-
</Figure>
238-
</ContentElementBox>
239-
<InlineFileRights configuration={configuration}
240-
context="afterElement"
241-
items={[{file: imageFile, label: 'image'}]} />
242-
</FitViewport>
243-
</div>
244-
</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>
245277
);
246-
});
278+
}

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+
});

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ import {CheckBoxInputView, SeparatorView} from 'pageflow/editor';
55
import {ItemsListView} from './ItemsListView';
66
import {ItemsCollection} from './models/ItemsCollection';
77

8+
import {SidebarRouter} from './SidebarRouter';
9+
import {SidebarController} from './SidebarController';
10+
811
import pictogram from './pictogram.svg';
912

13+
editor.registerSideBarRouting({
14+
router: SidebarRouter,
15+
controller: SidebarController
16+
});
17+
1018
editor.contentElementTypes.register('imageGallery', {
1119
pictogram,
1220
category: 'media',
@@ -16,7 +24,8 @@ editor.contentElementTypes.register('imageGallery', {
1624
configurationEditor({entry, contentElement}) {
1725
this.tab('general', function() {
1826
this.view(ItemsListView, {
19-
collection: ItemsCollection.forContentElement(this.model.parent, entry)
27+
contentElement,
28+
collection: ItemsCollection.forContentElement(this.model.parent, entry),
2029
});
2130
this.input('displayPeeks', CheckBoxInputView, {
2231
storeInverted: 'hidePeeks'
@@ -36,3 +45,16 @@ editor.contentElementTypes.register('imageGallery', {
3645

3746
defaultConfig: {displayPaginationIndicator: true}
3847
});
48+
49+
editor.registerFileSelectionHandler('imageGalleryItem', function (options) {
50+
const contentElement = options.entry.contentElements.get(options.contentElementId);
51+
const items = ItemsCollection.forContentElement(contentElement, options.entry)
52+
53+
this.call = function(file) {
54+
items.get(options.id).setReference(options.attributeName, file);
55+
};
56+
57+
this.getReferer = function() {
58+
return '/scrolled/imageGalleries/' + contentElement.id + '/' + options.id;
59+
};
60+
});

entry_types/scrolled/package/src/contentElements/imageGallery/editor/models/ItemsCollection.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const ItemsCollection = Backbone.Collection.extend({
1111
initialize(models, options) {
1212
this.entry = options.entry;
1313
this.contentElement = options.contentElement;
14-
this.listenTo(this, 'add remove sort', this.updateConfiguration);
14+
this.listenTo(this, 'add change remove sort', this.updateConfiguration);
1515
this.listenTo(this, 'remove', this.pruneCaptions);
1616
},
1717

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ storiesOfContentElement(module, {
3434
},
3535
inlineFileRights: true,
3636
variants: [
37+
{
38+
name: 'Portrait image',
39+
configuration: {
40+
items: [
41+
{
42+
id: 1,
43+
image: filePermaId('imageFiles', 'turtle'),
44+
portraitImage: filePermaId('imageFiles', 'churchBefore')
45+
}
46+
]
47+
},
48+
viewport: 'phone'
49+
},
3750
{
3851
name: 'Custom scroll button size',
3952
themeOptions: {

0 commit comments

Comments
 (0)