"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.NutrientOverlay = NutrientOverlay;
exports.NutrientOverlayItem = NutrientOverlayItem;
var react_1 = __importStar(require("react"));
var react_native_1 = require("react-native");
var NotificationCenter_1 = require("../notification-center/NotificationCenter");
var overlayLayout_1 = require("./overlayLayout");
var OverlayContext = (0, react_1.createContext)({
viewRef: null,
viewport: null,
});
/**
* A transparent layer that renders React Native components anchored to
* positions on the PDF, keeping them in sync with pan and zoom.
*
* Place it as a sibling of `<NutrientView>` inside a container the view fills,
* so the overlay and the view share the same frame origin:
*
* tsx
* <View style={{ flex: 1 }}>
* <NutrientView ref={viewRef} document={...} />
* <NutrientOverlay viewRef={viewRef}>
* <NutrientOverlayItem pageIndex={0} position={{ x: 100, y: 100 }}>
* <MyBadge />
* </NutrientOverlayItem>
* </NutrientOverlay>
* </View>
*
*
* This is the JS-positioned overlay: positions are recomputed from the
* `documentViewportChanged` event, so items can visibly lag during a fast fling
* or pinch. Native-driven, jank-free tracking is planned as a follow-up.
*/
function NutrientOverlay(props) {
var viewRef = props.viewRef, style = props.style, children = props.children;
var _a = (0, react_1.useState)(null), viewport = _a[0], setViewport = _a[1];
(0, react_1.useEffect)(function () {
var cancelled = false;
var retryTimer;
var subscription;
var onViewportChanged = function (payload) {
if (!cancelled) {
setViewport(payload);
}
};
var trySubscribe = function () {
if (cancelled) {
return;
}
var api = viewRef === null || viewRef === void 0 ? void 0 : viewRef.current;
var notificationCenter = api === null || api === void 0 ? void 0 : api.getNotificationCenter();
if (!api || !notificationCenter) {
// The native view may not be mounted yet; retry until it is.
retryTimer = setTimeout(trySubscribe, 100);
return;
}
subscription = notificationCenter.subscribe(NotificationCenter_1.NotificationCenter.DocumentEvent.VIEWPORT_CHANGED, onViewportChanged);
// Pull the current state so items position before the first scroll/zoom.
Promise.resolve(api.getViewportState())
.then(function (state) {
if (!cancelled && state) {
setViewport(state);
}
})
.catch(function () {
/* No page visible yet; the event will deliver the first state. */
});
};
trySubscribe();
return function () {
cancelled = true;
if (retryTimer) {
clearTimeout(retryTimer);
}
subscription === null || subscription === void 0 ? void 0 : subscription.remove();
};
}, [viewRef]);
return (react_1.default.createElement(react_native_1.View, { style: [react_native_1.StyleSheet.absoluteFill, style], pointerEvents: "box-none" },
react_1.default.createElement(OverlayContext.Provider, { value: { viewRef: viewRef, viewport: viewport } }, children)));
}
/**
* A single overlay element anchored to a PDF coordinate on a given page.
* Must be rendered inside a {@link NutrientOverlay}.
*/
function NutrientOverlayItem(props) {
var _a;
var pageIndex = props.pageIndex, position = props.position, disableAutoZoom = props.disableAutoZoom, onAppear = props.onAppear, onDisappear = props.onDisappear, children = props.children;
var _b = (0, react_1.useContext)(OverlayContext), viewRef = _b.viewRef, viewport = _b.viewport;
var _c = (0, react_1.useState)(null), screen = _c[0], setScreen = _c[1];
var _d = (0, react_1.useState)({ width: 0, height: 0 }), size = _d[0], setSize = _d[1];
var visibleRef = (0, react_1.useRef)(false);
// Guards against out-of-order async conversion results overwriting newer ones.
var generationRef = (0, react_1.useRef)(0);
// Kept in refs so effects that don't depend on these callbacks (the
// unmount-only effect, and the viewport effect below) always call the
// latest onAppear/onDisappear instead of a stale one from a prior render.
var onAppearRef = (0, react_1.useRef)(onAppear);
onAppearRef.current = onAppear;
var onDisappearRef = (0, react_1.useRef)(onDisappear);
onDisappearRef.current = onDisappear;
var setHidden = function () {
var _a;
if (visibleRef.current) {
visibleRef.current = false;
(_a = onDisappearRef.current) === null || _a === void 0 ? void 0 : _a.call(onDisappearRef);
}
setScreen(null);
};
(0, react_1.useEffect)(function () {
return function () {
var _a;
// Balance onAppear if the item unmounts while visible. No setState here:
// the component is going away.
if (visibleRef.current) {
visibleRef.current = false;
(_a = onDisappearRef.current) === null || _a === void 0 ? void 0 : _a.call(onDisappearRef);
}
};
}, []);
(0, react_1.useEffect)(function () {
var api = viewRef === null || viewRef === void 0 ? void 0 : viewRef.current;
if (!api || !viewport) {
// The view is gone (e.g. `NutrientView` unmounted); don't leave a stale
// item rendered at its last known position.
setHidden();
return;
}
var generation = ++generationRef.current;
var cancelled = false;
Promise.resolve(api.convertPointToScreen(pageIndex, position))
.then(function (point) {
var _a;
if (cancelled || generation !== generationRef.current) {
return;
}
if (point && typeof point.x === 'number' && typeof point.y === 'number') {
setScreen({ x: point.x, y: point.y });
if (!visibleRef.current) {
visibleRef.current = true;
(_a = onAppearRef.current) === null || _a === void 0 ? void 0 : _a.call(onAppearRef);
}
}
else {
// The page is not currently visible.
setHidden();
}
})
.catch(function () {
if (cancelled || generation !== generationRef.current) {
return;
}
setHidden();
});
return function () {
cancelled = true;
};
// `position` is spread into primitives so an inline object literal does not
// refire the effect on every render.
}, [viewRef, viewport, pageIndex, position.x, position.y]);
var onLayout = function (event) {
var _a = event.nativeEvent.layout, width = _a.width, height = _a.height;
setSize({ width: width, height: height });
};
if (!screen) {
return null;
}
var _e = (0, overlayLayout_1.computeOverlayItemLayout)(screen, size, (_a = viewport === null || viewport === void 0 ? void 0 : viewport.zoomScale) !== null && _a !== void 0 ? _a : 1, disableAutoZoom !== null && disableAutoZoom !== void 0 ? disableAutoZoom : false), left = _e.left, top = _e.top, scale = _e.scale;
return (react_1.default.createElement(react_native_1.View, { pointerEvents: "box-none", onLayout: onLayout, style: [
styles.item,
{ left: left, top: top },
scale !== 1 ? { transform: [{ scale: scale }] } : null,
] }, children));
}
var styles = react_native_1.StyleSheet.create({
item: {
position: 'absolute',
},
});