index.ts 4.49 KB
import { VNodeData } from "vue";

const pattern = {
  kebab: /-(\w)/g,
  styleProp: /:(.*)/,
	styleList: /;(?![^(]*\))/g,
} as const;

function camelReplace(_substr: string, match: string) {
  return match ? match.toUpperCase() : "";
}

function camelCase(str: string) {
  return str.replace(pattern.kebab, camelReplace);
}

function parseStyle(style: string) {
  let styleMap: Record<string, any> = {};

  for (let s of style.split(pattern.styleList)) {
    let [key, val] = s.split(pattern.styleProp);
    key = key.trim();
    if (!key) {
      continue;
    }
    // May be undefined if the `key: value` pair is incomplete.
    if (typeof val === "string") {
      val = val.trim();
    }
    styleMap[camelCase(key)] = val;
  }

  return styleMap;
}

/**
 * Intelligently merges data for createElement.
 * Merges arguments left to right, preferring the right argument.
 * Returns new VNodeData object.
 */
function mergeData(...vNodeData: VNodeData[]): VNodeData;
function mergeData(): VNodeData {
  let mergeTarget: VNodeData & Record<string, any> = {};
  let i: number = arguments.length;
  let prop: string;
  let event: string;

  // Allow for variadic argument length.
  while (i--) {
    // Iterate through the data properties and execute merge strategies
    // Object.keys eliminates need for hasOwnProperty call
    for (prop of Object.keys(arguments[i])) {
      switch (prop) {
        // Array merge strategy (array concatenation)
        case "class":
        case "style":
        case "directives":
          if (!Array.isArray(mergeTarget[prop])) {
            mergeTarget[prop] = [];
          }

          if (prop === "style") {
            let style: any[];
            if (Array.isArray(arguments[i].style)) {
              style = arguments[i].style;
            } else {
              style = [arguments[i].style];
            }
            for (let j = 0; j < style.length; j++) {
              let s = style[j];
              if (typeof s === "string") {
                style[j] = parseStyle(s);
              }
            }
            arguments[i].style = style;
          }

          // Repackaging in an array allows Vue runtime
          // to merge class/style bindings regardless of type.
          mergeTarget[prop] = mergeTarget[prop].concat(arguments[i][prop]);
          break;
        // Space delimited string concatenation strategy
        case "staticClass":
          if (!arguments[i][prop]) {
            break;
          }
          if (mergeTarget[prop] === undefined) {
            mergeTarget[prop] = "";
          }
          if (mergeTarget[prop]) {
            // Not an empty string, so concatenate
            mergeTarget[prop] += " ";
          }
          mergeTarget[prop] += arguments[i][prop].trim();
          break;
        // Object, the properties of which to merge via array merge strategy (array concatenation).
        // Callback merge strategy merges callbacks to the beginning of the array,
        // so that the last defined callback will be invoked first.
        // This is done since to mimic how Object.assign merging
        // uses the last given value to assign.
        case "on":
        case "nativeOn":
          if (!mergeTarget[prop]) {
            mergeTarget[prop] = {};
          }
          for (event of Object.keys(arguments[i][prop] || {})) {
            // Concat function to array of functions if callback present.
            if (mergeTarget[prop][event]) {
              // Insert current iteration data in beginning of merged array.
              mergeTarget[prop][event] = [].concat(
                mergeTarget[prop][event],
                arguments[i][prop][event]
              );
            } else {
              // Straight assign.
              mergeTarget[prop][event] = arguments[i][prop][event];
            }
          }
          break;
        // Object merge strategy
        case "attrs":
        case "props":
        case "domProps":
        case "scopedSlots":
        case "staticStyle":
        case "hook":
        case "transition":
          if (!mergeTarget[prop]) {
            mergeTarget[prop] = {};
          }
          mergeTarget[prop] = { ...arguments[i][prop], ...mergeTarget[prop] };
          break;
        // Reassignment strategy (no merge)
        case "slot":
        case "key":
        case "ref":
        case "tag":
        case "show":
        case "keepAlive":
        default:
          if (!mergeTarget[prop]) {
            mergeTarget[prop] = arguments[i][prop];
          }
      }
    }
  }

  return mergeTarget;
}

export { mergeData };