{children}
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 617aed1d29..9226ae913b 100644
--- a/entry_types/scrolled/package/src/contentElements/hotspots/editor/index.js
+++ b/entry_types/scrolled/package/src/contentElements/hotspots/editor/index.js
@@ -20,6 +20,7 @@ editor.contentElementTypes.register('hotspots', {
category: 'interactive',
supportedPositions: ['inline', 'side', 'sticky', 'standAlone', 'left', 'right', 'backdrop'],
supportedWidthRange: ['xxs', 'full'],
+ supportedStyles: ['boxShadow', 'outline'],
editorPath(contentElement) {
const activeAreaId = contentElement.transientState.get('activeAreaId');
diff --git a/entry_types/scrolled/package/src/contentElements/iframeEmbed/IframeEmbed.js b/entry_types/scrolled/package/src/contentElements/iframeEmbed/IframeEmbed.js
index 5135f7bf80..f27f893361 100644
--- a/entry_types/scrolled/package/src/contentElements/iframeEmbed/IframeEmbed.js
+++ b/entry_types/scrolled/package/src/contentElements/iframeEmbed/IframeEmbed.js
@@ -57,7 +57,7 @@ export function IframeEmbed({configuration}) {
style={{pointerEvents: isEditable && !isSelected ? 'none' : undefined}}>
-
+
{renderSpanningWrapper(
diff --git a/entry_types/scrolled/package/src/contentElements/iframeEmbed/editor.js b/entry_types/scrolled/package/src/contentElements/iframeEmbed/editor.js
index 6caf1cdc94..b53c0a6bbb 100644
--- a/entry_types/scrolled/package/src/contentElements/iframeEmbed/editor.js
+++ b/entry_types/scrolled/package/src/contentElements/iframeEmbed/editor.js
@@ -13,6 +13,7 @@ editor.contentElementTypes.register('iframeEmbed', {
featureName: 'iframe_embed_content_element',
supportedPositions: ['inline', 'side', 'sticky', 'standAlone', 'left', 'right'],
supportedWidthRange: ['xxs', 'full'],
+ supportedStyles: ['boxShadow', 'outline'],
configurationEditor({entry}) {
this.tab('general', function() {
diff --git a/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js b/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js
index e9ddad542d..d5524618d6 100644
--- a/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js
+++ b/entry_types/scrolled/package/src/contentElements/imageGallery/ImageGallery.js
@@ -148,7 +148,7 @@ function Scroller({
contentElementWidth === contentElementWidths.xl},
{[styles.full]:
contentElementWidth === contentElementWidths.full},
- {[styles.clip]: configuration.hidePeeks},
+ {[styles.hidePeeks]: configuration.hidePeeks},
{[styles.customMargin]: customMargin})}>
-
+
-
+
!!posterId,
+ binding: 'posterId'
+ },
+ 'outline'
+ ],
defaultConfig: {playerControlVariant: 'waveformBars'},
diff --git a/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/BeforeAfter.js b/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/BeforeAfter.js
index 39e8683200..369b105cc2 100644
--- a/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/BeforeAfter.js
+++ b/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/BeforeAfter.js
@@ -66,7 +66,7 @@ export function BeforeAfter(configuration) {
return (
-
+
diff --git a/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/editor.js b/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/editor.js
index f928db6f5a..cdd38c2be0 100644
--- a/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/editor.js
+++ b/entry_types/scrolled/package/src/contentElements/inlineBeforeAfter/editor.js
@@ -9,6 +9,7 @@ editor.contentElementTypes.register('inlineBeforeAfter', {
category: 'interactive',
supportedPositions: ['inline', 'side', 'sticky', 'standAlone', 'left', 'right', 'backdrop'],
supportedWidthRange: ['xxs', 'full'],
+ supportedStyles: ['boxShadow', 'outline'],
configurationEditor({entry}) {
this.tab('general', function() {
diff --git a/entry_types/scrolled/package/src/contentElements/inlineImage/InlineImage.js b/entry_types/scrolled/package/src/contentElements/inlineImage/InlineImage.js
index fd13dd2b3e..385ad88caf 100644
--- a/entry_types/scrolled/package/src/contentElements/inlineImage/InlineImage.js
+++ b/entry_types/scrolled/package/src/contentElements/inlineImage/InlineImage.js
@@ -10,7 +10,9 @@ import {
useFileWithInlineRights,
usePortraitOrientation,
ExpandableImage,
- InlineFileRights
+ InlineFileRights,
+ processImageModifiers,
+ useFileWithCropPosition
} from 'pageflow-scrolled/frontend';
import {features} from 'pageflow/frontend';
@@ -92,10 +94,12 @@ function ImageWithCaption({
-
+
imageModifier.name === name)?.value;
-}
-
-function useFileWithCropPosition(file, cropPosition) {
- return file && {...file, cropPosition};
-}
diff --git a/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js b/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js
index ff8c308c56..3bf23028a2 100644
--- a/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js
+++ b/entry_types/scrolled/package/src/contentElements/inlineImage/editor.js
@@ -1,5 +1,5 @@
import {editor, InlineFileRightsMenuItem, ImageModifierListInputView} from 'pageflow-scrolled/editor';
-import {contentElementWidths} from 'pageflow-scrolled/frontend';
+import {contentElementWidths, processImageModifiers} from 'pageflow-scrolled/frontend';
import {FileInputView} from 'pageflow/editor';
import {SeparatorView, CheckBoxInputView} from 'pageflow/ui';
@@ -10,6 +10,7 @@ editor.contentElementTypes.register('inlineImage', {
category: 'media',
supportedPositions: ['inline', 'side', 'sticky', 'standAlone', 'left', 'right'],
supportedWidthRange: ['xxs', 'full'],
+ supportedStyles: ['boxShadow', 'outline'],
defaultsInputs() {
this.input('enableFullscreen', CheckBoxInputView);
@@ -20,12 +21,12 @@ editor.contentElementTypes.register('inlineImage', {
this.input('id', FileInputView, {
collection: 'image_files',
fileSelectionHandler: 'contentElementConfiguration',
- positioning: imageModifiers => imageModifiers?.length,
+ positioning: imageModifiers => !!processImageModifiers(imageModifiers).aspectRatio,
positioningBinding: 'imageModifiers',
positioningOptions: () => {
- const aspectRatio = entry.getAspectRatio(this.model.get('imageModifiers')?.[0]?.value);
+ const {aspectRatio} = processImageModifiers(this.model.get('imageModifiers'));
return {
- preview: aspectRatio && (1 / aspectRatio)
+ preview: aspectRatio && (1 / entry.getAspectRatio(aspectRatio))
};
},
dropDownMenuItems: [InlineFileRightsMenuItem]
@@ -38,13 +39,13 @@ editor.contentElementTypes.register('inlineImage', {
this.input('portraitId', FileInputView, {
collection: 'image_files',
fileSelectionHandler: 'contentElementConfiguration',
- positioning: imageModifiers => imageModifiers?.length,
+ positioning: imageModifiers => !!processImageModifiers(imageModifiers).aspectRatio,
positioningBinding: 'portraitImageModifiers',
positioningOptions: () => {
- const aspectRatio = entry.getAspectRatio(this.model.get('portraitImageModifiers')?.[0]?.value);
+ const {aspectRatio} = processImageModifiers(this.model.get('portraitImageModifiers'));
return {
- preview: aspectRatio && (1 / aspectRatio)
- }
+ preview: aspectRatio && (1 / entry.getAspectRatio(aspectRatio))
+ };
}
});
this.input('portraitImageModifiers', ImageModifierListInputView, {
diff --git a/entry_types/scrolled/package/src/contentElements/inlineVideo/InlineVideo.js b/entry_types/scrolled/package/src/contentElements/inlineVideo/InlineVideo.js
index 738fafe9c6..fc1439a127 100644
--- a/entry_types/scrolled/package/src/contentElements/inlineVideo/InlineVideo.js
+++ b/entry_types/scrolled/package/src/contentElements/inlineVideo/InlineVideo.js
@@ -10,6 +10,9 @@ import {
InlineFileRights,
FitViewport,
PlayerEventContextDataProvider,
+ contentElementBoxProps,
+ processImageModifiers,
+ useFileWithCropPosition,
useBackgroundFile,
useContentElementEditorState,
useFileWithInlineRights,
@@ -27,38 +30,38 @@ import {
} from './handlers';
export function InlineVideo({contentElementId, configuration, sectionProps}) {
- const videoFile = useFileWithInlineRights({
- configuration,
- collectionName: 'videoFiles',
- propertyName: 'id'
- });
+ const videoFile = useFileWithCropPosition(
+ useFileWithInlineRights({
+ configuration, collectionName: 'videoFiles', propertyName: 'id'
+ }),
+ configuration.cropPosition
+ );
const posterImageFile = useFileWithInlineRights({
configuration,
collectionName: 'imageFiles',
propertyName: 'posterId'
});
- const portraitVideoFile = useFileWithInlineRights({
- configuration,
- collectionName: 'videoFiles',
- propertyName: 'portraitId'
- });
+ const portraitVideoFile = useFileWithCropPosition(
+ useFileWithInlineRights({
+ configuration, collectionName: 'videoFiles', propertyName: 'portraitId'
+ }),
+ configuration.portraitCropPosition
+ );
const portraitPosterImageFile = useFileWithInlineRights({
configuration,
collectionName: 'imageFiles',
propertyName: 'portraitPosterId'
});
- // Only render OrientationAwareInlineImage if a portrait image has
- // been selected. This prevents having the component rerender on
- // orientation changes even if it does not depend on orientation at
- // all.
if (portraitVideoFile) {
return (
-
-
-
-
-
-
-
-
+ {renderContentElementBox({rounded, configuration},
+
+
+
+
+
+
+ )}
@@ -154,6 +169,25 @@ function OrientationUnawareInlineVideo({
)
}
+function renderContentElementBox({rounded, configuration}, children) {
+ if (rounded === 'circle') {
+ const {style, className} = contentElementBoxProps(configuration, {borderRadius: 'circle'});
+
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
function CropPositionComputingPlayer({videoFile, motifArea, ...props}) {
const videoFileWithMotifArea = useBackgroundFile({
file: videoFile,
@@ -172,7 +206,8 @@ function PlayerWithControlBar({
inlineFileRightsItems,
playerState, playerActions,
contentElementId, configuration,
- sectionProps
+ sectionProps, fit, hideControlBar,
+ applyContentElementBoxStyles
}) {
const {isEditable, isSelected} = useContentElementEditorState();
@@ -225,8 +260,7 @@ function PlayerWithControlBar({
defaultTextTrackFilePermaId={configuration.defaultTextTrackFileId}
playerState={playerState}
playerActions={playerActions}
- hideControlBar={configuration.hideControlBar ||
- configuration.playbackMode === 'loop'}
+ hideControlBar={hideControlBar}
hideBigPlayButton={configuration.playbackMode === 'loop'}
inlineFileRightsItems={inlineFileRightsItems}
configuration={configuration}
@@ -238,7 +272,7 @@ function PlayerWithControlBar({
shouldLoad ? 'poster' :
'none'}
loop={configuration.playbackMode === 'loop'}
- fit={configuration.position === 'backdrop' ? 'cover' : 'contain'}
+ fit={fit}
playerState={playerState}
playerActions={playerActions}
videoFile={videoFile}
@@ -246,7 +280,8 @@ function PlayerWithControlBar({
defaultTextTrackFilePermaId={configuration.defaultTextTrackFileId}
quality={'high'}
playsInline={true}
- atmoDuringPlayback={configuration.atmoDuringPlayback} />
+ atmoDuringPlayback={configuration.atmoDuringPlayback}
+ applyContentElementBoxStyles={applyContentElementBoxStyles} />
);
diff --git a/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js b/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js
index fa683984a9..034889b8db 100644
--- a/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js
+++ b/entry_types/scrolled/package/src/contentElements/inlineVideo/editor.js
@@ -1,7 +1,8 @@
import Backbone from 'backbone';
import I18n from 'i18n-js';
-import {editor, InlineFileRightsMenuItem, EditMotifAreaDialogView} from 'pageflow-scrolled/editor';
+import {editor, InlineFileRightsMenuItem, EditMotifAreaDialogView, ImageModifierListInputView} from 'pageflow-scrolled/editor';
+import {processImageModifiers} from 'pageflow-scrolled/frontend';
import {FileInputView, CheckBoxInputView} from 'pageflow/editor';
import {SelectInputView, SeparatorView, LabelOnlyView} from 'pageflow/ui';
@@ -37,6 +38,7 @@ editor.contentElementTypes.register('inlineVideo', {
category: 'media',
supportedPositions: ['inline', 'side', 'sticky', 'standAlone', 'left', 'right', 'backdrop'],
supportedWidthRange: ['xxs', 'full'],
+ supportedStyles: ['boxShadow', 'outline'],
defaultsInputs() {
this.input('playbackMode', SelectInputView, {
@@ -51,7 +53,14 @@ editor.contentElementTypes.register('inlineVideo', {
this.input('id', FileInputView, {
collection: 'video_files',
fileSelectionHandler: 'contentElementConfiguration',
- positioning: false,
+ positioning: imageModifiers => !!processImageModifiers(imageModifiers).aspectRatio,
+ positioningBinding: 'imageModifiers',
+ positioningOptions: () => {
+ const {aspectRatio} = processImageModifiers(this.model.get('imageModifiers'));
+ return {
+ preview: aspectRatio && (1 / entry.getAspectRatio(aspectRatio))
+ };
+ },
defaultTextTrackFilePropertyName: 'defaultTextTrackFileId',
dropDownMenuItems: [EditMotifAreaMenuItem, InlineFileRightsMenuItem]
});
@@ -61,11 +70,23 @@ editor.contentElementTypes.register('inlineVideo', {
positioning: false,
dropDownMenuItems: [InlineFileRightsMenuItem]
});
+ this.input('imageModifiers', ImageModifierListInputView, {
+ entry,
+ visibleBinding: 'id',
+ visible: () => this.model.getReference('id', 'video_files')
+ });
this.input('portraitId', FileInputView, {
collection: 'video_files',
fileSelectionHandler: 'contentElementConfiguration',
- positioning: false,
+ positioning: imageModifiers => !!processImageModifiers(imageModifiers).aspectRatio,
+ positioningBinding: 'portraitImageModifiers',
+ positioningOptions: () => {
+ const {aspectRatio} = processImageModifiers(this.model.get('portraitImageModifiers'));
+ return {
+ preview: aspectRatio && (1 / entry.getAspectRatio(aspectRatio))
+ };
+ },
defaultTextTrackFilePropertyName: 'defaultTextTrackFileId',
dropDownMenuItems: [EditMotifAreaMenuItem, InlineFileRightsMenuItem]
});
@@ -77,6 +98,11 @@ editor.contentElementTypes.register('inlineVideo', {
visible: () => this.model.getReference('portraitId', 'video_files'),
dropDownMenuItems: [InlineFileRightsMenuItem]
});
+ this.input('portraitImageModifiers', ImageModifierListInputView, {
+ entry,
+ visibleBinding: 'portraitId',
+ visible: () => this.model.getReference('portraitId', 'video_files')
+ });
this.view(SeparatorView);
@@ -85,8 +111,10 @@ editor.contentElementTypes.register('inlineVideo', {
});
this.input('hideControlBar', CheckBoxInputView, {
- disabledBinding: 'playbackMode',
- disabled: playbackMode => playbackMode === 'loop',
+ disabledBinding: ['playbackMode', 'imageModifiers'],
+ disabled: ([playbackMode, imageModifiers]) =>
+ playbackMode === 'loop' ||
+ processImageModifiers(imageModifiers).rounded === 'circle',
displayCheckedIfDisabled: true
});
diff --git a/entry_types/scrolled/package/src/contentElements/videoEmbed/VideoEmbed.js b/entry_types/scrolled/package/src/contentElements/videoEmbed/VideoEmbed.js
index b9347e525a..4cd90b0a5e 100644
--- a/entry_types/scrolled/package/src/contentElements/videoEmbed/VideoEmbed.js
+++ b/entry_types/scrolled/package/src/contentElements/videoEmbed/VideoEmbed.js
@@ -35,7 +35,7 @@ export function VideoEmbed({contentElementId, configuration}) {
-
+
{shouldLoad &&
-
+
{renderLazyPanorama(configuration, imageFile, shouldLoad, aspectRatio)}
diff --git a/entry_types/scrolled/package/src/contentElements/vrImage/editor.js b/entry_types/scrolled/package/src/contentElements/vrImage/editor.js
index c6f7b3f490..c4e6cae051 100644
--- a/entry_types/scrolled/package/src/contentElements/vrImage/editor.js
+++ b/entry_types/scrolled/package/src/contentElements/vrImage/editor.js
@@ -10,6 +10,7 @@ editor.contentElementTypes.register('vrImage', {
category: 'interactive',
supportedPositions: ['inline', 'side', 'sticky', 'standAlone', 'left', 'right', 'backdrop'],
supportedWidthRange: ['xxs', 'full'],
+ supportedStyles: ['boxShadow', 'outline'],
configurationEditor({entry}) {
this.tab('general', function() {
diff --git a/entry_types/scrolled/package/src/editor/collections/StylesCollection.js b/entry_types/scrolled/package/src/editor/collections/StylesCollection.js
index d86f89e2ae..aee40bb619 100644
--- a/entry_types/scrolled/package/src/editor/collections/StylesCollection.js
+++ b/entry_types/scrolled/package/src/editor/collections/StylesCollection.js
@@ -1,6 +1,6 @@
import Backbone from 'backbone';
import I18n from 'i18n-js';
-import {features} from 'pageflow/frontend';
+import {attributeBindingUtils} from 'pageflow/ui';
import {Style} from '../models/Style';
export const StylesCollection = Backbone.Collection.extend({
@@ -8,22 +8,18 @@ export const StylesCollection = Backbone.Collection.extend({
initialize(models, options = {}) {
this.types = options.types || {};
+ this.bindingModel = options.bindingModel;
},
getUnusedStyles() {
const unusedStyles = new Backbone.Collection(
Object
- .entries(this.types)
- .filter(
- ([name, styleType]) => (
- features.isEnabled('decoration_effects') ||
- Style.getKind(name, this.types) !== 'decoration'
- )
- )
- .map(([name]) => ({name})),
+ .keys(this.types)
+ .map(name => ({name})),
{
comparator: style => Object.keys(this.types).indexOf(style.get('name')),
styles: this,
+ bindingModel: this.bindingModel,
model: UnusedStyle
}
);
@@ -48,8 +44,9 @@ function updateSeparation(styles, types) {
}
const UnusedStyle = Backbone.Model.extend({
- initialize({name}, {styles}) {
- const {items} = styles.types[name];
+ initialize({name}, {styles, bindingModel}) {
+ const type = styles.types[name];
+ const {items} = type;
this.set('label', Style.getLabel(name, styles.types));
@@ -62,18 +59,34 @@ const UnusedStyle = Backbone.Model.extend({
}
else {
this.selected = () => {
- styles.add({name: this.get('name')}, {types: styles.types});
+ styles.add({name: this.get('name')}, {types: styles.types, bindingModel: styles.bindingModel});
}
}
- const update = () => {
- this.set({
- hidden: !!styles.findWhere({name: this.get('name')}) && !items
- });
+ const updateHidden = () => {
+ const inUse = !!styles.findWhere({name: this.get('name')}) && !items;
+ this.set({hidden: inUse || !this.get('available')});
};
- this.listenTo(styles, 'add remove', update);
- update();
+ this.listenTo(styles, 'add remove', updateHidden);
+
+ if (type.binding && bindingModel) {
+ attributeBindingUtils.setup({
+ binding: type.binding,
+ model: bindingModel,
+ listener: this,
+ option: type.when,
+ callback: available => {
+ this.set({available});
+ updateHidden();
+ }
+ });
+ }
+ else {
+ this.set({available: true});
+ }
+
+ updateHidden();
}
});
@@ -138,7 +151,7 @@ const UnusedStyleItem = Backbone.Model.extend({
_applyStyle(currentStyle) {
this.styles.remove(currentStyle);
this._removeIncompatibleStyles();
- this.styles.add({name: this.styleName, value: this.get(('value'))}, {types: this.styles.types});
+ this.styles.add({name: this.styleName, value: this.get(('value'))}, {types: this.styles.types, bindingModel: this.styles.bindingModel});
},
_removeIncompatibleStyles() {
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 f4dbce3229..f608e996e9 100644
--- a/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js
+++ b/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js
@@ -28,7 +28,8 @@ const scaleDefaultPropertyNames = {
sectionPaddingTop: 'sectionDefaultPaddingTop',
sectionPaddingBottom: 'sectionDefaultPaddingBottom',
contentElementBoxBorderRadius: 'contentElementBoxBorderRadius',
- contentElementMargin: 'contentElementMarginStyleDefault'
+ contentElementMargin: 'contentElementMarginStyleDefault',
+ contentElementBoxShadow: 'contentElementBoxShadowStyleDefault'
};
const defaultAspectRatios = [{
diff --git a/entry_types/scrolled/package/src/editor/models/Style.js b/entry_types/scrolled/package/src/editor/models/Style.js
index 2d650210d4..e506269062 100644
--- a/entry_types/scrolled/package/src/editor/models/Style.js
+++ b/entry_types/scrolled/package/src/editor/models/Style.js
@@ -1,13 +1,27 @@
import Backbone from 'backbone';
import I18n from 'i18n-js';
+import {features} from 'pageflow/frontend';
+import {attributeBindingUtils} from 'pageflow/ui';
export const Style = Backbone.Model.extend({
- initialize({name}, {types}) {
+ initialize({name}, {types, bindingModel}) {
this.types = types;
if (!this.has('value')) {
this.set('value', this.defaultValue());
}
+
+ const type = types[name];
+
+ if (type?.binding && bindingModel) {
+ attributeBindingUtils.setup({
+ binding: type.binding,
+ model: bindingModel,
+ listener: this,
+ option: type.when,
+ callback: available => this.set({available})
+ });
+ }
},
label() {
@@ -49,6 +63,10 @@ export const Style = Backbone.Model.extend({
inputType() {
return this.types[this.get('name')].inputType || 'none';
+ },
+
+ inputOptions() {
+ return this.types[this.get('name')].inputOptions || {};
}
});
@@ -61,7 +79,7 @@ Style.getKind = function(name, types) {
return types[name].kind;
};
-Style.effectTypes = {
+const allEffectTypes = {
blur: {
inputType: 'slider',
minValue: 0,
@@ -125,13 +143,41 @@ Style.effectTypes = {
}
};
+Style.getEffectTypes = function() {
+ if (features.isEnabled('decoration_effects')) {
+ return allEffectTypes;
+ }
+
+ return Object.fromEntries(
+ Object.entries(allEffectTypes).filter(
+ ([, type]) => type.kind !== 'decoration'
+ )
+ );
+};
+
Style.getTypesForContentElement = function({entry, contentElement}) {
const marginScale = entry.getScale('contentElementMargin');
const defaultConfig = contentElement.getType().defaultConfig || {};
const result = {};
+ const supportedStyles = contentElement.getType().supportedStyles || [];
+
+ function findSupportedStyle(name) {
+ return supportedStyles.find(s => s === name || s.name === name);
+ }
+
+ function bindingOptions(name) {
+ const style = findSupportedStyle(name);
+ if (!style || typeof style === 'string') return {};
+ const {binding, when} = style;
+ return {
+ ...(binding && {binding, when})
+ };
+ }
+
if (marginScale.values.length > 0) {
result.marginTop = {
+ kind: 'spacing',
label: I18n.t('pageflow_scrolled.editor.content_element_style_list_input.marginTop'),
propertyName: 'marginTop',
inputType: 'slider',
@@ -142,6 +188,7 @@ Style.getTypesForContentElement = function({entry, contentElement}) {
};
result.marginBottom = {
+ kind: 'spacing',
label: I18n.t('pageflow_scrolled.editor.content_element_style_list_input.marginBottom'),
propertyName: 'marginBottom',
inputType: 'slider',
@@ -152,6 +199,40 @@ Style.getTypesForContentElement = function({entry, contentElement}) {
};
}
+ if (findSupportedStyle('boxShadow')) {
+ const boxShadowScale = entry.getScale('contentElementBoxShadow');
+
+ if (boxShadowScale.values.length > 0) {
+ result.boxShadow = {
+ kind: 'decoration',
+ label: I18n.t('pageflow_scrolled.editor.content_element_style_list_input.boxShadow'),
+ propertyName: 'boxShadow',
+ inputType: 'slider',
+ values: boxShadowScale.values,
+ texts: boxShadowScale.texts,
+ defaultValue: boxShadowScale.defaultValue,
+ ...bindingOptions('boxShadow')
+ };
+ }
+ }
+
+ if (findSupportedStyle('outline')) {
+ const themeProperties = entry.getThemeProperties();
+
+ result.outlineColor = {
+ kind: 'decoration',
+ label: I18n.t('pageflow_scrolled.editor.content_element_style_list_input.outlineColor'),
+ propertyName: 'outlineColor',
+ inputType: 'color',
+ defaultValue: themeProperties.root?.outlineColor,
+ inputOptions: {
+ alpha: true,
+ swatches: entry.getUsedContentElementColors('outlineColor')
+ },
+ ...bindingOptions('outline')
+ };
+ }
+
return result;
}
diff --git a/entry_types/scrolled/package/src/editor/views/inputs/EffectListInputView.js b/entry_types/scrolled/package/src/editor/views/inputs/EffectListInputView.js
index e42d67e2ad..5e9c2d8430 100644
--- a/entry_types/scrolled/package/src/editor/views/inputs/EffectListInputView.js
+++ b/entry_types/scrolled/package/src/editor/views/inputs/EffectListInputView.js
@@ -5,7 +5,7 @@ export const EffectListInputView = function(options) {
return new StyleListInputView({
...options,
hideLabel: true,
- types: Style.effectTypes,
+ types: Style.getEffectTypes(),
translationKeyPrefix: 'pageflow_scrolled.editor.effect_list_input'
});
};
diff --git a/entry_types/scrolled/package/src/editor/views/inputs/StyleListInputView.js b/entry_types/scrolled/package/src/editor/views/inputs/StyleListInputView.js
index 8ca561d442..f7c458df6c 100644
--- a/entry_types/scrolled/package/src/editor/views/inputs/StyleListInputView.js
+++ b/entry_types/scrolled/package/src/editor/views/inputs/StyleListInputView.js
@@ -22,7 +22,7 @@ export const StyleListInputView = Marionette.ItemView.extend({
initialize() {
this.styles = new StylesCollection(
this.readFromModel(),
- {types: this.options.types}
+ {types: this.options.types, bindingModel: this.model}
);
this.listenTo(this.styles, 'add remove change', () => {
@@ -112,13 +112,19 @@ const StyleListItemView = Marionette.ItemView.extend({
className: styles.item,
template: (data) => `
- ${data.label}
- ${renderInput(data.inputType)}
+
+
${data.label}
+ ${renderInput(data.inputType)}
+
`,
+ modelEvents: {
+ 'change:available': 'updateAvailability'
+ },
+
serializeData() {
return {
label: this.model.label(),
@@ -127,7 +133,7 @@ const StyleListItemView = Marionette.ItemView.extend({
};
},
- ui: cssModulesUtils.ui(styles, 'widget', 'value', 'colorInput'),
+ ui: cssModulesUtils.ui(styles, 'controls', 'widget', 'value', 'colorInput'),
events: cssModulesUtils.events(styles, {
'click remove': function() {
@@ -153,7 +159,14 @@ const StyleListItemView = Marionette.ItemView.extend({
}
}),
+ updateAvailability() {
+ const unavailable = this.model.get('available') === false;
+ this.$el.toggleClass(styles.unavailable, unavailable);
+ this.ui.controls.attr('inert', unavailable ? '' : null);
+ },
+
onRender() {
+ this.updateAvailability();
this.$el.addClass(styles[`input-${this.model.inputType()}`]);
const values = this.model.values();
@@ -190,6 +203,7 @@ const StyleListItemView = Marionette.ItemView.extend({
this._colorPicker = new ColorPicker(colorInput, {
defaultValue: this.model.defaultValue(),
+ ...this.model.inputOptions(),
onChange: (color) => {
this.model.set('value', color || '');
}
diff --git a/entry_types/scrolled/package/src/editor/views/inputs/StyleListInputView.module.css b/entry_types/scrolled/package/src/editor/views/inputs/StyleListInputView.module.css
index c617336e05..1b5e211ba4 100644
--- a/entry_types/scrolled/package/src/editor/views/inputs/StyleListInputView.module.css
+++ b/entry_types/scrolled/package/src/editor/views/inputs/StyleListInputView.module.css
@@ -119,6 +119,17 @@
height: size(4);
}
+.controls {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ min-width: 0;
+}
+
+.unavailable .controls {
+ opacity: 0.4;
+}
+
.remove {
composes: cancel from '../icons.module.css';
padding: 0 space(2);
diff --git a/entry_types/scrolled/package/src/frontend/ContentElementBox.js b/entry_types/scrolled/package/src/frontend/ContentElementBox.js
index 287c9a7256..67b17a0f96 100644
--- a/entry_types/scrolled/package/src/frontend/ContentElementBox.js
+++ b/entry_types/scrolled/package/src/frontend/ContentElementBox.js
@@ -2,6 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import {useContentElementAttributes} from './useContentElementAttributes';
+import {contentElementBoxProps} from './contentElementBoxStyle';
import {widths} from './layouts/widths';
import styles from './ContentElementBox.module.css';
@@ -12,21 +13,25 @@ import styles from './ContentElementBox.module.css';
*
* @param {Object} props
* @param {string} props.children - Content of box.
- * @param {string} props.borderRadius - Border radius value from theme scale, or "none" to render no wrapper.
+ * @param {Object} [props.configuration] - Content element configuration. Used to read box shadow and outline styles.
+ * @param {string} [props.borderRadius] - Border radius value from theme scale, or "none" to render no wrapper.
*/
-export function ContentElementBox({children, borderRadius, positioned}) {
+export function ContentElementBox({children, configuration, borderRadius, positioned}) {
const {position, width} = useContentElementAttributes();
- if (position === 'backdrop' || borderRadius === 'none') {
+ const {style} = contentElementBoxProps(configuration, {borderRadius});
+
+ if (position === 'backdrop') {
return children;
}
- const style = borderRadius ? {
- '--content-element-box-border-radius': `var(--theme-content-element-box-border-radius-${borderRadius})`
- } : {};
+ if (borderRadius === 'none' && !Object.keys(style).length) {
+ return children;
+ }
return (
+ {[styles.contentElementBox]: props.applyContentElementBoxStyles,
+ [styles.posterVisible]: posterVisible,
+ [textTrackStyles.inset]: props.textTracksInset})}>
{load === 'auto' &&
}
{load !== 'none' &&
}
+ objectPosition={props.objectPosition} />}
);
}
@@ -40,7 +44,7 @@ MediaPlayer.defaultProps = {
load: 'auto'
};
-function Poster({imageUrl, objectPosition, hide}) {
+function Poster({imageUrl, objectPosition}) {
if (!imageUrl) {
return null;
}
@@ -49,7 +53,6 @@ function Poster({imageUrl, objectPosition, hide}) {
);
diff --git a/entry_types/scrolled/package/src/frontend/contentElementBoxStyle.js b/entry_types/scrolled/package/src/frontend/contentElementBoxStyle.js
new file mode 100644
index 0000000000..715ce72da7
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/contentElementBoxStyle.js
@@ -0,0 +1,18 @@
+import styles from './ContentElementBox.module.css';
+
+export function contentElementBoxProps(configuration, {borderRadius} = {}) {
+ return {
+ className: styles.properties,
+ style: {
+ ...(borderRadius && borderRadius !== 'none' && {
+ '--content-element-box-border-radius': `var(--theme-content-element-box-border-radius-${borderRadius})`
+ }),
+ ...(configuration?.boxShadow && {
+ '--content-element-box-shadow': `var(--theme-content-element-box-shadow-${configuration.boxShadow})`
+ }),
+ ...(configuration?.outlineColor && {
+ '--content-element-box-outline-color': configuration.outlineColor
+ })
+ }
+ };
+}
diff --git a/entry_types/scrolled/package/src/frontend/imageModifiers.js b/entry_types/scrolled/package/src/frontend/imageModifiers.js
new file mode 100644
index 0000000000..49e3e4fd33
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/imageModifiers.js
@@ -0,0 +1,13 @@
+export function processImageModifiers(imageModifiers) {
+ const cropValue = getModifierValue(imageModifiers, 'crop');
+ const isCircleCrop = cropValue === 'circle';
+
+ return {
+ aspectRatio: isCircleCrop ? 'square' : cropValue,
+ rounded: isCircleCrop ? 'circle' : getModifierValue(imageModifiers, 'rounded')
+ };
+}
+
+function getModifierValue(imageModifiers, name) {
+ return (imageModifiers || []).find(imageModifier => imageModifier.name === name)?.value;
+}
diff --git a/entry_types/scrolled/package/src/frontend/index.js b/entry_types/scrolled/package/src/frontend/index.js
index 9dbb16ce51..f474d0599b 100644
--- a/entry_types/scrolled/package/src/frontend/index.js
+++ b/entry_types/scrolled/package/src/frontend/index.js
@@ -38,7 +38,10 @@ export * from './Atmo';
export * from './useAtmo';
export {ContentElementBox} from './ContentElementBox';
+export {contentElementBoxProps} from './contentElementBoxStyle';
export {ContentElementFigure} from './ContentElementFigure';
+export {processImageModifiers} from './imageModifiers';
+export {useFileWithCropPosition} from './useFileWithCropPosition';
export {MediaInteractionTracking} from './MediaInteractionTracking';
export {MediaPlayerControls} from './MediaPlayerControls';
export {VideoPlayerControls} from './VideoPlayerControls';
diff --git a/entry_types/scrolled/package/src/frontend/useFileWithCropPosition.js b/entry_types/scrolled/package/src/frontend/useFileWithCropPosition.js
new file mode 100644
index 0000000000..5503294386
--- /dev/null
+++ b/entry_types/scrolled/package/src/frontend/useFileWithCropPosition.js
@@ -0,0 +1,3 @@
+export function useFileWithCropPosition(file, cropPosition) {
+ return file && {...file, cropPosition};
+}
diff --git a/package/documentation.yml b/package/documentation.yml
index 921e695e2a..046c35915f 100644
--- a/package/documentation.yml
+++ b/package/documentation.yml
@@ -122,8 +122,9 @@ toc:
description: |
Utility functions exported by `pageflow/ui`.
children:
- - i18nUtils
+ - attributeBindingUtils
- cssModulesUtils
+ - i18nUtils
- name: Test Helpers
description: |
diff --git a/package/src/ui/index.js b/package/src/ui/index.js
index cb902cce8d..1bd379ef48 100644
--- a/package/src/ui/index.js
+++ b/package/src/ui/index.js
@@ -14,7 +14,14 @@ import * as i18nUtils from './utils/i18nUtils';
*/
import * as cssModulesUtils from './utils/cssModulesUtils';
-export { i18nUtils, cssModulesUtils };
+/**
+ * Helpers for setting up attribute bindings.
+ *
+ * @alias attributeBindingUtils
+ */
+import * as attributeBindingUtils from './utils/attributeBindingUtils';
+
+export { i18nUtils, cssModulesUtils, attributeBindingUtils };
export {default as Object} from '../Object';
export * from './models/mixins/serverSideValidation';
diff --git a/package/src/ui/utils/attributeBindingUtils.js b/package/src/ui/utils/attributeBindingUtils.js
new file mode 100644
index 0000000000..2433e01561
--- /dev/null
+++ b/package/src/ui/utils/attributeBindingUtils.js
@@ -0,0 +1,42 @@
+import _ from 'underscore';
+
+export function setup({
+ binding, model, listener, callback, normalize = value => value,
+ option, bindingValue
+}) {
+ if (binding) {
+ _.flatten([binding]).forEach(attribute => {
+ listener.listenTo(model, 'change:' + attribute, update);
+ });
+ }
+
+ update();
+
+ function update() {
+ callback(resolve({
+ binding, model, normalize, option, bindingValue
+ }));
+ }
+}
+
+export function resolve({
+ binding, model, normalize = value => value,
+ option, bindingValue
+}) {
+ const boundValue = Array.isArray(binding) ?
+ binding.map(attribute => model.get(attribute)) :
+ model.get(binding);
+
+ if (bindingValue !== undefined) {
+ return boundValue === bindingValue;
+ }
+ else if (typeof option === 'function') {
+ return normalize(option(boundValue));
+ }
+ else if (option !== undefined) {
+ return normalize(option);
+ }
+ else if (binding) {
+ return normalize(boundValue);
+ }
+}
diff --git a/package/src/ui/views/ColorPicker.js b/package/src/ui/views/ColorPicker.js
index e7563a13f0..5f0185349e 100644
--- a/package/src/ui/views/ColorPicker.js
+++ b/package/src/ui/views/ColorPicker.js
@@ -1,4 +1,5 @@
// Inspired by https://github.com/mdbassit/Coloris
+import I18n from 'i18n-js';
const ctx = typeof OffscreenCanvas !== 'undefined' &&
new OffscreenCanvas(1, 1).getContext('2d');
@@ -81,6 +82,12 @@ export default class ColorPicker {
this._alphaSlider = this._picker.querySelector('.color_picker-alpha input');
this._alphaMarker = this._picker.querySelector('.color_picker-alpha div');
this._swatchesContainer = this._picker.querySelector('.color_picker-swatches');
+
+ this._hueSlider.setAttribute('aria-label', I18n.t('pageflow.ui.color_picker.hue'));
+
+ if (this._alpha) {
+ this._alphaSlider.setAttribute('aria-label', I18n.t('pageflow.ui.color_picker.opacity'));
+ }
}
_wrapInput() {
diff --git a/package/src/ui/views/mixins/attributeBinding.js b/package/src/ui/views/mixins/attributeBinding.js
index 6fd377b7bd..cbb920dfcf 100644
--- a/package/src/ui/views/mixins/attributeBinding.js
+++ b/package/src/ui/views/mixins/attributeBinding.js
@@ -1,4 +1,4 @@
-import _ from 'underscore';
+import {setup, resolve} from '../../utils/attributeBindingUtils';
export const attributeBinding = {
setupBooleanAttributeBinding(optionName, updateMethod) {
@@ -9,44 +9,31 @@ export const attributeBinding = {
return this.getAttributeBoundOption(optionName, Boolean);
},
- setupAttributeBinding: function(optionName, updateMethod, normalize = value => value) {
+ setupAttributeBinding(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(model, 'change:' + attribute, update);
- });
- }
-
- update();
-
- function update() {
- updateMethod.call(view, view.getAttributeBoundOption(optionName, normalize));
- }
+ setup({
+ binding,
+ model,
+ listener: this,
+ normalize,
+ option: this.options[optionName],
+ bindingValue: this.options[`${optionName}BindingValue`],
+ callback: value => updateMethod.call(this, value)
+ });
},
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 => model.get(attribute)) :
- model.get(binding);
- if (bindingValueOptionName in this.options) {
- return value === this.options[bindingValueOptionName];
- }
- else if (typeof this.options[optionName] === 'function') {
- return normalize(this.options[optionName](value));
- }
- else if (optionName in this.options) {
- return normalize(this.options[optionName]);
- }
- else if (binding) {
- return normalize(value);
- }
+ return resolve({
+ binding,
+ model,
+ normalize,
+ option: this.options[optionName],
+ bindingValue: this.options[`${optionName}BindingValue`]
+ });
}
};