import { camelToDashCase } from './case'; export const attachEventProps = (node: HTMLElement, newProps: any, oldProps: any = {}) => { // add any classes in className to the class list const className = getClassName(node.classList, newProps, oldProps); if (className !== '') { node.className = className; } Object.keys(newProps).forEach(name => { if (name === 'children' || name === 'style' || name === 'ref' || name === 'class' || name === 'className' || name === 'forwardedRef') { return; } if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { const eventName = name.substring(2); const eventNameLc = eventName[0].toLowerCase() + eventName.substring(1); if (!isCoveredByReact(eventNameLc)) { syncEvent(node, eventNameLc, newProps[name]); } } else { if (typeof newProps[name] === 'object') { (node as any)[name] = newProps[name]; } else { node.setAttribute(camelToDashCase(name), newProps[name]); } } }); }; export const getClassName = (classList: DOMTokenList, newProps: any, oldProps: any) => { const newClassProp: string = newProps.className || newProps.class; const oldClassProp: string = oldProps.className || oldProps.class; // map the classes to Maps for performance const currentClasses = arrayToMap(classList); const incomingPropClasses = arrayToMap(newClassProp ? newClassProp.split(' ') : []); const oldPropClasses = arrayToMap(oldClassProp ? oldClassProp.split(' ') : []); const finalClassNames: string[] = []; // loop through each of the current classes on the component // to see if it should be a part of the classNames added currentClasses.forEach(currentClass => { if (incomingPropClasses.has(currentClass)) { // add it as its already included in classnames coming in from newProps finalClassNames.push(currentClass); incomingPropClasses.delete(currentClass); } else if (!oldPropClasses.has(currentClass)) { // add it as it has NOT been removed by user finalClassNames.push(currentClass); } }); incomingPropClasses.forEach(s => finalClassNames.push(s)); return finalClassNames.join(' '); }; /** * Checks if an event is supported in the current execution environment. * @license Modernizr 3.0.0pre (Custom Build) | MIT */ export const isCoveredByReact = (eventNameSuffix: string, doc: Document = document) => { const eventName = 'on' + eventNameSuffix; let isSupported = eventName in doc; if (!isSupported) { const element = doc.createElement('div'); element.setAttribute(eventName, 'return;'); isSupported = typeof (element as any)[eventName] === 'function'; } return isSupported; }; export const syncEvent = (node: Element & {__events?: {[key: string]: ((e: Event) => any) | undefined}}, eventName: string, newEventHandler?: (e: Event) => any) => { const eventStore = node.__events || (node.__events = {}); const oldEventHandler = eventStore[eventName]; // Remove old listener so they don't double up. if (oldEventHandler) { node.removeEventListener(eventName, oldEventHandler); } // Bind new listener. node.addEventListener(eventName, eventStore[eventName] = function handler(e: Event) { if (newEventHandler) { newEventHandler.call(this, e); } }); }; const arrayToMap = (arr: string[] | DOMTokenList) => { const map = new Map(); (arr as string[]).forEach((s: string) => map.set(s, s)); return map; };