diff --git a/app/assets/stylesheets/pageflow/editor/inputs.scss b/app/assets/stylesheets/pageflow/editor/inputs.scss index 466ab0c9f8..9b2e9f7d43 100644 --- a/app/assets/stylesheets/pageflow/editor/inputs.scss +++ b/app/assets/stylesheets/pageflow/editor/inputs.scss @@ -1,3 +1,4 @@ +@import "./inputs/edit_defaults_button"; @import "./inputs/file_input"; @import "./inputs/file_processing_state_display"; @import "./inputs/reference"; diff --git a/app/assets/stylesheets/pageflow/editor/inputs/edit_defaults_button.scss b/app/assets/stylesheets/pageflow/editor/inputs/edit_defaults_button.scss new file mode 100644 index 0000000000..1b04af3d33 --- /dev/null +++ b/app/assets/stylesheets/pageflow/editor/inputs/edit_defaults_button.scss @@ -0,0 +1,15 @@ +.edit_defaults_button { + button { + @include simple-button; + + width: 100%; + text-align: left; + padding-block: space(3); + padding-left: space(3); + margin-bottom: space(2); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M8 6l4 4 -4 4'/%3e%3c/svg%3e"); + background-position: right space(1) center; + background-repeat: no-repeat; + background-size: 1.5rem 1.5rem; + } +} diff --git a/config/locales/de.yml b/config/locales/de.yml index 4b8506c3fe..bae7e9e8b8 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1562,6 +1562,8 @@ de: unset: Verknüpfung entfernen theme_input_view: choose: Theme ändern + edit_defaults_input_view: + label: Voreinstellungen für neue Elemente oembed_url_input_view: status: resolving: "Auflösen..." diff --git a/config/locales/en.yml b/config/locales/en.yml index 0312a6e8fe..9d6a46a1bb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1555,6 +1555,8 @@ en: unset: Reset theme_input_view: choose: Change theme + edit_defaults_input_view: + label: Defaults for new elements oembed_url_input_view: status: resolving: "Resolving..." diff --git a/entry_types/scrolled/config/locales/de.yml b/entry_types/scrolled/config/locales/de.yml index 684547b5c8..10064a6edf 100644 --- a/entry_types/scrolled/config/locales/de.yml +++ b/entry_types/scrolled/config/locales/de.yml @@ -1417,6 +1417,27 @@ de: around: Um den Inhalt above: Über dem Inhalt below: Unter dem Inhalt + edit_defaults: + back: Erscheinungsbild + sections_info: Änderungen an diesen Einstellungen haben keine Auswirkungen auf existierende Abschnitte. + tabs: + sections: Neue Abschnitte + attributes: + defaultSectionLayout: + label: Vordergrund-Positionierung + values: + center: Mitte + centerRagged: Zentriert + left: Links + right: Rechts + topPaddingVisualization: + label: Abstand oben + defaultSectionPaddingTop: + label: Abstand oben + bottomPaddingVisualization: + label: Abstand unten + defaultSectionPaddingBottom: + label: Abstand unten 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 e2538cc478..ff5360e890 100644 --- a/entry_types/scrolled/config/locales/en.yml +++ b/entry_types/scrolled/config/locales/en.yml @@ -1400,6 +1400,27 @@ en: around: Around content above: Above content below: Below content + edit_defaults: + back: Appearance + sections_info: Changes to these settings have no effect on existing sections. + tabs: + sections: New sections + attributes: + defaultSectionLayout: + label: Content alignment + values: + center: Centered + centerRagged: Centered (Ragged) + left: Left + right: Right + topPaddingVisualization: + label: Top padding + defaultSectionPaddingTop: + label: Top padding + bottomPaddingVisualization: + label: Bottom padding + defaultSectionPaddingBottom: + label: Bottom padding typography_sizes: xl: Very large lg: Large 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 297d04b2e5..c1a338e043 100644 --- a/entry_types/scrolled/package/spec/editor/models/Chapter-spec.js +++ b/entry_types/scrolled/package/spec/editor/models/Chapter-spec.js @@ -32,6 +32,33 @@ describe('Chapter', () => { expect(section.configuration.get('transition')).toEqual('beforeAfter'); }); + it('uses default paddingTop from entry metadata configuration', () => { + const {entry} = testContext; + + entry.metadata.configuration.set('defaultSectionPaddingTop', 'lg'); + const section = entry.chapters.first().addSection(); + + expect(section.configuration.get('paddingTop')).toEqual('lg'); + }); + + it('uses default paddingBottom from entry metadata configuration', () => { + const {entry} = testContext; + + entry.metadata.configuration.set('defaultSectionPaddingBottom', 'sm'); + const section = entry.chapters.first().addSection(); + + expect(section.configuration.get('paddingBottom')).toEqual('sm'); + }); + + it('uses default layout from entry metadata configuration', () => { + const {entry} = testContext; + + entry.metadata.configuration.set('defaultSectionLayout', 'right'); + const section = entry.chapters.first().addSection(); + + expect(section.configuration.get('layout')).toEqual('right'); + }); + it('handles sparse positions correctly', () => { const {entry} = testContext; @@ -225,6 +252,16 @@ describe('Chapter', () => { expect(requests.length).toEqual(2); expect(requests[1].url).toBe('/editor/entries/1/scrolled/sections/102'); }); + + it('does not apply default configuration', () => { + const {entry} = testContext; + entry.metadata.configuration.set('defaultSectionLayout', 'right'); + + const chapter = entry.chapters.first(); + const section = chapter.duplicateSection(chapter.sections.first()); + + expect(section.configuration.get('layout')).toBeUndefined(); + }); }); describe('#isExcursion', () => { diff --git a/entry_types/scrolled/package/spec/editor/views/EditDefaultsView-spec.js b/entry_types/scrolled/package/spec/editor/views/EditDefaultsView-spec.js new file mode 100644 index 0000000000..5f56bba2f9 --- /dev/null +++ b/entry_types/scrolled/package/spec/editor/views/EditDefaultsView-spec.js @@ -0,0 +1,97 @@ +import '@testing-library/jest-dom/extend-expect'; + +import {EditDefaultsView} from 'editor/views/EditDefaultsView'; +import {features} from 'pageflow/frontend'; + +import {ConfigurationEditor, useFakeTranslations, renderBackboneView} from 'pageflow/testHelpers'; +import {useEditorGlobals} from 'support'; + +describe('EditDefaultsView', () => { + const {createEntry} = useEditorGlobals(); + + useFakeTranslations({ + 'pageflow_scrolled.editor.edit_defaults.tabs.sections': 'Sections', + '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', + 'pageflow_scrolled.editor.edit_defaults.attributes.defaultSectionLayout.values.center': 'Center', + '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.section_padding_visualization.top_padding': 'TopPadding', + 'pageflow_scrolled.editor.section_padding_visualization.bottom_padding': 'Bottom' + }); + + it('renders with sections tab', () => { + const entry = createEntry({}); + + const view = new EditDefaultsView({ + model: entry.metadata, + entry + }); + + const {getByRole} = renderBackboneView(view); + + expect(getByRole('tab', {name: 'Sections'})).toBeInTheDocument(); + }); + + it('contains defaultSectionLayout select input', () => { + const entry = createEntry({}); + + const view = new EditDefaultsView({ + model: entry.metadata, + entry + }); + + view.render(); + const configurationEditor = ConfigurationEditor.find(view); + + expect(configurationEditor.inputPropertyNames()).toContain('defaultSectionLayout'); + }); + + describe('with section_paddings feature flag', () => { + beforeEach(() => features.enable('frontend', ['section_paddings'])); + + it('contains defaultSectionPaddingTop slider input', () => { + const entry = createEntry({}); + + const view = new EditDefaultsView({ + model: entry.metadata, + entry + }); + + view.render(); + const configurationEditor = ConfigurationEditor.find(view); + + expect(configurationEditor.inputPropertyNames()).toContain('defaultSectionPaddingTop'); + }); + + it('contains defaultSectionPaddingBottom slider input', () => { + const entry = createEntry({}); + + const view = new EditDefaultsView({ + model: entry.metadata, + entry + }); + + view.render(); + const configurationEditor = ConfigurationEditor.find(view); + + expect(configurationEditor.inputPropertyNames()).toContain('defaultSectionPaddingBottom'); + }); + + it('shows padding visualizations', () => { + const entry = createEntry({}); + + const view = new EditDefaultsView({ + model: entry.metadata, + entry + }); + + const {getByRole} = renderBackboneView(view); + + expect(getByRole('img', {name: 'TopPadding'})).toBeInTheDocument(); + expect(getByRole('img', {name: 'Bottom'})).toBeInTheDocument(); + }); + }); +}); diff --git a/entry_types/scrolled/package/src/editor/config.js b/entry_types/scrolled/package/src/editor/config.js index 3b5d863ea8..c113a743c1 100644 --- a/entry_types/scrolled/package/src/editor/config.js +++ b/entry_types/scrolled/package/src/editor/config.js @@ -3,6 +3,7 @@ import {editor} from './api'; import {ScrolledEntry} from './models/ScrolledEntry'; import {ContentElementFileSelectionHandler} from './models/ContentElementFileSelectionHandler'; +import {EditDefaultsView} from './views/EditDefaultsView'; import {EntryOutlineView} from './views/EntryOutlineView'; import {EntryPreviewView} from './views/EntryPreviewView'; @@ -32,6 +33,7 @@ editor.registerEntryType('scrolled', { }); }, outlineView: EntryOutlineView, + editDefaultsView: EditDefaultsView, appearanceInputs(tabView) { tabView.input('darkWidgets', CheckBoxInputView); diff --git a/entry_types/scrolled/package/src/editor/models/Chapter.js b/entry_types/scrolled/package/src/editor/models/Chapter.js index 6c6083a345..ef82fc1751 100644 --- a/entry_types/scrolled/package/src/editor/models/Chapter.js +++ b/entry_types/scrolled/package/src/editor/models/Chapter.js @@ -48,15 +48,20 @@ export const Chapter = Backbone.Model.extend({ return !this.storyline.isMain(); }, - addSection(attributes, options) { + addSection(attributes, options = {}) { + const defaultConfiguration = options.skipDefaults ? {} : { + transition: this.entry.metadata.configuration.get('defaultTransition'), + layout: this.entry.metadata.configuration.get('defaultSectionLayout'), + paddingTop: this.entry.metadata.configuration.get('defaultSectionPaddingTop'), + paddingBottom: this.entry.metadata.configuration.get('defaultSectionPaddingBottom') + }; + const section = this.sections.create( new Section( { position: this.sections.length ? Math.max(...this.sections.pluck('position')) + 1 : 0, chapterId: this.id, - configuration: { - transition: this.entry.metadata.configuration.get('defaultTransition') - }, + configuration: defaultConfiguration, ...attributes }, { @@ -95,6 +100,7 @@ export const Chapter = Backbone.Model.extend({ duplicateSection(section) { const newSection = this.insertSection({after: section}, { url: `${section.url()}/duplicate`, + skipDefaults: true }); return newSection; diff --git a/entry_types/scrolled/package/src/editor/views/EditDefaultsView.js b/entry_types/scrolled/package/src/editor/views/EditDefaultsView.js new file mode 100644 index 0000000000..422636dbc8 --- /dev/null +++ b/entry_types/scrolled/package/src/editor/views/EditDefaultsView.js @@ -0,0 +1,57 @@ +import I18n from 'i18n-js'; +import {EditConfigurationView, InfoBoxView} from 'pageflow/editor'; +import {SelectInputView, SliderInputView} from 'pageflow/ui'; +import {features} from 'pageflow/frontend'; + +import {SectionPaddingVisualizationView} from './inputs/SectionPaddingVisualizationView'; + +import paddingTopIcon from './images/paddingTop.svg'; +import paddingBottomIcon from './images/paddingBottom.svg'; + +export const EditDefaultsView = EditConfigurationView.extend({ + translationKeyPrefix: 'pageflow_scrolled.editor.edit_defaults', + hideDestroyButton: true, + goBackPath: '/meta_data/widgets', + + configure: function(configurationEditor) { + const entry = this.options.entry; + + configurationEditor.tab('sections', function() { + this.view(InfoBoxView, { + text: I18n.t('pageflow_scrolled.editor.edit_defaults.sections_info'), + level: 'info' + }); + + this.input('defaultSectionLayout', SelectInputView, { + values: ['left', 'right', 'center', 'centerRagged'] + }); + + if (features.isEnabled('section_paddings')) { + const paddingTopScale = entry.getScale('sectionPaddingTop'); + const paddingBottomScale = entry.getScale('sectionPaddingBottom'); + + this.input('topPaddingVisualization', SectionPaddingVisualizationView, { + variant: 'topPadding' + }); + this.input('defaultSectionPaddingTop', SliderInputView, { + hideLabel: true, + icon: paddingTopIcon, + values: paddingTopScale.values, + texts: paddingTopScale.texts, + defaultValue: paddingTopScale.defaultValue + }); + + this.input('bottomPaddingVisualization', SectionPaddingVisualizationView, { + variant: 'bottomPadding' + }); + this.input('defaultSectionPaddingBottom', SliderInputView, { + hideLabel: true, + icon: paddingBottomIcon, + values: paddingBottomScale.values, + texts: paddingBottomScale.texts, + defaultValue: paddingBottomScale.defaultValue + }); + } + }); + } +}); diff --git a/package/spec/editor/controllers/SidebarController-spec.js b/package/spec/editor/controllers/SidebarController-spec.js new file mode 100644 index 0000000000..2af7205aab --- /dev/null +++ b/package/spec/editor/controllers/SidebarController-spec.js @@ -0,0 +1,54 @@ +import Marionette from 'backbone.marionette'; + +import {editor, SidebarController} from 'pageflow/editor'; + +import * as support from '$support'; + +describe('SidebarController', () => { + describe('#defaults', () => { + it('renders editDefaultsView registered by entry type', () => { + const EditDefaultsView = Marionette.View.extend(); + editor.registerEntryType('test', { + editDefaultsView: EditDefaultsView + }); + const entry = support.factories.entry(); + const region = fakeRegion(); + const controller = new SidebarController({region, entry}); + + controller.defaults(); + + expect(region.show).toHaveBeenCalledWith(expect.any(EditDefaultsView)); + }); + + it('passes entry metadata as model', () => { + const EditDefaultsView = Marionette.View.extend(); + editor.registerEntryType('test', { + editDefaultsView: EditDefaultsView + }); + const entry = support.factories.entry(); + const region = fakeRegion(); + const controller = new SidebarController({region, entry}); + + controller.defaults(); + + expect(region.show).toHaveBeenCalledWith( + expect.objectContaining({model: entry.metadata}) + ); + }); + + it('does not render if entry type has no editDefaultsView', () => { + editor.registerEntryType('test', {}); + const entry = support.factories.entry(); + const region = fakeRegion(); + const controller = new SidebarController({region, entry}); + + controller.defaults(); + + expect(region.show).not.toHaveBeenCalled(); + }); + }); + + function fakeRegion() { + return {show: jest.fn()}; + } +}); diff --git a/package/spec/editor/views/EditMetaDataView-spec.js b/package/spec/editor/views/EditMetaDataView-spec.js index 77b386e0ee..6e2a7b212a 100644 --- a/package/spec/editor/views/EditMetaDataView-spec.js +++ b/package/spec/editor/views/EditMetaDataView-spec.js @@ -226,4 +226,40 @@ describe('EditMetaDataView', () => { 'structured_data_type_name' ])); }); + + it('renders edit defaults button on widgets tab when entry type has editDefaultsView', () => { + const entry = factories.entry(); + editor.registerEntryType('test', { + editDefaultsView: function() {} + }); + const view = new EditMetaDataView({ + model: entry, + tab: 'widgets', + state: { + config: {} + }, + editor: editor + }); + + view.render(); + + expect(view.$el).toHaveDescendant('.edit_defaults_button'); + }); + + it('does not render edit defaults button when entry type has no editDefaultsView', () => { + const entry = factories.entry(); + editor.registerEntryType('test', {}); + const view = new EditMetaDataView({ + model: entry, + tab: 'widgets', + state: { + config: {} + }, + editor: editor + }); + + view.render(); + + expect(view.$el).not.toHaveDescendant('.edit_defaults_button'); + }); }); diff --git a/package/src/editor/controllers/SidebarController.js b/package/src/editor/controllers/SidebarController.js index bbd01b0b44..f402bd6b7e 100644 --- a/package/src/editor/controllers/SidebarController.js +++ b/package/src/editor/controllers/SidebarController.js @@ -86,4 +86,16 @@ export const SidebarController = Marionette.Controller.extend({ this.entry.trigger('selectWidget', model); }, + + defaults: function() { + const EditDefaultsView = editor.entryType.editDefaultsView; + + if (EditDefaultsView) { + this.region.show(new EditDefaultsView({ + model: this.entry.metadata, + entry: this.entry, + editor + })); + } + }, }); diff --git a/package/src/editor/index.js b/package/src/editor/index.js index e47b2c365a..16868e8834 100644 --- a/package/src/editor/index.js +++ b/package/src/editor/index.js @@ -131,6 +131,7 @@ export * from './views/inputs/ReferenceInputView'; export * from './views/inputs/ThemeInputView'; export * from './views/inputs/FileInputView'; export * from './views/inputs/OembedUrlInputView'; +export * from './views/inputs/EditDefaultsInputView'; export * from './views/EditEntryView'; export * from './views/TextTracksView'; export * from './views/FileThumbnailView'; diff --git a/package/src/editor/models/EntryMetadata.js b/package/src/editor/models/EntryMetadata.js index 200543a9ff..9229c41a08 100644 --- a/package/src/editor/models/EntryMetadata.js +++ b/package/src/editor/models/EntryMetadata.js @@ -1,8 +1,11 @@ import _ from 'underscore'; import {Configuration} from './Configuration'; import {EntryMetadataConfiguration} from './EntryMetadataConfiguration'; +import {failureTracking} from './mixins/failureTracking'; export const EntryMetadata = Configuration.extend({ + mixins: [failureTracking], + modelName: 'entry', i18nKey: 'pageflow/entry', diff --git a/package/src/editor/routers/SidebarRouter.js b/package/src/editor/routers/SidebarRouter.js index 3e5446ae40..93d2664efa 100644 --- a/package/src/editor/routers/SidebarRouter.js +++ b/package/src/editor/routers/SidebarRouter.js @@ -14,6 +14,7 @@ export const SidebarRouter = Marionette.AppRouter.extend({ 'meta_data': 'metaData', 'meta_data/:tab': 'metaData', + 'defaults': 'defaults', 'publish': 'publish', '?storyline=:id': 'index', diff --git a/package/src/editor/views/EditMetaDataView.js b/package/src/editor/views/EditMetaDataView.js index c69fd5f4e6..713af4507a 100644 --- a/package/src/editor/views/EditMetaDataView.js +++ b/package/src/editor/views/EditMetaDataView.js @@ -2,8 +2,9 @@ import I18n from 'i18n-js'; import Marionette from 'backbone.marionette'; import _ from 'underscore'; -import {CheckBoxGroupInputView, ConfigurationEditorView, SelectInputView, TextAreaInputView, TextInputView} from 'pageflow/ui'; +import {CheckBoxGroupInputView, ConfigurationEditorView, SelectInputView, SeparatorView, TextAreaInputView, TextInputView} from 'pageflow/ui'; +import {EditDefaultsInputView} from './inputs/EditDefaultsInputView'; import {EditWidgetsView} from './EditWidgetsView'; import {FileInputView} from './inputs/FileInputView'; import {ThemeInputView} from './inputs/ThemeInputView'; @@ -109,6 +110,14 @@ export const EditMetaDataView = Marionette.Layout.extend({ model: entry.metadata }); } + + if (editor.entryType.editDefaultsView) { + this.view(SeparatorView); + this.view(EditDefaultsInputView, { + entry, + editor + }); + } }); configurationEditor.tab('social', function() { diff --git a/package/src/editor/views/inputs/EditDefaultsInputView.js b/package/src/editor/views/inputs/EditDefaultsInputView.js new file mode 100644 index 0000000000..03bfed061f --- /dev/null +++ b/package/src/editor/views/inputs/EditDefaultsInputView.js @@ -0,0 +1,22 @@ +import I18n from 'i18n-js'; +import Marionette from 'backbone.marionette'; + +import {inputView} from 'pageflow/ui'; + +export const EditDefaultsInputView = Marionette.ItemView.extend({ + mixins: [inputView], + + className: 'edit_defaults_button', + + template: () => ` + + `, + + events: { + 'click button': function() { + this.options.editor.navigate('/defaults', {trigger: true}); + } + } +});