UNPKG

holakit

Version:

Yet another design-driven UI component set.

1,103 lines (1,069 loc) 98.6 kB
/*! Built with http://stenciljs.com */ (function(window,document,Context,namespace,hydratedCssClass,components,resourcesUrl){"use strict"; (function(s){s&&(resourcesUrl=s.getAttribute('data-resources-url'))})(document.querySelector("script[data-namespace='holakitcore']")); /** * SSR Attribute Names */ const SSR_VNODE_ID = 'ssrv'; const SSR_CHILD_ID = 'ssrc'; /** * Default style mode id */ const DEFAULT_STYLE_MODE = '$'; /** * Reusable empty obj/array * Don't add values to these!! */ const EMPTY_OBJ = {}; /** * Key Name to Key Code Map */ const KEY_CODE_MAP = { 'enter': 13, 'escape': 27, 'space': 32, 'tab': 9, 'left': 37, 'up': 38, 'right': 39, 'down': 40 }; function getScopeId(cmpMeta, mode) { return 'sc-' + cmpMeta.tagNameMeta + (mode && mode !== DEFAULT_STYLE_MODE ? '-' + mode : ''); } function getElementScopeId(scopeId, isHostElement) { return scopeId + (isHostElement ? '-h' : '-s'); } function initStyleTemplate(domApi, cmpMeta, encapsulation, style, styleMode, perf) { if (style) { false; // we got a style mode for this component, let's create an id for this style const styleModeId = cmpMeta.tagNameMeta + (styleMode || DEFAULT_STYLE_MODE); if (!cmpMeta[styleModeId]) { false; { // use <template> elements to clone styles // create the template element which will hold the styles // adding it to the dom via <template> so that we can // clone this for each potential shadow root that will need these styles // otherwise it'll be cloned and added to document.body.head // but that's for the renderer to figure out later const templateElm = domApi.$createElement('template'); // keep a reference to this template element within the // Constructor using the style mode id as the key cmpMeta[styleModeId] = templateElm; // add the style text to the template element's innerHTML true; { // hot module replacement enabled // add a style id attribute, but only useful during dev const styleContent = [ '<style', ` data-style-tag="${cmpMeta.tagNameMeta}"` ]; domApi.$setAttribute(templateElm, 'data-tmpl-style-tag', cmpMeta.tagNameMeta); if (styleMode) { styleContent.push(` data-style-mode="${styleMode}"`); domApi.$setAttribute(templateElm, 'data-tmpl-style-mode', styleMode); } if (2 /* ScopedCss */ === encapsulation || 1 /* ShadowDom */ === encapsulation && !domApi.$supportsShadowDom) { styleContent.push(' data-style-scoped="true"'); domApi.$setAttribute(templateElm, 'data-tmpl-style-scoped', 'true'); } styleContent.push('>'); styleContent.push(style); styleContent.push('</style>'); templateElm.innerHTML = styleContent.join(''); } // add our new template element to the head // so it can be cloned later domApi.$appendChild(domApi.$doc.head, templateElm); } } false; } } function attachStyles(plt, domApi, cmpMeta, hostElm) { // first see if we've got a style for a specific mode // either this host element should use scoped css // or it wants to use shadow dom but the browser doesn't support it // create a scope id which is useful for scoped css // and add the scope attribute to the host // create the style id w/ the host element's mode let styleId = cmpMeta.tagNameMeta + hostElm.mode; let styleTemplate = cmpMeta[styleId]; const shouldScopeCss = 2 /* ScopedCss */ === cmpMeta.encapsulationMeta || 1 /* ShadowDom */ === cmpMeta.encapsulationMeta && !plt.domApi.$supportsShadowDom; shouldScopeCss && (hostElm['s-sc'] = styleTemplate ? getScopeId(cmpMeta, hostElm.mode) : getScopeId(cmpMeta)); if (!styleTemplate) { // doesn't look like there's a style template with the mode // create the style id using the default style mode and try again styleId = cmpMeta.tagNameMeta + DEFAULT_STYLE_MODE; styleTemplate = cmpMeta[styleId]; } if (styleTemplate) { // cool, we found a style template element for this component let styleContainerNode = domApi.$doc.head; // if this browser supports shadow dom, then let's climb up // the dom and see if we're within a shadow dom if (true, domApi.$supportsShadowDom) if (1 /* ShadowDom */ === cmpMeta.encapsulationMeta) // we already know we're in a shadow dom // so shadow root is the container for these styles styleContainerNode = hostElm.shadowRoot; else { // climb up the dom and see if we're in a shadow dom let root = hostElm; while (root = domApi.$parentNode(root)) if (root.host && root.host.shadowRoot) { // looks like we are in shadow dom, let's use // this shadow root as the container for these styles styleContainerNode = root.host.shadowRoot; break; } } // if this container element already has these styles // then there's no need to apply them again // create an object to keep track if we'ready applied this component style let appliedStyles = plt.componentAppliedStyles.get(styleContainerNode); appliedStyles || plt.componentAppliedStyles.set(styleContainerNode, appliedStyles = {}); // check if we haven't applied these styles to this container yet if (!appliedStyles[styleId]) { let styleElm; false; { // this browser supports the <template> element // and all its native content.cloneNode() goodness // clone the template element to create a new <style> element styleElm = styleTemplate.content.cloneNode(true); // remember we don't need to do this again for this element appliedStyles[styleId] = true; // let's make sure we put the styles below the <style data-styles> element // so any visibility css overrides the default const dataStyles = styleContainerNode.querySelectorAll('[data-styles]'); domApi.$insertBefore(styleContainerNode, styleElm, dataStyles.length && dataStyles[dataStyles.length - 1].nextSibling || styleContainerNode.firstChild); } } } } const isDef = v => null != v; const toLowerCase = str => str.toLowerCase(); const dashToPascalCase = str => toLowerCase(str).split('-').map(segment => segment.charAt(0).toUpperCase() + segment.slice(1)).join(''); const noop = () => {}; function createDomApi(App, win, doc) { // using the $ prefix so that closure is // cool with property renaming each of these if (!App.ael) { App.ael = ((elm, eventName, cb, opts) => elm.addEventListener(eventName, cb, opts)); App.rel = ((elm, eventName, cb, opts) => elm.removeEventListener(eventName, cb, opts)); } const unregisterListenerFns = new WeakMap(); false; const domApi = { $doc: doc, $supportsShadowDom: !!doc.documentElement.attachShadow, $supportsEventOptions: false, $nodeType: node => node.nodeType, $createElement: tagName => doc.createElement(tagName), $createElementNS: (namespace, tagName) => doc.createElementNS(namespace, tagName), $createTextNode: text => doc.createTextNode(text), $createComment: data => doc.createComment(data), $insertBefore: (parentNode, childNode, referenceNode) => parentNode.insertBefore(childNode, referenceNode), // https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove // and it's polyfilled in es5 builds $remove: node => node.remove(), $appendChild: (parentNode, childNode) => parentNode.appendChild(childNode), $addClass: (elm, cssClass) => { true, false; elm.classList.add(cssClass); }, $childNodes: node => node.childNodes, $parentNode: node => node.parentNode, $nextSibling: node => node.nextSibling, $previousSibling: node => node.previousSibling, $tagName: elm => toLowerCase(elm.nodeName), $getTextContent: node => node.textContent, $setTextContent: (node, text) => node.textContent = text, $getAttribute: (elm, key) => elm.getAttribute(key), $setAttribute: (elm, key, val) => elm.setAttribute(key, val), $setAttributeNS: (elm, namespaceURI, qualifiedName, val) => elm.setAttributeNS(namespaceURI, qualifiedName, val), $removeAttribute: (elm, key) => elm.removeAttribute(key), $hasAttribute: (elm, key) => elm.hasAttribute(key), $getMode: elm => elm.getAttribute('mode') || (App.Context || {}).mode, $elementRef: (elm, referenceName) => { if ('child' === referenceName) return elm.firstElementChild; if ('parent' === referenceName) return domApi.$parentElement(elm); if ('body' === referenceName) return doc.body; if ('document' === referenceName) return doc; if ('window' === referenceName) return win; return elm; }, $addEventListener: (assignerElm, eventName, listenerCallback, useCapture, usePassive, attachTo, eventListenerOpts, splt) => { // remember the original name before we possibly change it const assignersEventName = eventName; let attachToElm = assignerElm; // get the existing unregister listeners for // this element from the unregister listeners weakmap let assignersUnregListeners = unregisterListenerFns.get(assignerElm); assignersUnregListeners && assignersUnregListeners[assignersEventName] && // removed any existing listeners for this event for the assigner element // this element already has this listener, so let's unregister it now assignersUnregListeners[assignersEventName](); if ('string' === typeof attachTo) // attachTo is a string, and is probably something like // "parent", "window", or "document" // and the eventName would be like "mouseover" or "mousemove" attachToElm = domApi.$elementRef(assignerElm, attachTo); else if ('object' === typeof attachTo) // we were passed in an actual element to attach to attachToElm = attachTo; else { // depending on the event name, we could actually be attaching // this element to something like the document or window splt = eventName.split(':'); if (splt.length > 1) { // document:mousemove // parent:touchend // body:keyup.enter attachToElm = domApi.$elementRef(assignerElm, splt[0]); eventName = splt[1]; } } if (!attachToElm) // somehow we're referencing an element that doesn't exist // let's not continue return; let eventListener = listenerCallback; // test to see if we're looking for an exact keycode splt = eventName.split('.'); if (splt.length > 1) { // looks like this listener is also looking for a keycode // keyup.enter eventName = splt[0]; eventListener = (ev => { // wrap the user's event listener with our own check to test // if this keyboard event has the keycode they're looking for ev.keyCode === KEY_CODE_MAP[splt[1]] && listenerCallback(ev); }); } // create the actual event listener options to use // this browser may not support event options eventListenerOpts = domApi.$supportsEventOptions ? { capture: !!useCapture, passive: !!usePassive } : !!useCapture; // ok, good to go, let's add the actual listener to the dom element App.ael(attachToElm, eventName, eventListener, eventListenerOpts); assignersUnregListeners || // we don't already have a collection, let's create it unregisterListenerFns.set(assignerElm, assignersUnregListeners = {}); // add the unregister listener to this element's collection assignersUnregListeners[assignersEventName] = (() => { // looks like it's time to say goodbye attachToElm && App.rel(attachToElm, eventName, eventListener, eventListenerOpts); assignersUnregListeners[assignersEventName] = null; }); }, $removeEventListener: (elm, eventName) => { // get the unregister listener functions for this element const assignersUnregListeners = unregisterListenerFns.get(elm); assignersUnregListeners && ( // this element has unregister listeners eventName ? // passed in one specific event name to remove assignersUnregListeners[eventName] && assignersUnregListeners[eventName]() : // remove all event listeners Object.keys(assignersUnregListeners).forEach(assignersEventName => { assignersUnregListeners[assignersEventName] && assignersUnregListeners[assignersEventName](); })); }, $dispatchEvent: (elm, eventName, data) => { // create and return the custom event, allows for cancel checks const e = new win.CustomEvent(eventName, data); elm && elm.dispatchEvent(e); return e; }, $parentElement: (elm, parentNode) => // if the parent node is a document fragment (shadow root) // then use the "host" property on it // otherwise use the parent node (parentNode = domApi.$parentNode(elm)) && 11 /* DocumentFragment */ === domApi.$nodeType(parentNode) ? parentNode.host : parentNode }; true; win.location.search.indexOf('shadow=false') > 0 && ( // by adding ?shadow=false it'll force the slot polyfill // only add this check when in dev mode domApi.$supportsShadowDom = false); true; domApi.$attachShadow = ((elm, shadowRootInit) => elm.attachShadow(shadowRootInit)); true; // test if this browser supports event options or not try { win.addEventListener('e', null, Object.defineProperty({}, 'passive', { get: () => domApi.$supportsEventOptions = true })); } catch (e) {} return domApi; } function updateAttribute(elm, memberName, newValue, isBooleanAttr = 'boolean' === typeof newValue) { const isXlinkNs = memberName !== (memberName = memberName.replace(/^xlink\:?/, '')); if (null == newValue || isBooleanAttr && (!newValue || 'false' === newValue)) isXlinkNs ? elm.removeAttributeNS(XLINK_NS$1, toLowerCase(memberName)) : elm.removeAttribute(memberName); else if ('function' !== typeof newValue) { newValue = isBooleanAttr ? '' : newValue.toString(); isXlinkNs ? elm.setAttributeNS(XLINK_NS$1, toLowerCase(memberName), newValue) : elm.setAttribute(memberName, newValue); } } const XLINK_NS$1 = 'http://www.w3.org/1999/xlink'; function setAccessor(plt, elm, memberName, oldValue, newValue, isSvg, isHostElement) { if ('class' !== memberName || isSvg) if ('style' === memberName) { // update style attribute, css properties and values for (const prop in oldValue) newValue && null != newValue[prop] || (/-/.test(prop) ? elm.style.removeProperty(prop) : elm.style[prop] = ''); for (const prop in newValue) oldValue && newValue[prop] === oldValue[prop] || (/-/.test(prop) ? elm.style.setProperty(prop, newValue[prop]) : elm.style[prop] = newValue[prop]); } else if ('o' !== memberName[0] || 'n' !== memberName[1] || !/[A-Z]/.test(memberName[2]) || memberName in elm) if ('list' !== memberName && 'type' !== memberName && !isSvg && (memberName in elm || -1 !== [ 'object', 'function' ].indexOf(typeof newValue) && null !== newValue) || false) { // Properties // - list and type are attributes that get applied as values on the element // - all svgs get values as attributes not props // - check if elm contains name or if the value is array, object, or function const cmpMeta = plt.getComponentMeta(elm); if (cmpMeta && cmpMeta.membersMeta && cmpMeta.membersMeta[memberName]) { // we know for a fact that this element is a known component // and this component has this member name as a property, // let's set the known @Prop on this element // set it directly as property on the element setProperty(elm, memberName, newValue); false; } else if ('ref' !== memberName) { // this member name is a property on this element, but it's not a component // this is a native property like "value" or something // also we can ignore the "ref" member name at this point setProperty(elm, memberName, null == newValue ? '' : newValue); null != newValue && false !== newValue || plt.domApi.$removeAttribute(elm, memberName); } } else null != newValue && 'key' !== memberName ? // Element Attributes updateAttribute(elm, memberName, newValue) : (isSvg || plt.domApi.$hasAttribute(elm, memberName) && (null == newValue || false === newValue)) && // remove svg attribute plt.domApi.$removeAttribute(elm, memberName); else { // Event Handlers // so if the member name starts with "on" and the 3rd characters is // a capital letter, and it's not already a member on the element, // then we're assuming it's an event listener // standard event // the JSX attribute could have been "onMouseOver" and the // member name "onmouseover" is on the element's prototype // so let's add the listener "mouseover", which is all lowercased memberName = toLowerCase(memberName) in elm ? toLowerCase(memberName.substring(2)) : toLowerCase(memberName[2]) + memberName.substring(3); newValue ? newValue !== oldValue && // add listener plt.domApi.$addEventListener(elm, memberName, newValue) : // remove listener plt.domApi.$removeEventListener(elm, memberName); } else // Class if (oldValue !== newValue) { const oldList = parseClassList(oldValue); const newList = parseClassList(newValue); // remove classes in oldList, not included in newList const toRemove = oldList.filter(item => !newList.includes(item)); const classList = parseClassList(elm.className).filter(item => !toRemove.includes(item)); // add classes from newValue that are not in oldList or classList const toAdd = newList.filter(item => !oldList.includes(item) && !classList.includes(item)); classList.push(...toAdd); elm.className = classList.join(' '); } } function parseClassList(value) { return null == value || '' === value ? [] : value.trim().split(/\s+/); } /** * Attempt to set a DOM property to the given value. * IE & FF throw for certain property-value combinations. */ function setProperty(elm, name, value) { try { elm[name] = value; } catch (e) {} } function updateElement(plt, oldVnode, newVnode, isSvgMode, memberName) { // if the element passed in is a shadow root, which is a document fragment // then we want to be adding attrs/props to the shadow root's "host" element // if it's not a shadow root, then we add attrs/props to the same element const elm = 11 /* DocumentFragment */ === newVnode.elm.nodeType && newVnode.elm.host ? newVnode.elm.host : newVnode.elm; const oldVnodeAttrs = oldVnode && oldVnode.vattrs || EMPTY_OBJ; const newVnodeAttrs = newVnode.vattrs || EMPTY_OBJ; // remove attributes no longer present on the vnode by setting them to undefined for (memberName in oldVnodeAttrs) newVnodeAttrs && null != newVnodeAttrs[memberName] || null == oldVnodeAttrs[memberName] || setAccessor(plt, elm, memberName, oldVnodeAttrs[memberName], void 0, isSvgMode, newVnode.ishost); // add new & update changed attributes for (memberName in newVnodeAttrs) memberName in oldVnodeAttrs && newVnodeAttrs[memberName] === ('value' === memberName || 'checked' === memberName ? elm[memberName] : oldVnodeAttrs[memberName]) || setAccessor(plt, elm, memberName, oldVnodeAttrs[memberName], newVnodeAttrs[memberName], isSvgMode, newVnode.ishost); } let isSvgMode = false; function createRendererPatch(plt, domApi) { // createRenderer() is only created once per app // the patch() function which createRenderer() returned is the function // which gets called numerous times by each component // internal variables to be reused per patch() call let useNativeShadowDom, scopeId, checkSlotFallbackVisibility, checkSlotRelocate, contentRef, hostTagName, hostElm; function createElm(oldParentVNode, newParentVNode, childIndex, parentElm, i, elm, childNode, newVNode, oldVNode) { newVNode = newParentVNode.vchildren[childIndex]; if (true, !useNativeShadowDom) { // remember for later we need to check to relocate nodes checkSlotRelocate = true; if ('slot' === newVNode.vtag) { scopeId && // scoped css needs to add its scoped id to the parent element domApi.$addClass(parentElm, scopeId + '-s'); newVNode.vchildren ? // slot element has fallback content // still create an element that "mocks" the slot element newVNode.isSlotFallback = true : // slot element does not have fallback content // create an html comment we'll use to always reference // where actual slot content should sit next to newVNode.isSlotReference = true; } } if (isDef(newVNode.vtext)) // create text node newVNode.elm = domApi.$createTextNode(newVNode.vtext); else if (true, newVNode.isSlotReference) // create a slot reference html text node newVNode.elm = domApi.$createTextNode(''); else { // create element elm = newVNode.elm = (true, isSvgMode || 'svg' === newVNode.vtag ? domApi.$createElementNS('http://www.w3.org/2000/svg', newVNode.vtag) : domApi.$createElement((true, newVNode.isSlotFallback ? 'slot-fb' : newVNode.vtag))); plt.isDefinedComponent(elm) && plt.isCmpReady.delete(hostElm); true; isSvgMode = 'svg' === newVNode.vtag || 'foreignObject' !== newVNode.vtag && isSvgMode; // add css classes, attrs, props, listeners, etc. updateElement(plt, null, newVNode, isSvgMode); isDef(scopeId) && elm['s-si'] !== scopeId && // if there is a scopeId and this is the initial render // then let's add the scopeId as an attribute domApi.$addClass(elm, elm['s-si'] = scopeId); false; if (newVNode.vchildren) for (i = 0; i < newVNode.vchildren.length; ++i) { // create the node childNode = createElm(oldParentVNode, newVNode, i, elm); // return node could have been null if (childNode) { false; // append our new node domApi.$appendChild(elm, childNode); false; } } (true, 'svg' === newVNode.vtag) && ( // Only reset the SVG context when we're exiting SVG element isSvgMode = false); } true; newVNode.elm['s-hn'] = hostTagName; if (newVNode.isSlotFallback || newVNode.isSlotReference) { // remember the content reference comment newVNode.elm['s-sr'] = true; // remember the content reference comment newVNode.elm['s-cr'] = contentRef; // remember the slot name, or empty string for default slot newVNode.elm['s-sn'] = newVNode.vname || ''; // check if we've got an old vnode for this slot oldVNode = oldParentVNode && oldParentVNode.vchildren && oldParentVNode.vchildren[childIndex]; oldVNode && oldVNode.vtag === newVNode.vtag && oldParentVNode.elm && // we've got an old slot vnode and the wrapper is being replaced // so let's move the old slot content back to it's original location putBackInOriginalLocation(oldParentVNode.elm); } return newVNode.elm; } function putBackInOriginalLocation(parentElm, recursive, i, childNode) { plt.tmpDisconnected = true; const oldSlotChildNodes = domApi.$childNodes(parentElm); for (i = oldSlotChildNodes.length - 1; i >= 0; i--) { childNode = oldSlotChildNodes[i]; if (childNode['s-hn'] !== hostTagName && childNode['s-ol']) { // this child node in the old element is from another component // remove this node from the old slot's parent domApi.$remove(childNode); // and relocate it back to it's original location domApi.$insertBefore(parentReferenceNode(childNode), childNode, referenceNode(childNode)); // remove the old original location comment entirely // later on the patch function will know what to do // and move this to the correct spot in need be domApi.$remove(childNode['s-ol']); childNode['s-ol'] = null; checkSlotRelocate = true; } recursive && putBackInOriginalLocation(childNode, recursive); } plt.tmpDisconnected = false; } function addVnodes(parentElm, before, parentVNode, vnodes, startIdx, endIdx, containerElm, childNode) { const contentRef = parentElm['s-cr']; containerElm = contentRef && domApi.$parentNode(contentRef) || parentElm; containerElm.shadowRoot && domApi.$tagName(containerElm) === hostTagName && (containerElm = containerElm.shadowRoot); for (;startIdx <= endIdx; ++startIdx) if (vnodes[startIdx]) { childNode = isDef(vnodes[startIdx].vtext) ? domApi.$createTextNode(vnodes[startIdx].vtext) : createElm(null, parentVNode, startIdx, parentElm); if (childNode) { vnodes[startIdx].elm = childNode; domApi.$insertBefore(containerElm, childNode, referenceNode(before)); } } } function removeVnodes(vnodes, startIdx, endIdx, node) { for (;startIdx <= endIdx; ++startIdx) if (isDef(vnodes[startIdx])) { node = vnodes[startIdx].elm; true; // we're removing this element // so it's possible we need to show slot fallback content now checkSlotFallbackVisibility = true; node['s-ol'] ? // remove the original location comment domApi.$remove(node['s-ol']) : // it's possible that child nodes of the node // that's being removed are slot nodes putBackInOriginalLocation(node, true); // remove the vnode's element from the dom domApi.$remove(node); } } function updateChildren(parentElm, oldCh, newVNode, newCh, idxInOld, i, node, elmToMove) { let oldStartIdx = 0, newStartIdx = 0; let oldEndIdx = oldCh.length - 1; let oldStartVnode = oldCh[0]; let oldEndVnode = oldCh[oldEndIdx]; let newEndIdx = newCh.length - 1; let newStartVnode = newCh[0]; let newEndVnode = newCh[newEndIdx]; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) if (null == oldStartVnode) // Vnode might have been moved left oldStartVnode = oldCh[++oldStartIdx]; else if (null == oldEndVnode) oldEndVnode = oldCh[--oldEndIdx]; else if (null == newStartVnode) newStartVnode = newCh[++newStartIdx]; else if (null == newEndVnode) newEndVnode = newCh[--newEndIdx]; else if (isSameVnode(oldStartVnode, newStartVnode)) { patchVNode(oldStartVnode, newStartVnode); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (isSameVnode(oldEndVnode, newEndVnode)) { patchVNode(oldEndVnode, newEndVnode); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (isSameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right 'slot' !== oldStartVnode.vtag && 'slot' !== newEndVnode.vtag || putBackInOriginalLocation(domApi.$parentNode(oldStartVnode.elm)); patchVNode(oldStartVnode, newEndVnode); domApi.$insertBefore(parentElm, oldStartVnode.elm, domApi.$nextSibling(oldEndVnode.elm)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (isSameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left 'slot' !== oldStartVnode.vtag && 'slot' !== newEndVnode.vtag || putBackInOriginalLocation(domApi.$parentNode(oldEndVnode.elm)); patchVNode(oldEndVnode, newStartVnode); domApi.$insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { // createKeyToOldIdx idxInOld = null; for (i = oldStartIdx; i <= oldEndIdx; ++i) if (oldCh[i] && isDef(oldCh[i].vkey) && oldCh[i].vkey === newStartVnode.vkey) { idxInOld = i; break; } if (isDef(idxInOld)) { elmToMove = oldCh[idxInOld]; if (elmToMove.vtag !== newStartVnode.vtag) node = createElm(oldCh && oldCh[newStartIdx], newVNode, idxInOld, parentElm); else { patchVNode(elmToMove, newStartVnode); oldCh[idxInOld] = void 0; node = elmToMove.elm; } newStartVnode = newCh[++newStartIdx]; } else { // new element node = createElm(oldCh && oldCh[newStartIdx], newVNode, newStartIdx, parentElm); newStartVnode = newCh[++newStartIdx]; } node && domApi.$insertBefore(parentReferenceNode(oldStartVnode.elm), node, referenceNode(oldStartVnode.elm)); } oldStartIdx > oldEndIdx ? addVnodes(parentElm, null == newCh[newEndIdx + 1] ? null : newCh[newEndIdx + 1].elm, newVNode, newCh, newStartIdx, newEndIdx) : newStartIdx > newEndIdx && removeVnodes(oldCh, oldStartIdx, oldEndIdx); } function isSameVnode(vnode1, vnode2) { // compare if two vnode to see if they're "technically" the same // need to have the same element tag, and same key to be the same if (vnode1.vtag === vnode2.vtag && vnode1.vkey === vnode2.vkey) { true; if ('slot' === vnode1.vtag) return vnode1.vname === vnode2.vname; return true; } return false; } function referenceNode(node) { true; if (node && node['s-ol']) // this node was relocated to a new location in the dom // because of some other component's slot // but we still have an html comment in place of where // it's original location was according to it's original vdom return node['s-ol']; return node; } function parentReferenceNode(node) { return domApi.$parentNode(node['s-ol'] ? node['s-ol'] : node); } function patchVNode(oldVNode, newVNode, defaultHolder) { const elm = newVNode.elm = oldVNode.elm; const oldChildren = oldVNode.vchildren; const newChildren = newVNode.vchildren; true; // test if we're rendering an svg element, or still rendering nodes inside of one // only add this to the when the compiler sees we're using an svg somewhere isSvgMode = newVNode.elm && isDef(domApi.$parentElement(newVNode.elm)) && void 0 !== newVNode.elm.ownerSVGElement; isSvgMode = 'svg' === newVNode.vtag || 'foreignObject' !== newVNode.vtag && isSvgMode; if (isDef(newVNode.vtext)) true, (defaultHolder = elm['s-cr']) ? // this element has slotted content domApi.$setTextContent(domApi.$parentNode(defaultHolder), newVNode.vtext) : oldVNode.vtext !== newVNode.vtext && // update the text content for the text only vnode // and also only if the text is different than before domApi.$setTextContent(elm, newVNode.vtext); else { // element node 'slot' !== newVNode.vtag && // either this is the first render of an element OR it's an update // AND we already know it's possible it could have changed // this updates the element's css classes, attrs, props, listeners, etc. updateElement(plt, oldVNode, newVNode, isSvgMode); if (isDef(oldChildren) && isDef(newChildren)) // looks like there's child vnodes for both the old and new vnodes updateChildren(elm, oldChildren, newVNode, newChildren); else if (isDef(newChildren)) { // no old child vnodes, but there are new child vnodes to add isDef(oldVNode.vtext) && // the old vnode was text, so be sure to clear it out domApi.$setTextContent(elm, ''); // add the new vnode children addVnodes(elm, null, newVNode, newChildren, 0, newChildren.length - 1); } else isDef(oldChildren) && // no new child vnodes, but there are old child vnodes to remove removeVnodes(oldChildren, 0, oldChildren.length - 1); } true; // reset svgMode when svg node is fully patched isSvgMode && 'svg' === newVNode.vtag && (isSvgMode = false); } function updateFallbackSlotVisibility(elm, childNode, childNodes, i, ilen, j, slotNameAttr, nodeType) { childNodes = domApi.$childNodes(elm); for (i = 0, ilen = childNodes.length; i < ilen; i++) { childNode = childNodes[i]; if (1 /* ElementNode */ === domApi.$nodeType(childNode)) { if (childNode['s-sr']) { // this is a slot fallback node // get the slot name for this slot reference node slotNameAttr = childNode['s-sn']; // by default always show a fallback slot node // then hide it if there are other slots in the light dom childNode.hidden = false; for (j = 0; j < ilen; j++) if (childNodes[j]['s-hn'] !== childNode['s-hn']) { // this sibling node is from a different component nodeType = domApi.$nodeType(childNodes[j]); if ('' !== slotNameAttr) { // this is a named fallback slot node if (1 /* ElementNode */ === nodeType && slotNameAttr === domApi.$getAttribute(childNodes[j], 'slot')) { childNode.hidden = true; break; } } else // this is a default fallback slot node // any element or text node (with content) // should hide the default fallback slot node if (1 /* ElementNode */ === nodeType || 3 /* TextNode */ === nodeType && '' !== domApi.$getTextContent(childNodes[j]).trim()) { childNode.hidden = true; break; } } } // keep drilling down updateFallbackSlotVisibility(childNode); } } } const relocateNodes = []; function relocateSlotContent(elm, childNodes, childNode, node, i, ilen, j, hostContentNodes, slotNameAttr, nodeType) { childNodes = domApi.$childNodes(elm); for (i = 0, ilen = childNodes.length; i < ilen; i++) { childNode = childNodes[i]; if (childNode['s-sr'] && (node = childNode['s-cr'])) { // first got the content reference comment node // then we got it's parent, which is where all the host content is in now hostContentNodes = domApi.$childNodes(domApi.$parentNode(node)); slotNameAttr = childNode['s-sn']; for (j = hostContentNodes.length - 1; j >= 0; j--) { node = hostContentNodes[j]; if (!node['s-cn'] && !node['s-nr'] && node['s-hn'] !== childNode['s-hn']) { // let's do some relocating to its new home // but never relocate a content reference node // that is suppose to always represent the original content location nodeType = domApi.$nodeType(node); if (((3 /* TextNode */ === nodeType || 8 /* CommentNode */ === nodeType) && '' === slotNameAttr || 1 /* ElementNode */ === nodeType && null === domApi.$getAttribute(node, 'slot') && '' === slotNameAttr || 1 /* ElementNode */ === nodeType && domApi.$getAttribute(node, 'slot') === slotNameAttr) && !relocateNodes.some(r => r.nodeToRelocate === node)) { // made some changes to slots // let's make sure we also double check // fallbacks are correctly hidden or shown checkSlotFallbackVisibility = true; node['s-sn'] = slotNameAttr; // add to our list of nodes to relocate relocateNodes.push({ slotRefNode: childNode, nodeToRelocate: node }); } } } } 1 /* ElementNode */ === domApi.$nodeType(childNode) && relocateSlotContent(childNode); } } return function patch(hostElement, oldVNode, newVNode, useNativeShadowDomVal, encapsulation, ssrPatchId, i, relocateNode, orgLocationNode, refNode, parentNodeRef, insertBeforeNode) { // patchVNode() is synchronous // so it is safe to set these variables and internally // the same patch() call will reference the same data hostElm = hostElement; hostTagName = domApi.$tagName(hostElm); contentRef = hostElm['s-cr']; useNativeShadowDom = useNativeShadowDomVal; false; true; // get the scopeId scopeId = hostElm['s-sc']; // always reset checkSlotRelocate = checkSlotFallbackVisibility = false; // synchronous patch patchVNode(oldVNode, newVNode); false; true; if (checkSlotRelocate) { relocateSlotContent(newVNode.elm); for (i = 0; i < relocateNodes.length; i++) { relocateNode = relocateNodes[i]; if (!relocateNode.nodeToRelocate['s-ol']) { // add a reference node marking this node's original location // keep a reference to this node for later lookups orgLocationNode = domApi.$createTextNode(''); orgLocationNode['s-nr'] = relocateNode.nodeToRelocate; domApi.$insertBefore(domApi.$parentNode(relocateNode.nodeToRelocate), relocateNode.nodeToRelocate['s-ol'] = orgLocationNode, relocateNode.nodeToRelocate); } } // while we're moving nodes around existing nodes, temporarily disable // the disconnectCallback from working plt.tmpDisconnected = true; for (i = 0; i < relocateNodes.length; i++) { relocateNode = relocateNodes[i]; // by default we're just going to insert it directly // after the slot reference node parentNodeRef = domApi.$parentNode(relocateNode.slotRefNode); insertBeforeNode = domApi.$nextSibling(relocateNode.slotRefNode); orgLocationNode = relocateNode.nodeToRelocate['s-ol']; while (orgLocationNode = domApi.$previousSibling(orgLocationNode)) if ((refNode = orgLocationNode['s-nr']) && refNode && refNode['s-sn'] === relocateNode.nodeToRelocate['s-sn'] && parentNodeRef === domApi.$parentNode(refNode) && (refNode = domApi.$nextSibling(refNode)) && refNode && !refNode['s-nr']) { insertBeforeNode = refNode; break; } if ((!insertBeforeNode && parentNodeRef !== domApi.$parentNode(relocateNode.nodeToRelocate) || domApi.$nextSibling(relocateNode.nodeToRelocate) !== insertBeforeNode) && relocateNode.nodeToRelocate !== insertBeforeNode) { // remove the node from the dom domApi.$remove(relocateNode.nodeToRelocate); // add it back to the dom but in its new home domApi.$insertBefore(parentNodeRef, relocateNode.nodeToRelocate, insertBeforeNode); } } // done moving nodes around // allow the disconnect callback to work again plt.tmpDisconnected = false; } checkSlotFallbackVisibility && updateFallbackSlotVisibility(newVNode.elm); // always reset relocateNodes.length = 0; // return our new vnode return newVNode; }; } function callNodeRefs(vNode, isDestroy) { if (vNode) { vNode.vattrs && vNode.vattrs.ref && vNode.vattrs.ref(isDestroy ? null : vNode.elm); vNode.vchildren && vNode.vchildren.forEach(vChild => { callNodeRefs(vChild, isDestroy); }); } } function createVNodesFromSsr(plt, domApi, rootElm) { const allSsrElms = rootElm.querySelectorAll(`[${SSR_VNODE_ID}]`); const ilen = allSsrElms.length; let elm, ssrVNodeId, ssrVNode, i, j, jlen; if (ilen > 0) { plt.isCmpReady.set(rootElm, true); for (i = 0; i < ilen; i++) { elm = allSsrElms[i]; ssrVNodeId = domApi.$getAttribute(elm, SSR_VNODE_ID); ssrVNode = {}; ssrVNode.vtag = domApi.$tagName(ssrVNode.elm = elm); plt.vnodeMap.set(elm, ssrVNode); for (j = 0, jlen = elm.childNodes.length; j < jlen; j++) addChildSsrVNodes(domApi, elm.childNodes[j], ssrVNode, ssrVNodeId, true); } } } function addChildSsrVNodes(domApi, node, parentVNode, ssrVNodeId, checkNestedElements) { const nodeType = domApi.$nodeType(node); let previousComment; let childVNodeId, childVNodeSplt, childVNode; if (checkNestedElements && 1 /* ElementNode */ === nodeType) { childVNodeId = domApi.$getAttribute(node, SSR_CHILD_ID); if (childVNodeId) { // split the start comment's data with a period childVNodeSplt = childVNodeId.split('.'); // ensure this this element is a child element of the ssr vnode if (childVNodeSplt[0] === ssrVNodeId) { // cool, this element is a child to the parent vnode childVNode = {}; childVNode.vtag = domApi.$tagName(childVNode.elm = node); // this is a new child vnode // so ensure its parent vnode has the vchildren array parentVNode.vchildren || (parentVNode.vchildren = []); // add our child vnode to a specific index of the vnode's children parentVNode.vchildren[childVNodeSplt[1]] = childVNode; // this is now the new parent vnode for all the next child checks parentVNode = childVNode; // if there's a trailing period, then it means there aren't any // more nested elements, but maybe nested text nodes // either way, don't keep walking down the tree after this next call checkNestedElements = '' !== childVNodeSplt[2]; } } // keep drilling down through the elements for (let i = 0; i < node.childNodes.length; i++) addChildSsrVNodes(domApi, node.childNodes[i], parentVNode, ssrVNodeId, checkNestedElements); } else if (3 /* TextNode */ === nodeType && (previousComment = node.previousSibling) && 8 /* CommentNode */ === domApi.$nodeType(previousComment)) { // split the start comment's data with a period childVNodeSplt = domApi.$getTextContent(previousComment).split('.'); // ensure this is an ssr text node start comment // which should start with an "s" and delimited by periods if ('s' === childVNodeSplt[0] && childVNodeSplt[1] === ssrVNodeId) { // cool, this is a text node and it's got a start comment childVNode = { vtext: domApi.$getTextContent(node) }; childVNode.elm = node; // this is a new child vnode // so ensure its parent vnode has the vchildren array parentVNode.vchildren || (parentVNode.vchildren = []); // add our child vnode to a specific index of the vnode's children parentVNode.vchildren[childVNodeSplt[2]] = childVNode; } } } function createQueueClient(App, win) { const now = () => win.performance.now(); const resolved = Promise.resolve(); const highPriority = []; const domReads = []; const domWrites = []; const domWritesLow = []; let congestion = 0; let rafPending = false; App.raf || (App.raf = win.requestAnimationFrame.bind(win)); function queueTask(queue) { return cb => { // queue dom reads queue.push(cb); if (!rafPending) { rafPending = true; App.raf(flush); } }; } function consume(queue) { for (let i = 0; i < queue.length; i++) try { queue[i](now()); } catch (e) { console.error(e); } queue.length = 0; } function consumeTimeout(queue, timeout) { let i = 0; let ts; while (i < queue.length && (ts = now()) < timeout) try { queue[i++](ts); } catch (e) { console.error(e); } i === queue.length ? queue.length = 0 : 0 !== i && queue.splice(0, i); } function flush() { congestion++; // always force a bunch of medium callbacks to run, but still have // a throttle on how many can run in a certain time // DOM READS!!! consume(domReads); const start = now() + 7 * Math.ceil(congestion * (1 / 22)); // DOM WRITES!!! consumeTimeout(domWrites, start); consumeTimeout(domWritesLow, start); if (domWrites.length > 0) { domWritesLow.push(...domWrites); domWrites.length = 0; } (rafPending = domReads.length + domWrites.length + domWritesLow.length > 0) ? // still more to do yet, but we've run out of time // let's let this thing cool off and try again in the next tick App.raf(flush) : congestion = 0; } return { tick(cb) { // queue high priority work to happen in next tick // uses Promise.resolve() for next tick highPriority.push(cb); 1 === highPriority.length && resolved.then(() => consume(highPriority)); }, read: queueTask(domReads), write: queueTask(domWrites) }; } function initElementListeners(plt, elm) { // so the element was just connected, which means it's in the DOM // however, the component instance hasn't been created yet // but what if an event it should be listening to get emitted right now?? // let's add our listeners right now to our element, and if it happens // to receive events between now and the instance being created let's // queue up all of the event data and fire it off on the instance when it's ready const cmpMeta = plt.getComponentMeta(elm); cmpMeta.listenersMeta && // we've got listens cmpMeta.listenersMeta.forEach(listenMeta => { // go through each listener listenMeta.eventDisabled || // only add ones that are not already disabled plt.domApi.$addEventListener(elm, listenMeta.eventName, createListenerCallback(plt, elm, listenMeta.eventMethodName), listenMeta.eventCapture, listenMeta.eventPassive); }); } function createListenerCallback(plt, elm, eventMethodName, val) { // create the function that gets called when the element receives // an event which it should be listening for return ev => { // get the instance if it exists val = plt.instanceMap.get(elm); if (val) // instance is ready, let's call it's member method for this event val[eventMethodName](ev); else { // instance is not ready!! // let's queue up this event data and replay it later // when the instance is ready val = plt.queuedEvents.get(elm) || []; val.push(eventMethodName, ev); plt.queuedEvents.set(elm, val); } }; } function generateDevInspector(namespace, win, plt, components) { const devInspector = win.devInspector = win.devInspector || {}; devInspector.apps = devInspector.apps || []; devInspector.apps.push(generateDevInspectorApp(namespace, plt, components)); devInspector.getInstance || (devInspector.getInstance = (elm => { return Promise.all(devInspector.apps.map(app => { return app.getInstance(elm); })).then(results => { return results.find(instance => !!instance); }); })); devInspector.getComponents || (devInspector.getComponents = (() => { const appsMetadata = []; devInspector.apps.forEach(app => { appsMetadata.push(app.getComponents()); }); return Promise.all(appsMetadata).then(appMetadata => { const allMetadata = []; appMetadata.forEach(metadata => { metadata.forEach(m => { allMetadata.push(m); }); }); return allMetadata; }); })); return devInspector; } function generateDevInspectorApp(namespace, plt, components) { const app = { namespace: namespace, getInstance: elm => { if (elm && elm.tagName) return Promise.all([ getComponentMeta(plt, elm.tagName), getComponentInstance(plt, elm) ]).then(results => { if (results[0] && results[1]) { const cmp = { meta: results[0], instance: results[1] }; return cmp; } return null; }); return Promise.resolve(null); }, getComponent: tagName => { return getComponentMeta(plt, tagName); }, getComponents: () => { return Promise.all(components.map(cmp => { return getComponentMeta(plt, cmp[0]); })).then(metadata => { return metadata.filter(m => m); }); } }; return app; } function getMembersMeta(properties) { return Object.keys(properties).reduce((membersMap, memberKey) => { const prop = properties[memberKey]; let category; const member = { name: memberKey }; if (prop.state) { category = 'states'; member.watchers = prop.watchCallbacks || []; } else if (prop.elementRef) category = 'elements'; else if (prop.method) category = 'methods'; else { category = 'props'; let type = 'any'; if (prop.type) { type = prop.type; 'function' === typeof prop.type && (type = prop.type.name); } member.type = type.toLowerCase(); member.mutable = prop.mutable || false; member.connect = prop.connect || '-'; member.context = prop.connect || '-'; member.watchers = prop.watchCallbacks || []; } membersMap[category].push(member); return membersMap; }, { props: [], states: [], elements: [], methods: [] }); } function getComponentMeta(plt, tagName) { const elm = { nodeName: tagName }; const internalMeta = plt.getComponentMeta(elm); if (!internalMeta || !internalMeta.componentConstructor) return Promise.resolve(null); const cmpCtr = internalMeta.componentConstructor; const members = getMembersMeta(cmpCtr.properties || {}); const listeners = (internalMeta.listenersMeta || []).map(listenerMeta => { return { event: listenerMeta.eventName, capture: listenerMeta.eventCapture, disabled: listenerMeta.eventDisabled, passive: listenerMeta.eventPassive, method: listenerMeta.eventMethodName }; }); const emmiters = cmpCtr.events || []; const meta = Object.assign({ tag: cmpCtr.is, bundle: internalMeta.bundleIds || 'unknown', encapsulation: cmpCtr.encapsulation || 'none' }, members, { events: { emmiters: emmiters, listeners: listeners } }); return Promise.resolve(meta); } function getComponentInstance(plt, elm) { return Promise.resolve(plt.instanceMap.get(elm)); } /** * Production h() function based on Preact by * Jason Miller (@developit) * Licensed under the MIT License * https://github.com/developit/preact/blob/master/LICENSE * * Modified for Stencil's compiler and vdom */ const stack = []; function h(nodeName, vnodeData) { let child