Skip to content

Commit e41b3e4

Browse files
committed
Add defaults tab for new content elements
Allow editors to configure default values for new content elements via the appearance settings. Content element types can provide defaultsInputs to contribute type-specific settings. Default values are automatically applied when creating new content elements.
1 parent 59799de commit e41b3e4

18 files changed

Lines changed: 675 additions & 8 deletions

File tree

entry_types/scrolled/config/locales/de.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,9 @@ de:
616616
mute: Ausblenden
617617
play: Weiterspielen
618618
turnDown: Leiser weiterspielen
619+
autoplay:
620+
inline_help: Automatisch abspielen, wenn das Element in den sichtbaren Bereich gescrollt wird.
621+
label: Autoplay
619622
id:
620623
label: Audio
621624
playerControlVariant:
@@ -1457,11 +1460,19 @@ de:
14571460
edit_defaults:
14581461
back: Erscheinungsbild
14591462
sections_info: Änderungen an diesen Einstellungen haben keine Auswirkungen auf existierende Abschnitte.
1463+
content_elements_info: |-
1464+
Änderungen an diesen Einstellungen haben keine Auswirkungen auf existierende Elemente.
1465+
Einstellungen können später für einzelne Elemente überschrieben werden.
1466+
all_elements: Alle Elemente
14601467
tabs:
14611468
sections: Neue Abschnitte
1469+
content_elements: Neue Elemente
14621470
attributes:
14631471
defaultSectionLayout:
14641472
label: Vordergrund-Positionierung
1473+
inline_help: |-
1474+
Standardposition der scrollenden Vordergrund-Ebene
1475+
neuer Abschnitte in Desktop-Darstellung.
14651476
values:
14661477
center: Mitte
14671478
centerRagged: Zentriert
@@ -1471,10 +1482,17 @@ de:
14711482
label: Abstand oben
14721483
defaultSectionPaddingTop:
14731484
label: Abstand oben
1485+
inline_help: Standardabstand oberhalb des Inhalts neuer Abschnitte.
14741486
bottomPaddingVisualization:
14751487
label: Abstand unten
14761488
defaultSectionPaddingBottom:
14771489
label: Abstand unten
1490+
inline_help: Standardabstand unterhalb des Inhalts neuer Abschnitte.
1491+
defaultContentElementFullWidthInPhoneLayout:
1492+
label: Volle Breite im Phone-Layout
1493+
inline_help: |-
1494+
Standardmäßig Elemente auf kleineren Bildschirmen
1495+
die gesamte Breite des Viewports nutzen lassen.
14781496
typography_sizes:
14791497
xl: Sehr groß
14801498
lg: Groß

entry_types/scrolled/config/locales/en.yml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -601,14 +601,17 @@ en:
601601
mute: Mute
602602
play: Keep playing
603603
turnDown: Keep playing at lower volume
604+
autoplay:
605+
inline_help: Start playing automatically when the element is scrolled into view.
606+
label: Autoplay
604607
id:
605608
label: Audio
606609
playerControlVariant:
607610
inline_help: Choose the style of player controls.
608-
label: Waveform Style
611+
label: Player Controls
609612
values:
610613
classic: Classic
611-
waveform: Waveform (Fein)
614+
waveform: Waveform (Fine)
612615
waveformBars: Waveform (Bars)
613616
waveformLines: Waveform (Lines)
614617
posterId:
@@ -1440,11 +1443,19 @@ en:
14401443
edit_defaults:
14411444
back: Appearance
14421445
sections_info: Changes to these settings have no effect on existing sections.
1446+
content_elements_info: |-
1447+
Changes to these settings have no effect on existing elements.
1448+
Settings can later be changed for individual elements.
1449+
all_elements: All elements
14431450
tabs:
14441451
sections: New sections
1452+
content_elements: New elements
14451453
attributes:
14461454
defaultSectionLayout:
14471455
label: Content alignment
1456+
inline_help: |-
1457+
Default position of the scrolling foreground layer of
1458+
new sections on desktop devices.
14481459
values:
14491460
center: Centered
14501461
centerRagged: Centered (Ragged)
@@ -1454,10 +1465,17 @@ en:
14541465
label: Top padding
14551466
defaultSectionPaddingTop:
14561467
label: Top padding
1468+
inline_help: Default vertical spacing above the content of new sections.
14571469
bottomPaddingVisualization:
14581470
label: Bottom padding
14591471
defaultSectionPaddingBottom:
14601472
label: Bottom padding
1473+
inline_help: Default vertical spacing below the content of new sections.
1474+
defaultContentElementFullWidthInPhoneLayout:
1475+
label: Full width in phone layout
1476+
inline_help: |-
1477+
By default, make elements span the full width of the
1478+
viewport on smaller screens.
14611479
typography_sizes:
14621480
xl: Very large
14631481
lg: Large

entry_types/scrolled/package/spec/editor/api/ContentElementTypeRegistry-spec.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,114 @@ describe('ContentElementTypeRegistry', () => {
146146
});
147147
});
148148

149+
describe('#getDefaultsInputsMapping', () => {
150+
it('returns empty object for type without defaultsInputs', () => {
151+
const registry = new ContentElementTypeRegistry({features: new Features()});
152+
registry.register('textBlock', {});
153+
154+
const mapping = registry.getDefaultsInputsMapping('textBlock');
155+
156+
expect(mapping).toEqual({});
157+
});
158+
159+
it('returns mapping from metadata keys to property names', () => {
160+
const registry = new ContentElementTypeRegistry({features: new Features()});
161+
registry.register('inlineImage', {
162+
defaultsInputs() {
163+
this.input('enableFullscreen');
164+
this.input('autoplay');
165+
}
166+
});
167+
168+
const mapping = registry.getDefaultsInputsMapping('inlineImage');
169+
170+
expect(mapping).toEqual({
171+
'default-inlineImage-enableFullscreen': 'enableFullscreen',
172+
'default-inlineImage-autoplay': 'autoplay'
173+
});
174+
});
175+
});
176+
177+
describe('#createDefaultsInputContext', () => {
178+
it('prefixes property names passed to input', () => {
179+
const registry = new ContentElementTypeRegistry({features: new Features()});
180+
const tabView = {
181+
input: jest.fn(),
182+
view: jest.fn()
183+
};
184+
185+
const context = registry.createDefaultsInputContext(tabView, 'inlineImage');
186+
context.input('enableFullscreen', 'CheckBoxInputView', {some: 'option'});
187+
188+
expect(tabView.input).toHaveBeenCalledWith(
189+
'default-inlineImage-enableFullscreen',
190+
'CheckBoxInputView',
191+
expect.objectContaining({some: 'option'})
192+
);
193+
});
194+
195+
it('adds attributeTranslationKeyPrefixes with fallback to normal attributes', () => {
196+
const registry = new ContentElementTypeRegistry({features: new Features()});
197+
const tabView = {
198+
input: jest.fn(),
199+
view: jest.fn()
200+
};
201+
202+
const context = registry.createDefaultsInputContext(tabView, 'inlineImage');
203+
context.input('enableFullscreen', 'CheckBoxInputView');
204+
205+
expect(tabView.input).toHaveBeenCalledWith(
206+
expect.anything(),
207+
expect.anything(),
208+
expect.objectContaining({
209+
attributeTranslationKeyPrefixes: [
210+
'pageflow_scrolled.editor.content_elements.inlineImage.defaults.attributes',
211+
'pageflow_scrolled.editor.content_elements.inlineImage.attributes'
212+
],
213+
attributeTranslationPropertyName: 'enableFullscreen'
214+
})
215+
);
216+
});
217+
218+
it('preserves existing attributeTranslationKeyPrefixes', () => {
219+
const registry = new ContentElementTypeRegistry({features: new Features()});
220+
const tabView = {
221+
input: jest.fn(),
222+
view: jest.fn()
223+
};
224+
225+
const context = registry.createDefaultsInputContext(tabView, 'inlineImage');
226+
context.input('enableFullscreen', 'CheckBoxInputView', {
227+
attributeTranslationKeyPrefixes: ['custom.prefix']
228+
});
229+
230+
expect(tabView.input).toHaveBeenCalledWith(
231+
expect.anything(),
232+
expect.anything(),
233+
expect.objectContaining({
234+
attributeTranslationKeyPrefixes: [
235+
'pageflow_scrolled.editor.content_elements.inlineImage.defaults.attributes',
236+
'pageflow_scrolled.editor.content_elements.inlineImage.attributes',
237+
'custom.prefix'
238+
]
239+
})
240+
);
241+
});
242+
243+
it('passes through view calls', () => {
244+
const registry = new ContentElementTypeRegistry({features: new Features()});
245+
const tabView = {
246+
input: jest.fn(),
247+
view: jest.fn()
248+
};
249+
250+
const context = registry.createDefaultsInputContext(tabView, 'inlineImage');
251+
context.view('SomeView', {some: 'option'});
252+
253+
expect(tabView.view).toHaveBeenCalledWith('SomeView', {some: 'option'});
254+
});
255+
});
256+
149257
describe('#toArray', () => {
150258
it('returns array of with options passed to register', () => {
151259
const registry = new ContentElementTypeRegistry({features: new Features()});

entry_types/scrolled/package/spec/editor/models/ContentElement-spec.js

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,133 @@ describe('ContentElement', () => {
515515
});
516516
});
517517

518+
describe('#applyDefaultConfiguration', () => {
519+
describe('with defaultsInputs', () => {
520+
beforeEach(() => {
521+
editor.contentElementTypes.register('contentElementWithDefaultsInputs', {
522+
defaultsInputs() {
523+
this.input('enableFullscreen');
524+
this.input('autoplay');
525+
}
526+
});
527+
});
528+
529+
it('copies defaults from entry metadata to configuration', () => {
530+
const entry = factories.entry(
531+
ScrolledEntry,
532+
{
533+
metadata: {
534+
configuration: {
535+
'default-contentElementWithDefaultsInputs-enableFullscreen': true,
536+
'default-contentElementWithDefaultsInputs-autoplay': false
537+
}
538+
}
539+
},
540+
{
541+
entryTypeSeed: normalizeSeed({
542+
contentElements: []
543+
})
544+
}
545+
);
546+
547+
const contentElement = new entry.contentElements.model({
548+
typeName: 'contentElementWithDefaultsInputs'
549+
});
550+
contentElement.applyDefaultConfiguration({entry});
551+
552+
expect(contentElement.configuration.get('enableFullscreen')).toEqual(true);
553+
expect(contentElement.configuration.get('autoplay')).toEqual(false);
554+
});
555+
556+
it('ignores undefined values in entry metadata', () => {
557+
const entry = factories.entry(
558+
ScrolledEntry,
559+
{
560+
metadata: {
561+
configuration: {
562+
'default-contentElementWithDefaultsInputs-enableFullscreen': true
563+
}
564+
}
565+
},
566+
{
567+
entryTypeSeed: normalizeSeed({
568+
contentElements: []
569+
})
570+
}
571+
);
572+
573+
const contentElement = new entry.contentElements.model({
574+
typeName: 'contentElementWithDefaultsInputs'
575+
});
576+
contentElement.applyDefaultConfiguration({entry});
577+
578+
expect(contentElement.configuration.get('enableFullscreen')).toEqual(true);
579+
expect(contentElement.configuration.has('autoplay')).toEqual(false);
580+
});
581+
});
582+
583+
describe('with defaultContentElementFullWidthInPhoneLayout', () => {
584+
beforeEach(() => {
585+
editor.contentElementTypes.register('elementSupportingFullWidth', {
586+
supportedWidthRange: ['md', 'full']
587+
});
588+
editor.contentElementTypes.register('elementNotSupportingFullWidth', {
589+
supportedWidthRange: ['md', 'xl']
590+
});
591+
});
592+
593+
it('sets fullWidthInPhoneLayout if element supports it', () => {
594+
const entry = factories.entry(
595+
ScrolledEntry,
596+
{
597+
metadata: {
598+
configuration: {
599+
defaultContentElementFullWidthInPhoneLayout: true
600+
}
601+
}
602+
},
603+
{
604+
entryTypeSeed: normalizeSeed({
605+
contentElements: []
606+
})
607+
}
608+
);
609+
610+
const contentElement = new entry.contentElements.model({
611+
typeName: 'elementSupportingFullWidth'
612+
});
613+
contentElement.applyDefaultConfiguration({entry});
614+
615+
expect(contentElement.configuration.get('fullWidthInPhoneLayout')).toEqual(true);
616+
});
617+
618+
it('does not set fullWidthInPhoneLayout if element does not support it', () => {
619+
const entry = factories.entry(
620+
ScrolledEntry,
621+
{
622+
metadata: {
623+
configuration: {
624+
defaultContentElementFullWidthInPhoneLayout: true
625+
}
626+
}
627+
},
628+
{
629+
entryTypeSeed: normalizeSeed({
630+
contentElements: []
631+
})
632+
}
633+
);
634+
635+
const contentElement = new entry.contentElements.model({
636+
typeName: 'elementNotSupportingFullWidth'
637+
});
638+
contentElement.applyDefaultConfiguration({entry});
639+
640+
expect(contentElement.configuration.has('fullWidthInPhoneLayout')).toEqual(false);
641+
});
642+
});
643+
});
644+
518645
describe('#getEditorPath', () => {
519646
it('returns content element path by default', () => {
520647
const entry = factories.entry(
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import '@testing-library/jest-dom/extend-expect';
2+
3+
import {ContentElementTypeSeparatorView} from 'editor/views/ContentElementTypeSeparatorView';
4+
import {renderBackboneView} from 'pageflow/testHelpers';
5+
6+
describe('ContentElementTypeSeparatorView', () => {
7+
it('renders pictogram as mask', () => {
8+
const view = new ContentElementTypeSeparatorView({
9+
pictogram: 'path/to/pictogram.svg',
10+
typeName: 'Some Element'
11+
});
12+
13+
renderBackboneView(view);
14+
15+
expect(view.el.querySelector('[style*="mask-image"]')).toBeInTheDocument();
16+
});
17+
18+
it('renders type name', () => {
19+
const {getByText} = renderBackboneView(new ContentElementTypeSeparatorView({
20+
pictogram: 'path/to/pictogram.svg',
21+
typeName: 'Some Element'
22+
}));
23+
24+
expect(getByText('Some Element')).toBeInTheDocument();
25+
});
26+
});

0 commit comments

Comments
 (0)