mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-11-03 20:34:40 +01:00 
			
		
		
		
	refactor: make element type conversion more generic (#9504)
* feat: add `reduceToCommonValue()` & improve opacity slider * feat: generalize and simplify type conversion cache * refactor: change cache from atoms to Map * feat: always attempt to reuse original fontSize when converting generic types
This commit is contained in:
		
							
								
								
									
										82
									
								
								packages/common/src/utils.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								packages/common/src/utils.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
import {
 | 
			
		||||
  isTransparent,
 | 
			
		||||
  mapFind,
 | 
			
		||||
  reduceToCommonValue,
 | 
			
		||||
} from "@excalidraw/common";
 | 
			
		||||
 | 
			
		||||
describe("@excalidraw/common/utils", () => {
 | 
			
		||||
  describe("isTransparent()", () => {
 | 
			
		||||
    it("should return true when color is rgb transparent", () => {
 | 
			
		||||
      expect(isTransparent("#ff00")).toEqual(true);
 | 
			
		||||
      expect(isTransparent("#fff00000")).toEqual(true);
 | 
			
		||||
      expect(isTransparent("transparent")).toEqual(true);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should return false when color is not transparent", () => {
 | 
			
		||||
      expect(isTransparent("#ced4da")).toEqual(false);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("reduceToCommonValue()", () => {
 | 
			
		||||
    it("should return the common value when all values are the same", () => {
 | 
			
		||||
      expect(reduceToCommonValue([1, 1])).toEqual(1);
 | 
			
		||||
      expect(reduceToCommonValue([0, 0])).toEqual(0);
 | 
			
		||||
      expect(reduceToCommonValue(["a", "a"])).toEqual("a");
 | 
			
		||||
      expect(reduceToCommonValue(new Set([1]))).toEqual(1);
 | 
			
		||||
      expect(reduceToCommonValue([""])).toEqual("");
 | 
			
		||||
      expect(reduceToCommonValue([0])).toEqual(0);
 | 
			
		||||
 | 
			
		||||
      const o = {};
 | 
			
		||||
      expect(reduceToCommonValue([o, o])).toEqual(o);
 | 
			
		||||
 | 
			
		||||
      expect(
 | 
			
		||||
        reduceToCommonValue([{ a: 1 }, { a: 1, b: 2 }], (o) => o.a),
 | 
			
		||||
      ).toEqual(1);
 | 
			
		||||
      expect(
 | 
			
		||||
        reduceToCommonValue(new Set([{ a: 1 }, { a: 1, b: 2 }]), (o) => o.a),
 | 
			
		||||
      ).toEqual(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should return `null` when values are different", () => {
 | 
			
		||||
      expect(reduceToCommonValue([1, 2, 3])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue(new Set([1, 2]))).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([{ a: 1 }, { a: 2 }], (o) => o.a)).toEqual(
 | 
			
		||||
        null,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should return `null` when some values are nullable", () => {
 | 
			
		||||
      expect(reduceToCommonValue([1, null, 1])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([null, 1])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([1, undefined])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([undefined, 1])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([null])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([undefined])).toEqual(null);
 | 
			
		||||
      expect(reduceToCommonValue([])).toEqual(null);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("mapFind()", () => {
 | 
			
		||||
    it("should return the first mapped non-null element", () => {
 | 
			
		||||
      {
 | 
			
		||||
        let counter = 0;
 | 
			
		||||
 | 
			
		||||
        const result = mapFind(["a", "b", "c"], (value) => {
 | 
			
		||||
          counter++;
 | 
			
		||||
          return value === "b" ? 42 : null;
 | 
			
		||||
        });
 | 
			
		||||
        expect(result).toEqual(42);
 | 
			
		||||
        expect(counter).toBe(2);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      expect(mapFind([1, 2], (value) => value * 0)).toBe(0);
 | 
			
		||||
      expect(mapFind([1, 2], () => false)).toBe(false);
 | 
			
		||||
      expect(mapFind([1, 2], () => "")).toBe("");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should return undefined if no mapped element is found", () => {
 | 
			
		||||
      expect(mapFind([1, 2], () => undefined)).toBe(undefined);
 | 
			
		||||
      expect(mapFind([1, 2], () => null)).toBe(undefined);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -544,6 +544,20 @@ export const findLastIndex = <T>(
 | 
			
		||||
  return -1;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** returns the first non-null mapped value */
 | 
			
		||||
export const mapFind = <T, K>(
 | 
			
		||||
  collection: readonly T[],
 | 
			
		||||
  iteratee: (value: T, index: number) => K | undefined | null,
 | 
			
		||||
): K | undefined => {
 | 
			
		||||
  for (let idx = 0; idx < collection.length; idx++) {
 | 
			
		||||
    const result = iteratee(collection[idx], idx);
 | 
			
		||||
    if (result != null) {
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return undefined;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const isTransparent = (color: string) => {
 | 
			
		||||
  const isRGBTransparent = color.length === 5 && color.substr(4, 1) === "0";
 | 
			
		||||
  const isRRGGBBTransparent = color.length === 9 && color.substr(7, 2) === "00";
 | 
			
		||||
@@ -1244,11 +1258,39 @@ export const isReadonlyArray = (value?: any): value is readonly any[] => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const sizeOf = (
 | 
			
		||||
  value: readonly number[] | Readonly<Map<any, any>> | Record<any, any>,
 | 
			
		||||
  value:
 | 
			
		||||
    | readonly unknown[]
 | 
			
		||||
    | Readonly<Map<string, unknown>>
 | 
			
		||||
    | Readonly<Record<string, unknown>>
 | 
			
		||||
    | ReadonlySet<unknown>,
 | 
			
		||||
): number => {
 | 
			
		||||
  return isReadonlyArray(value)
 | 
			
		||||
    ? value.length
 | 
			
		||||
    : value instanceof Map
 | 
			
		||||
    : value instanceof Map || value instanceof Set
 | 
			
		||||
    ? value.size
 | 
			
		||||
    : Object.keys(value).length;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const reduceToCommonValue = <T, R = T>(
 | 
			
		||||
  collection: readonly T[] | ReadonlySet<T>,
 | 
			
		||||
  getValue?: (item: T) => R,
 | 
			
		||||
): R | null => {
 | 
			
		||||
  if (sizeOf(collection) === 0) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const valueExtractor = getValue || ((item: T) => item as unknown as R);
 | 
			
		||||
 | 
			
		||||
  let commonValue: R | null = null;
 | 
			
		||||
 | 
			
		||||
  for (const item of collection) {
 | 
			
		||||
    const value = valueExtractor(item);
 | 
			
		||||
    if ((commonValue === null || commonValue === value) && value != null) {
 | 
			
		||||
      commonValue = value;
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return commonValue;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user