@quick-tv/react
Version:
QuickTV react framework
323 lines (302 loc) • 8.98 kB
text/typescript
/*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2017-2019 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable no-param-reassign */
import ViewNode from '../dom/view-node';
import Element from '../dom/element-node';
import * as UIManagerModule from '../modules/ui-manager-module';
import { Device } from '../global';
import { getRootViewId, getRootContainer } from '../utils/node';
import { deepCopy, isDev, trace, warn } from '../utils';
import { Platform } from '../types';
const componentName = ['%c[native]%c', 'color: red', 'color: auto'];
interface BatchType {
[key: string]: symbol;
}
const NODE_OPERATION_TYPES: BatchType = {
createNode: Symbol('createNode'),
updateNode: Symbol('updateNode'),
deleteNode: Symbol('deleteNode'),
};
interface BatchChunk {
type: symbol,
nodes: HippyTypes.NativeNode[]
}
let batchIdle = true;
let batchNodes: BatchChunk[] = [];
/**
* Convert an ordered node array into multiple fragments
*/
function chunkNodes(batchNodes: BatchChunk[]) {
const result: BatchChunk[] = [];
for (let i = 0; i < batchNodes.length; i += 1) {
const chunk: BatchChunk = batchNodes[i];
const { type, nodes } = chunk;
const lastChunk = result[result.length - 1];
if (!lastChunk || lastChunk.type !== type) {
result.push({
type,
nodes,
});
} else {
lastChunk.nodes = lastChunk.nodes.concat(nodes);
}
}
return result;
}
/**
* batch Updates from js to native
* @param {number} rootViewId
*/
function batchUpdate(rootViewId: number): void {
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);
if (__PLATFORM__ === Platform.ios || Device.platform.OS === 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);
if (__PLATFORM__ === Platform.ios || Device.platform.OS === Platform.ios) {
chunk.nodes.forEach(node => (
UIManagerModule.deleteNode(rootViewId, [node])
));
} else {
UIManagerModule.deleteNode(rootViewId, chunk.nodes);
}
break;
default:
// pass
}
});
}
/**
* endBatch - end batch update
* @param {boolean} isHookUsed - whether used commitEffects hook
*/
function endBatch(isHookUsed = false): void {
if (!batchIdle) return;
batchIdle = false;
if (batchNodes.length === 0) {
batchIdle = true;
return;
}
const rootViewId = getRootViewId();
UIManagerModule.startBatch();
// if commitEffectsHook used, call batchUpdate synchronously
if (isHookUsed) {
batchUpdate(rootViewId);
UIManagerModule.endBatch();
batchNodes = [];
batchIdle = true;
} else {
Promise.resolve().then(() => {
batchUpdate(rootViewId);
UIManagerModule.endBatch();
batchNodes = [];
batchIdle = true;
});
}
}
/**
* Translate to native props from attributes and meta
*/
function getNativeProps(node: Element) {
const { children, ...otherProps } = node.attributes;
return otherProps;
}
/**
* Get target node attributes, used to chrome devTool tag attribute show while debugging
*/
function getTargetNodeAttributes(targetNode: Element) {
try {
const targetNodeAttributes = deepCopy(targetNode.attributes);
const { id, nodeId } = targetNode;
const attributes = {
id,
hippyNodeId: `${nodeId}`,
...targetNodeAttributes,
};
delete attributes.text;
delete attributes.value;
return attributes;
} catch (e) {
warn('getTargetNodeAttributes error:', e);
return {};
}
}
/**
* Render Element to native
*/
function renderToNative(rootViewId: number, targetNode: Element): HippyTypes.NativeNode | null {
if (!targetNode.nativeName) {
warn('Component need to define the native name', targetNode);
return null;
}
if (targetNode.meta.skipAddToDom) {
return null;
}
if (!targetNode.meta.component) {
throw new Error(`Specific tag is not supported yet: ${targetNode.tagName}`);
}
// Translate to native node
const nativeNode: HippyTypes.NativeNode = {
id: targetNode.nodeId,
pId: (targetNode.parentNode?.nodeId) || rootViewId,
index: targetNode.index,
name: targetNode.nativeName,
props: {
...getNativeProps(targetNode),
style: targetNode.style,
},
tagName: targetNode.nativeName,
};
// Add nativeNode attributes info for debugging
if (isDev()) {
nativeNode.props!.attributes = getTargetNodeAttributes(targetNode);
}
return nativeNode;
}
/**
* Render Element with children to native
* @param {number} rootViewId - rootView id
* @param {ViewNode} node - current node
* @param {number} [atIndex] - current node index
* @param {Function} [callback] - function called on each traversing process
* @returns {HippyTypes.NativeNode[]}
*/
function renderToNativeWithChildren(
rootViewId: number,
node: ViewNode,
atIndex?: number,
callback?: Function,
): HippyTypes.NativeNode[] {
const nativeLanguages: HippyTypes.NativeNode[] = [];
let index = atIndex;
if (typeof index === 'undefined' && node && node.parentNode) {
index = node.parentNode.childNodes.indexOf(node);
}
node.traverseChildren((targetNode: Element) => {
const nativeNode = renderToNative(rootViewId, targetNode);
if (nativeNode) {
nativeLanguages.push(nativeNode);
}
if (typeof callback === 'function') {
callback(targetNode);
}
}, index);
return nativeLanguages;
}
function isLayout(node: ViewNode) {
const container = getRootContainer();
if (!container) {
return false;
}
// Determine node is a Document instance
return node instanceof container.containerInfo.constructor;
}
function insertChild(parentNode: ViewNode, childNode: ViewNode, atIndex = -1) {
if (!parentNode || !childNode) {
return;
}
if (childNode.meta.skipAddToDom) {
return;
}
const rootViewId = getRootViewId();
const renderRootNodeCondition = isLayout(parentNode) && !parentNode.isMounted;
const renderOtherNodeCondition = parentNode.isMounted && !childNode.isMounted;
// Render the root node or other nodes
if (renderRootNodeCondition || renderOtherNodeCondition) {
const translated = renderToNativeWithChildren(
rootViewId,
childNode,
atIndex,
(node: ViewNode) => {
if (!node.isMounted) {
node.isMounted = true;
}
},
);
batchNodes.push({
type: NODE_OPERATION_TYPES.createNode,
nodes: translated,
});
}
}
function removeChild(parentNode: ViewNode, childNode: ViewNode | null, index: number) {
if (!childNode || childNode.meta.skipAddToDom) {
return;
}
childNode.isMounted = false;
childNode.index = index;
const rootViewId = getRootViewId();
const deleteNodeIds: HippyTypes.NativeNode[] = [{
id: childNode.nodeId,
pId: childNode.parentNode ? childNode.parentNode.nodeId : rootViewId,
index: childNode.index,
}];
batchNodes.push({
type: NODE_OPERATION_TYPES.deleteNode,
nodes: deleteNodeIds,
});
}
function updateChild(parentNode: Element) {
if (!parentNode.isMounted) {
return;
}
const rootViewId = getRootViewId();
const translated = renderToNative(rootViewId, parentNode);
if (translated) {
batchNodes.push({
type: NODE_OPERATION_TYPES.updateNode,
nodes: [translated],
});
}
}
function updateWithChildren(parentNode: ViewNode) {
if (!parentNode.isMounted) {
return;
}
const rootViewId = getRootViewId();
const translated = renderToNativeWithChildren(rootViewId, parentNode);
batchNodes.push({
type: NODE_OPERATION_TYPES.updateNode,
nodes: translated,
});
}
export {
endBatch,
renderToNative,
renderToNativeWithChildren,
insertChild,
removeChild,
updateChild,
updateWithChildren,
};