From 54bfd0f331928611ecd07b012787c2f23b3a9c3b Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 14 Feb 2024 22:05:13 +0400 Subject: [PATCH] Add `emptyValue` property to StyleManager stack type. Closes #5583 --- src/style_manager/index.ts | 35 ++++--- src/style_manager/model/PropertyStack.ts | 104 +++++++++++++++---- src/style_manager/view/PropertyStackView.ts | 5 +- test/specs/style_manager/model/Properties.ts | 10 +- 4 files changed, 111 insertions(+), 43 deletions(-) diff --git a/src/style_manager/index.ts b/src/style_manager/index.ts index 5235bdfd8..c78953d24 100644 --- a/src/style_manager/index.ts +++ b/src/style_manager/index.ts @@ -81,7 +81,8 @@ import StyleableModel, { StyleProps } from '../domain_abstract/model/StyleableMo import { CustomPropertyView } from './view/PropertyView'; import { PropertySelectProps } from './model/PropertySelect'; import { PropertyNumberProps } from './model/PropertyNumber'; -import { PropertyStackProps } from './model/PropertyStack'; +import PropertyStack, { PropertyStackProps } from './model/PropertyStack'; +import PropertyComposite from './model/PropertyComposite'; export type PropertyTypes = PropertyStackProps | PropertySelectProps | PropertyNumberProps; @@ -785,7 +786,7 @@ export default class StyleManager extends ItemManagerModule< }); } - __upProp(prop: any, style: StyleProps, parentStyles: any[], opts: any) { + __upProp(prop: Property, style: StyleProps, parentStyles: any[], opts: any) { const name = prop.getName(); const value = style[name]; const hasVal = propDef(value); @@ -793,19 +794,21 @@ export default class StyleManager extends ItemManagerModule< const isComposite = prop.getType() === 'composite'; const opt = { ...opts, __up: true }; const canUpdate = !isComposite && !isStack; - let newLayers = isStack ? prop.__getLayersFromStyle(style) : []; - let newProps = isComposite ? prop.__getPropsFromStyle(style) : {}; + const propStack = prop as PropertyStack; + const propComp = prop as PropertyComposite; + let newLayers = isStack ? propStack.__getLayersFromStyle(style) : []; + let newProps = isComposite ? propComp.__getPropsFromStyle(style) : {}; let newValue = hasVal ? value : null; let parentTarget: any = null; if ((isStack && newLayers === null) || (isComposite && newProps === null)) { const method = isStack ? '__getLayersFromStyle' : '__getPropsFromStyle'; - const parentItem = parentStyles.filter(p => prop[method](p.style) !== null)[0]; + const parentItem = parentStyles.filter(p => propStack[method](p.style) !== null)[0]; if (parentItem) { newValue = parentItem.style[name]; parentTarget = parentItem.target; - const val = prop[method](parentItem.style); + const val = propStack[method](parentItem.style); if (isStack) { newLayers = val; } else { @@ -823,22 +826,26 @@ export default class StyleManager extends ItemManagerModule< } prop.__setParentTarget(parentTarget); - canUpdate && prop.__getFullValue() !== newValue && prop.upValue(newValue, opt); - isStack && prop.__setLayers(newLayers || []); + canUpdate && prop.__getFullValue() !== newValue && prop.upValue(newValue as string, opt); + if (isStack) { + propStack.__setLayers(newLayers || [], { + isEmptyValue: propStack.isEmptyValueStyle(style), + }); + } if (isComposite) { - const props = prop.getProperties(); + const props = propComp.getProperties(); // Detached has to be treathed as separate properties - if (prop.isDetached()) { - const newStyle = prop.__getPropsFromStyle(style, { byName: true }) || {}; + if (propComp.isDetached()) { + const newStyle = propComp.__getPropsFromStyle(style, { byName: true }) || {}; const newParentStyles = parentStyles.map(p => ({ ...p, - style: prop.__getPropsFromStyle(p.style, { byName: true }) || {}, + style: propComp.__getPropsFromStyle(p.style, { byName: true }) || {}, })); props.map((pr: any) => this.__upProp(pr, newStyle, newParentStyles, opts)); } else { - prop.__setProperties(newProps || {}, opt); - prop.getProperties().map((pr: any) => pr.__setParentTarget(parentTarget)); + propComp.__setProperties(newProps || {}, opt); + propComp.getProperties().map(pr => pr.__setParentTarget(parentTarget)); } } } diff --git a/src/style_manager/model/PropertyStack.ts b/src/style_manager/model/PropertyStack.ts index bf3e4086c..e92a60aa8 100644 --- a/src/style_manager/model/PropertyStack.ts +++ b/src/style_manager/model/PropertyStack.ts @@ -25,7 +25,7 @@ type FromStyleDataStack = Omit & { separatorLayers: RegExp; }; -export type OptionStyleStack = OptionsStyle & { number?: { min?: number; max?: number } }; +export type OptionStyleStack = OptionsStyle & { number?: { min?: number; max?: number }; __clear?: boolean }; /** @private */ export interface PropertyStackProps extends Omit { @@ -50,13 +50,28 @@ export interface PropertyStackProps extends Omit string; + + /** + * Empty value to apply when all layers are removed. + * @default 'unset' + * @example + * // use simple string + * emptyValue: 'inherit', + * // or a function for a custom style object + * emptyValue: () => ({ + * color: 'unset', + * width: 'auto' + * }), + */ + emptyValue?: string | ((data: { property: PropertyStack }) => PropValues); + toStyle?: (values: PropValues, data: ToStyleDataStack) => ReturnType; fromStyle?: (style: StyleProps, data: FromStyleDataStack) => ReturnType; parseLayer?: (data: { value: string; values: PropValues }) => PropValues; - emptyValue?: string | ((data: { property: PropertyStack }) => PropValues); selectedLayer?: Layer; prepend?: boolean; __layers?: PropValues[]; + isEmptyValue?: boolean; } /** @@ -76,6 +91,17 @@ export interface PropertyStackProps extends Omit ({ + * color: 'unset', + * width: 'auto' + * }), + * ``` * */ export default class PropertyStack extends PropertyComposite { @@ -109,16 +135,24 @@ export default class PropertyStack extends PropertyComposite PropertyComposite.callInit(this, props, opts); } + get layers() { + return this.get('layers') as unknown as Layers; + } + /** * Get all available layers. * @returns {Array<[Layer]>} */ getLayers() { - return this.__getLayers().models; + return this.layers.models; } - __getLayers() { - return this.get('layers') as unknown as Layers; + /** + * Check if the property has layers. + * @returns {Boolean} + */ + hasLayers() { + return this.getLayers().length > 0; } /** @@ -133,7 +167,7 @@ export default class PropertyStack extends PropertyComposite * const layerLast = property.getLayer(layers.length - 1); */ getLayer(index = 0): Layer | undefined { - return this.__getLayers().at(index) || undefined; + return this.layers.at(index) || undefined; } /** @@ -177,11 +211,12 @@ export default class PropertyStack extends PropertyComposite * property.moveLayer(layer, 0); */ moveLayer(layer: Layer, index = 0) { + const { layers } = this; const currIndex = layer ? layer.getIndex() : -1; - if (currIndex >= 0 && isNumber(index) && index >= 0 && index < this.getLayers().length && currIndex !== index) { + if (currIndex >= 0 && isNumber(index) && index >= 0 && index < layers.length && currIndex !== index) { this.removeLayer(layer); - this.__getLayers().add(layer, { at: index }); + layers.add(layer, { at: index }); } } @@ -202,7 +237,7 @@ export default class PropertyStack extends PropertyComposite const value = props[key]; values[key] = isUndefined(value) ? prop.getDefaultValue() : value; }); - const layer = this.__getLayers().push({ values } as any, opts); + const layer = this.layers.push({ values } as any, opts); return layer; } @@ -216,7 +251,7 @@ export default class PropertyStack extends PropertyComposite * property.removeLayer(layer); */ removeLayer(layer: Layer) { - return this.__getLayers().remove(layer); + return this.layers.remove(layer); } /** @@ -344,6 +379,14 @@ export default class PropertyStack extends PropertyComposite return isString(sep) ? new RegExp(`${sep}(?![^\\(]*\\))`) : sep; } + /** + * Check if the property is with an empty value. + * @returns {Boolean} + */ + hasEmptyValue() { + return !this.hasLayers() && !!this.attributes.isEmptyValue; + } + __upProperties(prop: Property, opts: any = {}) { const layer = this.getSelectedLayer(); if (!layer) return; @@ -362,7 +405,7 @@ export default class PropertyStack extends PropertyComposite } __upTargetsStyleProps(opts = {}) { - this.__upTargetsStyle(this.getStyleFromLayers(), opts); + this.__upTargetsStyle(this.getStyleFromLayers(opts), opts); } __upTargetsStyle(style: StyleProps, opts: any) { @@ -394,16 +437,17 @@ export default class PropertyStack extends PropertyComposite return this; } - __setLayers(newLayers: PropValues[] = []) { - const layers = this.__getLayers(); + __setLayers(newLayers: LayerValues[] = [], opts: { isEmptyValue?: boolean } = {}) { + const { layers } = this; const layersNew = newLayers.map(values => ({ values })); if (layers.length === layersNew.length) { layersNew.map((layer, n) => layers.at(n)?.upValues(layer.values)); } else { - this.__getLayers().reset(layersNew); + layers.reset(layersNew); } + this.set({ isEmptyValue: !!opts.isEmptyValue }); this.__upSelected({ noEvent: true }); } @@ -431,13 +475,14 @@ export default class PropertyStack extends PropertyComposite }, {} as PropValues); } - __getLayersFromStyle(style: StyleProps = {}) { + __getLayersFromStyle(style: StyleProps = {}): LayerValues[] | null { if (!this.__styleHasProps(style)) return null; + if (this.isEmptyValueStyle(style)) return []; const name = this.getName(); const props = this.getProperties(); const sep = this.getLayerSeparator(); - const fromStyle = this.get('fromStyle'); + const { fromStyle } = this.attributes; let result = fromStyle ? fromStyle(style, { property: this, name, separatorLayers: sep }) : []; if (!fromStyle) { @@ -473,7 +518,6 @@ export default class PropertyStack extends PropertyComposite getStyleFromLayers(opts: OptionStyleStack = {}) { let result: StyleProps = {}; - const { emptyValue } = this.attributes; const name = this.getName(); const layers = this.getLayers(); const props = this.getProperties(); @@ -508,14 +552,20 @@ export default class PropertyStack extends PropertyComposite return { ...result, - ...this.getEmptyValueStyle(), + ...(opts.__clear ? {} : this.getEmptyValueStyle()), }; } - getEmptyValueStyle() { + isEmptyValueStyle(style: StyleProps = {}) { + const emptyStyle = this.getEmptyValueStyle({ force: true }); + const props = keys(emptyStyle); + return !!props.length && props.every(prop => emptyStyle[prop] === style[prop]); + } + + getEmptyValueStyle(opts: { force?: boolean } = {}) { const { emptyValue } = this.attributes; - if (emptyValue && !this.getLayers().length) { + if (emptyValue && (!this.hasLayers() || opts.force)) { const name = this.getName(); const props = this.getProperties(); const result = isString(emptyValue) ? emptyValue : emptyValue({ property: this }); @@ -561,7 +611,7 @@ export default class PropertyStack extends PropertyComposite hasValue(opts: { noParent?: boolean } = {}) { const { noParent } = opts; const parentValue = noParent && this.getParentTarget(); - return this.getLayers().length > 0 && !parentValue; + return (this.hasLayers() || this.hasEmptyValue()) && !parentValue; } /** @@ -569,8 +619,8 @@ export default class PropertyStack extends PropertyComposite * @private */ clear(opts = {}) { - this.__getLayers().reset(); - this.__upTargetsStyleProps(opts); + this.layers.reset(); + this.__upTargetsStyleProps({ ...opts, __clear: true }); PropertyBase.prototype.clear.call(this); return this; } @@ -578,4 +628,12 @@ export default class PropertyStack extends PropertyComposite __canClearProp() { return false; } + + /** + * @deprecated + * @private + */ + __getLayers() { + return this.layers; + } } diff --git a/src/style_manager/view/PropertyStackView.ts b/src/style_manager/view/PropertyStackView.ts index f7646260b..6ecffdcc9 100644 --- a/src/style_manager/view/PropertyStackView.ts +++ b/src/style_manager/view/PropertyStackView.ts @@ -32,7 +32,8 @@ export default class PropertyStackView extends PropertyCompositeView { init() { const { model } = this; - this.listenTo(model.__getLayers(), 'change reset', this.updateStatus); + this.listenTo(model.layers, 'change reset', this.updateStatus); + this.listenTo(model, 'change:isEmptyValue', this.updateStatus); } addLayer() { @@ -75,7 +76,7 @@ export default class PropertyStackView extends PropertyCompositeView { propsView.render(); const layersView = new LayersView({ - collection: model.__getLayers(), + collection: model.layers, // @ts-ignore config, propertyView: this, diff --git a/test/specs/style_manager/model/Properties.ts b/test/specs/style_manager/model/Properties.ts index aa1835d29..a2ff2321b 100644 --- a/test/specs/style_manager/model/Properties.ts +++ b/test/specs/style_manager/model/Properties.ts @@ -737,12 +737,14 @@ describe('StyleManager properties logic', () => { describe('emptyValue', () => { test('Removing all layers with empty value as string', () => { - compTypeProp.set('emptyValue', 'unset'), compTypeProp.removeLayerAt(1); + compTypeProp.set('emptyValue', 'unset'); + compTypeProp.removeLayerAt(1); compTypeProp.removeLayerAt(0); + const res = { [propTest]: 'unset' }; + expect(compTypeProp.isEmptyValueStyle(res)).toBe(true); + expect(compTypeProp.isEmptyValueStyle({})).toBe(false); expect(compTypeProp.getLayers().length).toBe(0); - expect(rule1.getStyle()).toEqual({ - [propTest]: 'unset', - }); + expect(rule1.getStyle()).toEqual(res); }); test('Removing all layers with empty value as string (detached)', () => {