diff --git a/app/assets/stylesheets/pageflow/ui/input/color_input.scss b/app/assets/stylesheets/pageflow/ui/input/color_input.scss index 79a13a0d6c..f359937238 100644 --- a/app/assets/stylesheets/pageflow/ui/input/color_input.scss +++ b/app/assets/stylesheets/pageflow/ui/input/color_input.scss @@ -13,6 +13,10 @@ .minicolors-input-swatch { top: 5px; left: 5px; + + .minicolors-swatch-color { + background-color: var(--placeholder-color); + } } &.is_default input { @@ -22,4 +26,8 @@ .minicolors-focus input { color: var(--ui-on-surface-color); } + + .minicolors-swatches .minicolors-swatch { + margin: 2px; + } } diff --git a/entry_types/scrolled/config/locales/new/cards_colors.de.yml b/entry_types/scrolled/config/locales/new/cards_colors.de.yml new file mode 100644 index 0000000000..0ca01b9d31 --- /dev/null +++ b/entry_types/scrolled/config/locales/new/cards_colors.de.yml @@ -0,0 +1,8 @@ +de: + pageflow_scrolled: + editor: + edit_section: + attributes: + cardSurfaceColor: + label: Kartenhintergrundfarbe + auto: "(Automatisch)" diff --git a/entry_types/scrolled/config/locales/new/cards_colors.en.yml b/entry_types/scrolled/config/locales/new/cards_colors.en.yml new file mode 100644 index 0000000000..1eb66767a1 --- /dev/null +++ b/entry_types/scrolled/config/locales/new/cards_colors.en.yml @@ -0,0 +1,8 @@ +en: + pageflow_scrolled: + editor: + edit_section: + attributes: + cardSurfaceColor: + label: Cards background color + auto: "(Auto)" diff --git a/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb b/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb index 4251d305b5..d318ad2f61 100644 --- a/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb +++ b/entry_types/scrolled/lib/pageflow_scrolled/plugin.rb @@ -48,6 +48,7 @@ def configure(config) c.features.register('frontend_v2') c.features.register('scrolled_entry_fragment_caching') c.features.register('backdrop_content_elements') + c.features.register('custom_palette_colors') c.additional_frontend_seed_data.register( 'frontendVersion', diff --git a/entry_types/scrolled/package/spec/editor/models/ScrolledEntry/getUsedSectionBackgroundColors-spec.js b/entry_types/scrolled/package/spec/editor/models/ScrolledEntry/getUsedSectionBackgroundColors-spec.js new file mode 100644 index 0000000000..3a73a5b517 --- /dev/null +++ b/entry_types/scrolled/package/spec/editor/models/ScrolledEntry/getUsedSectionBackgroundColors-spec.js @@ -0,0 +1,103 @@ +import {ScrolledEntry} from 'editor/models/ScrolledEntry'; +import {factories} from 'pageflow/testHelpers'; +import {normalizeSeed} from 'support'; + +describe('ScrolledEntry', () => { + describe('#getUsedSectionBackgroundColors', () => { + it('returns unique sorted list of used background colors', () => { + const entry = factories.entry( + ScrolledEntry, + {}, + { + entryTypeSeed: normalizeSeed({ + sections: [ + { + configuration: { + backdropType: 'color', + backdropColor: '#400'} + }, + { + configuration: { + backdropType: 'color', + backdropColor: '#040' + } + }, + { + configuration: { + backdropType: 'color', + backdropColor: '#400' + } + }, + { + configuration: { + appearance: 'cards', + cardSurfaceColor: '#500' + } + }, + { + configuration: { + appearance: 'cards', + cardSurfaceColor: '#400' + } + } + ] + }) + } + ); + + const colors = entry.getUsedSectionBackgroundColors(); + + expect(colors).toEqual(['#400', '#500', '#040']); + }); + + it('ignores blank colors', () => { + const entry = factories.entry( + ScrolledEntry, + {}, + { + entryTypeSeed: normalizeSeed({ + sections: [ + { + configuration: { + backdropType: 'color' + } + } + ] + }) + } + ); + + const colors = entry.getUsedSectionBackgroundColors(); + + expect(colors).toEqual([]); + }); + + it('ignores invisible config options', () => { + const entry = factories.entry( + ScrolledEntry, + {}, + { + entryTypeSeed: normalizeSeed({ + sections: [ + { + configuration: { + backdropType: 'image', + backdropColor: '#400'} + }, + { + configuration: { + appearance: 'shadow', + cardSurfaceColor: '#500' + } + } + ] + }) + } + ); + + const colors = entry.getUsedSectionBackgroundColors(); + + expect(colors).toEqual([]); + }); + }); +}); diff --git a/entry_types/scrolled/package/spec/editor/views/inputs/ColorSelectOrCustomColorInputView-spec.js b/entry_types/scrolled/package/spec/editor/views/inputs/ColorSelectOrCustomColorInputView-spec.js new file mode 100644 index 0000000000..1564fd9863 --- /dev/null +++ b/entry_types/scrolled/package/spec/editor/views/inputs/ColorSelectOrCustomColorInputView-spec.js @@ -0,0 +1,162 @@ +import { + ColorSelectOrCustomColorInputView +} from 'editor/views/inputs/ColorSelectOrCustomColorInputView'; +import Backbone from 'backbone'; + +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom/extend-expect'; +import {useReactBasedBackboneViews} from 'support'; +import {useFakeTranslations} from 'pageflow/testHelpers'; + +describe('ColorSelectOrCustomColorInputView', () => { + const {render} = useReactBasedBackboneViews(); + + useFakeTranslations({ + 'pageflow_scrolled.editor.blank': 'Auto', + 'pageflow_scrolled.editor.custom_color': 'Custom Color' + }); + + it('loads theme palette color value', async () => { + const model = new Backbone.Model({ + color: 'brand-red' + }); + + const inputView = new ColorSelectOrCustomColorInputView({ + model, + label: 'Background Color', + customColorTranslationKey: 'pageflow_scrolled.editor.custom_color', + values: ['brand-red', 'brand-green'], + texts: ['Red', 'Green'], + propertyName: 'color' + }); + + const {getByRole, queryByRole} = render(inputView); + + expect(getByRole('button', {name: 'Red'})).not.toBeNull(); + expect(queryByRole('textbox')).toBeNull(); + }); + + it('loads custom color value', async () => { + const model = new Backbone.Model({ + color: '#ff0000' + }); + + const inputView = new ColorSelectOrCustomColorInputView({ + model, + label: 'Background Color', + customColorTranslationKey: 'pageflow_scrolled.editor.custom_color', + values: ['brand-red', 'brand-green'], + texts: ['Red', 'Green'], + propertyName: 'color' + }); + + const {getByRole} = render(inputView); + + expect(getByRole('button', {name: 'Custom Color'})).not.toBeNull(); + expect(getByRole('textbox')).not.toBeNull(); + expect(getByRole('textbox')).toHaveValue('#ff0000'); + }); + + it('updates model when selecting theme palette color', async () => { + const model = new Backbone.Model({ + color: '#ff0000' + }); + + const inputView = new ColorSelectOrCustomColorInputView({ + model, + label: 'Background Color', + customColorTranslationKey: 'pageflow_scrolled.editor.custom_color', + values: ['brand-red', 'brand-green'], + texts: ['Red', 'Green'], + propertyName: 'color' + }); + + const user = userEvent.setup(); + const {getByRole} = render(inputView); + await user.click(getByRole('button', {name: 'Custom Color'})); + await user.click(getByRole('option', {name: 'Red'})); + + expect(model.get('color')).toBe('brand-red'); + }); + + it('updates model when selecting custom color', async () => { + const model = new Backbone.Model({ + color: 'brand-red' + }); + + const inputView = new ColorSelectOrCustomColorInputView({ + model, + label: 'Background Color', + customColorTranslationKey: 'pageflow_scrolled.editor.custom_color', + values: ['brand-red', 'brand-green'], + texts: ['Red', 'Green'], + propertyName: 'color' + }); + + const user = userEvent.setup(); + const {getByRole} = render(inputView); + await user.click(getByRole('button', {name: 'Red'})); + await user.click(getByRole('option', {name: 'Custom Color'})); + + const input = getByRole('textbox'); + await user.clear(input); + await user.type(input, '#ff0000'); + await new Promise(resolve => setTimeout(resolve, 300)); + + expect(model.get('color')).toBe('#ff0000'); + }); + + it('shows custom color input only when custom is selected', async () => { + const model = new Backbone.Model({ + color: 'brand-red' + }); + + const inputView = new ColorSelectOrCustomColorInputView({ + model, + label: 'Background Color', + customColorTranslationKey: 'pageflow_scrolled.editor.custom_color', + values: ['brand-red', 'brand-green'], + texts: ['Red', 'Green'], + propertyName: 'color' + }); + + const user = userEvent.setup(); + const {getByRole, queryByRole} = render(inputView); + + expect(queryByRole('textbox')).toBeNull(); + + await user.click(getByRole('button', {name: 'Red'})); + await user.click(getByRole('option', {name: 'Custom Color'})); + + expect(getByRole('textbox')).not.toBeNull(); + + await user.click(getByRole('button', {name: 'Custom Color'})); + await user.click(getByRole('option', {name: 'Red'})); + + expect(queryByRole('textbox')).toBeNull(); + }); + + it('uses provided translations', async () => { + const model = new Backbone.Model(); + + const inputView = new ColorSelectOrCustomColorInputView({ + model, + label: 'Background Color', + includeBlank: true, + blankTranslationKey: 'pageflow_scrolled.editor.blank', + customColorTranslationKey: 'pageflow_scrolled.editor.custom_color', + values: ['brand-red', 'brand-green'], + texts: ['Red', 'Green'], + propertyName: 'color' + }); + + const {getByText, getByRole} = render(inputView); + + expect(getByText('Background Color')).not.toBeNull(); + + // Click the button to open the dropdown + await userEvent.click(getByRole('button')); + + expect(getByRole('option', {name: 'Auto'})).not.toBeNull(); + }); +}); diff --git a/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/index.js b/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/index.js index 6f2ec8c20e..ab5386d3c8 100644 --- a/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/index.js +++ b/entry_types/scrolled/package/src/contentElements/externalLinkList/editor/index.js @@ -1,5 +1,4 @@ import {editor} from 'pageflow-scrolled/editor'; -import {features} from 'pageflow/frontend'; import {SelectInputView, SliderInputView, SeparatorView, CheckBoxInputView} from 'pageflow/ui'; import {SidebarRouter} from './SidebarRouter'; diff --git a/entry_types/scrolled/package/src/contentElements/textBlock/editor.js b/entry_types/scrolled/package/src/contentElements/textBlock/editor.js index be91fc2829..9548aaa7f2 100644 --- a/entry_types/scrolled/package/src/contentElements/textBlock/editor.js +++ b/entry_types/scrolled/package/src/contentElements/textBlock/editor.js @@ -11,9 +11,35 @@ editor.contentElementTypes.register('textBlock', { supportedPositions: ['inline'], configurationEditor({entry, contentElement}) { - this.listenTo(contentElement.transientState, - 'change:exampleNode', - () => this.refresh()); + let pendingRefresh; + + this.listenTo( + contentElement.transientState, + 'change:exampleNode', + () => { + // This is a terrible hack to prevent closing the minicolors + // dropdown while adjusting colors. Calling refresh is needed + // to update typography drop downs. Delay until color picker + // is closed. + if (document.activeElement && + document.activeElement.tagName === 'INPUT' && + document.activeElement.className === 'minicolors-input') { + + if (!pendingRefresh) { + document.activeElement.addEventListener('blur', () => { + pendingRefresh = false; + this.refresh() + }, {once: true}); + + pendingRefresh = true; + } + + return; + } + + this.refresh() + } + ); this.tab('general', function() { const exampleNode = ensureTextContent( 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 29ca35035d..991cae6589 100644 --- a/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js +++ b/entry_types/scrolled/package/src/editor/models/ScrolledEntry/index.js @@ -10,6 +10,8 @@ import {insertContentElement} from './insertContentElement'; import {moveContentElement} from './moveContentElement'; import {deleteContentElement} from './deleteContentElement'; +import {sortColors} from './sortColors'; + const typographySizeSuffixes = ['xl', 'lg', 'md', 'sm', 'xs']; export const ScrolledEntry = Entry.extend({ @@ -276,6 +278,22 @@ export const ScrolledEntry = Entry.extend({ return [values, texts]; }, + getUsedSectionBackgroundColors() { + const colors = new Set(); + + this.sections.map(section => { + if (section.configuration.get('backdropType') === 'color') { + colors.add(section.configuration.get('backdropColor')); + } + + if (section.configuration.get('appearance') === 'cards') { + colors.add(section.configuration.get('cardSurfaceColor')); + } + }); + + return sortColors([...colors].filter(Boolean)); + }, + supportsSectionWidths() { const theme = this.scrolledSeed.config.theme; diff --git a/entry_types/scrolled/package/src/editor/models/ScrolledEntry/sortColors.js b/entry_types/scrolled/package/src/editor/models/ScrolledEntry/sortColors.js new file mode 100644 index 0000000000..6e0a3e5853 --- /dev/null +++ b/entry_types/scrolled/package/src/editor/models/ScrolledEntry/sortColors.js @@ -0,0 +1,60 @@ +export function sortColors(colors) { + return colors.sort((hex1, hex2) => { + const [h1, s1, l1] = hexToHSL(hex1); + const [h2, s2, l2] = hexToHSL(hex2); + + return (h1 - h2) || (s1 - s2) || (l1 - l2); + }); +} + +function hexToHSL(hex) { + let [r, g, b] = hexToRGB(hex); + + r /= 255; + g /= 255; + b /= 255; + + let cmin = Math.min(r,g,b), + cmax = Math.max(r,g,b), + delta = cmax - cmin, + h = 0, + s = 0, + l = 0; + + if (delta === 0) + h = 0; + else if (cmax === r) + h = ((g - b) / delta) % 6; + else if (cmax === g) + h = (b - r) / delta + 2; + else + h = (r - g) / delta + 4; + + h = Math.round(h * 60); + + if (h < 0) + h += 360; + + l = (cmax + cmin) / 2; + s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); + s = +(s * 100).toFixed(1); + l = +(l * 100).toFixed(1); + + return [h, s, l]; +} + +function hexToRGB(hex) { + let r = 0, g = 0, b = 0; + + if (hex.length === 4) { + r = "0x" + hex[1] + hex[1]; + g = "0x" + hex[2] + hex[2]; + b = "0x" + hex[3] + hex[3]; + } else if (hex.length === 7) { + r = "0x" + hex[1] + hex[2]; + g = "0x" + hex[3] + hex[4]; + b = "0x" + hex[5] + hex[6]; + } + + return [r, g, b]; +} diff --git a/entry_types/scrolled/package/src/editor/views/EditSectionView.js b/entry_types/scrolled/package/src/editor/views/EditSectionView.js index 335a35a874..4c6fd5c829 100644 --- a/entry_types/scrolled/package/src/editor/views/EditSectionView.js +++ b/entry_types/scrolled/package/src/editor/views/EditSectionView.js @@ -101,7 +101,8 @@ export const EditSectionView = EditConfigurationView.extend({ }); this.input('backdropColor', ColorInputView, { visibleBinding: 'backdropType', - visibleBindingValue: 'color' + visibleBindingValue: 'color', + swatches: entry.getUsedSectionBackgroundColors() }); this.input('backdropContentElement', BackdropContentElementInputView, { @@ -151,6 +152,17 @@ export const EditSectionView = EditConfigurationView.extend({ (!exposeMotifArea || motifAreaDisabled(motifAreaDisabledBindingValues)) && backdropType !== 'contentElement' }); + if (features.isEnabled('custom_palette_colors')) { + this.input('cardSurfaceColor', ColorInputView, { + visibleBinding: 'appearance', + visibleBindingValue: 'cards', + placeholder: I18n.t('pageflow_scrolled.editor.edit_section.attributes.cardSurfaceColor.auto'), + placeholderColorBinding: 'invert', + placeholderColor: invert => invert ? '#101010' : '#ffffff', + swatches: entry.getUsedSectionBackgroundColors() + }); + } + this.view(SeparatorView); this.input('atmoAudioFileId', FileInputView, { 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 08a88cbc18..120fb19973 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 @@ -1,9 +1,20 @@ -import {ConfigurationEditorTabView, CheckBoxInputView, SelectInputView, SliderInputView} from 'pageflow/ui'; +import {features} from 'pageflow/frontend'; + +import { + ConfigurationEditorTabView, + CheckBoxInputView, + SelectInputView, + SliderInputView +} from 'pageflow/ui'; import { TypographyVariantSelectInputView } from '../../inputs/TypographyVariantSelectInputView'; +import { + ColorSelectOrCustomColorInputView +} from '../../inputs/ColorSelectOrCustomColorInputView'; + import { ColorSelectInputView } from '../../inputs/ColorSelectInputView'; @@ -144,14 +155,20 @@ ConfigurationEditorTabView.groups.define( 'PaletteColor', function({propertyName, entry, model}) { const [values, texts] = entry.getPaletteColors(); + const inputView = features.isEnabled('custom_palette_colors') ? + ColorSelectOrCustomColorInputView : + ColorSelectInputView; if (values.length) { - this.input(propertyName, ColorSelectInputView, { + this.input(propertyName, inputView, { model: model || this.model, includeBlank: true, blankTranslationKey: 'pageflow_scrolled.editor.' + 'common_content_element_attributes.' + 'palette_color.blank', + customColorTranslationKey: 'pageflow_scrolled.editor.' + + 'common_content_element_attributes.' + + 'palette_color.custom', values, texts, }); diff --git a/entry_types/scrolled/package/src/editor/views/inputs/ColorSelectInputView.js b/entry_types/scrolled/package/src/editor/views/inputs/ColorSelectInputView.js index fcc02546af..5aa34c4f1b 100644 --- a/entry_types/scrolled/package/src/editor/views/inputs/ColorSelectInputView.js +++ b/entry_types/scrolled/package/src/editor/views/inputs/ColorSelectInputView.js @@ -16,7 +16,7 @@ function renderItem(item) { [{cssColorPropertyPrefix: '--theme-palette-color'}]; return ( -
+
{swatches.map((swatch, index) =>
{ + if (value === 'custom') { + this.model.set(this.options.propertyName, model.get('color')); + } + else { + this.model.set(this.options.propertyName, value); + } + + this.updateCustomColorInputView(); + }); + + this.listenTo(this.viewModel, `change:color`, (model, color) => { + if (model.get('value') === 'custom') { + this.model.set(this.options.propertyName, color); + } + }); + }, + + render() { + this.colorSelectInputView = new ColorSelectInputView({ + ...this.options, + label: this.labelText(), + model: this.viewModel, + propertyName: 'value', + values: [ + ...this.options.values, + 'custom' + ], + texts: [ + ...this.options.texts, + I18n.t(this.options.customColorTranslationKey) + ] + }) + + this.appendSubview(this.colorSelectInputView); + this.updateCustomColorInputView(); + + return this; + }, + + updateCustomColorInputView() { + const customColor = this.viewModel.get('value') === 'custom'; + + if (customColor && !this.colorInputView) { + this.colorInputView = new ColorInputView({ + model: this.viewModel, + propertyName: 'color' + }); + + this.appendSubview(this.colorInputView); + } + else if (!customColor && this.colorInputView) { + this.colorInputView.close(); + this.colorInputView = null; + } + } +}); diff --git a/entry_types/scrolled/package/src/editor/views/inputs/ColorSelectOrCustomColorInputView.module.css b/entry_types/scrolled/package/src/editor/views/inputs/ColorSelectOrCustomColorInputView.module.css new file mode 100644 index 0000000000..8ca269463d --- /dev/null +++ b/entry_types/scrolled/package/src/editor/views/inputs/ColorSelectOrCustomColorInputView.module.css @@ -0,0 +1,7 @@ +.container :global(.color_input) { + margin-top: space(2); +} + +.container :global(.color_input) label { + display: none; +} diff --git a/entry_types/scrolled/package/src/editor/views/inputs/ListboxInputView.js b/entry_types/scrolled/package/src/editor/views/inputs/ListboxInputView.js index 7306414afa..bacc753961 100644 --- a/entry_types/scrolled/package/src/editor/views/inputs/ListboxInputView.js +++ b/entry_types/scrolled/package/src/editor/views/inputs/ListboxInputView.js @@ -52,7 +52,7 @@ export const ListboxInputView = Marionette.ItemView.extend({ if (this.options.includeBlank) { this.items.unshift({ - name: '', + value: '', text: I18n.t(this.options.blankTranslationKey) }); } diff --git a/entry_types/scrolled/package/src/frontend/Section.js b/entry_types/scrolled/package/src/frontend/Section.js index 75d0f02771..0d2f88dbd0 100644 --- a/entry_types/scrolled/package/src/frontend/Section.js +++ b/entry_types/scrolled/package/src/frontend/Section.js @@ -161,7 +161,12 @@ function SectionContents({ appearance={section.appearance} contentAreaRef={setContentAreaRef} sectionProps={sectionProperties}> - {(children, boxProps) => {children}} + {(children, boxProps) => + + {children} + } diff --git a/entry_types/scrolled/package/src/frontend/__stories__/appearance-stories.js b/entry_types/scrolled/package/src/frontend/__stories__/appearance-stories.js index b231814f5b..24ef2b3358 100644 --- a/entry_types/scrolled/package/src/frontend/__stories__/appearance-stories.js +++ b/entry_types/scrolled/package/src/frontend/__stories__/appearance-stories.js @@ -182,13 +182,28 @@ storiesOf(`Frontend/Section Appearance`, module) } ) +storiesOf(`Frontend/Section Appearance`, module) + .add( + 'Custom Card Surface Color', + () => { + return ( + + + + ); + } + ) + function exampleSeed( - appearance, {short, invert, width, themeOptions, positionedElementTypeName, includeWidths} = {} + appearance, + {short, invert, width, cardSurfaceColor, + themeOptions, positionedElementTypeName, includeWidths} = {} ) { const sectionBaseConfiguration = { appearance, transition: 'reveal', fullHeight: true, + cardSurfaceColor, invert, width }; diff --git a/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.js b/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.js index 4e92132126..176fbdc2ce 100644 --- a/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.js +++ b/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.js @@ -11,8 +11,9 @@ export default function CardBoxWrapper(props) { return props.children; } - return( -
+ return ( +
{props.children} @@ -31,6 +32,7 @@ function className(props) { styles.card, props.inverted ? styles.cardBgBlack : styles.cardBgWhite, styles[`selfClear-${props.selfClear}`], + {[styles.blur]: props.cardSurfaceTransparency > 0}, {[styles.cardStart]: !props.openStart}, {[styles.cardEnd]: !props.openEnd} ); diff --git a/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.module.css b/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.module.css index 5ed81df5a6..ee1d90c3d2 100644 --- a/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.module.css +++ b/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.module.css @@ -72,11 +72,11 @@ @media screen { .cardBgWhite::before { - background-color: lightContentSurfaceColor; + background-color: var(--card-surface-color, lightContentSurfaceColor); } .cardBgBlack::before { - background-color: darkContentSurfaceColor; + background-color: var(--card-surface-color, darkContentSurfaceColor); } .cardBgWhite { diff --git a/package/spec/ui/views/inputs/ColorInputView-spec.js b/package/spec/ui/views/inputs/ColorInputView-spec.js index a48598bc58..84d0830df2 100644 --- a/package/spec/ui/views/inputs/ColorInputView-spec.js +++ b/package/spec/ui/views/inputs/ColorInputView-spec.js @@ -1,5 +1,6 @@ import Backbone from 'backbone'; import sinon from 'sinon'; +import '@testing-library/jest-dom/extend-expect'; import {ColorInputView} from 'pageflow/ui'; @@ -497,4 +498,59 @@ describe('pageflow.ColorInputView', () => { expect(model.has('color')).toBe(false); }); }); + + describe('with placeholderColor option', () => { + it('sets custom property', () => { + var colorInputView = new ColorInputView({ + model: new Backbone.Model(), + propertyName: 'color', + placeholderColor: '#fff' + }); + + var colorInput = ColorInput.render( + colorInputView, + {appendTo: testContext.htmlSandbox} + ); + + expect(colorInput.$el[0]).toHaveStyle('--placeholder-color: #fff'); + }); + }); + + describe('with function as placeholderColor and placeholderColorBinding option', () => { + it('sets custom property', () => { + var model = new Backbone.Model(); + var colorInputView = new ColorInputView({ + model, + propertyName: 'color', + placeholderColorBinding: 'invert', + placeholderColor: invert => invert ? '#000' : '#fff' + }); + + var colorInput = ColorInput.render( + colorInputView, + {appendTo: testContext.htmlSandbox} + ); + + expect(colorInput.$el[0]).toHaveStyle('--placeholder-color: #fff'); + }); + + it('updates custom property', () => { + var model = new Backbone.Model(); + var colorInputView = new ColorInputView({ + model, + propertyName: 'color', + placeholderColorBinding: 'invert', + placeholderColor: invert => invert ? '#000' : '#fff' + }); + + var colorInput = ColorInput.render( + colorInputView, + {appendTo: testContext.htmlSandbox} + ); + + model.set('invert', true); + + expect(colorInput.$el[0]).toHaveStyle('--placeholder-color: #000'); + }); + }); }); diff --git a/package/src/ui/templates/inputs/colorInput.jst b/package/src/ui/templates/inputs/colorInput.jst new file mode 100644 index 0000000000..65697a6305 --- /dev/null +++ b/package/src/ui/templates/inputs/colorInput.jst @@ -0,0 +1,5 @@ + + diff --git a/package/src/ui/views/inputs/ColorInputView.js b/package/src/ui/views/inputs/ColorInputView.js index 84f3853e10..8f452e7169 100644 --- a/package/src/ui/views/inputs/ColorInputView.js +++ b/package/src/ui/views/inputs/ColorInputView.js @@ -3,12 +3,15 @@ import _ from 'underscore'; import 'jquery.minicolors'; import {inputView} from '../mixins/inputView'; +import {inputWithPlaceholderText} from '../mixins/inputWithPlaceholderText'; -import template from '../../templates/inputs/textInput.jst'; +import template from '../../templates/inputs/colorInput.jst'; /** * Input view for a color value in hex representation. - * See {@link inputView} for further options + * + * See {@link inputWithPlaceholderText} for placeholder related + * further options. See {@link inputView} for further options. * * @param {Object} [options] * @@ -24,6 +27,14 @@ import template from '../../templates/inputs/textInput.jst'; * defaultValue option is set, the value of the defaultValueBinding * attribute will be used as default value. * + * @param {string|function} [options.placeholderColor] + * Color to display in swatch by default. + * + * @param {string} [options.placeholderColorBinding] + * Name of an attribute the placeholder color depends on. If a function + * is used as placeholderColor option, it will be passed the value of the + * placeholderColorBinding attribute each time it changes. + * * @param {string[]} [options.swatches] * Preset color values to be displayed inside the picker drop * down. The default value, if present, is always used as the @@ -32,7 +43,7 @@ import template from '../../templates/inputs/textInput.jst'; * @class */ export const ColorInputView = Marionette.ItemView.extend({ - mixins: [inputView], + mixins: [inputView, inputWithPlaceholderText], template, className: 'color_input', @@ -46,6 +57,8 @@ export const ColorInputView = Marionette.ItemView.extend({ }, onRender: function() { + this.setupAttributeBinding('placeholderColor', this.updatePlaceholderColor); + this.ui.input.minicolors({ changeDelay: 200, change: _.bind(function(color) { @@ -71,6 +84,10 @@ export const ColorInputView = Marionette.ItemView.extend({ this.updateSettings(); }, + updatePlaceholderColor(value) { + this.el.style.setProperty('--placeholder-color', value); + }, + updateSettings: function() { this.resetSwatchesInStoredSettings();