import { ALIAS, TIMING_FUNCTION, TRANSFORM_NAME, EASING_NAME, NAME_SEPARATOR } from "./consts"; import { isRole, getType, isPropertyObject, getValueByNames, isFixed, getNames, getEasing, isFrame } from "./utils"; import { toPropertyObject, splitStyle, toObject } from "./utils/property"; import { isObject, isArray, isString, getKeys, ANIMATION, TRANSFORM, FILTER, PROPERTY, FUNCTION, ARRAY, OBJECT, IObject, isUndefined, sortOrders, decamelize, camelize, } from "@daybrush/utils"; import { NameType, KeyValueChildren, FrameEvents } from "./types"; import OrderMap from "order-map"; import { Observe } from "@cfcs/core"; import EventEmitter from "@scena/event-emitter"; function toInnerProperties(obj: IObject, orders: NameType[] = []) { if (!obj) { return ""; } const arrObj = []; const keys = getKeys(obj); sortOrders(keys, orders); keys.forEach(name => { arrObj.push(`${name.replace(/\d$/g, "")}(${obj[name]})`); }); return arrObj.join(" "); } /* eslint-disable */ function clone(target: IObject, toValue = false) { return merge({}, target, toValue); } function merge(to: IObject, from: IObject, toValue = false) { for (const name in from) { const value = from[name]; const type = getType(value); if (type === PROPERTY) { to[name] = toValue ? value.toValue() : value.clone(); } else if (type === FUNCTION) { to[name] = toValue ? getValue([name], value) : value; } else if (type === ARRAY) { to[name] = value.slice(); } else if (type === OBJECT) { if (isObject(to[name]) && !isPropertyObject(to[name])) { merge(to[name], value, toValue); } else { to[name] = clone(value, toValue); } } else { to[name] = from[name]; } } return to; } /* eslint-enable */ function getPropertyName(args: NameType[]) { return args[0] in ALIAS ? ALIAS[args[0]] : args; } function getValue(names: NameType[], value: any): any { const type = getType(value); if (type === PROPERTY) { return value.toValue(); } else if (type === FUNCTION) { if (names[0] !== TIMING_FUNCTION) { return getValue(names, value()); } } else if (type === OBJECT) { return clone(value, true); } return value; } /** * Animation's Frame */ class Frame extends EventEmitter { public properties: IObject = {}; public orderMap: OrderMap = new OrderMap(NAME_SEPARATOR); /** * @param - properties * @example const frame = new Scene.Frame({ display: "none" transform: { translate: "50px", scale: "5, 5", } }); */ constructor(properties: string | Record = {}) { super(); this.properties = {}; // this.orders = []; this.set(properties); } /** * get property value * @param {...Number|String|PropertyObject} args - property name or value * @example frame.get("display") // => "none", "block", .... frame.get("transform", "translate") // => "10px,10px" */ public get(...args: NameType[]) { const value = this.raw(...args); return getValue(getPropertyName(args), value); } /** * get properties orders * @param - property names * @example frame.getOrders(["display"]) // => [] frame.getOrders(["transform"]) // => ["translate", "scale"] */ public getOrders(names: NameType[]): NameType[] | undefined { return this.orderMap.get(names); } /** * set properties orders * @param - property names * @param - orders * @example frame.getOrders(["transform"]) // => ["translate", "scale"] frame.setOrders(["transform"], ["scale", "tralsate"]) */ public setOrders(names: NameType[], orders: NameType[]): NameType[] { const result = this.orderMap.set(names, orders); this._update(); return result; } /** * get properties order object * @example console.log(frame.getOrderObject()); */ public getOrderObject() { return this.orderMap.getObject(); } /** * set properties orders object * @param - properties orders object * @example frame.setOrderObject({ "": ["transform"], "transform": ["scale", "tralsate"], }); */ public setOrderObject(obj: IObject) { this.orderMap.setObject(obj); this._update(); } /** * get property keys * @param - property names * @example frame.gets("display") // => [] frame.gets("transform") // => ["translate"] */ public getKeys(...args: NameType[]): string[] { const value = this.raw(...args); const keys = getType(value) === OBJECT ? getKeys(value) : []; sortOrders(keys, this.orderMap.get(args)); return keys; } /** * get properties array * @param - property names * @example frame.gets("display") // => [] frame.gets("transform") // => [{ key: "translate", value: "10px, 10px", children: [] }] */ public gets(...args: NameType[]): KeyValueChildren[] { const values = this.get(...args); const keys = this.getKeys(...args); return keys.map(key => { const nextValue = values[key]; return { key, value: nextValue, children: this.gets(...args, key) }; }); } public raw(...args: NameType[]) { return getValueByNames(getPropertyName(args), this.properties); } /** * remove property value * @param {...String} args - property name * @return {Frame} An instance itself * @example frame.remove("display") */ public remove(...args: NameType[]) { const params = getPropertyName(args); const length = params.length; if (!length) { return this; } this.orderMap.remove(params); const value = getValueByNames(params, this.properties, length - 1); if (isObject(value)) { delete value[params[length - 1]]; } this._update(); return this; } /** * set property * @param {...Number|String|PropertyObject} args - property names or values * @return {Frame} An instance itself * @example // one parameter frame.set({ display: "none", transform: { translate: "10px, 10px", scale: "1", }, filter: { brightness: "50%", grayscale: "100%" } }); // two parameters frame.set("transform", { translate: "10px, 10px", scale: "1", }); // three parameters frame.set("transform", "translate", "50px"); */ public set(...args: any[]) { this._set(...args); this._update(); return this; } /** * Gets the names of properties. * @return the names of properties. * @example // one parameter frame.set({ display: "none", transform: { translate: "10px, 10px", scale: "1", }, }); // [["display"], ["transform", "translate"], ["transform", "scale"]] console.log(frame.getNames()); */ public getNames(): string[][] { return getNames(this.properties, []); } /** * check that has property. * @param {...String} args - property name * @example frame.has("property", "display") // => true or false */ public has(...args: NameType[]) { const params = getPropertyName(args); const length = params.length; if (!length) { return false; } return !isUndefined(getValueByNames(params, this.properties, length)); } /** * clone frame. * @return {Frame} An instance of clone * @example frame.clone(); */ public clone() { const frame = new Frame(); frame.setOrderObject(this.orderMap.orderMap); return frame.merge(this); } /** * merge one frame to other frame. * @param - target frame. * @return {Frame} An instance itself * @example frame.merge(frame2); */ public merge(frame: Frame) { const properties = this.properties; const frameProperties = frame.properties; if (frameProperties) { merge(properties, frameProperties); } return this; } /** * Specifies an css object that coverted the frame. * @param - If you want to return camel case name like css property or react, use the following parameter * @return {object} cssObject */ public toCSSObject(useCamelCase?: boolean) { const properties = this.get(); const cssObject: IObject = {}; for (let name in properties) { if (isRole([name], true)) { continue; } let value = properties[name]; if (name === TIMING_FUNCTION) { name = TIMING_FUNCTION.replace("animation", ANIMATION); value = (isString(value) ? value : value[EASING_NAME]) || "initial"; } if (useCamelCase) { name = camelize(name.replace(/^[-]+/g, "")); } cssObject[name] = value; } const transform = toInnerProperties(properties[TRANSFORM_NAME], this.orderMap.get([TRANSFORM_NAME])); const filter = toInnerProperties(properties.filter, this.orderMap.get([FILTER])); TRANSFORM && transform && (cssObject[TRANSFORM] = transform); FILTER && filter && (cssObject[FILTER] = filter); return cssObject; } /** * Specifies an css text that coverted the frame. * @return {string} cssText */ public toCSSText() { const cssObject = this.toCSSObject(); const cssArray = []; const keys = getKeys(cssObject); sortOrders(keys, this.orderMap.get([])); keys.forEach(name => { cssArray.push(`${decamelize(name, "-")}:${cssObject[name]};`); }); return cssArray.join(""); } /** * Specifies an css text that coverted the frame. * Use `toCSSText()` method. * @deprecated * @return {string} cssText */ public toCSS() { const cssObject = this.toCSSObject(); const cssArray = []; const keys = getKeys(cssObject); sortOrders(keys, this.orderMap.get([])); keys.forEach(name => { cssArray.push(`${name}:${cssObject[name]};`); }); return cssArray.join(""); } /** * Remove All Properties * @return {Frame} An instance itself */ public clear() { this.properties = {}; this.orderMap.clear(); return this; } private _set(...args: any[]) { const self = this; const length = args.length; const params = args.slice(0, -1); const value = args[length - 1]; const firstParam = params[0]; if (length === 1 && isFrame(value)) { self.merge(value); } else if (firstParam in ALIAS) { self._setByPath(ALIAS[firstParam], value); } else if (length === 2 && isArray(firstParam)) { self._setByPath(firstParam, value); } else if (isPropertyObject(value)) { if (isRole(params)) { self._set(...params, toObject(value)); } else { self._setByPath(params, value); } } else if (isArray(value)) { self._setByPath(params, value); } else if (isObject(value)) { if (!self.has(...params) && isRole(params)) { self._setByPath(params, {}); } for (const name in value) { self._set(...params, name, value[name]); } } else if (isString(value)) { if (isRole(params, true)) { if (isFixed(params) || !isRole(params)) { this._setByPath(params, value); } else { const obj = toPropertyObject(value); if (isObject(obj)) { self._set(...params, obj); } } return this; } else { const { styles, length: stylesLength } = splitStyle(value); for (const name in styles) { self._set(...params, name, styles[name]); } if (stylesLength) { return this; } } self._setByPath(params, value); } else { self._setByPath(params, value); } } private _setByPath(path: NameType[], value: any) { let properties = this.properties; const length = path.length; for (let i = 0; i < length - 1; ++i) { const name = path[i]; !(name in properties) && (properties[name] = {}); properties = properties[name]; } if (!length) { return; } const lastParam = path[length - 1]; this.orderMap.add(path); if (length === 1 && lastParam === TIMING_FUNCTION) { properties[lastParam] = getEasing(value); } else { properties[lastParam] = isString(value) && !isFixed(path) ? toPropertyObject(value, lastParam) : value; } } private _update() { this.emit("update"); } } export default Frame;