diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e5a3abb630..6ec75d6232e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- refactor(): Align shadow with class registry, part of #9144 [#9626](https://github.com/fabricjs/fabric.js/pull/9626) - cd() Surface the minified build as standard when importing. [#9624](https://github.com/fabricjs/fabric.js/pull/9624) - chore(): removed unused code from Path render function [#9619](https://github.com/fabricjs/fabric.js/pull/9619) diff --git a/src/Shadow.ts b/src/Shadow.ts index 1223ca1e5f5..29316d2adf7 100644 --- a/src/Shadow.ts +++ b/src/Shadow.ts @@ -1,5 +1,7 @@ +import { classRegistry } from './ClassRegistry'; import { Color } from './color/Color'; import { config } from './config'; +import { reNum } from './parser/constants'; import { Point } from './Point'; import type { FabricObject } from './shapes/Object/FabricObject'; import type { TClassProperties } from './typedefs'; @@ -9,6 +11,36 @@ import { degreesToRadians } from './util/misc/radiansDegreesConversion'; import { toFixed } from './util/misc/toFixed'; import { rotateVector } from './util/misc/vectors'; +/** + * Regex matching shadow offsetX, offsetY and blur (ex: "2px 2px 10px rgba(0,0,0,0.2)", "rgb(0,255,0) 2px 2px") + * - (?:\s|^): This part captures either a whitespace character (\s) or the beginning of a line (^). It's non-capturing (due to (?:...)), meaning it doesn't create a capturing group. + * - (-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?: This captures the first component of the shadow, which is the horizontal offset. Breaking it down: + * - (-?\d+): Captures an optional minus sign followed by one or more digits (integer part of the number). + * - (?:\.\d*)?: Optionally captures a decimal point followed by zero or more digits (decimal part of the number). + * - (?:px)?: Optionally captures the "px" unit. + * - (?:\s?|$): Captures either an optional whitespace or the end of the line. This whole part is wrapped in a non-capturing group and marked as optional with ?. + * - (-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?: Similar to the previous step, this captures the vertical offset. + +(\d+(?:\.\d*)?(?:px)?)?: This captures the blur radius. It's similar to the horizontal offset but without the optional minus sign. + +(?:\s+(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?){0,1}: This captures an optional part for the color. It allows for whitespace followed by a component with an optional minus sign, digits, decimal point, and "px" unit. + +(?:$|\s): This captures either the end of the line or a whitespace character. It ensures that the match ends either at the end of the string or with a whitespace character. + */ +// eslint-disable-next-line max-len + +const shadowOffsetRegex = '(-?\\d+(?:\\.\\d*)?(?:px)?(?:\\s?|$))?'; + +const reOffsetsAndBlur = () => + new RegExp( + '(?:\\s|^)' + + shadowOffsetRegex + + shadowOffsetRegex + + '(' + + reNum + + '?(?:px)?)?(?:\\s?|$)(?:$|\\s)' + ); + export const shadowDefaultValues: Partial> = { color: 'rgb(0,0,0)', blur: 0, @@ -26,6 +58,7 @@ export type SerializedShadowOptions = { offsetY: number; affectStroke: boolean; nonScaling: boolean; + type: string; }; export class Shadow { @@ -82,6 +115,9 @@ export class Shadow { declare id: number; static ownDefaults = shadowDefaultValues; + + static type = 'shadow'; + /** * @see {@link http://fabricjs.com/shadows|Shadow demo} * @param {Object|String} [options] Options object with any of color, blur, offsetX, offsetY properties or string (e.g. "rgba(0,0,0,0.2) 2px 2px 10px") @@ -106,12 +142,11 @@ export class Shadow { */ static parseShadow(value: string) { const shadowStr = value.trim(), - [__, offsetX = 0, offsetY = 0, blur = 0] = ( - Shadow.reOffsetsAndBlur.exec(shadowStr) || [] + regex = reOffsetsAndBlur(), + [, offsetX = 0, offsetY = 0, blur = 0] = ( + regex.exec(shadowStr) || [] ).map((value) => parseFloat(value) || 0), - color = ( - shadowStr.replace(Shadow.reOffsetsAndBlur, '') || 'rgb(0,0,0)' - ).trim(); + color = (shadowStr.replace(regex, '') || 'rgb(0,0,0)').trim(); return { color, @@ -198,17 +233,17 @@ export class Shadow { offsetY: this.offsetY, affectStroke: this.affectStroke, nonScaling: this.nonScaling, + type: (this.constructor as typeof Shadow).type, }; - const defaults = Shadow.ownDefaults; + const defaults = Shadow.ownDefaults as SerializedShadowOptions; return !this.includeDefaultValues ? pickBy(data, (value, key) => value !== defaults[key]) : data; } - /** - * Regex matching shadow offsetX, offsetY and blur (ex: "2px 2px 10px rgba(0,0,0,0.2)", "rgb(0,255,0) 2px 2px") - */ - // eslint-disable-next-line max-len - static reOffsetsAndBlur = - /(?:\s|^)(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(-?\d+(?:\.\d*)?(?:px)?(?:\s?|$))?(\d+(?:\.\d*)?(?:px)?)?(?:\s?|$)(?:$|\s)/; + static fromObject(options: Partial>) { + return new this(options); + } } + +classRegistry.setClass(Shadow, 'shadow'); diff --git a/src/parser/selectorMatches.ts b/src/parser/selectorMatches.ts index 3c04a2d0284..fe149276e2a 100644 --- a/src/parser/selectorMatches.ts +++ b/src/parser/selectorMatches.ts @@ -2,22 +2,20 @@ export function selectorMatches(element: HTMLElement, selector: string) { const nodeName = element.nodeName; const classNames = element.getAttribute('class'); const id = element.getAttribute('id'); + const azAz = '(?![a-zA-Z\\-]+)'; let matcher; // i check if a selector matches slicing away part from it. // if i get empty string i should match matcher = new RegExp('^' + nodeName, 'i'); selector = selector.replace(matcher, ''); if (id && selector.length) { - matcher = new RegExp('#' + id + '(?![a-zA-Z\\-]+)', 'i'); + matcher = new RegExp('#' + id + azAz, 'i'); selector = selector.replace(matcher, ''); } if (classNames && selector.length) { const splitClassNames = classNames.split(' '); for (let i = splitClassNames.length; i--; ) { - matcher = new RegExp( - '\\.' + splitClassNames[i] + '(?![a-zA-Z\\-]+)', - 'i' - ); + matcher = new RegExp('\\.' + splitClassNames[i] + azAz, 'i'); selector = selector.replace(matcher, ''); } } diff --git a/src/util/misc/objectEnlive.ts b/src/util/misc/objectEnlive.ts index e160f85baeb..7439905b9cc 100644 --- a/src/util/misc/objectEnlive.ts +++ b/src/util/misc/objectEnlive.ts @@ -13,6 +13,7 @@ import type { BaseFilter } from '../../filters/BaseFilter'; import type { FabricObject as BaseFabricObject } from '../../shapes/Object/Object'; import { FabricError, SignalAbortedError } from '../internals/console'; import type { Gradient } from '../../gradient'; +import type { Shadow } from '../../Shadow'; export type LoadImageOptions = Abortable & { /** @@ -67,7 +68,7 @@ export type EnlivenObjectOptions = Abortable & { * Method for further parsing of object elements, * called after each fabric object created. */ - reviver?: ( + reviver?: ( serializedObj: Record, instance: T ) => void; @@ -83,7 +84,7 @@ export type EnlivenObjectOptions = Abortable & { * @returns {Promise} */ export const enlivenObjects = < - T extends BaseFabricObject | FabricObject | BaseFilter + T extends BaseFabricObject | FabricObject | BaseFilter | Shadow >( objects: any[], { signal, reviver = noop }: EnlivenObjectOptions = {} @@ -135,7 +136,7 @@ export const enlivenObjectEnlivables = < { signal }: Abortable = {} ) => new Promise((resolve, reject) => { - const instances: (FabricObject | TFiller)[] = []; + const instances: (FabricObject | TFiller | Shadow)[] = []; signal && signal.addEventListener('abort', reject, { once: true }); // enlive every possible property const promises = Object.values(serializedObject).map((value: any) => { @@ -146,9 +147,9 @@ export const enlivenObjectEnlivables = < if (value.colorStops) { return new (classRegistry.getClass('gradient'))(value); } - // clipPath + // clipPath or shadow if (value.type) { - return enlivenObjects([value], { signal }).then( + return enlivenObjects([value], { signal }).then( ([enlived]) => { instances.push(enlived); return enlived; diff --git a/test/unit/shadow.js b/test/unit/shadow.js index 7f18b598543..214cc5de973 100644 --- a/test/unit/shadow.js +++ b/test/unit/shadow.js @@ -160,7 +160,7 @@ assert.ok(typeof shadow.toObject === 'function'); var object = shadow.toObject(); - assert.equal(JSON.stringify(object), '{"color":"rgb(0,0,0)","blur":0,"offsetX":0,"offsetY":0,"affectStroke":false,"nonScaling":false}'); + assert.equal(JSON.stringify(object), '{"color":"rgb(0,0,0)","blur":0,"offsetX":0,"offsetY":0,"affectStroke":false,"nonScaling":false,"type":"shadow"}'); }); QUnit.test('clone with affectStroke', function(assert) { @@ -177,13 +177,18 @@ var shadow = new fabric.Shadow(); shadow.includeDefaultValues = false; - assert.equal(JSON.stringify(shadow.toObject()), '{}'); + assert.equal(JSON.stringify(shadow.toObject()), '{"type":"shadow"}'); shadow.color = 'red'; - assert.equal(JSON.stringify(shadow.toObject()), '{"color":"red"}'); + assert.equal(JSON.stringify(shadow.toObject()), '{"color":"red","type":"shadow"}'); shadow.offsetX = 15; - assert.equal(JSON.stringify(shadow.toObject()), '{"color":"red","offsetX":15}'); + assert.equal(JSON.stringify(shadow.toObject()), '{"color":"red","offsetX":15,"type":"shadow"}'); + }); + + QUnit.test('fromObject', assert => { + const shadow = fabric.Shadow.fromObject({ color: 'red', offsetX: 15 }); + assert.ok(shadow instanceof fabric.Shadow); }); QUnit.test('toSVG', function(assert) {