diff --git a/Gemfile b/Gemfile index a60622184a..29a2a595aa 100644 --- a/Gemfile +++ b/Gemfile @@ -34,3 +34,7 @@ gem 'capybara-chromedriver-logger', git: 'https://github.com/codevise/capybara-c # See https://github.com/charkost/prosopite/pull/79 gem 'prosopite', git: 'https://github.com/tf/prosopite', branch: 'location-backtrace-cleaner' + +# See https://github.com/rack/rackup/issues/22 +# Remove once https://github.com/puma/puma/pull/3532 is merged +gem 'rackup', '1.0.0', require: false diff --git a/entry_types/scrolled/config/locales/new/caption_settings.de.yml b/entry_types/scrolled/config/locales/new/caption_settings.de.yml new file mode 100644 index 0000000000..f833467147 --- /dev/null +++ b/entry_types/scrolled/config/locales/new/caption_settings.de.yml @@ -0,0 +1,14 @@ +de: + pageflow_scrolled: + editor: + content_element_text_inline_file_rights_attributes: + showTextInlineFileRightsBackdrop: + label: "Abblendung hinter Rechteangabe" + inline_help: "Lesbarkeit des Texts auf unruhigen Hintergründen sicherstellen." + inline_help_disabled: "Steht nur zur Verfügung, wenn Rechteangaben am Element angezeigt werden." + common_content_element_attributes: + captionVariant: + label: Beschriftungsvariante + inline_help: Ändere die Darstellung der Beschriftung unterhalb des Element. + inline_help_disabled: Füge eine Beschriftung unterhalb des Elements hinzu, um zwischen Varianten zu wählen. + blank: "(Standard)" diff --git a/entry_types/scrolled/config/locales/new/caption_settings.en.yml b/entry_types/scrolled/config/locales/new/caption_settings.en.yml new file mode 100644 index 0000000000..d69604f15d --- /dev/null +++ b/entry_types/scrolled/config/locales/new/caption_settings.en.yml @@ -0,0 +1,14 @@ +en: + pageflow_scrolled: + editor: + content_element_text_inline_file_rights_attributes: + showTextInlineFileRightsBackdrop: + label: "Backdrop behind inline file rights" + inline_help: "Improve readability on busy backgrounds." + inline_help_disabled: "Only available when rights texts are displayed at the element." + common_content_element_attributes: + captionVariant: + label: Caption Variant + inline_help: Switch between different looks of the caption below the element. + inline_help_disabled: Add a caption below the element to switch between different looks. + blank: "(Default)" diff --git a/entry_types/scrolled/package/spec/editor/models/ScrolledEntry-spec.js b/entry_types/scrolled/package/spec/editor/models/ScrolledEntry-spec.js index 699e18f633..2524d041c2 100644 --- a/entry_types/scrolled/package/spec/editor/models/ScrolledEntry-spec.js +++ b/entry_types/scrolled/package/spec/editor/models/ScrolledEntry-spec.js @@ -2915,13 +2915,13 @@ describe('ScrolledEntry', () => { ); const contentElement = entry.contentElements.get(5); - const [values, translationKeys] = entry.getContentElementVariants({contentElement}); + const [values, texts] = entry.getContentElementVariants({contentElement}); expect(values).toEqual([]); - expect(translationKeys).toEqual([]); + expect(texts).toEqual([]); }); - it('selects typography rules based on content element type name', () => { + it('selects property scopes based on content element type name', () => { editor.contentElementTypes.register('someElement', {}); const entry = factories.entry( @@ -3046,6 +3046,129 @@ describe('ScrolledEntry', () => { }); }); + describe('getComponentVariants', () => { + it('returns empty arrays by default', () => { + const entry = factories.entry( + ScrolledEntry, + {}, + { + entryTypeSeed: normalizeSeed() + } + ); + + const [values, texts] = entry.getComponentVariants({name: 'figureCaption'}); + + expect(values).toEqual([]); + expect(texts).toEqual([]); + }); + + it('selects property scopes based on name', () => { + const entry = factories.entry( + ScrolledEntry, + {}, + { + entryTypeSeed: normalizeSeed({ + themeOptions: { + properties: { + 'figureCaption-blue': { + surface_color: 'blue' + }, + 'figureCaption-green': { + surface_color: 'green' + } + } + } + }) + } + ); + + const [values] = entry.getComponentVariants({name: 'figureCaption'}); + + expect(values).toEqual(['blue', 'green']); + }); + + describe('with shared translations', () => { + const commonPrefix = 'pageflow_scrolled.editor.component_variants' + + useFakeTranslations({ + [`${commonPrefix}.figureCaption-blue`]: 'Blue', + [`${commonPrefix}.figureCaption-green`]: 'Green' + }); + + it('returns translated display names', () => { + const entry = factories.entry( + ScrolledEntry, + { + metadata: {theme_name: 'custom'} + }, + { + entryTypeSeed: normalizeSeed({ + themeOptions: { + properties: { + 'figureCaption-blue': { + surface_color: 'blue' + }, + 'figureCaption-green': { + surface_color: 'green' + } + } + } + }) + } + ); + + const [, texts] = entry.getComponentVariants({name: 'figureCaption'}); + + expect(texts).toEqual([ + 'Blue', + 'Green' + ]); + }); + }); + + describe('with theme specific translations', () => { + const commonPrefix = 'pageflow_scrolled.editor.component_variants' + const themePrefix = `pageflow_scrolled.editor.themes.custom` + + useFakeTranslations({ + [`${commonPrefix}.figureCaption-blue`]: 'Blue', + [`${commonPrefix}.figureCaption-green`]: 'Green', + [`${themePrefix}.component_variants.figureCaption-blue`]: 'Custom Blue', + [`${themePrefix}.component_variants.figureCaption-green`]: 'Custom Green' + }); + + it('prefers theme specific translations', () => { + const entry = factories.entry( + ScrolledEntry, + { + metadata: {theme_name: 'custom'} + }, + { + entryTypeSeed: normalizeSeed({ + themeOptions: { + properties: { + 'figureCaption-blue': { + surface_color: 'blue' + }, + 'figureCaption-green': { + surface_color: 'green' + } + } + } + }) + } + ); + + const [, texts] = entry.getComponentVariants({name: 'figureCaption'}); + + expect(texts).toEqual([ + 'Custom Blue', + 'Custom Green' + ]); + }); + }); + }); + describe('supportsSectionWidths', () => { it('returns false by default', () => { const entry = factories.entry( diff --git a/entry_types/scrolled/package/spec/frontend/ContentElementFigure-spec.js b/entry_types/scrolled/package/spec/frontend/ContentElementFigure-spec.js new file mode 100644 index 0000000000..2b2c68c62a --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/ContentElementFigure-spec.js @@ -0,0 +1,104 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect' + +import {renderInContentElement} from 'support'; + +import {ContentElementFigure} from 'frontend/ContentElementFigure'; + +describe('ContentElementFigure', () => { + it('just renders children by default', () => { + const {queryByTestId} = + renderInContentElement( +
, + { + seed: {} + } + ); + + expect(queryByTestId('content')).not.toBeNull(); + }); + + it('applies variant scope', () => { + const configuration = { + caption: [{ + type: 'paragraph', + children: [{ text: 'Some caption text' }], + }], + captionVariant: 'invert' + }; + + const {container} = + renderInContentElement( + , + { + seed: {} + } + ); + + expect(container.querySelector('figcaption')).toHaveClass('scope-figureCaption-invert'); + }); + + it('sets hasCaption to true in transient state on mount', () => { + const configuration = { + caption: [{ + type: 'paragraph', + children: [{ text: 'Some caption text' }], + }], + captionVariant: 'invert' + }; + const setTransientState = jest.fn(); + + renderInContentElement( + , + { + seed: {}, + editorState: {isEditable: true, setTransientState} + } + ); + + expect(setTransientState).toHaveBeenCalledWith({hasCaption: true}) + }); + + it('sets hasCaption to false in transient state on unmount', () => { + const configuration = { + caption: [{ + type: 'paragraph', + children: [{ text: 'Some caption text' }], + }], + captionVariant: 'invert' + }; + const setTransientState = jest.fn(); + + const {unmount} = renderInContentElement( + , + { + seed: {}, + editorState: {isEditable: true, setTransientState} + } + ); + unmount(); + + expect(setTransientState).toHaveBeenCalledWith({hasCaption: false}) + }); + + it('does not render transient state component outside of editor', () => { + const configuration = { + caption: [{ + type: 'paragraph', + children: [{ text: 'Some caption text' }], + }], + captionVariant: 'invert' + }; + const setTransientState = jest.fn(); + + renderInContentElement( + , + { + seed: {}, + editorState: {isEditable: false, setTransientState} + } + ); + + expect(setTransientState).not.toHaveBeenCalled(); + }); +}); diff --git a/entry_types/scrolled/package/spec/frontend/Figure-spec.js b/entry_types/scrolled/package/spec/frontend/Figure-spec.js index e40ac07a52..7ba41a6412 100644 --- a/entry_types/scrolled/package/spec/frontend/Figure-spec.js +++ b/entry_types/scrolled/package/spec/frontend/Figure-spec.js @@ -39,4 +39,17 @@ describe('Figure', () => { expect(queryByRole('figure')).toHaveTextContent('Some caption text'); expect(queryByTestId('content')).not.toBeNull(); }); + + it('applies variant scope', () => { + const value = [{ + type: 'paragraph', + children: [{ text: 'Some caption text' }], + }]; + const {container} = + renderInEntry(
, { + seed: {} + }); + + expect(container.querySelector('figcaption')).toHaveClass('scope-figureCaption-invert'); + }); }); diff --git a/entry_types/scrolled/package/spec/frontend/InlineFileRights-spec.js b/entry_types/scrolled/package/spec/frontend/InlineFileRights-spec.js index 7cd080a04a..fce8b7c85a 100644 --- a/entry_types/scrolled/package/spec/frontend/InlineFileRights-spec.js +++ b/entry_types/scrolled/package/spec/frontend/InlineFileRights-spec.js @@ -45,10 +45,14 @@ describe('InlineFileRights', () => { it('passes props to widget', () => { api.widgetTypes.register('inlineFileRightsWithProps', { - component: function ({children, context, playerControlsFadedOut, playerControlsStandAlone}) { + component: function ({ + children, + context, playerControlsFadedOut, playerControlsStandAlone, + configuration + }) { return (
- {context} {playerControlsFadedOut.toString()} {playerControlsStandAlone.toString()} + {context} {playerControlsFadedOut.toString()} {playerControlsStandAlone.toString()} {configuration.some}
) } @@ -64,6 +68,7 @@ describe('InlineFileRights', () => { const {container} = renderInEntry( , { @@ -73,7 +78,7 @@ describe('InlineFileRights', () => { } ); - expect(container).toHaveTextContent('playerControls false true'); + expect(container).toHaveTextContent('playerControls false true customOption'); }); it('renders items for rights', () => { diff --git a/entry_types/scrolled/package/spec/widgets/textInlineFileRights/TextInlineFileRights-spec.js b/entry_types/scrolled/package/spec/widgets/textInlineFileRights/TextInlineFileRights-spec.js new file mode 100644 index 0000000000..585455c8ea --- /dev/null +++ b/entry_types/scrolled/package/spec/widgets/textInlineFileRights/TextInlineFileRights-spec.js @@ -0,0 +1,66 @@ +import React from 'react'; +import '@testing-library/jest-dom/extend-expect' + +import {renderInContentElement} from 'support'; + +import {TextInlineFileRights} from 'widgets/textInlineFileRights/TextInlineFileRights'; + +describe('TextInlineFileRights', () => { + it('renders children', () => { + const {queryByTestId} = + renderInContentElement( + +
+ , + { + seed: {} + } + ); + + expect(queryByTestId('content')).not.toBeNull(); + }); + + it('sets hasFileRights to true in transient state on mount', () => { + const setTransientState = jest.fn(); + + renderInContentElement( + , + { + seed: {}, + editorState: {isEditable: true, setTransientState} + } + ); + + expect(setTransientState).toHaveBeenCalledWith({hasFileRights: true}) + }); + + it('sets hasFileRights to false in transient state on unmount', () => { + const setTransientState = jest.fn(); + + const {unmount} = renderInContentElement( + , + { + seed: {}, + editorState: {isEditable: true, setTransientState} + } + ); + unmount(); + + expect(setTransientState).toHaveBeenCalledWith({hasFileRights: false}) + }); + + it('does not set transient state if context unsupported', () => { + const setTransientState = jest.fn(); + + const {unmount} = renderInContentElement( + , + { + seed: {}, + editorState: {isEditable: true, setTransientState} + } + ); + unmount(); + + expect(setTransientState).not.toHaveBeenCalled() + }); +}); diff --git a/entry_types/scrolled/package/src/contentElements/dataWrapperChart/editor.js b/entry_types/scrolled/package/src/contentElements/dataWrapperChart/editor.js index 7dd9b21e92..fbe435a9a9 100644 --- a/entry_types/scrolled/package/src/contentElements/dataWrapperChart/editor.js +++ b/entry_types/scrolled/package/src/contentElements/dataWrapperChart/editor.js @@ -1,6 +1,6 @@ import I18n from 'i18n-js'; import {editor} from 'pageflow-scrolled/editor'; -import {UrlInputView, TextInputView, ColorInputView} from 'pageflow/ui'; +import {SeparatorView, UrlInputView, TextInputView, ColorInputView} from 'pageflow/ui'; import {DatawrapperAdView} from './editor/DataWrapperAdView'; import pictogram from './pictogram.svg'; @@ -11,7 +11,7 @@ editor.contentElementTypes.register('dataWrapperChart', { supportedPositions: ['inline', 'sticky', 'standAlone', 'left', 'right'], supportedWidthRange: ['xxs', 'full'], - configurationEditor() { + configurationEditor({entry}) { this.tab('general', function() { this.input('url', UrlInputView, { supportedHosts: [ @@ -33,6 +33,8 @@ editor.contentElementTypes.register('dataWrapperChart', { }); this.group('ContentElementPosition'); + this.view(SeparatorView); + this.group('ContentElementCaption', {entry}); }); } }); diff --git a/entry_types/scrolled/package/src/contentElements/hotspots/Hotspots.js b/entry_types/scrolled/package/src/contentElements/hotspots/Hotspots.js index 8acab44095..1fc3a6c75e 100644 --- a/entry_types/scrolled/package/src/contentElements/hotspots/Hotspots.js +++ b/entry_types/scrolled/package/src/contentElements/hotspots/Hotspots.js @@ -293,7 +293,9 @@ export function HotspotsImage({ {renderIndicators()}
{renderFullscreenToggle()} - + )} } /> @@ -301,7 +303,9 @@ export function HotspotsImage({ } />
- + diff --git a/entry_types/scrolled/package/src/contentElements/hotspots/editor/index.js b/entry_types/scrolled/package/src/contentElements/hotspots/editor/index.js index 0103cce095..3d4b15e6e9 100644 --- a/entry_types/scrolled/package/src/contentElements/hotspots/editor/index.js +++ b/entry_types/scrolled/package/src/contentElements/hotspots/editor/index.js @@ -59,6 +59,9 @@ editor.contentElementTypes.register('hotspots', { displayUncheckedIfDisabled: true }); this.group('ContentElementPosition'); + this.view(SeparatorView); + this.group('ContentElementCaption', {entry}); + this.group('ContentElementInlineFileRightsSettings'); }); } }); diff --git a/entry_types/scrolled/package/src/contentElements/iframeEmbed/editor.js b/entry_types/scrolled/package/src/contentElements/iframeEmbed/editor.js index 6679e3a28c..03ab6ce83b 100644 --- a/entry_types/scrolled/package/src/contentElements/iframeEmbed/editor.js +++ b/entry_types/scrolled/package/src/contentElements/iframeEmbed/editor.js @@ -1,7 +1,7 @@ import I18n from 'i18n-js'; import {editor} from 'pageflow-scrolled/editor'; import {InfoBoxView} from 'pageflow/editor'; -import {TextInputView, SelectInputView, CheckBoxInputView} from 'pageflow/ui'; +import {TextInputView, SelectInputView, CheckBoxInputView, SeparatorView} from 'pageflow/ui'; import pictogram from './pictogram.svg'; @@ -42,6 +42,8 @@ editor.contentElementTypes.register('iframeEmbed', { values: ['p100', 'p75', 'p50', 'p33'] }); this.group('ContentElementPosition'); + this.view(SeparatorView); + this.group('ContentElementCaption', {entry}); }); } }); diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js b/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js index fb6f7dbb4a..d894ca53ec 100644 --- a/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js @@ -158,7 +158,7 @@ function Scroller({ ref={setChildRef(index)} item={item} current={index === visibleIndex} - captions={configuration.captions || {}} + configuration={configuration} onClick={handleClick}> {displayFullscreenToggle &&
@@ -210,11 +212,15 @@ const Item = forwardRef(function({item, captions, current, onClick, children}, r {children} - +
- + diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/editor/index.js b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/index.js index 8b8a3f064f..8776c6cc1c 100644 --- a/entry_types/scrolled/package/src/contentElements/imageGallery/editor/index.js +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/editor/index.js @@ -1,6 +1,6 @@ import {editor} from 'pageflow-scrolled/editor'; import {contentElementWidths} from 'pageflow-scrolled/frontend'; -import {CheckBoxInputView} from 'pageflow/editor'; +import {CheckBoxInputView, SeparatorView} from 'pageflow/editor'; import {ItemsListView} from './ItemsListView'; import {ItemsCollection} from './models/ItemsCollection'; @@ -24,6 +24,9 @@ editor.contentElementTypes.register('imageGallery', { displayUncheckedIfDisabled: true }); this.group('ContentElementPosition'); + this.view(SeparatorView); + this.group('ContentElementCaption', {entry, disableWhenNoCaption: false}); + this.group('ContentElementInlineFileRightsSettings', {entry, disableWhenNoFileRights: false}); }); } }); diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/stories.js b/entry_types/scrolled/package/src/contentElements/imageGallery/stories.js index 7a4943cc99..ab0259fdc3 100644 --- a/entry_types/scrolled/package/src/contentElements/imageGallery/stories.js +++ b/entry_types/scrolled/package/src/contentElements/imageGallery/stories.js @@ -7,39 +7,15 @@ storiesOfContentElement(module, { items: [ { id: 1, - image: filePermaId('imageFiles', 'turtle'), - caption: [ - { - type: 'paragraph', - children: [ - {text: 'At vero eos et accusam et justo duo dolores et ea rebum.'} - ] - } - ] + image: filePermaId('imageFiles', 'turtle') }, { id: 2, - image: filePermaId('imageFiles', 'churchBefore'), - caption: [ - { - type: 'paragraph', - children: [ - {text: 'At vero eos et accusam et justo duo dolores et ea rebum.'} - ] - } - ] + image: filePermaId('imageFiles', 'churchBefore') }, { id: 3, - image: filePermaId('imageFiles', 'churchAfter'), - caption: [ - { - type: 'paragraph', - children: [ - {text: 'At vero eos et accusam et justo duo dolores et ea rebum.'} - ] - } - ] + image: filePermaId('imageFiles', 'churchAfter') }, { id: 4, @@ -66,6 +42,47 @@ storiesOfContentElement(module, { } } } + }, + { + name: 'With Captions', + configuration: { + captions: { + 1: [ + { + type: 'paragraph', + children: [ + {text: 'At vero eos et accusam et justo duo dolores et ea rebum.'} + ] + } + ] + } + } + }, + { + name: 'With Caption Variant', + configuration: { + captions: { + 1: [ + { + type: 'paragraph', + children: [ + {text: 'At vero eos et accusam et justo duo dolores et ea rebum.'} + ] + } + ] + }, + captionVariant: 'inverted' + }, + themeOptions: { + properties: { + 'figureCaption-inverted': { + darkContentSurfaceColor: 'var(--root-light-content-surface-color)', + lightContentSurfaceColor: 'var(--root-dark-content-surface-color)', + darkContentTextColor: 'var(--root-light-content-text-color)', + lightContentTextColor: 'var(--root-dark-content-text-color)' + } + } + } } ] }); diff --git a/entry_types/scrolled/package/src/contentElements/inlineAudio/InlineAudio.js b/entry_types/scrolled/package/src/contentElements/inlineAudio/InlineAudio.js index 7ce4b14f95..af81698642 100644 --- a/entry_types/scrolled/package/src/contentElements/inlineAudio/InlineAudio.js +++ b/entry_types/scrolled/package/src/contentElements/inlineAudio/InlineAudio.js @@ -103,7 +103,9 @@ export function InlineAudio({contentElementId, configuration}) {
- + ) } diff --git a/entry_types/scrolled/package/src/contentElements/inlineAudio/editor.js b/entry_types/scrolled/package/src/contentElements/inlineAudio/editor.js index 33edfd026a..83d4266f3b 100644 --- a/entry_types/scrolled/package/src/contentElements/inlineAudio/editor.js +++ b/entry_types/scrolled/package/src/contentElements/inlineAudio/editor.js @@ -53,6 +53,11 @@ editor.contentElementTypes.register('inlineAudio', { this.view(SeparatorView); this.group('ContentElementPosition'); + + this.view(SeparatorView); + + this.group('ContentElementCaption', {entry}); + this.group('ContentElementInlineFileRightsSettings'); }); } }); diff --git a/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/BeforeAfter.js b/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/BeforeAfter.js index 22f14c3596..06013ee001 100644 --- a/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/BeforeAfter.js +++ b/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/BeforeAfter.js @@ -86,11 +86,15 @@ export function BeforeAfter(configuration) { ); }} - + - + ); diff --git a/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/editor.js b/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/editor.js index 322e95dffd..e6b8a84e67 100644 --- a/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/editor.js +++ b/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/editor.js @@ -1,6 +1,6 @@ import {editor, InlineFileRightsMenuItem} from 'pageflow-scrolled/editor'; import {ColorInputView, FileInputView} from 'pageflow/editor'; -import {SliderInputView, TextInputView} from 'pageflow/ui'; +import {SliderInputView, TextInputView, SeparatorView} from 'pageflow/ui'; import pictogram from './pictogram.svg'; @@ -10,7 +10,7 @@ editor.contentElementTypes.register('inlineBeforeAfter', { supportedPositions: ['inline', 'sticky', 'standAlone', 'left', 'right', 'backdrop'], supportedWidthRange: ['xxs', 'full'], - configurationEditor() { + configurationEditor({entry}) { this.tab('general', function() { this.input('before_id', FileInputView, { collection: 'image_files', @@ -30,6 +30,11 @@ editor.contentElementTypes.register('inlineBeforeAfter', { this.input('slider_color', ColorInputView); this.group('ContentElementPosition'); + + this.view(SeparatorView); + + this.group('ContentElementCaption', {entry}); + this.group('ContentElementInlineFileRightsSettings'); }); }, diff --git a/entry_types/scrolled/package/src/contentElements/inlineImage/InlineImage.js b/entry_types/scrolled/package/src/contentElements/inlineImage/InlineImage.js index fd6a765864..d549371c7b 100644 --- a/entry_types/scrolled/package/src/contentElements/inlineImage/InlineImage.js +++ b/entry_types/scrolled/package/src/contentElements/inlineImage/InlineImage.js @@ -82,11 +82,15 @@ function ImageWithCaption({imageFile, contentElementId, contentElementWidth, con 'large' : 'medium'} preferSvg={true} /> - + - + ); } diff --git a/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js b/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js index 70bdf3f990..09b9069dcf 100644 --- a/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js +++ b/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js @@ -1,6 +1,7 @@ import {editor, InlineFileRightsMenuItem} from 'pageflow-scrolled/editor'; import {contentElementWidths} from 'pageflow-scrolled/frontend'; -import {FileInputView, CheckBoxInputView} from 'pageflow/editor'; +import {FileInputView} from 'pageflow/editor'; +import {SeparatorView, CheckBoxInputView} from 'pageflow/ui'; import pictogram from './pictogram.svg'; @@ -10,7 +11,7 @@ editor.contentElementTypes.register('inlineImage', { supportedPositions: ['inline', 'sticky', 'standAlone', 'left', 'right'], supportedWidthRange: ['xxs', 'full'], - configurationEditor({contentElement}) { + configurationEditor({entry, contentElement}) { this.tab('general', function() { this.input('id', FileInputView, { collection: 'image_files', @@ -29,6 +30,11 @@ editor.contentElementTypes.register('inlineImage', { displayUncheckedIfDisabled: true }); this.group('ContentElementPosition'); + + this.view(SeparatorView); + + this.group('ContentElementCaption', {entry}); + this.group('ContentElementInlineFileRightsSettings'); }); } }); diff --git a/entry_types/scrolled/package/src/contentElements/inlineImage/stories.js b/entry_types/scrolled/package/src/contentElements/inlineImage/stories.js index 369f127f96..3419748e4a 100644 --- a/entry_types/scrolled/package/src/contentElements/inlineImage/stories.js +++ b/entry_types/scrolled/package/src/contentElements/inlineImage/stories.js @@ -10,6 +10,23 @@ storiesOfContentElement(module, { { name: 'With Caption', configuration: {caption: 'Some text here'} + }, + { + name: 'With Caption Variant', + configuration: { + caption: 'Some text here', + captionVariant: 'inverted' + }, + themeOptions: { + properties: { + 'figureCaption-inverted': { + darkContentSurfaceColor: 'var(--root-light-content-surface-color)', + lightContentSurfaceColor: 'var(--root-dark-content-surface-color)', + darkContentTextColor: 'var(--root-light-content-text-color)', + lightContentTextColor: 'var(--root-dark-content-text-color)' + } + } + } } ], inlineFileRights: true diff --git a/entry_types/scrolled/package/src/contentElements/inlineVideo/InlineVideo.js b/entry_types/scrolled/package/src/contentElements/inlineVideo/InlineVideo.js index 92d0f7ef5d..2a956218b9 100644 --- a/entry_types/scrolled/package/src/contentElements/inlineVideo/InlineVideo.js +++ b/entry_types/scrolled/package/src/contentElements/inlineVideo/InlineVideo.js @@ -146,7 +146,9 @@ function OrientationUnawareInlineVideo({ - + ) diff --git a/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js b/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js index 34de4495cb..d778f9dc54 100644 --- a/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js +++ b/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js @@ -38,7 +38,7 @@ editor.contentElementTypes.register('inlineVideo', { supportedPositions: ['inline', 'sticky', 'standAlone', 'left', 'right', 'backdrop'], supportedWidthRange: ['xxs', 'full'], - configurationEditor() { + configurationEditor({entry}) { migrateLegacyAutoplay(this.model); this.tab('general', function() { @@ -104,6 +104,11 @@ editor.contentElementTypes.register('inlineVideo', { this.view(SeparatorView); this.group('ContentElementPosition'); + + this.view(SeparatorView); + + this.group('ContentElementCaption', {entry}); + this.group('ContentElementInlineFileRightsSettings'); }); } }); diff --git a/entry_types/scrolled/package/src/contentElements/videoEmbed/editor.js b/entry_types/scrolled/package/src/contentElements/videoEmbed/editor.js index 4ccfeff597..4cc8bf3c48 100644 --- a/entry_types/scrolled/package/src/contentElements/videoEmbed/editor.js +++ b/entry_types/scrolled/package/src/contentElements/videoEmbed/editor.js @@ -10,7 +10,7 @@ editor.contentElementTypes.register('videoEmbed', { supportedPositions: ['inline', 'sticky', 'standAlone', 'left', 'right'], supportedWidthRange: ['xxs', 'full'], - configurationEditor() { + configurationEditor({entry}) { this.tab('general', function() { this.input('videoSource', UrlInputView, { supportedHosts: [ @@ -47,6 +47,10 @@ editor.contentElementTypes.register('videoEmbed', { this.view(SeparatorView); this.group('ContentElementPosition'); + + this.view(SeparatorView); + + this.group('ContentElementCaption', {entry}); }); } }); diff --git a/entry_types/scrolled/package/src/contentElements/vrImage/VrImage.js b/entry_types/scrolled/package/src/contentElements/vrImage/VrImage.js index e900db6896..a76bc93dbb 100644 --- a/entry_types/scrolled/package/src/contentElements/vrImage/VrImage.js +++ b/entry_types/scrolled/package/src/contentElements/vrImage/VrImage.js @@ -32,11 +32,15 @@ export function VrImage({configuration, contentElementWidth}) { {renderLazyPanorama(configuration, imageFile, shouldLoad)} - + - + ); diff --git a/entry_types/scrolled/package/src/contentElements/vrImage/editor.js b/entry_types/scrolled/package/src/contentElements/vrImage/editor.js index 9b24ca962c..f3b0378d09 100644 --- a/entry_types/scrolled/package/src/contentElements/vrImage/editor.js +++ b/entry_types/scrolled/package/src/contentElements/vrImage/editor.js @@ -1,5 +1,5 @@ import {editor, InlineFileRightsMenuItem} from 'pageflow-scrolled/editor'; -import {SelectInputView, FileInputView, EnumTableCellView, SliderInputView} from 'pageflow/editor'; +import {SelectInputView, FileInputView, EnumTableCellView, SliderInputView, SeparatorView} from 'pageflow/editor'; import pictogram from './pictogram.svg'; @@ -10,7 +10,7 @@ editor.contentElementTypes.register('vrImage', { supportedWidthRange: ['xxs', 'full'], configurationEditor() { - this.tab('general', function() { + this.tab('general', function({entry}) { this.input('image', FileInputView, { collection: 'image_files', fileSelectionHandler: 'contentElementConfiguration', @@ -29,6 +29,9 @@ editor.contentElementTypes.register('vrImage', { maxValue: 60 }); this.group('ContentElementPosition'); + this.view(SeparatorView); + this.group('ContentElementCaption', {entry}); + this.group('ContentElementInlineFileRightsSettings'); }); } }); diff --git a/entry_types/scrolled/package/src/editor/config.js b/entry_types/scrolled/package/src/editor/config.js index e17dd144e7..45acf398b0 100644 --- a/entry_types/scrolled/package/src/editor/config.js +++ b/entry_types/scrolled/package/src/editor/config.js @@ -75,3 +75,19 @@ editor.widgetTypes.register('defaultNavigation', { } }) }); + +editor.widgetTypes.register('textInlineFileRights', { + configurationEditorTabViewGroups: { + ContentElementInlineFileRightsSettings: function({disableWhenNoFileRights = true}) { + this.input('showTextInlineFileRightsBackdrop', CheckBoxInputView, { + disabledBindingModel: this.model.parent.transientState, + disabledBinding: 'hasFileRights', + disabled: hasFileRights => disableWhenNoFileRights && !hasFileRights, + displayUncheckedIfDisabled: true, + attributeTranslationKeyPrefixes: [ + 'pageflow_scrolled.editor.content_element_text_inline_file_rights_attributes' + ], + }); + } + } +}); 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 8d34704670..958c15326b 100644 --- a/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js +++ b/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js @@ -177,23 +177,30 @@ export const ScrolledEntry = Entry.extend({ }, getContentElementVariants({contentElement}) { + return this.getComponentVariants({ + name: contentElement.get('typeName'), + translationKeysScope: 'content_element_variants' + }); + }, + + getComponentVariants({name, translationKeysScope = 'component_variants'}) { const scopeNames = Object.keys( this.scrolledSeed.config.theme.options.properties || {} ); - const scopeNamePrefix = `${contentElement.get('typeName')}-`; - + const scopeNamePrefix = `${name}-`; const matchingScopeNames = scopeNames.filter( name => name.indexOf(scopeNamePrefix) === 0 ); + const values = matchingScopeNames.map( name => name.replace(scopeNamePrefix, '') ); const texts = matchingScopeNames.map(name => I18n.t( `pageflow_scrolled.editor.themes.${this.metadata.get('theme_name')}` + - `.content_element_variants.${name}`, - {defaultValue: I18n.t(`pageflow_scrolled.editor.content_element_variants.${name}`)} + `.${translationKeysScope}.${name}`, + {defaultValue: I18n.t(`pageflow_scrolled.editor.${translationKeysScope}.${name}`)} ) ); diff --git a/entry_types/scrolled/package/src/editor/views/configurationEditors/groups/CommonContentElementAttributes.js b/entry_types/scrolled/package/src/editor/views/configurationEditors/groups/CommonContentElementAttributes.js index fa522f038f..3beaa14d22 100644 --- a/entry_types/scrolled/package/src/editor/views/configurationEditors/groups/CommonContentElementAttributes.js +++ b/entry_types/scrolled/package/src/editor/views/configurationEditors/groups/CommonContentElementAttributes.js @@ -117,3 +117,27 @@ ConfigurationEditorTabView.groups.define( } } ); + +ConfigurationEditorTabView.groups.define( + 'ContentElementCaption', + function({entry, disableWhenNoCaption = true}) { + const [variants, texts] = entry.getComponentVariants({ + name: 'figureCaption' + }); + + this.input('captionVariant', SelectInputView, { + attributeTranslationKeyPrefixes: [ + 'pageflow_scrolled.editor.common_content_element_attributes' + ], + includeBlank: true, + blankTranslationKey: 'pageflow_scrolled.editor.' + + 'common_content_element_attributes.' + + 'captionVariant.blank', + values: variants, + texts, + disabledBindingModel: this.model.parent.transientState, + disabledBinding: 'hasCaption', + disabled: hasCaption => disableWhenNoCaption && !hasCaption + }); + } +); diff --git a/entry_types/scrolled/package/src/frontend/ContentElementFigure.js b/entry_types/scrolled/package/src/frontend/ContentElementFigure.js index 6b96213b47..d3b4a7fbc3 100644 --- a/entry_types/scrolled/package/src/frontend/ContentElementFigure.js +++ b/entry_types/scrolled/package/src/frontend/ContentElementFigure.js @@ -1,8 +1,9 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import {Figure} from './Figure'; import {useContentElementConfigurationUpdate} from './useContentElementConfigurationUpdate'; import {useContentElementAttributes} from './useContentElementAttributes'; +import {useContentElementEditorState} from './useContentElementEditorState'; import {widths} from './layouts'; /** @@ -13,6 +14,7 @@ import {widths} from './layouts'; export function ContentElementFigure({configuration, children}) { const updateConfiguration = useContentElementConfigurationUpdate(); const {width, position} = useContentElementAttributes(); + const {isEditable} = useContentElementEditorState(); if (position === 'backdrop') { return children; @@ -20,9 +22,22 @@ export function ContentElementFigure({configuration, children}) { return (
isEditable && } onCaptionChange={caption => updateConfiguration({caption})} addCaptionButtonPosition={width === widths.full ? 'outsideIndented' : 'outside'}> {children}
); } + +function HasCaptionTransientState() { + const {setTransientState} = useContentElementEditorState(); + + useEffect(() => { + setTransientState({hasCaption: true}); + return () => setTransientState({hasCaption: false}); + }, [setTransientState]); + + return null; +} diff --git a/entry_types/scrolled/package/src/frontend/Figure.js b/entry_types/scrolled/package/src/frontend/Figure.js index 5f66132f92..aaa30bebfc 100644 --- a/entry_types/scrolled/package/src/frontend/Figure.js +++ b/entry_types/scrolled/package/src/frontend/Figure.js @@ -16,14 +16,17 @@ import styles from './Figure.module.css'; * @param {Object} props * @param {string} props.children - Content of figure. * @param {Object[]|string} props.caption - Formatted text data as provided by onCaptionChange. + * @param {string} [props.variant] - Name of figureCaption property scope to apply. * @param {Function} props.onCaptionChange - Receives updated value when it changes. * @param {boolean} [props.addCaptionButtonVisible=true] - Control visiblility of action button. * @param {string} [props.captionButtonPosition='outside'] - Position of action button. */ export function Figure({ children, + variant, caption, onCaptionChange, - addCaptionButtonVisible = true, addCaptionButtonPosition = 'outside' + addCaptionButtonVisible = true, addCaptionButtonPosition = 'outside', + renderInsideCaption }) { const darkBackground = useDarkBackground(); const {isSelected, isEditable} = useContentElementEditorState(); @@ -49,7 +52,9 @@ export function Figure({ onClick={() => setIsEditingCaption(true)} />} {(!isBlankEditableTextValue(caption) || isEditingCaption) && -
setIsEditingCaption(false)}> +
setIsEditingCaption(false)}> + {renderInsideCaption?.()} figcaption { - padding: 3px 10px 5px; - background-color: lightContentSurfaceColor; - color: darkContentTextColor; + padding: 3px var(--theme-figure-caption-padding-inline, 10px) 5px; + background-color: var(--theme-figure-caption-surface-color, lightContentSurfaceColor); + color: var(--theme-figure-caption-text-color, darkContentTextColor); } .root > figcaption p { @@ -20,6 +20,6 @@ } .invert > figcaption { - background-color: darkContentSurfaceColor; - color: lightContentTextColor; + background-color: var(--theme-figure-caption-surface-color, darkContentSurfaceColor); + color: var(--theme-figure-caption-text-color, lightContentTextColor); } diff --git a/entry_types/scrolled/package/src/frontend/InlineFileRights.js b/entry_types/scrolled/package/src/frontend/InlineFileRights.js index fa50bf5301..33f26aa367 100644 --- a/entry_types/scrolled/package/src/frontend/InlineFileRights.js +++ b/entry_types/scrolled/package/src/frontend/InlineFileRights.js @@ -9,7 +9,8 @@ import styles from './InlineFileRights.module.css'; export function InlineFileRights({items = [], context = 'standAlone', playerControlsFadedOut, - playerControlsStandAlone}) { + playerControlsStandAlone, + configuration = {}}) { const {t} = useI18n(); const filteredItems = items.filter(item => item.file && item.file.inlineRights && !isBlank(item.file.rights) @@ -21,7 +22,7 @@ export function InlineFileRights({items = [], return ( + props={{context, playerControlsFadedOut, playerControlsStandAlone, configuration}}>
    {filteredItems.map(({label, file}) =>
  • diff --git a/entry_types/scrolled/package/src/widgets/textInlineFileRights/TextInlineFileRights.js b/entry_types/scrolled/package/src/widgets/textInlineFileRights/TextInlineFileRights.js index 6809bc45ba..138c29aa06 100644 --- a/entry_types/scrolled/package/src/widgets/textInlineFileRights/TextInlineFileRights.js +++ b/entry_types/scrolled/package/src/widgets/textInlineFileRights/TextInlineFileRights.js @@ -1,19 +1,35 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import classNames from 'classnames'; -import {useDarkBackground} from 'pageflow-scrolled/frontend'; +import {useDarkBackground, useContentElementEditorState} from 'pageflow-scrolled/frontend'; import styles from './TextInlineFileRights.module.css'; -export function TextInlineFileRights({context, children}) { +export function TextInlineFileRights({configuration, context, children}) { const darkBackground = useDarkBackground(); + const {setTransientState} = useContentElementEditorState(); + const supported = context !== 'insideElement' && context !== 'playerControls'; - if (context === 'insideElement' || context === 'playerControls') { + useEffect(() => { + if (supported) { + setTransientState({hasFileRights: true}); + } + + return () => { + if (supported) { + setTransientState({hasFileRights: false}); + } + }; + }, [setTransientState, supported]); + + if (!supported) { return null; } return (
    {children} diff --git a/entry_types/scrolled/package/src/widgets/textInlineFileRights/TextInlineFileRights.module.css b/entry_types/scrolled/package/src/widgets/textInlineFileRights/TextInlineFileRights.module.css index 180203b34e..029d54132e 100644 --- a/entry_types/scrolled/package/src/widgets/textInlineFileRights/TextInlineFileRights.module.css +++ b/entry_types/scrolled/package/src/widgets/textInlineFileRights/TextInlineFileRights.module.css @@ -25,26 +25,30 @@ display: none; } -.forSection { - position: absolute; - bottom: 0; - right: 0; -} - -.forSection li { +.withBackdrop li { padding: 0.1em 0.3em; border-radius: 0.25rem; - margin: 0 0.2em 0.1em auto; + margin: 0.2em 0 0 auto; background-color: color-mix(in srgb, lightContentSurfaceColor, transparent); color: darkContentTextColor; width: fit-content; } -.forSection.darkBackground li { +.withBackdrop.darkBackground li { background-color: color-mix(in srgb, darkContentSurfaceColor, transparent); color: lightContentTextColor; } +.forSection { + position: absolute; + bottom: 0; + right: 0; +} + +.forSection li { + margin: 0 0.2em 0.1em auto; +} + .text a { color: inherit; text-decoration-color: color-mix(in srgb, currentColor, transparent); diff --git a/package/spec/editor/collections/widgetsCollection-spec.js b/package/spec/editor/collections/widgetsCollection-spec.js index 43705724c4..416f316e8e 100644 --- a/package/spec/editor/collections/widgetsCollection-spec.js +++ b/package/spec/editor/collections/widgetsCollection-spec.js @@ -1,4 +1,5 @@ import {WidgetsCollection} from 'editor/collections/WidgetsCollection'; +import {CheckBoxInputView, ConfigurationEditorTabView} from 'pageflow/ui'; import {factories} from '$support'; describe('WidgetsCollection', () => { @@ -47,4 +48,129 @@ describe('WidgetsCollection', () => { expect(subsetCollection.pluck('type_name')).toEqual(['consent_bar']); }); + + it('can define configuration editor tab view groups of widget types', () => { + const widgetTypes = factories.widgetTypes( + [ + { + role: 'inlineFileRights', + name: 'textInlineFileRights', + insertPoint: 'react' + } + ], + w => { + w.register('textInlineFileRights', { + configurationEditorTabViewGroups: { + ContentElementInlineFileRightsSettings() { + this.input('showBackdrop', CheckBoxInputView); + } + } + }); + } + ); + const widgets = new WidgetsCollection([ + {type_name: 'textInlineFileRights', role: 'inlineFileRights'}, + ], {widgetTypes}); + const groups = new ConfigurationEditorTabView.Groups(); + widgets.subject = factories.entry(); + + widgets.setupConfigurationEditorTabViewGroups(groups); + const context = { + input: jest.fn() + }; + groups.apply( + 'ContentElementInlineFileRightsSettings', + context + ); + + expect(context.input).toHaveBeenCalledWith('showBackdrop', CheckBoxInputView); + }); + + it('defines stub configuration editor tab view groups for unused widget types', () => { + const widgetTypes = factories.widgetTypes( + [ + { + role: 'inlineFileRights', + name: 'iconInlineFileRights', + insertPoint: 'react' + }, + { + role: 'inlineFileRights', + name: 'textInlineFileRights', + insertPoint: 'react' + } + ], + w => { + w.register('textInlineFileRights', { + configurationEditorTabViewGroups: { + ContentElementInlineFileRightsSettings() { + this.input('showBackdrop', CheckBoxInputView); + } + } + }); + } + ); + const widgets = new WidgetsCollection([ + {type_name: 'iconInlineFileRights', role: 'inlineFileRights'}, + ], {widgetTypes}); + const groups = new ConfigurationEditorTabView.Groups(); + widgets.subject = factories.entry(); + + widgets.setupConfigurationEditorTabViewGroups(groups); + const context = { + input: jest.fn() + }; + + expect(() => { + groups.apply( + 'ContentElementInlineFileRightsSettings', + context + ); + }).not.toThrowError(); + + expect(context.input).not.toHaveBeenCalled(); + }); + + it('redefines configuration editor tab view groups on type change', () => { + const widgetTypes = factories.widgetTypes( + [ + { + role: 'inlineFileRights', + name: 'iconInlineFileRights', + insertPoint: 'react' + }, + { + role: 'inlineFileRights', + name: 'textInlineFileRights', + insertPoint: 'react' + } + ], + w => { + w.register('textInlineFileRights', { + configurationEditorTabViewGroups: { + ContentElementInlineFileRightsSettings() { + this.input('showBackdrop', CheckBoxInputView); + } + } + }); + } + ); + const widgets = new WidgetsCollection([ + {type_name: 'iconInlineFileRights', role: 'inlineFileRights'}, + ], {widgetTypes}); + const groups = new ConfigurationEditorTabView.Groups(); + widgets.subject = factories.entry(); + + widgets.setupConfigurationEditorTabViewGroups(groups); + widgets.first().set('type_name', 'textInlineFileRights'); + const context = { + input: jest.fn() + }; + groups.apply( + 'ContentElementInlineFileRightsSettings', + context + ); + + expect(context.input).toHaveBeenCalled(); + }); }); diff --git a/package/spec/ui/views/mixins/inputView-spec.js b/package/spec/ui/views/mixins/inputView-spec.js index 44048a9dc8..b4bec5cc3b 100644 --- a/package/spec/ui/views/mixins/inputView-spec.js +++ b/package/spec/ui/views/mixins/inputView-spec.js @@ -396,6 +396,23 @@ describe('pageflow.inputView', () => { }); }); + describe('with disabledBindingModel option', () => { + it('adds listener to passed model instead', () => { + const otherModel = new Backbone.Model(); + var view = createInputViewWithInput({ + model: new Backbone.Model(), + disabledBinding: 'disable', + disabledBindingModel: otherModel, + disabledBindingValue: true + }); + + view.render(); + otherModel.set({disable: true}); + + expect(view.ui.input).toHaveAttr('disabled'); + }); + }); + describe('with function for disabled option', () => { it('disables input when function returns true', () => { var view = createInputViewWithInput({ @@ -433,6 +450,21 @@ describe('pageflow.inputView', () => { expect(view.ui.input).not.toHaveAttr('disabled'); }); + describe('with disabledBindingModel', () => { + it('passes value from binding model to function', () => { + var otherModel = new Backbone.Model({state: 'disabled'}) + var view = createInputViewWithInput({ + model: new Backbone.Model({}), + disabledBindingModel: otherModel, + disabledBinding: 'state', + disabled: function(value) { return value === 'disabled'; } + }); + + view.render(); + + expect(view.ui.input).toHaveAttr('disabled'); + }); + }); describe('with multiple binding attributes', () => { it('passes array of values to function', () => { const disabledFunction = jest.fn(); diff --git a/package/src/editor/api/WidgetType.js b/package/src/editor/api/WidgetType.js index fbf90ba388..ed6cc6d439 100644 --- a/package/src/editor/api/WidgetType.js +++ b/package/src/editor/api/WidgetType.js @@ -1,5 +1,4 @@ import _ from 'underscore'; - import {Object} from 'pageflow/ui'; export const WidgetType = Object.extend({ @@ -8,6 +7,7 @@ export const WidgetType = Object.extend({ this.translationKey = serverSideConfig.translationKey; this.insertPoint = serverSideConfig.insertPoint; this.configurationEditorView = clientSideConfig.configurationEditorView; + this.configurationEditorTabViewGroups = clientSideConfig.configurationEditorTabViewGroups || {}; this.isOptional = clientSideConfig.isOptional; }, @@ -23,5 +23,17 @@ export const WidgetType = Object.extend({ 'pageflow.editor.widgets.common_attributes' ] }, options)); + }, + + defineStubConfigurationEditorTabViewGroups(groups) { + _.each(this.configurationEditorTabViewGroups, (fn, name) => + groups.define(name, () => {}) + ); + }, + + defineConfigurationEditorTabViewGroups(groups) { + _.each(this.configurationEditorTabViewGroups, (fn, name) => + groups.define(name, fn) + ); } }); diff --git a/package/src/editor/api/WidgetTypes.js b/package/src/editor/api/WidgetTypes.js index 392c2e8cb1..b438772fb2 100644 --- a/package/src/editor/api/WidgetTypes.js +++ b/package/src/editor/api/WidgetTypes.js @@ -55,5 +55,11 @@ export const WidgetTypes = Object.extend({ isOptional: function(role) { return !!this._optionalRoles[role]; + }, + + defineStubConfigurationEditorTabViewGroups(groups) { + _.each(this._widgetTypesByName, widgetType => + widgetType.defineStubConfigurationEditorTabViewGroups(groups) + ); } }); diff --git a/package/src/editor/collections/WidgetsCollection.js b/package/src/editor/collections/WidgetsCollection.js index c5375a6832..c55db289a4 100644 --- a/package/src/editor/collections/WidgetsCollection.js +++ b/package/src/editor/collections/WidgetsCollection.js @@ -7,7 +7,9 @@ import {SubsetCollection} from './SubsetCollection'; export const WidgetsCollection = Backbone.Collection.extend({ model: Widget, - initialize: function() { + initialize: function(widgets, options) { + this.widgetTypes = options.widgetTypes; + this.listenTo(this, 'change:type_name change:configuration', function() { this.batchSave(); }); @@ -35,6 +37,18 @@ export const WidgetsCollection = Backbone.Collection.extend({ })); }, + setupConfigurationEditorTabViewGroups(groups) { + this.defineConfigurationEditorTabViewGroups(groups); + this.listenTo(this, 'change:type_name', () => + this.defineConfigurationEditorTabViewGroups(groups) + ); + }, + + defineConfigurationEditorTabViewGroups(groups) { + this.widgetTypes.defineStubConfigurationEditorTabViewGroups(groups); + this.each(widget => widget.defineConfigurationEditorTabViewGroups(groups)); + }, + withInsertPoint(insertPoint) { return new SubsetCollection({ parent: this, diff --git a/package/src/editor/initializers/setupCollections.js b/package/src/editor/initializers/setupCollections.js index 9e54bbfe3b..47a63cc6f0 100644 --- a/package/src/editor/initializers/setupCollections.js +++ b/package/src/editor/initializers/setupCollections.js @@ -2,6 +2,7 @@ import Backbone from 'backbone'; import _ from 'underscore'; import {events} from 'pageflow/frontend'; +import {ConfigurationEditorTabView} from 'pageflow/ui'; import {ChaptersCollection} from '../collections/ChaptersCollection'; import {FilesCollection} from '../collections/FilesCollection'; @@ -38,6 +39,7 @@ app.addInitializer(function(options) { state.account = new Backbone.Model(options.account); widgets.subject = state.entry; + widgets.setupConfigurationEditorTabViewGroups(ConfigurationEditorTabView.groups); state.storylineOrdering = new StorylineOrdering(state.storylines, state.pages); state.storylineOrdering.sort({silent: true}); diff --git a/package/src/editor/models/Widget.js b/package/src/editor/models/Widget.js index ebc50d50a9..d66a4b8df3 100644 --- a/package/src/editor/models/Widget.js +++ b/package/src/editor/models/Widget.js @@ -23,6 +23,11 @@ export const Widget = Backbone.Model.extend({ return this.get('type_name') && this.widgetTypes.findByName(this.get('type_name')); }, + defineConfigurationEditorTabViewGroups(groups) { + this.widgetType() && + this.widgetType().defineConfigurationEditorTabViewGroups(groups); + }, + hasConfiguration: function() { return !!(this.widgetType() && this.widgetType().hasConfiguration()); }, diff --git a/package/src/ui/views/mixins/attributeBinding.js b/package/src/ui/views/mixins/attributeBinding.js index ea9c5c0ba9..6fd377b7bd 100644 --- a/package/src/ui/views/mixins/attributeBinding.js +++ b/package/src/ui/views/mixins/attributeBinding.js @@ -11,11 +11,12 @@ export const attributeBinding = { setupAttributeBinding: function(optionName, updateMethod, normalize = value => value) { const binding = this.options[`${optionName}Binding`]; + const model = this.options[`${optionName}BindingModel`] || this.model; const view = this; if (binding) { _.flatten([binding]).forEach(attribute => { - this.listenTo(this.model, 'change:' + attribute, update); + this.listenTo(model, 'change:' + attribute, update); }); } @@ -28,11 +29,12 @@ export const attributeBinding = { getAttributeBoundOption(optionName, normalize = value => value) { const binding = this.options[`${optionName}Binding`]; + const model = this.options[`${optionName}BindingModel`] || this.model; const bindingValueOptionName = `${optionName}BindingValue`; const value = Array.isArray(binding) ? - binding.map(attribute => this.model.get(attribute)) : - this.model.get(binding); + binding.map(attribute => model.get(attribute)) : + model.get(binding); if (bindingValueOptionName in this.options) { return value === this.options[bindingValueOptionName]; diff --git a/package/src/ui/views/mixins/inputView.js b/package/src/ui/views/mixins/inputView.js index 5e7241307c..65f8120907 100644 --- a/package/src/ui/views/mixins/inputView.js +++ b/package/src/ui/views/mixins/inputView.js @@ -113,6 +113,9 @@ import {attributeBinding} from './attributeBinding'; * Input will be disabled whenever the value of the `disabledBinding` * attribute equals the value of this option. * + * @param {Backbone.Model} [options.disabledBindingModel] + * Alternative model to bind to. + * * @param {string|string[]} [options.visibleBinding] * Name of an attribute to control whether the input is visible. If * the `visible` and `visibleBindingValue` options are not set, @@ -129,6 +132,9 @@ import {attributeBinding} from './attributeBinding'; * Input will be visible whenever the value of the `visibleBinding` * attribute equals the value of this option. * + * @param {Backbone.Model} [options.visibleBindingModel] + * Alternative model to bind to. + * * @mixin */ export const inputView = {