UNPKG

@huantv/vue

Version:
531 lines (486 loc) 14.2 kB
/* eslint-disable no-underscore-dangle */ /* eslint-disable no-param-reassign */ /** * Virtual DOM to Native DOM */ import Native, { UIManagerModule } from '../../runtime/native'; import { fromAstNodes, SelectorsMap } from './style'; import { GLOBAL_STYLE_NAME } from '../../runtime/constants'; import { getApp, trace, warn, isFunction, capitalizeFirstLetter, convertImageLocalPath, } from '../../util'; import { isRTL, } from '../../util/i18n'; const componentName = ['%c[native]%c', 'color: red', 'color: auto']; if (typeof global.Symbol !== 'function') { global.Symbol = str => str; } /** * UI Operations */ const NODE_OPERATION_TYPES = { createNode: Symbol('createNode'), updateNode: Symbol('updateNode'), deleteNode: Symbol('deleteNode'), }; let __batchIdle = true; let __batchNodes = []; /** * Convert an ordered node array into multiple fragments */ function chunkNodes(batchNodes) { const result = []; for (let i = 0; i < batchNodes.length; i += 1) { const chunk = batchNodes[i]; const { type, nodes } = chunk; const _chunk = result[result.length - 1]; if (!_chunk || _chunk.type !== type) { result.push({ type, nodes, }); } else { _chunk.nodes = _chunk.nodes.concat(nodes); } } return result; } /** * Initial CSS Map; */ let __cssMap; function startBatch() { if (__batchIdle) { UIManagerModule.startBatch(); } } function endBatch(app) { if (!__batchIdle) { return; } __batchIdle = false; const { $nextTick, $options: { rootViewId, }, } = app; $nextTick(() => { const chunks = chunkNodes(__batchNodes); chunks.forEach((chunk) => { switch (chunk.type) { case NODE_OPERATION_TYPES.createNode: trace(...componentName, 'createNode', chunk.nodes); UIManagerModule.createNode(rootViewId, chunk.nodes); break; case NODE_OPERATION_TYPES.updateNode: trace(...componentName, 'updateNode', chunk.nodes); // FIXME: iOS should be able to update multiple nodes at once. if (__PLATFORM__ === 'ios' || Native.Platform === 'ios') { chunk.nodes.forEach(node => ( UIManagerModule.updateNode(rootViewId, [node]) )); } else { UIManagerModule.updateNode(rootViewId, chunk.nodes); } break; case NODE_OPERATION_TYPES.deleteNode: trace(...componentName, 'deleteNode', chunk.nodes); // FIXME: iOS should be able to delete mutiple nodes at once. if (__PLATFORM__ === 'ios' || Native.Platform === 'ios') { chunk.nodes.forEach(node => ( UIManagerModule.deleteNode(rootViewId, [node]) )); } else { UIManagerModule.deleteNode(rootViewId, chunk.nodes); } break; default: // pass } }); UIManagerModule.endBatch(); __batchIdle = true; __batchNodes = []; }); } function getCssMap() { // To support dynamic import. __cssMap can be loaded from differnet js file. // __cssMap should be create/append if global[GLOBAL_STYLE_NAME] exists; if (__cssMap && !global[GLOBAL_STYLE_NAME]) { return __cssMap; } // HERE IS A SECRET STARTUP OPTION: beforeStyleLoadHook // Usage for process the styles while styles loading. const cssRules = fromAstNodes(global[GLOBAL_STYLE_NAME]); if (__cssMap) { __cssMap.append(cssRules); } else { __cssMap = new SelectorsMap(cssRules); } global[GLOBAL_STYLE_NAME] = undefined; return __cssMap; } /** * Translate to native props from attributes and meta */ function getNativeProps(node) { // Initial the props with empty const props = {}; // Get the default native props from meta if (node.meta.component.defaultNativeProps) { Object.keys(node.meta.component.defaultNativeProps).forEach((key) => { // Skip the defined attribute if (node.getAttribute(key) !== undefined) { return; } // Get the default props const defaultNativeProps = node.meta.component.defaultNativeProps[key]; if (isFunction(defaultNativeProps)) { props[key] = defaultNativeProps(node); } else { props[key] = defaultNativeProps; } }); } // Get the proceed props from node attributes Object.keys(node.attributes).forEach((key) => { let value = node.getAttribute(key); // No defined map if (!node.meta.component.attributeMaps) { props[key] = value; return; } if (!node.meta.component.attributeMaps[key]) { props[key] = value; return; } // Defined mapped props. const map = node.meta.component.attributeMaps[key]; if (typeof map === 'string') { props[map] = value; return; } // Define mapped props is a function. if (isFunction(map)) { props[key] = map(value); return; } // Defined object map with value const { name: propsKey, propsValue } = map; if (isFunction(propsValue)) { value = propsValue(value); } props[propsKey] = value; }); // Get the force props from meta, it's can't be override if (node.meta.component.nativeProps) { Object.assign(props, node.meta.component.nativeProps); } // FIXME: Workaround for Image src props, should unify to use src. if (node.tagName === 'img' && (__PLATFORM__ === 'ios' || Native.Platform === 'ios')) { props.source = [{ uri: props.src, }]; props.src = undefined; } return props; } /** * parse TextInput component on special condition * @param targetNode * @param style */ function parseTextInputComponent(targetNode, style) { if (targetNode.meta.component.name === 'TextInput') { // Change textAlign to right if display direction is right to left. if (isRTL()) { if (!style.textAlign) { style.textAlign = 'right'; } } } } /** * parse view component on special condition * @param targetNode * @param nativeNode * @param style */ function parseViewComponent(targetNode, nativeNode, style) { if (targetNode.meta.component.name === 'View') { // Change View to ScrollView when meet overflow=scroll style. if (style.overflowX === 'scroll' && style.overflowY === 'scroll') { warn('overflow-x and overflow-y for View can not work together'); } if (style.overflowY === 'scroll') { nativeNode.name = 'ScrollView'; } else if (style.overflowX === 'scroll') { nativeNode.name = 'ScrollView'; // Necessary for horizontal scrolling nativeNode.props.horizontal = true; // Change flexDirection to row-reverse if display direction is right to left. style.flexDirection = isRTL() ? 'row-reverse' : 'row'; } // Change the ScrollView child collapsable attribute if (nativeNode.name === 'ScrollView') { if (targetNode.childNodes.length !== 1) { warn('Only one child node is acceptable for View with overflow'); } if (targetNode.childNodes.length) { targetNode.childNodes[0].setStyle('collapsable', false); } } // TODO backgroundImage would use local path if webpack file-loader active, which needs native support if (style.backgroundImage) { style.backgroundImage = convertImageLocalPath(style.backgroundImage); } } } /** * Get target node attributes, use to chrome devTool tag attribute show while debugging * @param targetNode * @returns attributes */ function getTargetNodeAttributes(targetNode) { try { const targetNodeAttributes = JSON.parse(JSON.stringify(targetNode.attributes)); const classInfo = Array.from(targetNode.classList || []).join(' '); const attributes = { id: targetNode.id, class: classInfo, ...targetNodeAttributes, }; delete attributes.text; delete attributes.value; return attributes; } catch (e) { warn('getTargetNodeAttributes error:', e); return {}; } } /** * Render Element to native */ function renderToNative(rootViewId, targetNode) { if (targetNode.meta.skipAddToDom) { return null; } if (!targetNode.meta.component) { throw new Error(`Specific tag is not supported yet: ${targetNode.tagName}`); } let style = {}; // Apply styles when the targetNode attach to document at first time. if (targetNode.meta.component.defaultNativeStyle) { style = { ...targetNode.meta.component.defaultNativeStyle }; } // Apply styles from CSS const matchedSelectors = getCssMap().query(targetNode); matchedSelectors.selectors.forEach((matchedSelector) => { matchedSelector.ruleSet.declarations.forEach((cssStyle) => { style[cssStyle.property] = cssStyle.value; }); }); // Apply style from style attribute. style = { ...style, ...targetNode.style }; // Convert to real native event const events = {}; // FIXME: Bad accessing the private property. if (targetNode._emitter) { const vueEventNames = Object.keys(targetNode._emitter.getEventListeners()); const { eventNamesMap } = targetNode.meta.component; if (eventNamesMap) { vueEventNames.forEach((vueEventName) => { try { if (vueEventName) { const nativeEventName = eventNamesMap[vueEventName]; if (nativeEventName) { events[nativeEventName] = true; } else { events[`on${capitalizeFirstLetter(vueEventName)}`] = true; } } } catch (e) { } }); } else { try { vueEventNames.forEach((vueEventName) => { events[`on${capitalizeFirstLetter(vueEventName)}`] = true; }); } catch (e) { } } } // Translate to native node const nativeNode = { id: targetNode.nodeId, pId: (targetNode.parentNode && targetNode.parentNode.nodeId) || rootViewId, index: targetNode.index, name: targetNode.meta.component.name, props: { ...getNativeProps(targetNode), ...events, style, }, }; // Add nativeNode attributes info for debugging if (process.env.NODE_ENV !== 'production') { nativeNode.tagName = targetNode.tagName; nativeNode.props.attributes = getTargetNodeAttributes(targetNode); } parseViewComponent(targetNode, nativeNode, style); parseTextInputComponent(targetNode, style); return nativeNode; } /** * Render Element with child to native */ function renderToNativeWithChildren(rootViewId, node) { const nativeLanguages = []; node.traverseChildren((targetNode) => { const nativeNode = renderToNative(rootViewId, targetNode); if (nativeNode) { nativeLanguages.push(nativeNode); } }); return nativeLanguages; } function isLayout(node, rootView) { // First time init rootViewId always be 3. if (node.nodeId === 3) { return true; } // Check the id is specific for rootView. if (process.env.NODE_ENV !== 'production') { if (!rootView) { warn('rootView option is necessary for new HippyVue()'); } if (rootView.charAt(0) !== '#') { warn('rootView option must be unique ID selector start with # '); } } return node.id === rootView.slice(1 - rootView.length); } function insertChild(parentNode, childNode, atIndex = -1) { if (!parentNode) { return; } if (parentNode.meta && isFunction(parentNode.meta.insertChild)) { parentNode.meta.insertChild(parentNode, childNode, atIndex); } if (childNode.meta.skipAddToDom) { return; } const app = getApp(); if (!app) { return; } const { $options: { rootViewId, rootView } } = app; // Render the root node if (isLayout(parentNode, rootView) && !parentNode.isMounted) { // Start real native work. const translated = renderToNativeWithChildren(rootViewId, parentNode); startBatch(); __batchNodes.push({ type: NODE_OPERATION_TYPES.createNode, nodes: translated, }); endBatch(app); parentNode.traverseChildren((node) => { if (!node.isMounted) { node.isMounted = true; } }); // Render others child nodes. } else if (parentNode.isMounted && !childNode.isMounted) { const translated = renderToNativeWithChildren(rootViewId, childNode); startBatch(); __batchNodes.push({ type: NODE_OPERATION_TYPES.createNode, nodes: translated, }); endBatch(app); childNode.traverseChildren((node) => { if (!node.isMounted) { node.isMounted = true; } }); } } function removeChild(parentNode, childNode) { if (parentNode && parentNode.meta && isFunction(parentNode.meta.removeChild)) { parentNode.meta.removeChild(parentNode, childNode); } if (!childNode || childNode.meta.skipAddToDom) { return; } childNode.isMounted = false; childNode.traverseChildren((node) => { if (node.isMounted) { node.isMounted = false; } }); const app = getApp(); const deleteNodeIds = []; childNode.traverseChildren((targetNode) => { if (targetNode.meta.skipAddToDom) { return; } deleteNodeIds.push({ id: targetNode.nodeId, index: targetNode.index, pId: targetNode.parentNode.nodeId, }); }); startBatch(); __batchNodes.push({ type: NODE_OPERATION_TYPES.deleteNode, nodes: deleteNodeIds, }); endBatch(app); } function updateChild(parentNode) { if (!parentNode.isMounted) { return; } const app = getApp(); const { $options: { rootViewId } } = app; const translated = renderToNative(rootViewId, parentNode); if (translated) { startBatch(); __batchNodes.push({ type: NODE_OPERATION_TYPES.updateNode, nodes: [translated], }); endBatch(app); } } function updateWithChildren(parentNode) { if (!parentNode.isMounted) { return; } const app = getApp(); const { $options: { rootViewId } } = app; const translated = renderToNativeWithChildren(rootViewId, parentNode); startBatch(); __batchNodes.push({ type: NODE_OPERATION_TYPES.updateNode, nodes: translated, }); endBatch(app); } export { renderToNative, renderToNativeWithChildren, insertChild, removeChild, updateChild, updateWithChildren, };