diff --git a/entry_types/scrolled/config/locales/de.yml b/entry_types/scrolled/config/locales/de.yml
index 2181f72fd0..ad1e5209da 100644
--- a/entry_types/scrolled/config/locales/de.yml
+++ b/entry_types/scrolled/config/locales/de.yml
@@ -616,6 +616,9 @@ de:
mute: Ausblenden
play: Weiterspielen
turnDown: Leiser weiterspielen
+ autoplay:
+ inline_help: Automatisch abspielen, wenn das Element in den sichtbaren Bereich gescrollt wird.
+ label: Autoplay
id:
label: Audio
playerControlVariant:
@@ -1457,24 +1460,46 @@ de:
edit_defaults:
back: Erscheinungsbild
sections_info: Änderungen an diesen Einstellungen haben keine Auswirkungen auf existierende Abschnitte.
+ content_elements_info: |-
+ Änderungen an diesen Einstellungen haben keine Auswirkungen auf existierende Elemente.
+ Einstellungen können später für einzelne Elemente überschrieben werden.
+ all_elements: Alle Elemente
tabs:
sections: Neue Abschnitte
+ content_elements: Neue Elemente
attributes:
defaultSectionLayout:
label: Vordergrund-Positionierung
+ inline_help: |-
+ Standardposition der scrollenden Vordergrund-Ebene
+ neuer Abschnitte in Desktop-Darstellung.
values:
center: Mitte
centerRagged: Zentriert
left: Links
right: Rechts
+ defaultSectionAppearance:
+ label: Abblendung
+ inline_help: Standard-Erscheinung neuer Abschnitte.
+ values:
+ cards: Karte
+ shadow: Schatten
+ transparent: Keine
topPaddingVisualization:
label: Abstand oben
defaultSectionPaddingTop:
label: Abstand oben
+ inline_help: Standardabstand oberhalb des Inhalts neuer Abschnitte.
bottomPaddingVisualization:
label: Abstand unten
defaultSectionPaddingBottom:
label: Abstand unten
+ inline_help: Standardabstand unterhalb des Inhalts neuer Abschnitte.
+ defaultContentElementFullWidthInPhoneLayout:
+ label: Volle Breite im Phone-Layout
+ inline_help: |-
+ Standardmäßig Elemente auf kleineren Bildschirmen
+ die gesamte Breite des Viewports nutzen lassen.
typography_sizes:
xl: Sehr groß
lg: Groß
diff --git a/entry_types/scrolled/config/locales/en.yml b/entry_types/scrolled/config/locales/en.yml
index 8684142885..8cdc148cf7 100644
--- a/entry_types/scrolled/config/locales/en.yml
+++ b/entry_types/scrolled/config/locales/en.yml
@@ -601,14 +601,17 @@ en:
mute: Mute
play: Keep playing
turnDown: Keep playing at lower volume
+ autoplay:
+ inline_help: Start playing automatically when the element is scrolled into view.
+ label: Autoplay
id:
label: Audio
playerControlVariant:
inline_help: Choose the style of player controls.
- label: Waveform Style
+ label: Player Controls
values:
classic: Classic
- waveform: Waveform (Fein)
+ waveform: Waveform (Fine)
waveformBars: Waveform (Bars)
waveformLines: Waveform (Lines)
posterId:
@@ -1440,24 +1443,46 @@ en:
edit_defaults:
back: Appearance
sections_info: Changes to these settings have no effect on existing sections.
+ content_elements_info: |-
+ Changes to these settings have no effect on existing elements.
+ Settings can later be changed for individual elements.
+ all_elements: All elements
tabs:
sections: New sections
+ content_elements: New elements
attributes:
defaultSectionLayout:
label: Content alignment
+ inline_help: |-
+ Default position of the scrolling foreground layer of
+ new sections on desktop devices.
values:
center: Centered
centerRagged: Centered (Ragged)
left: Left
right: Right
+ defaultSectionAppearance:
+ label: Text background
+ inline_help: Default appearance of new sections.
+ values:
+ cards: Card
+ shadow: Shadow
+ transparent: Transparent
topPaddingVisualization:
label: Top padding
defaultSectionPaddingTop:
label: Top padding
+ inline_help: Default vertical spacing above the content of new sections.
bottomPaddingVisualization:
label: Bottom padding
defaultSectionPaddingBottom:
label: Bottom padding
+ inline_help: Default vertical spacing below the content of new sections.
+ defaultContentElementFullWidthInPhoneLayout:
+ label: Full width in phone layout
+ inline_help: |-
+ By default, make elements span the full width of the
+ viewport on smaller screens.
typography_sizes:
xl: Very large
lg: Large
diff --git a/entry_types/scrolled/doc/creating_content_element_types.md b/entry_types/scrolled/doc/creating_content_element_types.md
index c43eddde33..6f1393a7b1 100644
--- a/entry_types/scrolled/doc/creating_content_element_types.md
+++ b/entry_types/scrolled/doc/creating_content_element_types.md
@@ -379,6 +379,57 @@ pageflow_scrolled:
inline_help: "..."
```
+## Defaults Inputs
+
+Content element types can contribute inputs to the entry defaults
+settings. These inputs allow editors to configure default values that
+are applied when creating new content elements of that type.
+
+To define defaults inputs, provide a `defaultsInputs` function when
+registering your content element type:
+
+```javascript
+import {editor} from 'pageflow-scrolled/editor';
+import {CheckBoxInputView} from 'pageflow/ui';
+
+editor.contentElementTypes.register('inlineImage', {
+ configurationEditor({entry}) {
+ // ...
+ },
+
+ defaultsInputs() {
+ this.input('enableFullscreen', CheckBoxInputView);
+ }
+});
+```
+
+The `defaultsInputs` function is called in a similar context as the
+`this.tab` callback in `configurationEditor`. Property names are
+automatically prefixed with `default-{typeName}-` when stored in the
+entry metadata configuration.
+
+For translations, Pageflow first looks for keys under a `defaults`
+namespace, then falls back to normal attribute translations:
+
+```
+pageflow_scrolled:
+ editor:
+ content_elements:
+ inlineImage:
+ defaults:
+ attributes:
+ enableFullscreen:
+ label: "..."
+ inline_help: "..."
+ attributes:
+ enableFullscreen:
+ label: "..."
+ inline_help: "..."
+```
+
+If the `defaults.attributes` translation is not found, the normal
+`attributes` translation is used as a fallback.
+
## Using Palette Colors
[Palette
diff --git a/entry_types/scrolled/package/spec/editor/api/ContentElementTypeRegistry-spec.js b/entry_types/scrolled/package/spec/editor/api/ContentElementTypeRegistry-spec.js
index f6f902b42c..779defeb3a 100644
--- a/entry_types/scrolled/package/spec/editor/api/ContentElementTypeRegistry-spec.js
+++ b/entry_types/scrolled/package/spec/editor/api/ContentElementTypeRegistry-spec.js
@@ -146,6 +146,114 @@ describe('ContentElementTypeRegistry', () => {
});
});
+ describe('#getDefaultsInputsMapping', () => {
+ it('returns empty object for type without defaultsInputs', () => {
+ const registry = new ContentElementTypeRegistry({features: new Features()});
+ registry.register('textBlock', {});
+
+ const mapping = registry.getDefaultsInputsMapping('textBlock');
+
+ expect(mapping).toEqual({});
+ });
+
+ it('returns mapping from metadata keys to property names', () => {
+ const registry = new ContentElementTypeRegistry({features: new Features()});
+ registry.register('inlineImage', {
+ defaultsInputs() {
+ this.input('enableFullscreen');
+ this.input('autoplay');
+ }
+ });
+
+ const mapping = registry.getDefaultsInputsMapping('inlineImage');
+
+ expect(mapping).toEqual({
+ 'default-inlineImage-enableFullscreen': 'enableFullscreen',
+ 'default-inlineImage-autoplay': 'autoplay'
+ });
+ });
+ });
+
+ describe('#createDefaultsInputContext', () => {
+ it('prefixes property names passed to input', () => {
+ const registry = new ContentElementTypeRegistry({features: new Features()});
+ const tabView = {
+ input: jest.fn(),
+ view: jest.fn()
+ };
+
+ const context = registry.createDefaultsInputContext(tabView, 'inlineImage');
+ context.input('enableFullscreen', 'CheckBoxInputView', {some: 'option'});
+
+ expect(tabView.input).toHaveBeenCalledWith(
+ 'default-inlineImage-enableFullscreen',
+ 'CheckBoxInputView',
+ expect.objectContaining({some: 'option'})
+ );
+ });
+
+ it('adds attributeTranslationKeyPrefixes with fallback to normal attributes', () => {
+ const registry = new ContentElementTypeRegistry({features: new Features()});
+ const tabView = {
+ input: jest.fn(),
+ view: jest.fn()
+ };
+
+ const context = registry.createDefaultsInputContext(tabView, 'inlineImage');
+ context.input('enableFullscreen', 'CheckBoxInputView');
+
+ expect(tabView.input).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ expect.objectContaining({
+ attributeTranslationKeyPrefixes: [
+ 'pageflow_scrolled.editor.content_elements.inlineImage.defaults.attributes',
+ 'pageflow_scrolled.editor.content_elements.inlineImage.attributes'
+ ],
+ attributeTranslationPropertyName: 'enableFullscreen'
+ })
+ );
+ });
+
+ it('preserves existing attributeTranslationKeyPrefixes', () => {
+ const registry = new ContentElementTypeRegistry({features: new Features()});
+ const tabView = {
+ input: jest.fn(),
+ view: jest.fn()
+ };
+
+ const context = registry.createDefaultsInputContext(tabView, 'inlineImage');
+ context.input('enableFullscreen', 'CheckBoxInputView', {
+ attributeTranslationKeyPrefixes: ['custom.prefix']
+ });
+
+ expect(tabView.input).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ expect.objectContaining({
+ attributeTranslationKeyPrefixes: [
+ 'pageflow_scrolled.editor.content_elements.inlineImage.defaults.attributes',
+ 'pageflow_scrolled.editor.content_elements.inlineImage.attributes',
+ 'custom.prefix'
+ ]
+ })
+ );
+ });
+
+ it('passes through view calls', () => {
+ const registry = new ContentElementTypeRegistry({features: new Features()});
+ const tabView = {
+ input: jest.fn(),
+ view: jest.fn()
+ };
+
+ const context = registry.createDefaultsInputContext(tabView, 'inlineImage');
+ context.view('SomeView', {some: 'option'});
+
+ expect(tabView.view).toHaveBeenCalledWith('SomeView', {some: 'option'});
+ });
+ });
+
describe('#toArray', () => {
it('returns array of with options passed to register', () => {
const registry = new ContentElementTypeRegistry({features: new Features()});
diff --git a/entry_types/scrolled/package/spec/editor/models/Chapter-spec.js b/entry_types/scrolled/package/spec/editor/models/Chapter-spec.js
index fa24dad7b0..082751eb65 100644
--- a/entry_types/scrolled/package/spec/editor/models/Chapter-spec.js
+++ b/entry_types/scrolled/package/spec/editor/models/Chapter-spec.js
@@ -59,6 +59,15 @@ describe('Chapter', () => {
expect(section.configuration.get('layout')).toEqual('right');
});
+ it('uses default appearance from entry metadata configuration', () => {
+ const {entry} = testContext;
+
+ entry.metadata.configuration.set('defaultSectionAppearance', 'cards');
+ const section = entry.chapters.first().addSection();
+
+ expect(section.configuration.get('appearance')).toEqual('cards');
+ });
+
it('handles sparse positions correctly', () => {
const {entry} = testContext;
diff --git a/entry_types/scrolled/package/spec/editor/models/ContentElement-spec.js b/entry_types/scrolled/package/spec/editor/models/ContentElement-spec.js
index 540b3f21c3..cb8f17e410 100644
--- a/entry_types/scrolled/package/spec/editor/models/ContentElement-spec.js
+++ b/entry_types/scrolled/package/spec/editor/models/ContentElement-spec.js
@@ -515,6 +515,133 @@ describe('ContentElement', () => {
});
});
+ describe('#applyDefaultConfiguration', () => {
+ describe('with defaultsInputs', () => {
+ beforeEach(() => {
+ editor.contentElementTypes.register('contentElementWithDefaultsInputs', {
+ defaultsInputs() {
+ this.input('enableFullscreen');
+ this.input('autoplay');
+ }
+ });
+ });
+
+ it('copies defaults from entry metadata to configuration', () => {
+ const entry = factories.entry(
+ ScrolledEntry,
+ {
+ metadata: {
+ configuration: {
+ 'default-contentElementWithDefaultsInputs-enableFullscreen': true,
+ 'default-contentElementWithDefaultsInputs-autoplay': false
+ }
+ }
+ },
+ {
+ entryTypeSeed: normalizeSeed({
+ contentElements: []
+ })
+ }
+ );
+
+ const contentElement = new entry.contentElements.model({
+ typeName: 'contentElementWithDefaultsInputs'
+ });
+ contentElement.applyDefaultConfiguration({entry});
+
+ expect(contentElement.configuration.get('enableFullscreen')).toEqual(true);
+ expect(contentElement.configuration.get('autoplay')).toEqual(false);
+ });
+
+ it('ignores undefined values in entry metadata', () => {
+ const entry = factories.entry(
+ ScrolledEntry,
+ {
+ metadata: {
+ configuration: {
+ 'default-contentElementWithDefaultsInputs-enableFullscreen': true
+ }
+ }
+ },
+ {
+ entryTypeSeed: normalizeSeed({
+ contentElements: []
+ })
+ }
+ );
+
+ const contentElement = new entry.contentElements.model({
+ typeName: 'contentElementWithDefaultsInputs'
+ });
+ contentElement.applyDefaultConfiguration({entry});
+
+ expect(contentElement.configuration.get('enableFullscreen')).toEqual(true);
+ expect(contentElement.configuration.has('autoplay')).toEqual(false);
+ });
+ });
+
+ describe('with defaultContentElementFullWidthInPhoneLayout', () => {
+ beforeEach(() => {
+ editor.contentElementTypes.register('elementSupportingFullWidth', {
+ supportedWidthRange: ['md', 'full']
+ });
+ editor.contentElementTypes.register('elementNotSupportingFullWidth', {
+ supportedWidthRange: ['md', 'xl']
+ });
+ });
+
+ it('sets fullWidthInPhoneLayout if element supports it', () => {
+ const entry = factories.entry(
+ ScrolledEntry,
+ {
+ metadata: {
+ configuration: {
+ defaultContentElementFullWidthInPhoneLayout: true
+ }
+ }
+ },
+ {
+ entryTypeSeed: normalizeSeed({
+ contentElements: []
+ })
+ }
+ );
+
+ const contentElement = new entry.contentElements.model({
+ typeName: 'elementSupportingFullWidth'
+ });
+ contentElement.applyDefaultConfiguration({entry});
+
+ expect(contentElement.configuration.get('fullWidthInPhoneLayout')).toEqual(true);
+ });
+
+ it('does not set fullWidthInPhoneLayout if element does not support it', () => {
+ const entry = factories.entry(
+ ScrolledEntry,
+ {
+ metadata: {
+ configuration: {
+ defaultContentElementFullWidthInPhoneLayout: true
+ }
+ }
+ },
+ {
+ entryTypeSeed: normalizeSeed({
+ contentElements: []
+ })
+ }
+ );
+
+ const contentElement = new entry.contentElements.model({
+ typeName: 'elementNotSupportingFullWidth'
+ });
+ contentElement.applyDefaultConfiguration({entry});
+
+ expect(contentElement.configuration.has('fullWidthInPhoneLayout')).toEqual(false);
+ });
+ });
+ });
+
describe('#getEditorPath', () => {
it('returns content element path by default', () => {
const entry = factories.entry(
diff --git a/entry_types/scrolled/package/spec/editor/views/ContentElementTypeSeparatorView-spec.js b/entry_types/scrolled/package/spec/editor/views/ContentElementTypeSeparatorView-spec.js
new file mode 100644
index 0000000000..03b2d97311
--- /dev/null
+++ b/entry_types/scrolled/package/spec/editor/views/ContentElementTypeSeparatorView-spec.js
@@ -0,0 +1,26 @@
+import '@testing-library/jest-dom/extend-expect';
+
+import {ContentElementTypeSeparatorView} from 'editor/views/ContentElementTypeSeparatorView';
+import {renderBackboneView} from 'pageflow/testHelpers';
+
+describe('ContentElementTypeSeparatorView', () => {
+ it('renders pictogram as mask', () => {
+ const view = new ContentElementTypeSeparatorView({
+ pictogram: 'path/to/pictogram.svg',
+ typeName: 'Some Element'
+ });
+
+ renderBackboneView(view);
+
+ expect(view.el.querySelector('[style*="mask-image"]')).toBeInTheDocument();
+ });
+
+ it('renders type name', () => {
+ const {getByText} = renderBackboneView(new ContentElementTypeSeparatorView({
+ pictogram: 'path/to/pictogram.svg',
+ typeName: 'Some Element'
+ }));
+
+ expect(getByText('Some Element')).toBeInTheDocument();
+ });
+});
diff --git a/entry_types/scrolled/package/spec/editor/views/EditDefaultsView-spec.js b/entry_types/scrolled/package/spec/editor/views/EditDefaultsView-spec.js
index 5f56bba2f9..155feb055c 100644
--- a/entry_types/scrolled/package/spec/editor/views/EditDefaultsView-spec.js
+++ b/entry_types/scrolled/package/spec/editor/views/EditDefaultsView-spec.js
@@ -1,16 +1,21 @@
import '@testing-library/jest-dom/extend-expect';
+import {fireEvent} from '@testing-library/react';
import {EditDefaultsView} from 'editor/views/EditDefaultsView';
import {features} from 'pageflow/frontend';
+import {CheckBoxInputView} from 'pageflow/ui';
import {ConfigurationEditor, useFakeTranslations, renderBackboneView} from 'pageflow/testHelpers';
import {useEditorGlobals} from 'support';
+import {editor} from 'editor/api';
+
describe('EditDefaultsView', () => {
const {createEntry} = useEditorGlobals();
useFakeTranslations({
'pageflow_scrolled.editor.edit_defaults.tabs.sections': 'Sections',
+ 'pageflow_scrolled.editor.edit_defaults.tabs.content_elements': 'New Elements',
'pageflow_scrolled.editor.edit_defaults.attributes.defaultSectionLayout.label': 'Default layout',
'pageflow_scrolled.editor.edit_defaults.attributes.defaultSectionLayout.values.left': 'Left',
'pageflow_scrolled.editor.edit_defaults.attributes.defaultSectionLayout.values.right': 'Right',
@@ -18,6 +23,9 @@ describe('EditDefaultsView', () => {
'pageflow_scrolled.editor.edit_defaults.attributes.defaultSectionLayout.values.centerRagged': 'Center ragged',
'pageflow_scrolled.editor.edit_defaults.attributes.defaultSectionPaddingTop.label': 'Default top padding',
'pageflow_scrolled.editor.edit_defaults.attributes.defaultSectionPaddingBottom.label': 'Default bottom padding',
+ 'pageflow_scrolled.editor.edit_defaults.attributes.defaultContentElementEnableFullscreen.label': 'Enable fullscreen',
+ 'pageflow_scrolled.editor.edit_defaults.attributes.defaultContentElementFullWidthInPhoneLayout.label': 'Full width in phone layout',
+ 'pageflow_scrolled.editor.edit_defaults.content_elements_info': 'Changes to these settings have no effect on existing elements.',
'pageflow_scrolled.editor.section_padding_visualization.top_padding': 'TopPadding',
'pageflow_scrolled.editor.section_padding_visualization.bottom_padding': 'Bottom'
});
@@ -35,6 +43,19 @@ describe('EditDefaultsView', () => {
expect(getByRole('tab', {name: 'Sections'})).toBeInTheDocument();
});
+ it('renders with content elements tab', () => {
+ const entry = createEntry({});
+
+ const view = new EditDefaultsView({
+ model: entry.metadata,
+ entry
+ });
+
+ const {getByRole} = renderBackboneView(view);
+
+ expect(getByRole('tab', {name: 'New Elements'})).toBeInTheDocument();
+ });
+
it('contains defaultSectionLayout select input', () => {
const entry = createEntry({});
@@ -49,6 +70,35 @@ describe('EditDefaultsView', () => {
expect(configurationEditor.inputPropertyNames()).toContain('defaultSectionLayout');
});
+ it('contains defaultContentElementFullWidthInPhoneLayout checkbox input', () => {
+ const entry = createEntry({});
+
+ const view = new EditDefaultsView({
+ model: entry.metadata,
+ entry
+ });
+
+ const {getByRole} = renderBackboneView(view);
+ fireEvent.click(getByRole('tab', {name: 'New Elements'}));
+ const configurationEditor = ConfigurationEditor.find(view);
+
+ expect(configurationEditor.inputPropertyNames()).toContain('defaultContentElementFullWidthInPhoneLayout');
+ });
+
+ it('shows info box in content elements tab', () => {
+ const entry = createEntry({});
+
+ const view = new EditDefaultsView({
+ model: entry.metadata,
+ entry
+ });
+
+ const {getByRole, getByText} = renderBackboneView(view);
+ fireEvent.click(getByRole('tab', {name: 'New Elements'}));
+
+ expect(getByText('Changes to these settings have no effect on existing elements.')).toBeInTheDocument();
+ });
+
describe('with section_paddings feature flag', () => {
beforeEach(() => features.enable('frontend', ['section_paddings']));
@@ -94,4 +144,106 @@ describe('EditDefaultsView', () => {
expect(getByRole('img', {name: 'Bottom'})).toBeInTheDocument();
});
});
+
+ describe('with content element type defaultsInputs', () => {
+ useFakeTranslations({
+ 'pageflow_scrolled.editor.edit_defaults.tabs.sections': 'Sections',
+ 'pageflow_scrolled.editor.edit_defaults.tabs.content_elements': 'New Elements',
+ 'pageflow_scrolled.editor.edit_defaults.content_elements_info': 'Info',
+ 'pageflow_scrolled.editor.content_elements.testElement.name': 'Test Element',
+ 'pageflow_scrolled.editor.content_elements.testElement.description': 'A test element',
+ 'pageflow_scrolled.editor.content_elements.testElement.defaults.attributes.enableFullscreen.label':
+ 'Test Fullscreen'
+ });
+
+ beforeEach(() => {
+ editor.contentElementTypes.register('testElement', {
+ defaultsInputs() {
+ this.input('enableFullscreen', CheckBoxInputView);
+ }
+ });
+ });
+
+ it('renders separator with type name above inputs', () => {
+ const entry = createEntry({});
+
+ const view = new EditDefaultsView({
+ model: entry.metadata,
+ entry
+ });
+
+ const {getByRole, getByText} = renderBackboneView(view);
+ fireEvent.click(getByRole('tab', {name: 'New Elements'}));
+
+ expect(getByText('Test Element')).toBeInTheDocument();
+ });
+
+ it('renders inputs from content element defaultsInputs with prefixed property name', () => {
+ const entry = createEntry({});
+
+ const view = new EditDefaultsView({
+ model: entry.metadata,
+ entry
+ });
+
+ const {getByRole} = renderBackboneView(view);
+ fireEvent.click(getByRole('tab', {name: 'New Elements'}));
+ const configurationEditor = ConfigurationEditor.find(view);
+
+ expect(configurationEditor.inputPropertyNames()).toContain('default-testElement-enableFullscreen');
+ });
+
+ it('applies attributeTranslationKeyPrefixes to inputs', () => {
+ const entry = createEntry({});
+
+ const view = new EditDefaultsView({
+ model: entry.metadata,
+ entry
+ });
+
+ const {getByRole, getByText} = renderBackboneView(view);
+ fireEvent.click(getByRole('tab', {name: 'New Elements'}));
+
+ expect(getByText('Test Fullscreen')).toBeInTheDocument();
+ });
+ });
+
+ describe('with multiple content element types with defaultsInputs', () => {
+ useFakeTranslations({
+ 'pageflow_scrolled.editor.edit_defaults.tabs.sections': 'Sections',
+ 'pageflow_scrolled.editor.edit_defaults.tabs.content_elements': 'New Elements',
+ 'pageflow_scrolled.editor.edit_defaults.content_elements_info': 'Info',
+ 'pageflow_scrolled.editor.content_elements.elementA.defaults.attributes.optionA.label': 'Option A',
+ 'pageflow_scrolled.editor.content_elements.elementB.defaults.attributes.optionB.label': 'Option B'
+ });
+
+ beforeEach(() => {
+ editor.contentElementTypes.register('elementA', {
+ defaultsInputs() {
+ this.input('optionA', CheckBoxInputView);
+ }
+ });
+ editor.contentElementTypes.register('elementB', {
+ defaultsInputs() {
+ this.input('optionB', CheckBoxInputView);
+ }
+ });
+ });
+
+ it('renders inputs from all content element types', () => {
+ const entry = createEntry({});
+
+ const view = new EditDefaultsView({
+ model: entry.metadata,
+ entry
+ });
+
+ const {getByRole} = renderBackboneView(view);
+ fireEvent.click(getByRole('tab', {name: 'New Elements'}));
+ const configurationEditor = ConfigurationEditor.find(view);
+
+ expect(configurationEditor.inputPropertyNames()).toContain('default-elementA-optionA');
+ expect(configurationEditor.inputPropertyNames()).toContain('default-elementB-optionB');
+ });
+ });
});
diff --git a/entry_types/scrolled/package/src/contentElements/inlineAudio/editor.js b/entry_types/scrolled/package/src/contentElements/inlineAudio/editor.js
index 0803588566..ba58fd831e 100644
--- a/entry_types/scrolled/package/src/contentElements/inlineAudio/editor.js
+++ b/entry_types/scrolled/package/src/contentElements/inlineAudio/editor.js
@@ -12,6 +12,13 @@ editor.contentElementTypes.register('inlineAudio', {
defaultConfig: {playerControlVariant: 'waveformBars'},
+ defaultsInputs() {
+ this.input('autoplay', CheckBoxInputView);
+ this.input('playerControlVariant', SelectInputView, {
+ values: ['waveformBars', 'waveformLines', 'waveform', 'classic']
+ });
+ },
+
configurationEditor({entry}) {
const themeOptions = entry.getTheme().get('options');
diff --git a/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js b/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js
index 96678efc2b..ff8c308c56 100644
--- a/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js
+++ b/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js
@@ -11,6 +11,10 @@ editor.contentElementTypes.register('inlineImage', {
supportedPositions: ['inline', 'side', 'sticky', 'standAlone', 'left', 'right'],
supportedWidthRange: ['xxs', 'full'],
+ defaultsInputs() {
+ this.input('enableFullscreen', CheckBoxInputView);
+ },
+
configurationEditor({entry, contentElement}) {
this.tab('general', function() {
this.input('id', FileInputView, {
diff --git a/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js b/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js
index 7959cad2a2..fa683984a9 100644
--- a/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js
+++ b/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js
@@ -38,6 +38,12 @@ editor.contentElementTypes.register('inlineVideo', {
supportedPositions: ['inline', 'side', 'sticky', 'standAlone', 'left', 'right', 'backdrop'],
supportedWidthRange: ['xxs', 'full'],
+ defaultsInputs() {
+ this.input('playbackMode', SelectInputView, {
+ values: ['manual', 'autoplay', 'autoplayIfUnmuted', 'loop']
+ });
+ },
+
configurationEditor({entry}) {
migrateLegacyAutoplay(this.model);
diff --git a/entry_types/scrolled/package/src/editor/api/ContentElementTypeRegistry.js b/entry_types/scrolled/package/src/editor/api/ContentElementTypeRegistry.js
index 2e09c3c5e2..cbf6b01515 100644
--- a/entry_types/scrolled/package/src/editor/api/ContentElementTypeRegistry.js
+++ b/entry_types/scrolled/package/src/editor/api/ContentElementTypeRegistry.js
@@ -115,4 +115,46 @@ export class ContentElementTypeRegistry {
!contentElement.featureName || this.features.isEnabled(contentElement.featureName)
);
}
+
+ getDefaultsInputsMapping(typeName) {
+ const type = this.contentElementTypes[typeName];
+
+ if (!type?.defaultsInputs) {
+ return {};
+ }
+
+ const mapping = {};
+
+ type.defaultsInputs.call({
+ input(propertyName) {
+ mapping[`default-${typeName}-${propertyName}`] = propertyName;
+ },
+ view() {}
+ });
+
+ return mapping;
+ }
+
+ createDefaultsInputContext(tabView, typeName) {
+ return {
+ input(propertyName, View, options = {}) {
+ tabView.input(
+ `default-${typeName}-${propertyName}`,
+ View,
+ {
+ ...options,
+ attributeTranslationKeyPrefixes: [
+ `pageflow_scrolled.editor.content_elements.${typeName}.defaults.attributes`,
+ `pageflow_scrolled.editor.content_elements.${typeName}.attributes`,
+ ...(options.attributeTranslationKeyPrefixes || [])
+ ],
+ attributeTranslationPropertyName: propertyName
+ }
+ );
+ },
+ view(View, options) {
+ tabView.view(View, options);
+ }
+ };
+ }
}
diff --git a/entry_types/scrolled/package/src/editor/models/Chapter.js b/entry_types/scrolled/package/src/editor/models/Chapter.js
index b28b32a5c1..07e4dca9f5 100644
--- a/entry_types/scrolled/package/src/editor/models/Chapter.js
+++ b/entry_types/scrolled/package/src/editor/models/Chapter.js
@@ -65,6 +65,7 @@ export const Chapter = Backbone.Model.extend({
const defaultConfiguration = options.skipDefaults ? {} : {
transition: this.entry.metadata.configuration.get('defaultTransition'),
layout: this.entry.metadata.configuration.get('defaultSectionLayout'),
+ appearance: this.entry.metadata.configuration.get('defaultSectionAppearance'),
paddingTop: this.entry.metadata.configuration.get('defaultSectionPaddingTop'),
paddingBottom: this.entry.metadata.configuration.get('defaultSectionPaddingBottom')
};
diff --git a/entry_types/scrolled/package/src/editor/models/ContentElement.js b/entry_types/scrolled/package/src/editor/models/ContentElement.js
index 1c2d11f19b..971dc37e74 100644
--- a/entry_types/scrolled/package/src/editor/models/ContentElement.js
+++ b/entry_types/scrolled/package/src/editor/models/ContentElement.js
@@ -70,8 +70,11 @@ export const ContentElement = Backbone.Model.extend({
];
},
- applyDefaultConfiguration(sibling) {
- const defaultConfig = {...this.getType().defaultConfig};
+ applyDefaultConfiguration({entry, sibling}) {
+ const defaultConfig = {
+ ...this.getType().defaultConfig,
+ ...this.getDefaultsFromEntryMetadata(entry)
+ };
const defaultPosition = sibling?.getPosition();
const supportedPositions = this.getType().supportedPositions || [];
@@ -87,6 +90,32 @@ export const ContentElement = Backbone.Model.extend({
this.configuration.set(defaultConfig);
},
+ getDefaultsFromEntryMetadata(entry) {
+ const defaults = {};
+
+ Object.entries(this.getEntryMetadataDefaultsMapping()).forEach(([metadataKey, propertyName]) => {
+ const value = entry.metadata.configuration.get(metadataKey);
+
+ if (value !== undefined) {
+ defaults[propertyName] = value;
+ }
+ });
+
+ return defaults;
+ },
+
+ getEntryMetadataDefaultsMapping() {
+ const mapping = {
+ ...editor.contentElementTypes.getDefaultsInputsMapping(this.get('typeName'))
+ };
+
+ if (this.supportsFullWidthInPhoneLayout()) {
+ mapping.defaultContentElementFullWidthInPhoneLayout = 'fullWidthInPhoneLayout';
+ }
+
+ return mapping;
+ },
+
getPosition() {
return this.configuration.get('position');
},
diff --git a/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js b/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js
index f650b3ca91..c4c1782780 100644
--- a/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js
+++ b/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js
@@ -102,7 +102,7 @@ export const ScrolledEntry = Entry.extend({
position: this.contentElements.length,
...attributes
});
- contentElement.applyDefaultConfiguration();
+ contentElement.applyDefaultConfiguration({entry: this});
this.sections.get(id).contentElements.add(contentElement);
contentElement.save();
diff --git a/entry_types/scrolled/package/src/editor/models/ScrolledEntry/insertContentElement.js b/entry_types/scrolled/package/src/editor/models/ScrolledEntry/insertContentElement.js
index 5d6e6ef262..627eb4c3ea 100644
--- a/entry_types/scrolled/package/src/editor/models/ScrolledEntry/insertContentElement.js
+++ b/entry_types/scrolled/package/src/editor/models/ScrolledEntry/insertContentElement.js
@@ -12,7 +12,7 @@ export function insertContentElement(entry, sibling, attributes, {at, splitPoint
}
let contentElement = new ContentElement(attributes);
- contentElement.applyDefaultConfiguration(sibling);
+ contentElement.applyDefaultConfiguration({entry, sibling});
if (at === 'before') {
batch.insertBefore(sibling, contentElement);
diff --git a/entry_types/scrolled/package/src/editor/views/ContentElementTypeSeparatorView.js b/entry_types/scrolled/package/src/editor/views/ContentElementTypeSeparatorView.js
new file mode 100644
index 0000000000..bffa59c0d9
--- /dev/null
+++ b/entry_types/scrolled/package/src/editor/views/ContentElementTypeSeparatorView.js
@@ -0,0 +1,25 @@
+import Marionette from 'backbone.marionette';
+
+import styles from './ContentElementTypeSeparatorView.module.css';
+
+export const ContentElementTypeSeparatorView = Marionette.ItemView.extend({
+ className: styles.separator,
+
+ template: (data) => `
+
+ ${data.typeName}
+ ${data.pictogram ? `` : ''}
+
+ `,
+
+ serializeData() {
+ return {
+ pictogram: this.options.pictogram,
+ typeName: this.options.typeName
+ };
+ }
+});
+
+function escapeCssUrl(url) {
+ return url.replace(/'/g, "\\'").replace(/\n/g, '');
+}
diff --git a/entry_types/scrolled/package/src/editor/views/ContentElementTypeSeparatorView.module.css b/entry_types/scrolled/package/src/editor/views/ContentElementTypeSeparatorView.module.css
new file mode 100644
index 0000000000..fc3be301a0
--- /dev/null
+++ b/entry_types/scrolled/package/src/editor/views/ContentElementTypeSeparatorView.module.css
@@ -0,0 +1,24 @@
+.separator {
+ display: flex;
+ align-items: center;
+ gap: space(2);
+ padding: space(4) 0 space(2);
+}
+
+.typeName {
+ font-weight: 500;
+}
+
+.pictogram {
+ width: 16px;
+ height: 16px;
+ background: var(--ui-primary-color-light);
+ mask-size: contain;
+ mask-repeat: no-repeat;
+ mask-position: center;
+}
+
+.rule {
+ flex: 1;
+ border-bottom: solid 1px var(--ui-on-surface-color-lightest);
+}
diff --git a/entry_types/scrolled/package/src/editor/views/EditDefaultsView.js b/entry_types/scrolled/package/src/editor/views/EditDefaultsView.js
index 65b5000f7a..1a4ff82a73 100644
--- a/entry_types/scrolled/package/src/editor/views/EditDefaultsView.js
+++ b/entry_types/scrolled/package/src/editor/views/EditDefaultsView.js
@@ -1,10 +1,13 @@
import I18n from 'i18n-js';
import {EditConfigurationView, InfoBoxView} from 'pageflow/editor';
-import {SelectInputView, SliderInputView} from 'pageflow/ui';
+import {CheckBoxInputView, SelectInputView, SliderInputView} from 'pageflow/ui';
import {features} from 'pageflow/frontend';
+import {editor} from '../api';
+import {ContentElementTypeSeparatorView} from './ContentElementTypeSeparatorView';
import {SectionPaddingVisualizationView} from './inputs/SectionPaddingVisualizationView';
+import defaultPictogram from './images/defaultPictogram.svg';
import paddingTopIcon from './images/paddingTop.svg';
import paddingBottomIcon from './images/paddingBottom.svg';
@@ -25,6 +28,10 @@ export const EditDefaultsView = EditConfigurationView.extend({
values: ['left', 'right', 'center', 'centerRagged']
});
+ this.input('defaultSectionAppearance', SelectInputView, {
+ values: ['shadow', 'cards', 'transparent']
+ });
+
if (features.isEnabled('section_paddings')) {
const paddingTopScale = entry.getScale('sectionPaddingTop');
const paddingBottomScale = entry.getScale('sectionPaddingBottom');
@@ -52,5 +59,34 @@ export const EditDefaultsView = EditConfigurationView.extend({
});
}
});
+
+ configurationEditor.tab('content_elements', function() {
+ this.view(InfoBoxView, {
+ text: I18n.t('pageflow_scrolled.editor.edit_defaults.content_elements_info'),
+ level: 'info'
+ });
+
+ this.view(ContentElementTypeSeparatorView, {
+ typeName: I18n.t('pageflow_scrolled.editor.edit_defaults.all_elements')
+ });
+
+ this.input('defaultContentElementFullWidthInPhoneLayout', CheckBoxInputView);
+
+ const tabView = this;
+ editor.contentElementTypes.toArray().forEach(contentElementType => {
+ if (contentElementType.defaultsInputs) {
+ tabView.view(ContentElementTypeSeparatorView, {
+ pictogram: contentElementType.pictogram || defaultPictogram,
+ typeName: contentElementType.displayName
+ });
+
+ const context = editor.contentElementTypes.createDefaultsInputContext(
+ tabView,
+ contentElementType.typeName
+ );
+ contentElementType.defaultsInputs.call(context);
+ }
+ });
+ });
}
});
diff --git a/package/spec/ui/views/mixins/inputView-spec.js b/package/spec/ui/views/mixins/inputView-spec.js
index 158f981d33..8213f8f1f8 100644
--- a/package/spec/ui/views/mixins/inputView-spec.js
+++ b/package/spec/ui/views/mixins/inputView-spec.js
@@ -47,6 +47,29 @@ describe('pageflow.inputView', () => {
}
);
});
+
+ describe('with attributeTranslationPropertyName', () => {
+ it(
+ 'uses attributeTranslationPropertyName instead of propertyName for translation keys',
+ () => {
+ var view = createInputView({
+ attributeTranslationKeyPrefixes: [
+ 'pageflow.rainbows.page_attributes'
+ ],
+ model: createModel({}, {i18nKey: 'page'}),
+ propertyName: 'prefixed_title',
+ attributeTranslationPropertyName: 'title'
+ });
+
+ var result = view.attributeTranslationKeys('label', {fallbackPrefix: 'activerecord.attributes'});
+
+ expect(result).toEqual([
+ 'pageflow.rainbows.page_attributes.title.label',
+ 'activerecord.attributes.page.title'
+ ]);
+ }
+ );
+ });
});
describe('#labelText', () => {
@@ -119,6 +142,27 @@ describe('pageflow.inputView', () => {
expect(result).toBe('AR Text');
});
});
+
+ describe('with attributeTranslationPropertyName', () => {
+ support.useFakeTranslations({
+ 'pageflow.rainbows.page_attributes.title.label': 'Rainbow Text'
+ });
+
+ it('uses attributeTranslationPropertyName for prefixed translation lookup', () => {
+ var view = createInputView({
+ attributeTranslationKeyPrefixes: [
+ 'pageflow.rainbows.page_attributes'
+ ],
+ model: createModel({}, {i18nKey: 'page'}),
+ propertyName: 'prefixed_title',
+ attributeTranslationPropertyName: 'title'
+ });
+
+ var result = view.labelText();
+
+ expect(result).toBe('Rainbow Text');
+ });
+ });
});
describe('#inlineHelpText', () => {
diff --git a/package/src/ui/views/mixins/inputView.js b/package/src/ui/views/mixins/inputView.js
index e66e6b5da3..0b7837b74d 100644
--- a/package/src/ui/views/mixins/inputView.js
+++ b/package/src/ui/views/mixins/inputView.js
@@ -92,6 +92,11 @@ import {attributeBinding} from './attributeBinding';
* An array of prefixes to lookup translations for labels and
* inline help texts based on attribute names.
*
+ * @param {string} [options.attributeTranslationPropertyName]
+ * Use this property name instead of `propertyName` for looking up
+ * translations. Useful when the property name has been transformed
+ * but translations should use the original name.
+ *
* @param {string} [options.additionalInlineHelpText]
* A text that will be appended to the translation based inline
* text.
@@ -170,7 +175,7 @@ export const inputView = {
*/
attributeTranslationKeys: function(keyName, options) {
return attributeTranslationKeys(
- this.options.propertyName,
+ this.options.attributeTranslationPropertyName || this.options.propertyName,
keyName,
_.extend({
prefixes: this.options.attributeTranslationKeyPrefixes,