UNPKG

react-jsbox

Version:

A custom React renderer for writing JSBox apps in React.

473 lines (373 loc) 11.8 kB
import ReactFiberReconciler from 'react-reconciler'; import { useState, useEffect, useRef, useCallback, useMemo, useReducer, useLayoutEffect } from 'react'; const hasOwnProperty = Object.prototype.hasOwnProperty; // HighRes but slower then Date.now during invoke // export const now = () => $objc('NSDate').invoke('date').invoke('timeIntervalSince1970') * 1000 const { now } = Date; const is = { obj: a => a === Object(a), str: a => typeof a === 'string', num: a => typeof a === 'number', und: a => a === void 0, arr: a => Array.isArray(a), equ(a, b) { // Wrong type, doesn't match if (typeof a !== typeof b) return false // Atomic, just compare a against b if (is.str(a) || is.num(a) || is.obj(a)) return a === b // Array, shallow compare first to see if it's a match if (is.arr(a) && a == b) return true // Last resort, go through keys let i; for (i in a) if (!(i in b)) return false for (i in b) if (a[i] !== b[i]) return false return is.und(i) ? a === b : true } }; function filterProps(oldProps = {}, newProps) { const sameProps = Object.keys(newProps).filter(key => is.equ(newProps[key], oldProps[key])); const leftOvers = Object.keys(oldProps).filter(key => newProps[key] === void 0); const filteredProps = [...sameProps, 'events', 'children', 'key', 'ref'].reduce((acc, prop) => { let { [prop]: _, ...rest } = acc; return rest }, newProps); leftOvers.forEach(key => key !== 'children' && (filteredProps[key] = undefined)); return filteredProps } class View { constructor(type, props) { const { layout, events, animate } = props; this._element = $ui.create({ type, props, events }); this._layout = layout; this._animate = animate; } _element = null _layout = null _animate = null get element() { return this._element } applyLayout() { if (typeof this._layout === 'function') { this.element.layout(this._layout); } } updateLayout() { if (typeof this._layout === 'function') { this.element.updateLayout(this._layout); } } remakeLayout() { if (typeof this._layout === 'function') { this.element.remakeLayout(this._layout); } } appendChild(child) { this.element.add(child.element); child.applyLayout(); } removeChild(child) { child.element.remove(); } insertBefore(child, beforeChild) { this.element.insertBelow(child.element, beforeChild.element); child.applyLayout(); } update(updatePayload) { let needsUpdateLayout = false; if (hasOwnProperty.call(updatePayload, 'layout')) { this._layout = updatePayload.layout; needsUpdateLayout = true; delete updatePayload.layout; } if (hasOwnProperty.call(updatePayload, 'animate')) { this._animate = updatePayload.animate; delete updatePayload.animate; } const element = this.element; if (this._animate) { const { duration = 0.4, damping = 0, velocity = 0, options = 0, completion = () => {} } = this._animate; $ui.animate({ duration, animation() { Object.keys(updatePayload).forEach(prop => { element[prop] = updatePayload[prop]; }); }, damping, velocity, options, completion }); this.showOverlay(); return } Object.keys(updatePayload).forEach(prop => { element[prop] = updatePayload[prop]; }); needsUpdateLayout && this.updateLayout(); this.showOverlay(); } showOverlay() { if (!global.__REACT_JSBOX_HIGHLIGHT_UPDATES__) { return } const { cornerRadius, smoothCorners, size } = this.element; const overlayView = $ui.create({ type: 'view', props: { frame: $rect(0, 0, size.width, size.height), alpha: 0.6, cornerRadius, smoothCorners, bgcolor: $color('clear'), borderColor: $color('#37afa9'), borderWidth: 2, userInteractionEnabled: false } }); this.element.add(overlayView); setTimeout(() => { overlayView.remove(); }, 300); } } const NO_CONTEXT = true; const hostConfig = { now, setTimeout, clearTimeout, scheduleTimeout: setTimeout, cancelTimeout: clearTimeout, noTimeout: -1, supportsMutation: true, supportsPersistence: false, supportsHydration: false, isPrimaryRenderer: true, getPublicInstance({ element }) { return element }, getRootHostContext() { return NO_CONTEXT }, getChildHostContext() { return NO_CONTEXT }, prepareForCommit() { // noop }, resetAfterCommit() { // noop }, createInstance(type, props, internalInstanceHandle) { return new View(type, props) }, appendInitialChild(parentInstance, child) { parentInstance.appendChild(child); child.applyLayout(); }, finalizeInitialChildren(parentInstance, type, props) { return false }, prepareUpdate(instance, type, oldProps, newProps) { return filterProps(oldProps, newProps) }, shouldSetTextContent() { return false }, shouldDeprioritizeSubtree(type, props) { return !!props.hidden }, createTextInstance() { return null }, appendChild(parentInstance, child) { parentInstance.appendChild(child); }, appendChildToContainer(parentInstance, child) { const parent = parentInstance.element || parentInstance; parent.add(child.element); child.applyLayout(); }, commitMount(instance, updatePayload, type, oldProps, newProps) { // noop }, commitUpdate(instance, updatePayload, type, oldProps, newProps) { if (updatePayload) { instance.update(updatePayload); } }, insertBefore(parentInstance, child, beforeChild) { parentInstance.insertBefore(child, beforeChild); }, insertInContainerBefore(parentInstance, child, beforeChild) { const parent = parentInstance.element || parentInstance; parent.insertBelow(child.element, beforeChild.element); }, removeChild(parentInstance, child) { parentInstance.removeChild(child); }, removeChildFromContainer(parentInstance, child) { child.element.remove(); }, resetTextContent() { // noop }, hideInstance(instance) { instance.element.hidden = true; }, unhideInstance(instance) { instance.element.hidden = false; }, hideTextInstance(instance) { // noop }, unhideTextInstance(instance, props) { // noop }, getFundamentalComponentInstance(fundamentalInstance) { throw new Error('Not yet implemented.') }, mountFundamentalComponent(fundamentalInstance) { throw new Error('Not yet implemented.') }, shouldUpdateFundamentalComponent(fundamentalInstance) { console.warn('Not yet implemented.'); return false }, updateFundamentalComponent(fundamentalInstance) { throw new Error('Not yet implemented.') }, unmountFundamentalComponent(fundamentalInstance) { throw new Error('Not yet implemented.') }, cloneFundamentalInstance(fundamentalInstance) { throw new Error('Not yet implemented.') }, clearContainer(container) { container?.views?.forEach(view => view?.remove()); }, getInstanceFromNode() { throw new Error('Not yet implemented.') }, beforeActiveInstanceBlur() { // noop }, afterActiveInstanceBlur() { // noop }, preparePortalMount() { // noop }, prepareScopeUpdate() {}, getInstanceFromScope() { throw new Error('Not yet implemented.') } }; const reconciler = ReactFiberReconciler(hostConfig); const isConcurrent = true; const hydrate = false; const defaultOptions = { onInit: () => {}, onRender: () => {} }; function render(element, container, options) { const rendererOptions = Object.assign({}, defaultOptions, options); let fiberRoot = container._reactRootContainer; if (!fiberRoot) { container.views.forEach(view => view.remove()); const newFiberRoot = reconciler.createContainer(container, isConcurrent, hydrate); // eslint-disable-next-line fiberRoot = container._reactRootContainer = newFiberRoot; } rendererOptions.onInit(reconciler); return reconciler.updateContainer(element, fiberRoot, null, rendererOptions.onRender) } const useCache = (key, initialValue) => { const [state, setState] = useState(() => { const cacheValue = $cache.get(key); if (cacheValue === undefined) { $cache.set(key, initialValue); return initialValue } return cacheValue }); useEffect(() => void $cache.set(key, state)); return [state, setState] }; function useTimeoutFn(fn, ms = 0) { const ready = useRef(false); const timeout = useRef(); const callback = useRef(fn); const isReady = useCallback(() => ready.current, []); const set = useCallback(() => { ready.current = false; timeout.current && clearTimeout(timeout.current); timeout.current = setTimeout(() => { ready.current = true; callback.current(); }, ms); }, [ms]); const clear = useCallback(() => { ready.current = null; timeout.current && clearTimeout(timeout.current); }, []); // update ref when function changes useEffect(() => { callback.current = fn; }, [fn]); // set on mount, clear on unmount useEffect(() => { set(); return clear }, [ms]); return [isReady, clear, set] } function useDebounce(fn, ms = 0, deps = []) { const [isReady, cancel, reset] = useTimeoutFn(fn, ms); useEffect(reset, deps); return [isReady, cancel] } const useEventHandler = (eventHandlerMap, deps) => useMemo(() => eventHandlerMap, deps); const useFirstMountState = () => { const isFirst = useRef(true); if (isFirst.current) { isFirst.current = false; return true } return isFirst.current }; const useLatest = value => { const ref = useRef(value); ref.current = value; return ref }; function useRendersCount() { return ++useRef(0).current } const updateReducer = num => (num + 1) % 1000000; const useUpdate = () => { const [, update] = useReducer(updateReducer, 0); return update }; const useUpdateEffect = (effect, deps) => { const isFirstMount = useFirstMountState(); useEffect(() => { if (!isFirstMount) { return effect() } }, deps); }; const useUpdateLayoutEffect = (effect, deps) => { const isFirstMount = useFirstMountState(); useLayoutEffect(() => { if (!isFirstMount) { return effect() } }, deps); }; export { render, useCache, useDebounce, useEventHandler, useFirstMountState, useLatest, useRendersCount, useTimeoutFn, useUpdate, useUpdateEffect, useUpdateLayoutEffect };