UNPKG

react-view-router

Version:
314 lines 10.1 kB
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;