react-view-router
Version:
react-view-router
314 lines • 10.1 kB
JavaScript
import "core-js/modules/web.dom.iterable.js";
import React, { Fragment, useRef, useCallback, useLayoutEffect, useImperativeHandle, useState, useMemo } from 'react';
import { innumerable } from './util';
const KEEP_ALIVE_ANCHOR = 'keep-alive-anchor';
const KEEP_ALIVE_REPLACOR = 'keep-alive-replacor';
const KEEP_ALIVE_POSITION = 'keep-alive-position';
const KEEP_ALIVE_KEEP_COPIES = 'keep-alive-keep-copies';
function Component(props) {
const {
utils,
active,
children,
name,
anchor = null,
inner
} = props;
const {
appendChild,
insertBefore
} = utils;
const [$refs] = useState(() => {
const holder = utils.createDocumentFragment();
return {
name,
inner,
holder,
active,
anchor,
anchorRoot: null,
mountRoot: null,
childNodes: [],
unmounting: false,
insertBefore,
appendChild,
position: null
};
});
$refs.anchor = anchor;
$refs.anchorRoot = useMemo(() => {
const {
anchor
} = $refs;
if (!anchor) return null;
return inner ? anchor : anchor.parentNode || null;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [$refs.anchor, inner]);
$refs.active = $refs.active || active;
$refs.insertBefore = insertBefore;
$refs.appendChild = appendChild;
useLayoutEffect(() => {
const unhooks = [];
const hook = (el, methodName, cb, replacor) => {
const old = el[methodName];
if (!old || old._hooked) {
console.error(`[keep-alive]warning: hook method "${methodName}" ${old ? 'already hooked' : 'not exist'}!`);
return;
}
const newMethod = function () {
const {
mountRoot,
active,
unmounting
} = $refs;
if (!mountRoot || !active || unmounting) return old.apply(this, arguments);
// @ts-ignore
// eslint-disable-next-line prefer-spread
cb && cb.apply(this, arguments);
if (replacor) return replacor(old, ...arguments);
// eslint-disable-next-line prefer-spread
return mountRoot[methodName].apply(mountRoot, arguments);
};
newMethod._hooked = true;
el[methodName] = newMethod;
unhooks.push(() => el[methodName] = old);
};
hook($refs.holder, 'appendChild', node => {
$refs.childNodes.push(node);
}, $refs.inner ? undefined : (fn, node) => $refs.anchorRoot.insertBefore(node, $refs.anchor));
hook($refs.holder, 'removeChild', node => {
const idx = $refs.childNodes.findIndex(v => v === node);
if (~idx) $refs.childNodes.splice(idx, 1);
});
hook($refs.holder, 'insertBefore', (newNode, node) => {
const idx = $refs.childNodes.findIndex(v => v === node);
if (~idx) $refs.childNodes.splice(idx, 0, newNode);
});
hook($refs.holder, 'replaceChild', (newChild, oldChild) => {
const idx = $refs.childNodes.findIndex(v => v === oldChild);
if (~idx) $refs.childNodes.splice(idx, 1, newChild);
});
['hasChildNodes', 'contains', 'getRootNode'].forEach(name => hook($refs.holder, name));
return () => unhooks.forEach(cb => cb());
}, [$refs, $refs.holder]);
const mountView = useCallback((mountRoot, anchor) => {
if (!anchor || !mountRoot) return;
const {
holder,
appendChild,
insertBefore,
inner
} = $refs;
if (anchor.mountName && anchor.mountName !== $refs.name) {
anchor.unmountView();
}
$refs.childNodes = [...holder.childNodes];
$refs.childNodes.forEach(child => {
if (inner) appendChild(mountRoot, child);else insertBefore(mountRoot, child, anchor);
// if (child && child[KEEP_ALIVE_REPLACOR] && child.mountName) {
// let replacor = (child as any)[KEEP_ALIVE_REPLACOR];
// let item = replacor[child.mountName];
// item && item.mountView(mountRoot, child);
// }
});
anchor.mountName = $refs.name;
$refs.mountRoot = mountRoot;
const position = $refs.position;
if (position && mountRoot.scrollTo) mountRoot.scrollTo(position.x, position.y);
}, [$refs]);
const unmountView = useCallback(() => {
const {
childNodes,
active
} = $refs;
if (!active || !childNodes.length) return;
$refs.unmounting = true;
try {
const {
appendChild,
insertBefore,
holder,
mountRoot,
anchor,
inner
} = $refs;
const position = {
x: mountRoot.scrollLeft,
y: mountRoot.scrollTop
};
const isValidChild = anchor && mountRoot.contains(anchor);
const nodes = childNodes.splice(0, childNodes.length);
nodes.forEach(child => {
if (child && child[KEEP_ALIVE_REPLACOR]) child.unmountView();else {
const p = {
x: child.scrollLeft,
y: child.scrollTop
};
innumerable(child, KEEP_ALIVE_POSITION, p.x || p.y ? p : null);
}
appendChild(holder, child);
if (mountRoot.dataset?.keepAliveKeepCopies) {
const cloneNode = child.cloneNode();
if (inner || !isValidChild) appendChild(mountRoot, cloneNode);else insertBefore(mountRoot, cloneNode, anchor);
}
});
$refs.mountRoot = null;
$refs.position = position.x || position.y ? position : null;
if (anchor.mountName === $refs.name) anchor.mountName = '';
} finally {
$refs.unmounting = false;
}
}, [$refs]);
useMemo(() => {
if (!anchor) return;
if (!anchor.unmountView) {
anchor.unmountView = function () {
if (!this.mountName || !this[KEEP_ALIVE_REPLACOR]) return;
let replacor = this[KEEP_ALIVE_REPLACOR];
let item = replacor && replacor[this.mountName];
item && item.unmountView();
};
}
let replacor = anchor[KEEP_ALIVE_REPLACOR];
if (!replacor) {
replacor = {};
innumerable(anchor, KEEP_ALIVE_REPLACOR, replacor);
}
let item = replacor[$refs.name] = {};
item.$refs = $refs;
item.unmountView = unmountView;
item.mountView = mountView;
}, [$refs, anchor, mountView, unmountView]);
useLayoutEffect(() => {
if (!$refs.active) return;
const {
anchor,
anchorRoot,
mountRoot
} = $refs;
if (mountRoot && !anchorRoot) unmountView();
if (!anchorRoot) return;
if (active) {
if (anchorRoot !== mountRoot || anchor.mountName !== $refs.name) mountView(anchorRoot, anchor);
} else unmountView();
}, [active, $refs, mountView, unmountView]);
useLayoutEffect(() => () => {
const {
active,
mountRoot
} = $refs;
if (active && mountRoot) unmountView();
}, [$refs, unmountView]);
return $refs.active ? utils.createPortal(children, $refs.holder, name) : null;
}
const KeepAliveAnchor = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
utils,
children = ''
} = props;
const anchorRef = useRef(null);
useImperativeHandle(ref, () => anchorRef.current, [anchorRef]);
useLayoutEffect(() => {
const {
current
} = anchorRef;
if (!current || current[KEEP_ALIVE_ANCHOR]) return;
current.style?.setProperty('display', 'none', 'important');
innumerable(current, KEEP_ALIVE_ANCHOR, true);
}, [anchorRef, utils]);
useLayoutEffect(() => {
const {
current
} = anchorRef;
if (!current) return;
if (current.textContent != children) current.textContent = children;
}, [anchorRef, children]);
return /*#__PURE__*/React.createElement('i', {
key: KEEP_ALIVE_ANCHOR,
style: {
display: 'none'
},
ref: anchorRef
});
});
function createAnchor(utils, ref, text = '') {
return /*#__PURE__*/React.createElement(KeepAliveAnchor, {
ref,
utils
}, text);
}
function createAnchorText(anchorName) {
return anchorName ? `${KEEP_ALIVE_ANCHOR} ${anchorName}` : KEEP_ALIVE_ANCHOR;
}
const KeepAlive = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
activeName,
anchorName = '',
anchor,
anchorRef,
utils,
children,
extra = {}
} = props;
const [ready, setReady] = useState(0);
const [nodes, setNodes] = useState([]);
const [$refs] = useState(() => Object.assign(anchorRef || {
current: null
}, {
activeName: ''
}));
$refs.ready = ready;
$refs.nodes = nodes;
$refs.extra = extra;
$refs.remove = useCallback((name, triggerRender = true) => {
const idx = nodes.findIndex(res => res.name === name);
if (~idx) {
nodes.splice(idx, 1);
triggerRender && setNodes([...nodes]);
}
return idx;
}, [nodes]);
$refs.find = useCallback(name => nodes.find(res => res.name === name), [nodes]);
useImperativeHandle(ref, () => $refs);
useLayoutEffect(() => {
const current = $refs.current;
anchorRef && ($refs.current = anchorRef.current);
setReady(ready => {
if (!$refs.current) return 0;
return $refs.current === current ? ready || 1 : ready + 1;
});
}, [$refs, anchorRef]);
useLayoutEffect(() => {
if (!activeName) {
$refs.activeName = '';
return;
}
const idx = nodes.findIndex(res => res.name === activeName);
if (~idx) {
if (children == null) nodes.splice(idx, 1);else nodes[idx].node = children;
} else nodes.push(Object.assign({
name: activeName,
node: children
}, $refs.extra));
$refs.activeName = activeName;
}, [$refs, nodes, children, activeName]);
useLayoutEffect(() => {
if (!activeName) {
$refs.activeNode = undefined;
return;
}
$refs.activeNode = nodes.find(v => v.name === activeName);
}, [$refs, activeName, nodes]);
return /*#__PURE__*/React.createElement(Fragment, {}, anchor || createAnchor(utils, $refs, createAnchorText(anchorName)), Boolean(ready) && nodes.map(({
name,
node
}) => /*#__PURE__*/React.createElement(Component, {
active: name === activeName,
anchor: $refs.current,
name,
key: name,
utils
}, node)));
});
export { createAnchor, createAnchorText, KEEP_ALIVE_ANCHOR, KEEP_ALIVE_REPLACOR, KEEP_ALIVE_KEEP_COPIES };
export default KeepAlive;