@prose-reader/core 1.123.0 → 1.125.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -265,7 +265,7 @@
265
265
  })
266
266
  );
267
267
  const resetLockViewportFree$ = createResetLock$(
268
- reader.navigation.viewportFree$
268
+ reader.viewportFree$
269
269
  ).pipe(operators.take(1));
270
270
  const pageTurnMode$ = reader.settings.values$.pipe(
271
271
  operators.map(() => reader.settings.values.computedPageTurnMode),
@@ -3417,15 +3417,16 @@
3417
3417
  return this.resolveLast(doc, opts);
3418
3418
  }
3419
3419
  }
3420
- const extractProseMetadataFromCfi = (cfi) => {
3420
+ const parseCfi = (cfi) => {
3421
3421
  var _a, _b;
3422
- const [itemId] = ((_a = cfi.match(/\|(\[prose\~anchor[^\]]*\])+/gi)) == null ? void 0 : _a.map((s) => s.replace(/\|\[prose\~anchor\~/, ``).replace(/\]/, ``))) || [];
3422
+ const [itemIndex] = ((_a = cfi.match(/\|(\[prose\~anchor[^\]]*\])+/gi)) == null ? void 0 : _a.map((s) => s.replace(/\|\[prose\~anchor\~/, ``).replace(/\]/, ``))) || [];
3423
3423
  const [offset] = ((_b = cfi.match(/\|(\[prose\~offset[^\]]*\])+/gi)) == null ? void 0 : _b.map((s) => s.replace(/\|\[prose\~offset\~/, ``).replace(/\]/, ``))) || [];
3424
3424
  const cleanedCfi = cfi.replace(/\|(\[prose\~[^\]]*\~[^\]]*\])+/gi, ``);
3425
3425
  const foundOffset = parseInt(offset || ``);
3426
+ const foundItemIndex = parseInt(itemIndex || ``);
3426
3427
  return {
3427
3428
  cleanedCfi,
3428
- itemId: itemId ? decodeURIComponent(itemId) : itemId,
3429
+ itemIndex: isNaN(foundItemIndex) ? void 0 : foundItemIndex,
3429
3430
  offset: isNaN(foundOffset) ? void 0 : foundOffset
3430
3431
  };
3431
3432
  };
@@ -3436,9 +3437,8 @@
3436
3437
  var _a, _b;
3437
3438
  if (!cfi) return void 0;
3438
3439
  const spineItem = spineItemsManager.getSpineItemFromCfi(cfi);
3439
- const spineItemIndex = spineItemsManager.getSpineItemIndex(spineItem) || 0;
3440
3440
  if (!spineItem) return void 0;
3441
- const { cleanedCfi, offset } = extractProseMetadataFromCfi(cfi);
3441
+ const { cleanedCfi, offset } = parseCfi(cfi);
3442
3442
  const cfiHandler = new CfiHandler(cleanedCfi, {});
3443
3443
  const rendererElement = (_a = spineItem.renderer.layers[0]) == null ? void 0 : _a.element;
3444
3444
  if (rendererElement instanceof HTMLIFrameElement) {
@@ -3449,20 +3449,17 @@
3449
3449
  return {
3450
3450
  node,
3451
3451
  offset: offset ?? resolvedOffset,
3452
- spineItemIndex,
3453
3452
  spineItem
3454
3453
  };
3455
3454
  } catch (e) {
3456
3455
  Report.error(e);
3457
3456
  return {
3458
- spineItemIndex,
3459
3457
  spineItem
3460
3458
  };
3461
3459
  }
3462
3460
  }
3463
3461
  }
3464
3462
  return {
3465
- spineItemIndex,
3466
3463
  spineItem
3467
3464
  };
3468
3465
  };
@@ -5011,11 +5008,19 @@
5011
5008
  return rxjs.combineLatest(destroyFns);
5012
5009
  }
5013
5010
  }
5014
- const getItemAnchor = (spineItem) => `|[prose~anchor~${encodeURIComponent(spineItem.item.id)}]`;
5011
+ const getItemAnchor = (item) => `|[prose~anchor~${encodeURIComponent(item.index)}]`;
5015
5012
  const getRootCfi = (spineItem) => {
5016
- const itemAnchor = getItemAnchor(spineItem);
5013
+ const itemAnchor = getItemAnchor(spineItem.item);
5017
5014
  return `epubcfi(/0${itemAnchor})`;
5018
5015
  };
5016
+ const generateCfi = (node, offset, item) => {
5017
+ const offsetAnchor = `[prose~offset~${offset || 0}]`;
5018
+ return CfiHandler.generate(
5019
+ node,
5020
+ offset,
5021
+ `${getItemAnchor(item)}|${offsetAnchor}`
5022
+ );
5023
+ };
5019
5024
  const generateCfiForSpineItemPage = Report.measurePerformance(
5020
5025
  `getCfi`,
5021
5026
  10,
@@ -5030,13 +5035,11 @@
5030
5035
  spineItem
5031
5036
  );
5032
5037
  const rendererElement = (_a = spineItem.renderer.layers[0]) == null ? void 0 : _a.element;
5033
- const itemAnchor = getItemAnchor(spineItem);
5034
- const offset = `|[prose~offset~${(nodeOrRange == null ? void 0 : nodeOrRange.offset) || 0}]`;
5035
5038
  if (nodeOrRange && rendererElement instanceof HTMLIFrameElement && ((_b = rendererElement.contentWindow) == null ? void 0 : _b.document)) {
5036
- const cfiString = CfiHandler.generate(
5039
+ const cfiString = generateCfi(
5037
5040
  nodeOrRange.node,
5038
- 0,
5039
- `${itemAnchor}${offset}`
5041
+ nodeOrRange.offset,
5042
+ spineItem.item
5040
5043
  );
5041
5044
  return cfiString.trim();
5042
5045
  }
@@ -5160,1144 +5163,1135 @@
5160
5163
  end,
5161
5164
  endNode
5162
5165
  }, item) => {
5163
- const startCFI = CfiHandler.generate(
5164
- startNode,
5165
- start,
5166
- `|[prose~anchor~${encodeURIComponent(item.id)}]`
5167
- );
5168
- const endCFI = CfiHandler.generate(
5169
- endNode,
5170
- end,
5171
- `|[prose~anchor~${encodeURIComponent(item.id)}]`
5172
- );
5166
+ const startCFI = generateCfi(startNode, start, item);
5167
+ const endCFI = generateCfi(endNode, end, item);
5173
5168
  return { start: startCFI, end: endCFI };
5174
5169
  };
5175
- class SpineItemsManager extends DestroyableClass {
5176
- constructor(context, settings) {
5177
- super();
5170
+ class DocumentRenderer {
5171
+ constructor(context, settings, hookManager, item, containerElement, resourcesHandler) {
5178
5172
  this.context = context;
5179
5173
  this.settings = settings;
5180
- this.orderedSpineItemsSubject = new rxjs.BehaviorSubject([]);
5181
- this.items$ = this.orderedSpineItemsSubject.asObservable();
5174
+ this.hookManager = hookManager;
5175
+ this.item = item;
5176
+ this.containerElement = containerElement;
5177
+ this.resourcesHandler = resourcesHandler;
5178
+ this.stateSubject = new rxjs.BehaviorSubject(`idle`);
5179
+ this.triggerSubject = new rxjs.Subject();
5180
+ this.layers = [];
5181
+ this.unload$ = this.triggerSubject.pipe(
5182
+ rxjs.withLatestFrom(this.stateSubject),
5183
+ rxjs.filter(
5184
+ ([trigger, state]) => trigger === `unload` && state !== "idle" && state !== "unloading"
5185
+ ),
5186
+ rxjs.map(() => void 0),
5187
+ rxjs.share()
5188
+ );
5189
+ this.load$ = this.triggerSubject.pipe(
5190
+ rxjs.withLatestFrom(this.stateSubject),
5191
+ rxjs.filter(
5192
+ ([trigger, state]) => trigger === `load` && state !== "ready" && state !== "loaded" && state !== "loading"
5193
+ ),
5194
+ rxjs.map(() => void 0),
5195
+ rxjs.share()
5196
+ );
5197
+ this.destroy$ = this.triggerSubject.pipe(
5198
+ rxjs.filter((trigger) => trigger === `destroy`)
5199
+ );
5200
+ this.load$.pipe(
5201
+ rxjs.switchMap(() => {
5202
+ this.stateSubject.next(`loading`);
5203
+ const createDocument$ = this.onCreateDocument().pipe(
5204
+ rxjs.endWith(null),
5205
+ rxjs.first()
5206
+ );
5207
+ return createDocument$.pipe(
5208
+ rxjs.tap(() => {
5209
+ this.hookManager.execute(`item.onDocumentCreated`, this.item.id, {
5210
+ itemId: this.item.id,
5211
+ layers: this.layers
5212
+ });
5213
+ }),
5214
+ rxjs.switchMap(() => {
5215
+ const loadDocument$ = this.onLoadDocument().pipe(
5216
+ rxjs.endWith(null),
5217
+ rxjs.first()
5218
+ );
5219
+ return loadDocument$;
5220
+ }),
5221
+ waitForSwitch(this.context.bridgeEvent.viewportFree$),
5222
+ rxjs.switchMap(() => {
5223
+ const hookResults = this.hookManager.execute(`item.onDocumentLoad`, this.item.id, {
5224
+ itemId: this.item.id,
5225
+ layers: this.layers
5226
+ }).filter(
5227
+ (result) => result instanceof rxjs.Observable
5228
+ );
5229
+ return rxjs.combineLatest([rxjs.of(null), ...hookResults]).pipe(rxjs.first());
5230
+ }),
5231
+ rxjs.tap(() => {
5232
+ this.stateSubject.next(`loaded`);
5233
+ this.stateSubject.next(`ready`);
5234
+ }),
5235
+ rxjs.takeUntil(rxjs.merge(this.destroy$, this.unload$))
5236
+ );
5237
+ })
5238
+ ).subscribe();
5239
+ this.unload$.pipe(
5240
+ rxjs.switchMap(() => {
5241
+ this.stateSubject.next(`unloading`);
5242
+ return this.context.bridgeEvent.viewportFree$.pipe(
5243
+ rxjs.first(),
5244
+ rxjs.tap(() => {
5245
+ this.hookManager.destroy(`item.onDocumentLoad`, this.item.id);
5246
+ }),
5247
+ rxjs.switchMap(() => {
5248
+ const unload$ = this.onUnload().pipe(rxjs.endWith(null), rxjs.first());
5249
+ return unload$;
5250
+ }),
5251
+ rxjs.tap(() => {
5252
+ this.stateSubject.next(`idle`);
5253
+ }),
5254
+ rxjs.takeUntil(rxjs.merge(this.destroy$, this.load$))
5255
+ );
5256
+ })
5257
+ ).subscribe();
5182
5258
  }
5183
- get(indexOrId) {
5184
- if (typeof indexOrId === `number`) {
5185
- return this.orderedSpineItemsSubject.value[indexOrId];
5186
- }
5187
- if (typeof indexOrId === `string`) {
5188
- return this.orderedSpineItemsSubject.value.find(
5189
- ({ item }) => item.id === indexOrId
5190
- );
5191
- }
5192
- return indexOrId;
5259
+ get state$() {
5260
+ return this.stateSubject;
5193
5261
  }
5194
- comparePositionOf(toCompare, withItem) {
5195
- const toCompareIndex = this.getSpineItemIndex(toCompare) ?? 0;
5196
- const withIndex = this.getSpineItemIndex(withItem) ?? 0;
5197
- return toCompareIndex > withIndex ? `after` : toCompareIndex === withIndex ? `same` : `before`;
5262
+ load() {
5263
+ this.triggerSubject.next(`load`);
5198
5264
  }
5199
- getSpineItemIndex(spineItem) {
5200
- if (!spineItem) return void 0;
5201
- const index = this.orderedSpineItemsSubject.value.indexOf(spineItem);
5202
- return index < 0 ? void 0 : index;
5265
+ unload() {
5266
+ this.triggerSubject.next(`unload`);
5203
5267
  }
5204
- addMany(spineItems) {
5205
- this.orderedSpineItemsSubject.next([
5206
- ...this.orderedSpineItemsSubject.getValue(),
5207
- ...spineItems
5208
- ]);
5268
+ destroy() {
5269
+ this.triggerSubject.next(`destroy`);
5270
+ this.triggerSubject.complete();
5271
+ this.stateSubject.complete();
5209
5272
  }
5210
- // @todo move
5211
- getSpineItemFromCfi(cfi) {
5212
- const { itemId } = extractProseMetadataFromCfi(cfi);
5213
- if (itemId) {
5214
- const { itemId: itemId2 } = extractProseMetadataFromCfi(cfi);
5215
- const spineItem = (itemId2 ? this.get(itemId2) : void 0) || this.get(0);
5216
- return spineItem;
5217
- }
5273
+ get writingMode() {
5218
5274
  return void 0;
5219
5275
  }
5220
- get items() {
5221
- return this.orderedSpineItemsSubject.value;
5276
+ get readingDirection() {
5277
+ return void 0;
5222
5278
  }
5223
- /**
5224
- * @todo handle reload, remove subscription to each items etc. See add()
5225
- */
5226
- destroyItems() {
5227
- this.orderedSpineItemsSubject.value.forEach((item) => item.destroy());
5279
+ }
5280
+ class DefaultRenderer extends DocumentRenderer {
5281
+ onUnload() {
5282
+ return rxjs.EMPTY;
5283
+ }
5284
+ onCreateDocument() {
5285
+ return rxjs.EMPTY;
5286
+ }
5287
+ onLoadDocument() {
5288
+ return rxjs.EMPTY;
5289
+ }
5290
+ layout() {
5291
+ return void 0;
5228
5292
  }
5229
5293
  }
5230
- const getSpineItemFromPosition = ({
5231
- position,
5232
- spineItemsManager,
5233
- spineLayout,
5234
- settings
5235
- }) => {
5236
- const spineItem = spineItemsManager.items.find((item) => {
5237
- const { left, right, bottom, top } = spineLayout.getAbsolutePositionOf(item);
5238
- const isWithinXAxis = position.x >= left && position.x < right;
5239
- if (settings.values.computedPageTurnDirection === `horizontal`) {
5240
- return isWithinXAxis;
5241
- } else {
5242
- return isWithinXAxis && position.y >= top && position.y < bottom;
5243
- }
5244
- });
5245
- if (position.x === 0 && !spineItem) {
5246
- return spineItemsManager.items[0];
5294
+ const defaultGetResource = (item) => new URL(item.href);
5295
+ class ResourceHandler {
5296
+ constructor(item, settings) {
5297
+ this.item = item;
5298
+ this.settings = settings;
5247
5299
  }
5248
- return spineItem;
5249
- };
5250
- const isItemVisibleByThresholdForPosition = ({
5251
- itemHeight,
5252
- itemWidth,
5253
- visibleWidthOfItem,
5254
- visibleHeightOfItem,
5255
- threshold
5256
- }) => {
5257
- const visibleWidthRatioOfSpineItem = visibleWidthOfItem / itemWidth;
5258
- const visibleHeightRatioOfSpineItem = visibleHeightOfItem / itemHeight;
5259
- const isItemVisibleEnough = visibleWidthRatioOfSpineItem >= threshold && visibleHeightRatioOfSpineItem >= threshold;
5260
- return isItemVisibleEnough;
5261
- };
5262
- const isItemVisibleOnScreenByThresholdForPosition = ({
5263
- visibleWidthOfItem,
5264
- visibleHeightOfItem,
5265
- threshold,
5266
- context
5267
- }) => {
5268
- const widthRatioOfSpaceTakenInScreen = visibleWidthOfItem / context.state.visibleAreaRect.width;
5269
- const heightRatioOfSpaceTakenInScreen = visibleHeightOfItem / context.state.visibleAreaRect.height;
5270
- const isItemVisibleEnoughOnScreen = heightRatioOfSpaceTakenInScreen >= threshold && widthRatioOfSpaceTakenInScreen >= threshold;
5271
- return isItemVisibleEnoughOnScreen;
5272
- };
5273
- const getItemVisibilityForPosition = ({
5274
- itemPosition: {
5275
- bottom,
5276
- left,
5277
- right,
5278
- top,
5279
- width: itemWidth,
5280
- height: itemHeight
5281
- },
5282
- threshold,
5283
- viewportPosition,
5284
- restrictToScreen,
5285
- context
5286
- }) => {
5287
- const viewportLeft = viewportPosition.x;
5288
- const viewportRight = viewportPosition.x + (context.state.visibleAreaRect.width - 1);
5289
- const viewportTop = viewportPosition.y;
5290
- const viewportBottom = Math.max(
5291
- viewportPosition.y + (context.state.visibleAreaRect.height - 1),
5292
- 0
5293
- );
5294
- const visibleWidthOfItem = Math.max(
5295
- 0,
5296
- Math.min(right, viewportRight) - Math.max(left, viewportLeft)
5297
- );
5298
- const visibleHeightOfItem = Math.max(
5299
- 0,
5300
- Math.min(bottom, viewportBottom) - Math.max(top, viewportTop)
5301
- );
5302
- const itemIsOnTheOuterEdge = visibleWidthOfItem <= 0 || visibleHeightOfItem <= 0;
5303
- if (itemIsOnTheOuterEdge) return { visible: false };
5304
- const isItemVisibleEnoughOnScreen = isItemVisibleOnScreenByThresholdForPosition({
5305
- threshold,
5306
- visibleHeightOfItem,
5307
- visibleWidthOfItem,
5308
- context
5309
- });
5310
- if (restrictToScreen) {
5311
- return { visible: isItemVisibleEnoughOnScreen };
5300
+ async getResource() {
5301
+ var _a, _b;
5302
+ const resource = await rxjs.lastValueFrom(
5303
+ ((_b = (_a = this.settings.values).getResource) == null ? void 0 : _b.call(_a, this.item)) ?? rxjs.of(void 0)
5304
+ );
5305
+ return resource ?? defaultGetResource(this.item);
5312
5306
  }
5313
- const isItemVisibleEnough = isItemVisibleByThresholdForPosition({
5314
- itemHeight,
5315
- itemWidth,
5316
- threshold,
5317
- visibleHeightOfItem,
5318
- visibleWidthOfItem
5319
- });
5320
- return {
5321
- visible: isItemVisibleEnough || isItemVisibleEnoughOnScreen
5322
- };
5323
- };
5324
- const getVisibleSpineItemsFromPosition = ({
5325
- position,
5326
- threshold,
5327
- restrictToScreen,
5328
- spineItemsManager,
5329
- context,
5330
- settings,
5331
- spineLayout
5332
- }) => {
5333
- const fallbackSpineItem = getSpineItemFromPosition({
5334
- position,
5335
- settings,
5336
- spineItemsManager,
5337
- spineLayout
5338
- }) || spineItemsManager.get(0);
5339
- const spineItemsVisible = spineItemsManager.items.reduce(
5340
- (acc, spineItem) => {
5341
- const itemPosition = spineLayout.getAbsolutePositionOf(spineItem);
5342
- const { visible } = getItemVisibilityForPosition({
5343
- itemPosition,
5344
- threshold,
5345
- viewportPosition: position,
5346
- restrictToScreen,
5347
- context
5348
- });
5349
- if (visible) {
5350
- return [...acc, spineItem];
5307
+ async fetchResource() {
5308
+ const resource = await this.getResource();
5309
+ if (resource instanceof Response) return resource;
5310
+ if (resource instanceof URL) return fetch(resource);
5311
+ return resource;
5312
+ }
5313
+ }
5314
+ class SpineItem {
5315
+ constructor(item, parentElement, context, settings, hookManager, index) {
5316
+ var _a, _b;
5317
+ this.item = item;
5318
+ this.parentElement = parentElement;
5319
+ this.context = context;
5320
+ this.settings = settings;
5321
+ this.hookManager = hookManager;
5322
+ this.index = index;
5323
+ this.adjustPositionOfElement = ({
5324
+ right,
5325
+ left,
5326
+ top
5327
+ }) => {
5328
+ if (right !== void 0) {
5329
+ this.containerElement.style.right = `${right}px`;
5330
+ } else {
5331
+ this.containerElement.style.removeProperty(`right`);
5351
5332
  }
5352
- return acc;
5353
- },
5354
- []
5355
- );
5356
- const beginItem = spineItemsVisible[0] ?? fallbackSpineItem;
5357
- const endItem = spineItemsVisible[spineItemsVisible.length - 1] ?? beginItem;
5358
- if (!beginItem || !endItem) return void 0;
5359
- const beginItemIndex = spineItemsManager.getSpineItemIndex(beginItem);
5360
- const endItemIndex = spineItemsManager.getSpineItemIndex(endItem);
5361
- return {
5362
- beginIndex: beginItemIndex ?? 0,
5363
- endIndex: endItemIndex ?? 0
5364
- };
5365
- };
5366
- const getSpinePositionFromSpineItemPosition = ({
5367
- spineItemPosition,
5368
- itemLayout: { left, top }
5369
- }) => {
5370
- return {
5371
- x: left + spineItemPosition.x,
5372
- y: top + spineItemPosition.y
5373
- };
5374
- };
5375
- const getSpineInfoFromAbsolutePageIndex = ({
5376
- absolutePageIndex,
5377
- spineLayout,
5378
- spineItemsManager,
5379
- context,
5380
- settings
5381
- }) => {
5382
- const items = spineItemsManager.items;
5383
- const { found, currentAbsolutePage } = items.reduce(
5384
- (acc, item) => {
5385
- if (acc.found) return acc;
5386
- const itemLayout = spineLayout.getAbsolutePositionOf(item);
5387
- const numberOfPages = getSpineItemNumberOfPages({
5388
- isUsingVerticalWriting: !!item.isUsingVerticalWriting(),
5389
- itemHeight: itemLayout.height,
5390
- itemWidth: itemLayout.width,
5391
- context,
5392
- settings
5393
- });
5394
- const possiblePageIndex = absolutePageIndex - acc.currentAbsolutePage;
5395
- const currentAbsolutePage2 = acc.currentAbsolutePage + numberOfPages;
5396
- if (possiblePageIndex <= numberOfPages - 1) {
5397
- return {
5398
- ...acc,
5399
- currentAbsolutePage: currentAbsolutePage2,
5400
- found: { item, pageIndex: possiblePageIndex }
5401
- };
5333
+ if (left !== void 0) {
5334
+ this.containerElement.style.left = `${left}px`;
5335
+ } else {
5336
+ this.containerElement.style.removeProperty(`left`);
5337
+ }
5338
+ if (top !== void 0) {
5339
+ this.containerElement.style.top = `${top}px`;
5340
+ } else {
5341
+ this.containerElement.style.removeProperty(`top`);
5402
5342
  }
5403
- return {
5404
- ...acc,
5405
- currentAbsolutePage: currentAbsolutePage2
5406
- };
5407
- },
5408
- { currentAbsolutePage: 0 }
5409
- );
5410
- if (found) {
5411
- return {
5412
- spineItem: found.item,
5413
- pageIndex: found.pageIndex,
5414
- itemIndex: spineItemsManager.getSpineItemIndex(found.item) ?? 0,
5415
- currentAbsolutePage
5416
5343
  };
5417
- }
5418
- return void 0;
5419
- };
5420
- const getAbsolutePageIndexFromPageIndex = ({
5421
- pageIndex,
5422
- spineItemOrId,
5423
- spineLayout,
5424
- spineItemsManager,
5425
- context,
5426
- settings
5427
- }) => {
5428
- const items = spineItemsManager.items;
5429
- const spineItem = spineItemsManager.get(spineItemOrId);
5430
- if (!spineItem) return void 0;
5431
- const { currentAbsolutePage } = items.reduce(
5432
- (acc, item) => {
5433
- if (acc.found) return acc;
5434
- const itemLayout = spineLayout.getAbsolutePositionOf(item);
5435
- const numberOfPages = getSpineItemNumberOfPages({
5436
- isUsingVerticalWriting: !!item.isUsingVerticalWriting(),
5437
- itemHeight: itemLayout.height,
5438
- itemWidth: itemLayout.width,
5439
- context,
5440
- settings
5441
- });
5442
- if (spineItem === item) {
5443
- if (pageIndex <= numberOfPages - 1) {
5444
- return {
5445
- currentAbsolutePage: acc.currentAbsolutePage + pageIndex,
5446
- found: true
5447
- };
5344
+ this.getBoundingRectOfElementFromSelector = (selector) => {
5345
+ var _a2, _b2, _c, _d, _e;
5346
+ const frameElement = (_a2 = this.renderer.layers[0]) == null ? void 0 : _a2.element;
5347
+ if (frameElement && frameElement instanceof HTMLIFrameElement && selector) {
5348
+ if (selector.startsWith(`#`)) {
5349
+ return (_c = (_b2 = frameElement.contentDocument) == null ? void 0 : _b2.getElementById(selector.replace(`#`, ``))) == null ? void 0 : _c.getBoundingClientRect();
5448
5350
  }
5351
+ return (_e = (_d = frameElement.contentDocument) == null ? void 0 : _d.querySelector(selector)) == null ? void 0 : _e.getBoundingClientRect();
5449
5352
  }
5450
- return {
5451
- ...acc,
5452
- currentAbsolutePage: acc.currentAbsolutePage + numberOfPages
5453
- };
5454
- },
5455
- { currentAbsolutePage: 0, found: false }
5456
- );
5457
- return currentAbsolutePage;
5458
- };
5459
- const createSpineLocator = ({
5460
- spineItemsManager,
5461
- context,
5462
- spineItemLocator,
5463
- settings,
5464
- spineLayout
5465
- }) => {
5466
- const getSpineItemPositionFromSpinePosition = Report.measurePerformance(
5467
- `getSpineItemPositionFromSpinePosition`,
5468
- 10,
5469
- (position, spineItem) => {
5470
- const { left, top } = spineLayout.getAbsolutePositionOf(spineItem);
5471
- return {
5472
- /**
5473
- * when using spread the item could be on the right and therefore will be negative
5474
- * @example
5475
- * 400 (position = viewport), page of 200
5476
- * 400 - 600 = -200.
5477
- * However we can assume we are at 0, because we in fact can see the beginning of the item
5478
- */
5479
- x: Math.max(position.x - left, 0),
5480
- y: Math.max(position.y - top, 0)
5481
- };
5482
- },
5483
- { disable: true }
5484
- );
5485
- const getSpinePositionFromSpineItem = (spineItem) => {
5486
- return getSpinePositionFromSpineItemPosition({
5487
- spineItemPosition: { x: 0, y: 0 },
5488
- itemLayout: spineLayout.getAbsolutePositionOf(spineItem)
5489
- });
5490
- };
5491
- const getSpineItemFromIframe = (iframe) => {
5492
- return spineItemsManager.items.find((item) => {
5493
- var _a;
5494
- return ((_a = item.renderer.layers[0]) == null ? void 0 : _a.element) === iframe;
5495
- });
5496
- };
5497
- const getSpineItemPageIndexFromNode = (node, offset, spineItemOrIndex) => {
5498
- if (typeof spineItemOrIndex === `number`) {
5499
- const spineItem = spineItemsManager.get(spineItemOrIndex);
5500
- return spineItem ? spineItemLocator.getSpineItemPageIndexFromNode(
5501
- node,
5502
- offset || 0,
5503
- spineItem
5504
- ) : void 0;
5505
- }
5506
- return spineItemLocator.getSpineItemPageIndexFromNode(
5507
- node,
5508
- offset || 0,
5509
- spineItemOrIndex
5510
- );
5511
- };
5512
- const getVisiblePagesFromViewportPosition = ({
5513
- position,
5514
- threshold,
5515
- spineItem,
5516
- restrictToScreen
5517
- }) => {
5518
- const { height, width } = spineItem.getElementDimensions();
5519
- const numberOfPages = spineItemLocator.getSpineItemNumberOfPages({
5520
- isUsingVerticalWriting: !!spineItem.isUsingVerticalWriting(),
5521
- itemHeight: height,
5522
- itemWidth: width
5523
- });
5524
- const pages = Array.from(Array(numberOfPages)).map((_, index) => {
5525
- const spineItemPosition = spineItemLocator.getSpineItemPositionFromPageIndex({
5526
- pageIndex: index,
5527
- isUsingVerticalWriting: !!spineItem.isUsingVerticalWriting(),
5528
- itemLayout: spineItem.getElementDimensions()
5529
- });
5530
- const spinePosition = getSpinePositionFromSpineItemPosition({
5531
- spineItemPosition,
5532
- itemLayout: spineLayout.getAbsolutePositionOf(spineItem)
5353
+ };
5354
+ this.layout = ({
5355
+ blankPagePosition,
5356
+ minimumWidth,
5357
+ spreadPosition
5358
+ }) => {
5359
+ this.hookManager.execute(`item.onBeforeLayout`, void 0, {
5360
+ blankPagePosition,
5361
+ item: this.item,
5362
+ minimumWidth
5533
5363
  });
5534
- return {
5535
- index,
5536
- absolutePosition: {
5537
- width: context.getPageSize().width,
5538
- height: context.getPageSize().height,
5539
- left: spinePosition.x,
5540
- top: spinePosition.y,
5541
- bottom: spinePosition.y + context.getPageSize().height,
5542
- right: spinePosition.x + context.getPageSize().width
5543
- }
5364
+ const { height, width } = this.renderer.layout({
5365
+ blankPagePosition,
5366
+ minPageSpread: minimumWidth / this.context.getPageSize().width,
5367
+ spreadPosition
5368
+ }) ?? { width: 0, height: 0 };
5369
+ const minHeight = Math.max(height, this.context.getPageSize().height);
5370
+ const minWidth = Math.max(width, minimumWidth);
5371
+ this.containerElement.style.width = `${minWidth}px`;
5372
+ this.containerElement.style.height = `${minHeight}px`;
5373
+ this.hookManager.execute(`item.onAfterLayout`, void 0, {
5374
+ blankPagePosition,
5375
+ item: this.item,
5376
+ minimumWidth
5377
+ });
5378
+ return { width: minWidth, height: minHeight };
5379
+ };
5380
+ this.load = () => this.renderer.load();
5381
+ this.unload = () => {
5382
+ this.renderer.unload();
5383
+ };
5384
+ this.getElementDimensions = () => {
5385
+ const rect = this.containerElement.getBoundingClientRect();
5386
+ const normalizedValues = {
5387
+ ...rect,
5388
+ // we want to round to first decimal because it's possible to have half pixel
5389
+ // however browser engine can also gives back x.yyyy based on their precision
5390
+ width: Math.round(rect.width * 10) / 10,
5391
+ height: Math.round(rect.height * 10) / 10
5544
5392
  };
5545
- });
5546
- const pagesVisible = pages.reduce(
5547
- (acc, { absolutePosition, index }) => {
5548
- const { visible } = getItemVisibilityForPosition({
5549
- context,
5550
- viewportPosition: position,
5551
- restrictToScreen,
5552
- threshold,
5553
- itemPosition: absolutePosition
5554
- });
5555
- if (visible) {
5556
- return [...acc, index];
5557
- }
5558
- return acc;
5559
- },
5560
- []
5561
- );
5562
- const beginPageIndex = pagesVisible[0];
5563
- const endPageIndex = pagesVisible[pagesVisible.length - 1] ?? beginPageIndex;
5564
- if (beginPageIndex === void 0 || endPageIndex === void 0)
5565
- return void 0;
5566
- return {
5567
- beginPageIndex,
5568
- endPageIndex
5393
+ return normalizedValues;
5569
5394
  };
5570
- };
5571
- const isPositionWithinSpineItem = (position, spineItem) => {
5572
- const { bottom, left, right, top } = spineLayout.getAbsolutePositionOf(spineItem);
5573
- return position.x >= left && position.x <= right && position.y <= bottom && position.y >= top;
5574
- };
5575
- const getSafeSpineItemPositionFromUnsafeSpineItemPosition = (unsafePosition, spineItem) => {
5576
- const { height, width } = spineLayout.getAbsolutePositionOf(spineItem);
5577
- return {
5578
- x: Math.min(Math.max(0, unsafePosition.x), width),
5579
- y: Math.min(Math.max(0, unsafePosition.y), height)
5395
+ this.destroy = () => {
5396
+ this.destroySubject$.next();
5397
+ this.containerElement.remove();
5398
+ this.destroySubject$.complete();
5399
+ this.renderer.destroy();
5580
5400
  };
5581
- };
5582
- return {
5583
- getSpinePositionFromSpineItemPosition: ({
5584
- spineItem,
5585
- spineItemPosition
5586
- }) => {
5587
- const itemLayout = spineLayout.getAbsolutePositionOf(spineItem);
5588
- return getSpinePositionFromSpineItemPosition({
5589
- itemLayout,
5590
- spineItemPosition
5591
- });
5592
- },
5593
- getAbsolutePageIndexFromPageIndex: (params) => getAbsolutePageIndexFromPageIndex({
5594
- ...params,
5595
- context,
5596
- settings,
5597
- spineItemsManager,
5598
- spineLayout
5599
- }),
5600
- getSpineInfoFromAbsolutePageIndex: (params) => getSpineInfoFromAbsolutePageIndex({
5601
- ...params,
5602
- context,
5603
- settings,
5604
- spineItemsManager,
5605
- spineLayout
5606
- }),
5607
- getSpinePositionFromSpineItem,
5608
- getSpineItemPositionFromSpinePosition,
5609
- getSpineItemFromPosition: (position) => getSpineItemFromPosition({
5610
- position,
5611
- settings,
5612
- spineItemsManager,
5613
- spineLayout
5614
- }),
5615
- getSpineItemFromIframe,
5616
- getSpineItemPageIndexFromNode,
5617
- getVisibleSpineItemsFromPosition: (params) => getVisibleSpineItemsFromPosition({
5401
+ this.isUsingVerticalWriting = () => {
5402
+ var _a2;
5403
+ return !!((_a2 = this.renderer.writingMode) == null ? void 0 : _a2.startsWith(`vertical`));
5404
+ };
5405
+ this.destroySubject$ = new rxjs.Subject();
5406
+ this.containerElement = createContainerElement(
5407
+ parentElement,
5408
+ item,
5409
+ hookManager
5410
+ );
5411
+ parentElement.appendChild(this.containerElement);
5412
+ const RendererClass = ((_b = (_a = this.settings.values).getRenderer) == null ? void 0 : _b.call(_a, item)) ?? DefaultRenderer;
5413
+ this.resourcesHandler = new ResourceHandler(item, this.settings);
5414
+ this.renderer = new RendererClass(
5618
5415
  context,
5619
5416
  settings,
5620
- spineItemsManager,
5621
- spineLayout,
5622
- ...params
5623
- }),
5624
- getVisiblePagesFromViewportPosition,
5625
- isPositionWithinSpineItem,
5626
- spineItemLocator,
5627
- getSafeSpineItemPositionFromUnsafeSpineItemPosition
5628
- };
5629
- };
5630
- const loadItems = ({
5631
- spineItemsManager,
5632
- settings
5633
- }) => (stream) => stream.pipe(
5634
- rxjs.tap(([beginIndex, endIndex]) => {
5635
- const { numberOfAdjacentSpineItemToPreLoad } = settings.values;
5636
- const leftMaximumIndex = beginIndex - numberOfAdjacentSpineItemToPreLoad;
5637
- const rightMaximumIndex = endIndex + numberOfAdjacentSpineItemToPreLoad;
5638
- spineItemsManager.items.forEach((orderedSpineItem, index) => {
5639
- const isWithinRange = index >= leftMaximumIndex && index <= rightMaximumIndex;
5640
- if (isWithinRange) {
5641
- orderedSpineItem.load();
5642
- } else {
5643
- orderedSpineItem.unload();
5644
- }
5645
- });
5646
- })
5647
- );
5648
- const mapToItemsToLoad = ({ spineLocator }) => (stream) => stream.pipe(
5649
- rxjs.map((position) => {
5650
- const { beginIndex = 0, endIndex = 0 } = spineLocator.getVisibleSpineItemsFromPosition({
5651
- position,
5652
- threshold: 0
5653
- }) || {};
5654
- return [beginIndex, endIndex];
5655
- })
5656
- );
5657
- class SpineItemsLoader extends DestroyableClass {
5658
- constructor(context, spineItemsManager, spineLocator, settings, spineLayout) {
5659
- super();
5660
- this.context = context;
5661
- this.spineItemsManager = spineItemsManager;
5662
- this.spineLocator = spineLocator;
5663
- this.settings = settings;
5664
- this.spineLayout = spineLayout;
5665
- const navigationUpdate$ = this.context.bridgeEvent.navigation$;
5666
- const layoutHasChanged$ = this.spineLayout.layout$.pipe(
5667
- rxjs.filter(({ hasChanged }) => hasChanged)
5417
+ hookManager,
5418
+ item,
5419
+ this.containerElement,
5420
+ this.resourcesHandler
5668
5421
  );
5669
- const numberOfAdjacentSpineItemToPreLoad$ = settings.values$.pipe(
5670
- rxjs.map(
5671
- ({ numberOfAdjacentSpineItemToPreLoad }) => numberOfAdjacentSpineItemToPreLoad
5672
- ),
5673
- rxjs.skip(1),
5674
- rxjs.distinctUntilChanged()
5422
+ const contentLayoutChange$ = rxjs.merge(
5423
+ this.unloaded$.pipe(operators.map(() => ({ isFirstLayout: false }))),
5424
+ this.ready$.pipe(operators.map(() => ({ isFirstLayout: true })))
5675
5425
  );
5676
- const loadSpineItems$ = rxjs.merge(
5677
- navigationUpdate$,
5678
- layoutHasChanged$,
5679
- numberOfAdjacentSpineItemToPreLoad$
5680
- ).pipe(
5681
- // this can be changed by whatever we want and SHOULD not break navigation.
5682
- // Ideally loading faster is better but loading too close to user navigating can
5683
- // be dangerous.
5684
- rxjs.debounceTime(100, rxjs.animationFrameScheduler),
5685
- waitForSwitch(this.context.bridgeEvent.viewportFree$),
5686
- rxjs.withLatestFrom(this.context.bridgeEvent.navigation$),
5687
- rxjs.map(([, navigation]) => navigation.position),
5688
- mapToItemsToLoad({ spineLocator }),
5689
- loadItems({ spineItemsManager, settings })
5426
+ this.contentLayout$ = contentLayoutChange$.pipe(
5427
+ operators.withLatestFrom(this.isReady$),
5428
+ operators.map(([data, isReady]) => ({
5429
+ isFirstLayout: data.isFirstLayout,
5430
+ isReady
5431
+ }))
5690
5432
  );
5691
- loadSpineItems$.pipe(rxjs.takeUntil(this.destroy$)).subscribe();
5692
5433
  }
5693
- }
5694
- class SpineItemsObserver extends DestroyableClass {
5695
- constructor(spineItemsManager, spineLocator) {
5696
- super();
5697
- this.spineItemsManager = spineItemsManager;
5698
- this.spineLocator = spineLocator;
5699
- this.itemIsReady$ = this.spineItemsManager.items$.pipe(
5700
- rxjs.switchMap((items) => {
5701
- const itemsIsReady$ = items.map(
5702
- (item) => item.isReady$.pipe(rxjs.map((isReady) => ({ item, isReady })))
5703
- );
5704
- return rxjs.merge(...itemsIsReady$);
5705
- }),
5706
- rxjs.share()
5707
- );
5708
- this.itemResize$ = this.spineItemsManager.items$.pipe(
5709
- rxjs.switchMap((items) => {
5710
- const resize$ = items.map(
5711
- (item) => observeResize(item.element).pipe(
5712
- rxjs.map((entries) => ({ entries, item }))
5713
- )
5714
- );
5715
- return rxjs.merge(...resize$);
5716
- }),
5717
- rxjs.share()
5718
- );
5719
- this.itemLoaded$ = this.spineItemsManager.items$.pipe(
5720
- rxjs.switchMap((items) => {
5721
- const itemsIsReady$ = items.map(
5722
- (item) => item.loaded$.pipe(rxjs.map(() => item))
5723
- );
5724
- return rxjs.merge(...itemsIsReady$);
5725
- })
5726
- );
5727
- }
5728
- }
5729
- const convertSpinePositionToLayoutPosition = ({
5730
- position,
5731
- pageSize
5732
- }) => {
5733
- return {
5734
- x: position.x,
5735
- y: position.y,
5736
- left: position.x,
5737
- top: position.y,
5738
- width: pageSize.width,
5739
- height: pageSize.height,
5740
- bottom: position.y + pageSize.height,
5741
- right: position.x + pageSize.width
5742
- };
5743
- };
5744
- const NAMESPACE = `SpineLayout`;
5745
- class SpineLayout extends DestroyableClass {
5746
- constructor(spineItemsManager, context, settings) {
5747
- super();
5748
- this.spineItemsManager = spineItemsManager;
5749
- this.context = context;
5750
- this.settings = settings;
5751
- this.itemLayoutInformation = [];
5752
- this.layoutSubject = new rxjs.Subject();
5753
- spineItemsManager.items$.pipe(
5754
- rxjs.switchMap((items) => {
5755
- const itemsLayout$ = items.map(
5756
- (spineItem) => spineItem.contentLayout$.pipe(
5757
- rxjs.tap(() => {
5758
- this.layout();
5759
- })
5760
- )
5761
- );
5762
- const writingModeUpdate$ = items.map(
5763
- (spineItem) => spineItem.loaded$.pipe(
5764
- rxjs.tap(() => {
5765
- if (spineItem.isUsingVerticalWriting()) {
5766
- this.context.update({
5767
- hasVerticalWriting: true
5768
- });
5769
- } else {
5770
- this.context.update({
5771
- hasVerticalWriting: false
5772
- });
5773
- }
5774
- })
5775
- )
5776
- );
5777
- return rxjs.merge(...itemsLayout$, ...writingModeUpdate$);
5778
- })
5779
- ).pipe(rxjs.takeUntil(this.destroy$)).subscribe();
5780
- this.layout$ = this.layoutSubject.pipe(
5781
- rxjs.map((hasChanged) => ({ hasChanged })),
5782
- rxjs.startWith({ hasChanged: true }),
5783
- rxjs.map(({ hasChanged }) => {
5784
- const items = spineItemsManager.items;
5785
- const spineItemsPagesAbsolutePositions = items.map((item) => {
5786
- const itemLayout = this.getAbsolutePositionOf(item);
5787
- const numberOfPages = getSpineItemNumberOfPages({
5788
- isUsingVerticalWriting: !!item.isUsingVerticalWriting(),
5789
- itemHeight: itemLayout.height,
5790
- itemWidth: itemLayout.width,
5791
- context,
5792
- settings
5793
- });
5794
- const pages2 = new Array(numberOfPages).fill(void 0);
5795
- return pages2.map(
5796
- (_, pageIndex) => convertSpinePositionToLayoutPosition({
5797
- pageSize: this.context.getPageSize(),
5798
- position: getSpinePositionFromSpineItemPosition({
5799
- itemLayout,
5800
- spineItemPosition: getSpineItemPositionFromPageIndex({
5801
- isUsingVerticalWriting: !!item.isUsingVerticalWriting(),
5802
- itemLayout,
5803
- pageIndex,
5804
- context
5805
- })
5806
- })
5807
- })
5808
- );
5809
- });
5810
- const pages = spineItemsPagesAbsolutePositions.reduce(
5811
- (acc, itemPages, itemIndex) => {
5812
- const itemPagesInfo = itemPages.map(
5813
- (absolutePosition, pageIndex) => ({
5814
- itemIndex,
5815
- absolutePageIndex: itemIndex + pageIndex,
5816
- absolutePosition
5817
- })
5818
- );
5819
- return [...acc, ...itemPagesInfo];
5820
- },
5821
- []
5822
- );
5823
- return {
5824
- hasChanged,
5825
- spineItemsAbsolutePositions: items.map(
5826
- (item) => this.getAbsolutePositionOf(item)
5827
- ),
5828
- spineItemsPagesAbsolutePositions,
5829
- pages
5830
- };
5831
- }),
5832
- rxjs.shareReplay(1)
5833
- );
5434
+ get element() {
5435
+ return this.containerElement;
5834
5436
  }
5835
5437
  /**
5836
- * @todo
5837
- * move this logic to the spine
5838
- *
5839
- * @todo
5840
- * make sure to check how many times it is being called and try to reduce number of layouts
5841
- * it is called eery time an item is being unload (which can adds up quickly for big books)
5438
+ * @important
5439
+ * Do not use this value for layout and navigation. It will be in possible conflict
5440
+ * with the global reading direction. A book should not mix them anyway. A page can have
5441
+ * a different reading direction for style reason but it should be in theory pre-paginated.
5442
+ * For example an english page in a japanese manga. That's expected and will
5443
+ * be confined to a single page.
5842
5444
  */
5843
- layout() {
5844
- const manifest = this.context.manifest;
5845
- const newItemLayoutInformation = [];
5846
- const isGloballyPrePaginated = (manifest == null ? void 0 : manifest.renditionLayout) === `pre-paginated`;
5847
- this.spineItemsManager.items.reduce(
5848
- ({ horizontalOffset, verticalOffset }, item, index) => {
5849
- let minimumWidth = this.context.getPageSize().width;
5850
- let blankPagePosition = `none`;
5851
- const itemStartOnNewScreen = horizontalOffset % this.context.state.visibleAreaRect.width === 0;
5852
- const isLastItem = index === this.spineItemsManager.items.length - 1;
5853
- if (this.context.state.isUsingSpreadMode) {
5854
- if (!isGloballyPrePaginated && item.item.renditionLayout === `reflowable` && !isLastItem) {
5855
- minimumWidth = this.context.getPageSize().width * 2;
5856
- }
5857
- if (!isGloballyPrePaginated && item.item.renditionLayout === `reflowable` && isLastItem && itemStartOnNewScreen) {
5858
- minimumWidth = this.context.getPageSize().width * 2;
5859
- }
5860
- const lastItemStartOnNewScreenInAPrepaginatedBook = itemStartOnNewScreen && isLastItem && isGloballyPrePaginated;
5861
- if (item.item.pageSpreadRight && itemStartOnNewScreen && !this.context.isRTL()) {
5862
- blankPagePosition = `before`;
5863
- minimumWidth = this.context.getPageSize().width * 2;
5864
- } else if (item.item.pageSpreadLeft && itemStartOnNewScreen && this.context.isRTL()) {
5865
- blankPagePosition = `before`;
5866
- minimumWidth = this.context.getPageSize().width * 2;
5867
- } else if (lastItemStartOnNewScreenInAPrepaginatedBook) {
5868
- if (this.context.isRTL()) {
5869
- blankPagePosition = `before`;
5870
- } else {
5871
- blankPagePosition = `after`;
5872
- }
5873
- minimumWidth = this.context.getPageSize().width * 2;
5874
- }
5875
- }
5876
- const { width, height } = item.layout({
5877
- minimumWidth,
5878
- blankPagePosition,
5879
- spreadPosition: this.context.state.isUsingSpreadMode ? itemStartOnNewScreen ? this.context.isRTL() ? `right` : `left` : this.context.isRTL() ? `left` : `right` : `none`
5880
- });
5881
- if (this.settings.values.computedPageTurnDirection === `vertical`) {
5882
- const currentValidEdgeYForVerticalPositioning = itemStartOnNewScreen ? verticalOffset : verticalOffset - this.context.state.visibleAreaRect.height;
5883
- const currentValidEdgeXForVerticalPositioning = itemStartOnNewScreen ? 0 : horizontalOffset;
5884
- if (this.context.isRTL()) {
5885
- item.adjustPositionOfElement({
5886
- top: currentValidEdgeYForVerticalPositioning,
5887
- left: currentValidEdgeXForVerticalPositioning
5888
- });
5889
- } else {
5890
- item.adjustPositionOfElement({
5891
- top: currentValidEdgeYForVerticalPositioning,
5892
- left: currentValidEdgeXForVerticalPositioning
5893
- });
5894
- }
5895
- const newEdgeX = width + currentValidEdgeXForVerticalPositioning;
5896
- const newEdgeY = height + currentValidEdgeYForVerticalPositioning;
5897
- newItemLayoutInformation.push({
5898
- left: currentValidEdgeXForVerticalPositioning,
5899
- right: newEdgeX,
5900
- top: currentValidEdgeYForVerticalPositioning,
5901
- bottom: newEdgeY,
5902
- height,
5903
- width,
5904
- x: currentValidEdgeXForVerticalPositioning,
5905
- y: currentValidEdgeYForVerticalPositioning
5906
- });
5907
- return {
5908
- horizontalOffset: newEdgeX,
5909
- verticalOffset: newEdgeY
5910
- };
5911
- }
5912
- item.adjustPositionOfElement(
5913
- this.context.isRTL() ? { right: horizontalOffset, top: 0 } : { left: horizontalOffset, top: 0 }
5914
- );
5915
- const left = this.context.isRTL() ? this.context.state.visibleAreaRect.width - horizontalOffset - width : horizontalOffset;
5916
- newItemLayoutInformation.push({
5917
- right: this.context.isRTL() ? this.context.state.visibleAreaRect.width - horizontalOffset : horizontalOffset + width,
5918
- left,
5919
- x: left,
5920
- top: verticalOffset,
5921
- bottom: height,
5922
- height,
5923
- width,
5924
- y: verticalOffset
5925
- });
5926
- return {
5927
- horizontalOffset: horizontalOffset + width,
5928
- verticalOffset: 0
5929
- };
5930
- },
5931
- { horizontalOffset: 0, verticalOffset: 0 }
5445
+ get readingDirection() {
5446
+ return this.renderer.readingDirection;
5447
+ }
5448
+ get isReady() {
5449
+ return this.renderer.state$.getValue() === "ready";
5450
+ }
5451
+ get ready$() {
5452
+ return this.renderer.state$.pipe(
5453
+ operators.distinctUntilChanged(),
5454
+ operators.filter((state) => state === "ready")
5932
5455
  );
5933
- const hasLayoutChanges = this.itemLayoutInformation.length !== newItemLayoutInformation.length || this.itemLayoutInformation.some(
5934
- (old, index) => !shared.isShallowEqual(old, newItemLayoutInformation[index])
5456
+ }
5457
+ get loaded$() {
5458
+ return this.renderer.state$.pipe(
5459
+ operators.distinctUntilChanged(),
5460
+ operators.filter((state) => state === "loaded")
5935
5461
  );
5936
- this.itemLayoutInformation = newItemLayoutInformation;
5937
- Report.log(NAMESPACE, `layout`, {
5938
- hasLayoutChanges,
5939
- itemLayoutInformation: this.itemLayoutInformation
5940
- });
5941
- this.layoutSubject.next(hasLayoutChanges);
5462
+ }
5463
+ get unloaded$() {
5464
+ return this.renderer.state$.pipe(
5465
+ operators.distinctUntilChanged(),
5466
+ operators.filter((state) => state !== "idle"),
5467
+ operators.switchMap(
5468
+ () => this.renderer.state$.pipe(
5469
+ operators.filter((state) => state === `idle`),
5470
+ operators.first()
5471
+ )
5472
+ )
5473
+ );
5474
+ }
5475
+ get isReady$() {
5476
+ return this.renderer.state$.pipe(operators.map((state) => state === "ready"));
5942
5477
  }
5943
5478
  /**
5944
- * It's important to not use x,y since we need the absolute position of each element. Otherwise x,y would be relative to
5945
- * current window (viewport).
5479
+ * Helper that will inject CSS into the document frame.
5480
+ *
5481
+ * @important
5482
+ * The document needs to be detected as a frame.
5946
5483
  */
5947
- getAbsolutePositionOf(spineItemOrIndex) {
5948
- const fallback = {
5949
- left: 0,
5950
- right: 0,
5951
- top: 0,
5952
- bottom: 0,
5953
- width: 0,
5954
- height: 0,
5955
- x: 0,
5956
- y: 0
5957
- };
5958
- const spineItem = this.spineItemsManager.get(spineItemOrIndex);
5959
- const indexOfItem = spineItem ? this.spineItemsManager.items.indexOf(spineItem) : -1;
5960
- const layoutInformation = this.itemLayoutInformation[indexOfItem];
5961
- return layoutInformation || fallback;
5962
- }
5963
- destroy() {
5964
- super.destroy();
5965
- this.layoutSubject.complete();
5484
+ upsertCSS(id, style, prepend) {
5485
+ this.renderer.layers.forEach((layer) => {
5486
+ if (layer.element instanceof HTMLIFrameElement) {
5487
+ upsertCSS(layer.element, id, style, prepend);
5488
+ }
5489
+ });
5966
5490
  }
5967
5491
  }
5968
- class DocumentRenderer {
5969
- constructor(context, settings, hookManager, item, containerElement, resourcesHandler) {
5492
+ const createContainerElement = (containerElement, item, hookManager) => {
5493
+ const element = containerElement.ownerDocument.createElement(`div`);
5494
+ element.classList.add(`spineItem`);
5495
+ element.classList.add(`spineItem-${item.renditionLayout}`);
5496
+ element.style.cssText = `
5497
+ position: absolute;
5498
+ overflow: hidden;
5499
+ `;
5500
+ hookManager.execute("item.onBeforeContainerCreated", void 0, { element });
5501
+ return element;
5502
+ };
5503
+ class SpineItemsManager extends DestroyableClass {
5504
+ constructor(context, settings) {
5505
+ super();
5970
5506
  this.context = context;
5971
5507
  this.settings = settings;
5972
- this.hookManager = hookManager;
5973
- this.item = item;
5974
- this.containerElement = containerElement;
5975
- this.resourcesHandler = resourcesHandler;
5976
- this.stateSubject = new rxjs.BehaviorSubject(`idle`);
5977
- this.triggerSubject = new rxjs.Subject();
5978
- this.layers = [];
5979
- this.unload$ = this.triggerSubject.pipe(
5980
- rxjs.withLatestFrom(this.stateSubject),
5981
- rxjs.filter(
5982
- ([trigger, state]) => trigger === `unload` && state !== "idle" && state !== "unloading"
5983
- ),
5984
- rxjs.map(() => void 0),
5985
- rxjs.share()
5986
- );
5987
- this.load$ = this.triggerSubject.pipe(
5988
- rxjs.withLatestFrom(this.stateSubject),
5989
- rxjs.filter(
5990
- ([trigger, state]) => trigger === `load` && state !== "ready" && state !== "loaded" && state !== "loading"
5991
- ),
5992
- rxjs.map(() => void 0),
5993
- rxjs.share()
5994
- );
5995
- this.destroy$ = this.triggerSubject.pipe(
5996
- rxjs.filter((trigger) => trigger === `destroy`)
5997
- );
5998
- this.load$.pipe(
5999
- rxjs.switchMap(() => {
6000
- this.stateSubject.next(`loading`);
6001
- const createDocument$ = this.onCreateDocument().pipe(
6002
- rxjs.endWith(null),
6003
- rxjs.first()
6004
- );
6005
- return createDocument$.pipe(
6006
- rxjs.tap(() => {
6007
- this.hookManager.execute(`item.onDocumentCreated`, this.item.id, {
6008
- itemId: this.item.id,
6009
- layers: this.layers
6010
- });
6011
- }),
6012
- rxjs.switchMap(() => {
6013
- const loadDocument$ = this.onLoadDocument().pipe(
6014
- rxjs.endWith(null),
6015
- rxjs.first()
6016
- );
6017
- return loadDocument$;
6018
- }),
6019
- waitForSwitch(this.context.bridgeEvent.viewportFree$),
6020
- rxjs.switchMap(() => {
6021
- const hookResults = this.hookManager.execute(`item.onDocumentLoad`, this.item.id, {
6022
- itemId: this.item.id,
6023
- layers: this.layers
6024
- }).filter(
6025
- (result) => result instanceof rxjs.Observable
6026
- );
6027
- return rxjs.combineLatest([rxjs.of(null), ...hookResults]).pipe(rxjs.first());
6028
- }),
6029
- rxjs.tap(() => {
6030
- this.stateSubject.next(`loaded`);
6031
- this.stateSubject.next(`ready`);
6032
- }),
6033
- rxjs.takeUntil(rxjs.merge(this.destroy$, this.unload$))
6034
- );
6035
- })
6036
- ).subscribe();
6037
- this.unload$.pipe(
6038
- rxjs.switchMap(() => {
6039
- this.stateSubject.next(`unloading`);
6040
- return this.context.bridgeEvent.viewportFree$.pipe(
6041
- rxjs.first(),
6042
- rxjs.tap(() => {
6043
- this.hookManager.destroy(`item.onDocumentLoad`, this.item.id);
6044
- }),
6045
- rxjs.switchMap(() => {
6046
- const unload$ = this.onUnload().pipe(rxjs.endWith(null), rxjs.first());
6047
- return unload$;
6048
- }),
6049
- rxjs.tap(() => {
6050
- this.stateSubject.next(`idle`);
6051
- }),
6052
- rxjs.takeUntil(rxjs.merge(this.destroy$, this.load$))
6053
- );
6054
- })
6055
- ).subscribe();
6056
- }
6057
- get state$() {
6058
- return this.stateSubject;
5508
+ this.orderedSpineItemsSubject = new rxjs.BehaviorSubject([]);
5509
+ this.items$ = this.orderedSpineItemsSubject.asObservable();
6059
5510
  }
6060
- load() {
6061
- this.triggerSubject.next(`load`);
5511
+ get(indexOrId) {
5512
+ if (typeof indexOrId === `number`) {
5513
+ return this.orderedSpineItemsSubject.value[indexOrId];
5514
+ }
5515
+ if (typeof indexOrId === `string`) {
5516
+ return this.orderedSpineItemsSubject.value.find(
5517
+ ({ item }) => item.id === indexOrId
5518
+ );
5519
+ }
5520
+ return indexOrId;
6062
5521
  }
6063
- unload() {
6064
- this.triggerSubject.next(`unload`);
5522
+ comparePositionOf(toCompare, withItem) {
5523
+ const toCompareIndex = this.getSpineItemIndex(toCompare) ?? 0;
5524
+ const withIndex = this.getSpineItemIndex(withItem) ?? 0;
5525
+ return toCompareIndex > withIndex ? `after` : toCompareIndex === withIndex ? `same` : `before`;
6065
5526
  }
6066
- destroy() {
6067
- this.triggerSubject.next(`destroy`);
6068
- this.triggerSubject.complete();
6069
- this.stateSubject.complete();
5527
+ getSpineItemIndex(spineItemOrId) {
5528
+ const spineItem = spineItemOrId instanceof SpineItem ? spineItemOrId : this.get(spineItemOrId);
5529
+ if (!spineItem) return void 0;
5530
+ const index = this.orderedSpineItemsSubject.value.indexOf(spineItem);
5531
+ return index < 0 ? void 0 : index;
6070
5532
  }
6071
- get writingMode() {
6072
- return void 0;
5533
+ addMany(spineItems) {
5534
+ this.orderedSpineItemsSubject.next([
5535
+ ...this.orderedSpineItemsSubject.getValue(),
5536
+ ...spineItems
5537
+ ]);
6073
5538
  }
6074
- get readingDirection() {
5539
+ // @todo move
5540
+ getSpineItemFromCfi(cfi) {
5541
+ const { itemIndex } = parseCfi(cfi);
5542
+ if (itemIndex !== void 0) {
5543
+ return this.get(itemIndex);
5544
+ }
6075
5545
  return void 0;
6076
5546
  }
6077
- }
6078
- class DefaultRenderer extends DocumentRenderer {
6079
- onUnload() {
6080
- return rxjs.EMPTY;
5547
+ get items() {
5548
+ return this.orderedSpineItemsSubject.value;
6081
5549
  }
6082
- onCreateDocument() {
6083
- return rxjs.EMPTY;
5550
+ /**
5551
+ * @todo handle reload, remove subscription to each items etc. See add()
5552
+ */
5553
+ destroyItems() {
5554
+ this.orderedSpineItemsSubject.value.forEach((item) => item.destroy());
6084
5555
  }
6085
- onLoadDocument() {
6086
- return rxjs.EMPTY;
5556
+ }
5557
+ const getSpineItemFromPosition = ({
5558
+ position,
5559
+ spineItemsManager,
5560
+ spineLayout,
5561
+ settings
5562
+ }) => {
5563
+ const spineItem = spineItemsManager.items.find((item) => {
5564
+ const { left, right, bottom, top } = spineLayout.getAbsolutePositionOf(item);
5565
+ const isWithinXAxis = position.x >= left && position.x < right;
5566
+ if (settings.values.computedPageTurnDirection === `horizontal`) {
5567
+ return isWithinXAxis;
5568
+ } else {
5569
+ return isWithinXAxis && position.y >= top && position.y < bottom;
5570
+ }
5571
+ });
5572
+ if (position.x === 0 && !spineItem) {
5573
+ return spineItemsManager.items[0];
6087
5574
  }
6088
- layout() {
6089
- return void 0;
5575
+ return spineItem;
5576
+ };
5577
+ const isItemVisibleByThresholdForPosition = ({
5578
+ itemHeight,
5579
+ itemWidth,
5580
+ visibleWidthOfItem,
5581
+ visibleHeightOfItem,
5582
+ threshold
5583
+ }) => {
5584
+ const visibleWidthRatioOfSpineItem = visibleWidthOfItem / itemWidth;
5585
+ const visibleHeightRatioOfSpineItem = visibleHeightOfItem / itemHeight;
5586
+ const isItemVisibleEnough = visibleWidthRatioOfSpineItem >= threshold && visibleHeightRatioOfSpineItem >= threshold;
5587
+ return isItemVisibleEnough;
5588
+ };
5589
+ const isItemVisibleOnScreenByThresholdForPosition = ({
5590
+ visibleWidthOfItem,
5591
+ visibleHeightOfItem,
5592
+ threshold,
5593
+ context
5594
+ }) => {
5595
+ const widthRatioOfSpaceTakenInScreen = visibleWidthOfItem / context.state.visibleAreaRect.width;
5596
+ const heightRatioOfSpaceTakenInScreen = visibleHeightOfItem / context.state.visibleAreaRect.height;
5597
+ const isItemVisibleEnoughOnScreen = heightRatioOfSpaceTakenInScreen >= threshold && widthRatioOfSpaceTakenInScreen >= threshold;
5598
+ return isItemVisibleEnoughOnScreen;
5599
+ };
5600
+ const getItemVisibilityForPosition = ({
5601
+ itemPosition: {
5602
+ bottom,
5603
+ left,
5604
+ right,
5605
+ top,
5606
+ width: itemWidth,
5607
+ height: itemHeight
5608
+ },
5609
+ threshold,
5610
+ viewportPosition,
5611
+ restrictToScreen,
5612
+ context
5613
+ }) => {
5614
+ const viewportLeft = viewportPosition.x;
5615
+ const viewportRight = viewportPosition.x + (context.state.visibleAreaRect.width - 1);
5616
+ const viewportTop = viewportPosition.y;
5617
+ const viewportBottom = Math.max(
5618
+ viewportPosition.y + (context.state.visibleAreaRect.height - 1),
5619
+ 0
5620
+ );
5621
+ const visibleWidthOfItem = Math.max(
5622
+ 0,
5623
+ Math.min(right, viewportRight) - Math.max(left, viewportLeft)
5624
+ );
5625
+ const visibleHeightOfItem = Math.max(
5626
+ 0,
5627
+ Math.min(bottom, viewportBottom) - Math.max(top, viewportTop)
5628
+ );
5629
+ const itemIsOnTheOuterEdge = visibleWidthOfItem <= 0 || visibleHeightOfItem <= 0;
5630
+ if (itemIsOnTheOuterEdge) return { visible: false };
5631
+ const isItemVisibleEnoughOnScreen = isItemVisibleOnScreenByThresholdForPosition({
5632
+ threshold,
5633
+ visibleHeightOfItem,
5634
+ visibleWidthOfItem,
5635
+ context
5636
+ });
5637
+ if (restrictToScreen) {
5638
+ return { visible: isItemVisibleEnoughOnScreen };
6090
5639
  }
6091
- }
6092
- const defaultGetResource = (item) => new URL(item.href);
6093
- class ResourceHandler {
6094
- constructor(item, settings) {
6095
- this.item = item;
5640
+ const isItemVisibleEnough = isItemVisibleByThresholdForPosition({
5641
+ itemHeight,
5642
+ itemWidth,
5643
+ threshold,
5644
+ visibleHeightOfItem,
5645
+ visibleWidthOfItem
5646
+ });
5647
+ return {
5648
+ visible: isItemVisibleEnough || isItemVisibleEnoughOnScreen
5649
+ };
5650
+ };
5651
+ const getVisibleSpineItemsFromPosition = ({
5652
+ position,
5653
+ threshold,
5654
+ restrictToScreen,
5655
+ spineItemsManager,
5656
+ context,
5657
+ settings,
5658
+ spineLayout
5659
+ }) => {
5660
+ const fallbackSpineItem = getSpineItemFromPosition({
5661
+ position,
5662
+ settings,
5663
+ spineItemsManager,
5664
+ spineLayout
5665
+ }) || spineItemsManager.get(0);
5666
+ const spineItemsVisible = spineItemsManager.items.reduce(
5667
+ (acc, spineItem) => {
5668
+ const itemPosition = spineLayout.getAbsolutePositionOf(spineItem);
5669
+ const { visible } = getItemVisibilityForPosition({
5670
+ itemPosition,
5671
+ threshold,
5672
+ viewportPosition: position,
5673
+ restrictToScreen,
5674
+ context
5675
+ });
5676
+ if (visible) {
5677
+ return [...acc, spineItem];
5678
+ }
5679
+ return acc;
5680
+ },
5681
+ []
5682
+ );
5683
+ const beginItem = spineItemsVisible[0] ?? fallbackSpineItem;
5684
+ const endItem = spineItemsVisible[spineItemsVisible.length - 1] ?? beginItem;
5685
+ if (!beginItem || !endItem) return void 0;
5686
+ const beginItemIndex = spineItemsManager.getSpineItemIndex(beginItem);
5687
+ const endItemIndex = spineItemsManager.getSpineItemIndex(endItem);
5688
+ return {
5689
+ beginIndex: beginItemIndex ?? 0,
5690
+ endIndex: endItemIndex ?? 0
5691
+ };
5692
+ };
5693
+ const getSpinePositionFromSpineItemPosition = ({
5694
+ spineItemPosition,
5695
+ itemLayout: { left, top }
5696
+ }) => {
5697
+ return {
5698
+ x: left + spineItemPosition.x,
5699
+ y: top + spineItemPosition.y
5700
+ };
5701
+ };
5702
+ const getSpineInfoFromAbsolutePageIndex = ({
5703
+ absolutePageIndex,
5704
+ spineLayout,
5705
+ spineItemsManager,
5706
+ context,
5707
+ settings
5708
+ }) => {
5709
+ const items = spineItemsManager.items;
5710
+ const { found, currentAbsolutePage } = items.reduce(
5711
+ (acc, item) => {
5712
+ if (acc.found) return acc;
5713
+ const itemLayout = spineLayout.getAbsolutePositionOf(item);
5714
+ const numberOfPages = getSpineItemNumberOfPages({
5715
+ isUsingVerticalWriting: !!item.isUsingVerticalWriting(),
5716
+ itemHeight: itemLayout.height,
5717
+ itemWidth: itemLayout.width,
5718
+ context,
5719
+ settings
5720
+ });
5721
+ const possiblePageIndex = absolutePageIndex - acc.currentAbsolutePage;
5722
+ const currentAbsolutePage2 = acc.currentAbsolutePage + numberOfPages;
5723
+ if (possiblePageIndex <= numberOfPages - 1) {
5724
+ return {
5725
+ ...acc,
5726
+ currentAbsolutePage: currentAbsolutePage2,
5727
+ found: { item, pageIndex: possiblePageIndex }
5728
+ };
5729
+ }
5730
+ return {
5731
+ ...acc,
5732
+ currentAbsolutePage: currentAbsolutePage2
5733
+ };
5734
+ },
5735
+ { currentAbsolutePage: 0 }
5736
+ );
5737
+ if (found) {
5738
+ return {
5739
+ spineItem: found.item,
5740
+ pageIndex: found.pageIndex,
5741
+ itemIndex: spineItemsManager.getSpineItemIndex(found.item) ?? 0,
5742
+ currentAbsolutePage
5743
+ };
5744
+ }
5745
+ return void 0;
5746
+ };
5747
+ const getAbsolutePageIndexFromPageIndex = ({
5748
+ pageIndex,
5749
+ spineItemOrId,
5750
+ spineLayout,
5751
+ spineItemsManager,
5752
+ context,
5753
+ settings
5754
+ }) => {
5755
+ const items = spineItemsManager.items;
5756
+ const spineItem = spineItemsManager.get(spineItemOrId);
5757
+ if (!spineItem) return void 0;
5758
+ const { currentAbsolutePage } = items.reduce(
5759
+ (acc, item) => {
5760
+ if (acc.found) return acc;
5761
+ const itemLayout = spineLayout.getAbsolutePositionOf(item);
5762
+ const numberOfPages = getSpineItemNumberOfPages({
5763
+ isUsingVerticalWriting: !!item.isUsingVerticalWriting(),
5764
+ itemHeight: itemLayout.height,
5765
+ itemWidth: itemLayout.width,
5766
+ context,
5767
+ settings
5768
+ });
5769
+ if (spineItem === item) {
5770
+ if (pageIndex <= numberOfPages - 1) {
5771
+ return {
5772
+ currentAbsolutePage: acc.currentAbsolutePage + pageIndex,
5773
+ found: true
5774
+ };
5775
+ }
5776
+ }
5777
+ return {
5778
+ ...acc,
5779
+ currentAbsolutePage: acc.currentAbsolutePage + numberOfPages
5780
+ };
5781
+ },
5782
+ { currentAbsolutePage: 0, found: false }
5783
+ );
5784
+ return currentAbsolutePage;
5785
+ };
5786
+ const createSpineLocator = ({
5787
+ spineItemsManager,
5788
+ context,
5789
+ spineItemLocator,
5790
+ settings,
5791
+ spineLayout
5792
+ }) => {
5793
+ const getSpineItemPositionFromSpinePosition = Report.measurePerformance(
5794
+ `getSpineItemPositionFromSpinePosition`,
5795
+ 10,
5796
+ (position, spineItem) => {
5797
+ const { left, top } = spineLayout.getAbsolutePositionOf(spineItem);
5798
+ return {
5799
+ /**
5800
+ * when using spread the item could be on the right and therefore will be negative
5801
+ * @example
5802
+ * 400 (position = viewport), page of 200
5803
+ * 400 - 600 = -200.
5804
+ * However we can assume we are at 0, because we in fact can see the beginning of the item
5805
+ */
5806
+ x: Math.max(position.x - left, 0),
5807
+ y: Math.max(position.y - top, 0)
5808
+ };
5809
+ },
5810
+ { disable: true }
5811
+ );
5812
+ const getSpinePositionFromSpineItem = (spineItem) => {
5813
+ return getSpinePositionFromSpineItemPosition({
5814
+ spineItemPosition: { x: 0, y: 0 },
5815
+ itemLayout: spineLayout.getAbsolutePositionOf(spineItem)
5816
+ });
5817
+ };
5818
+ const getSpineItemFromIframe = (iframe) => {
5819
+ return spineItemsManager.items.find((item) => {
5820
+ var _a;
5821
+ return ((_a = item.renderer.layers[0]) == null ? void 0 : _a.element) === iframe;
5822
+ });
5823
+ };
5824
+ const getSpineItemPageIndexFromNode = (node, offset, spineItemOrIndex) => {
5825
+ if (typeof spineItemOrIndex === `number`) {
5826
+ const spineItem = spineItemsManager.get(spineItemOrIndex);
5827
+ return spineItem ? spineItemLocator.getSpineItemPageIndexFromNode(
5828
+ node,
5829
+ offset || 0,
5830
+ spineItem
5831
+ ) : void 0;
5832
+ }
5833
+ return spineItemLocator.getSpineItemPageIndexFromNode(
5834
+ node,
5835
+ offset || 0,
5836
+ spineItemOrIndex
5837
+ );
5838
+ };
5839
+ const getVisiblePagesFromViewportPosition = ({
5840
+ position,
5841
+ threshold,
5842
+ spineItem,
5843
+ restrictToScreen
5844
+ }) => {
5845
+ const { height, width } = spineItem.getElementDimensions();
5846
+ const numberOfPages = spineItemLocator.getSpineItemNumberOfPages({
5847
+ isUsingVerticalWriting: !!spineItem.isUsingVerticalWriting(),
5848
+ itemHeight: height,
5849
+ itemWidth: width
5850
+ });
5851
+ const pages = Array.from(Array(numberOfPages)).map((_, index) => {
5852
+ const spineItemPosition = spineItemLocator.getSpineItemPositionFromPageIndex({
5853
+ pageIndex: index,
5854
+ isUsingVerticalWriting: !!spineItem.isUsingVerticalWriting(),
5855
+ itemLayout: spineItem.getElementDimensions()
5856
+ });
5857
+ const spinePosition = getSpinePositionFromSpineItemPosition({
5858
+ spineItemPosition,
5859
+ itemLayout: spineLayout.getAbsolutePositionOf(spineItem)
5860
+ });
5861
+ return {
5862
+ index,
5863
+ absolutePosition: {
5864
+ width: context.getPageSize().width,
5865
+ height: context.getPageSize().height,
5866
+ left: spinePosition.x,
5867
+ top: spinePosition.y,
5868
+ bottom: spinePosition.y + context.getPageSize().height,
5869
+ right: spinePosition.x + context.getPageSize().width
5870
+ }
5871
+ };
5872
+ });
5873
+ const pagesVisible = pages.reduce(
5874
+ (acc, { absolutePosition, index }) => {
5875
+ const { visible } = getItemVisibilityForPosition({
5876
+ context,
5877
+ viewportPosition: position,
5878
+ restrictToScreen,
5879
+ threshold,
5880
+ itemPosition: absolutePosition
5881
+ });
5882
+ if (visible) {
5883
+ return [...acc, index];
5884
+ }
5885
+ return acc;
5886
+ },
5887
+ []
5888
+ );
5889
+ const beginPageIndex = pagesVisible[0];
5890
+ const endPageIndex = pagesVisible[pagesVisible.length - 1] ?? beginPageIndex;
5891
+ if (beginPageIndex === void 0 || endPageIndex === void 0)
5892
+ return void 0;
5893
+ return {
5894
+ beginPageIndex,
5895
+ endPageIndex
5896
+ };
5897
+ };
5898
+ const isPositionWithinSpineItem = (position, spineItem) => {
5899
+ const { bottom, left, right, top } = spineLayout.getAbsolutePositionOf(spineItem);
5900
+ return position.x >= left && position.x <= right && position.y <= bottom && position.y >= top;
5901
+ };
5902
+ const getSafeSpineItemPositionFromUnsafeSpineItemPosition = (unsafePosition, spineItem) => {
5903
+ const { height, width } = spineLayout.getAbsolutePositionOf(spineItem);
5904
+ return {
5905
+ x: Math.min(Math.max(0, unsafePosition.x), width),
5906
+ y: Math.min(Math.max(0, unsafePosition.y), height)
5907
+ };
5908
+ };
5909
+ return {
5910
+ getSpinePositionFromSpineItemPosition: ({
5911
+ spineItem,
5912
+ spineItemPosition
5913
+ }) => {
5914
+ const itemLayout = spineLayout.getAbsolutePositionOf(spineItem);
5915
+ return getSpinePositionFromSpineItemPosition({
5916
+ itemLayout,
5917
+ spineItemPosition
5918
+ });
5919
+ },
5920
+ getAbsolutePageIndexFromPageIndex: (params) => getAbsolutePageIndexFromPageIndex({
5921
+ ...params,
5922
+ context,
5923
+ settings,
5924
+ spineItemsManager,
5925
+ spineLayout
5926
+ }),
5927
+ getSpineInfoFromAbsolutePageIndex: (params) => getSpineInfoFromAbsolutePageIndex({
5928
+ ...params,
5929
+ context,
5930
+ settings,
5931
+ spineItemsManager,
5932
+ spineLayout
5933
+ }),
5934
+ getSpinePositionFromSpineItem,
5935
+ getSpineItemPositionFromSpinePosition,
5936
+ getSpineItemFromPosition: (position) => getSpineItemFromPosition({
5937
+ position,
5938
+ settings,
5939
+ spineItemsManager,
5940
+ spineLayout
5941
+ }),
5942
+ getSpineItemFromIframe,
5943
+ getSpineItemPageIndexFromNode,
5944
+ getVisibleSpineItemsFromPosition: (params) => getVisibleSpineItemsFromPosition({
5945
+ context,
5946
+ settings,
5947
+ spineItemsManager,
5948
+ spineLayout,
5949
+ ...params
5950
+ }),
5951
+ getVisiblePagesFromViewportPosition,
5952
+ isPositionWithinSpineItem,
5953
+ spineItemLocator,
5954
+ getSafeSpineItemPositionFromUnsafeSpineItemPosition
5955
+ };
5956
+ };
5957
+ const loadItems = ({
5958
+ spineItemsManager,
5959
+ settings
5960
+ }) => (stream) => stream.pipe(
5961
+ rxjs.tap(([beginIndex, endIndex]) => {
5962
+ const { numberOfAdjacentSpineItemToPreLoad } = settings.values;
5963
+ const leftMaximumIndex = beginIndex - numberOfAdjacentSpineItemToPreLoad;
5964
+ const rightMaximumIndex = endIndex + numberOfAdjacentSpineItemToPreLoad;
5965
+ spineItemsManager.items.forEach((orderedSpineItem, index) => {
5966
+ const isWithinRange = index >= leftMaximumIndex && index <= rightMaximumIndex;
5967
+ if (isWithinRange) {
5968
+ orderedSpineItem.load();
5969
+ } else {
5970
+ orderedSpineItem.unload();
5971
+ }
5972
+ });
5973
+ })
5974
+ );
5975
+ const mapToItemsToLoad = ({ spineLocator }) => (stream) => stream.pipe(
5976
+ rxjs.map((position) => {
5977
+ const { beginIndex = 0, endIndex = 0 } = spineLocator.getVisibleSpineItemsFromPosition({
5978
+ position,
5979
+ threshold: 0
5980
+ }) || {};
5981
+ return [beginIndex, endIndex];
5982
+ })
5983
+ );
5984
+ class SpineItemsLoader extends DestroyableClass {
5985
+ constructor(context, spineItemsManager, spineLocator, settings, spineLayout) {
5986
+ super();
5987
+ this.context = context;
5988
+ this.spineItemsManager = spineItemsManager;
5989
+ this.spineLocator = spineLocator;
6096
5990
  this.settings = settings;
6097
- }
6098
- async getResource() {
6099
- var _a, _b;
6100
- const resource = await rxjs.lastValueFrom(
6101
- ((_b = (_a = this.settings.values).getResource) == null ? void 0 : _b.call(_a, this.item)) ?? rxjs.of(void 0)
5991
+ this.spineLayout = spineLayout;
5992
+ const navigationUpdate$ = this.context.bridgeEvent.navigation$;
5993
+ const layoutHasChanged$ = this.spineLayout.layout$.pipe(
5994
+ rxjs.filter(({ hasChanged }) => hasChanged)
6102
5995
  );
6103
- return resource ?? defaultGetResource(this.item);
6104
- }
6105
- async fetchResource() {
6106
- const resource = await this.getResource();
6107
- if (resource instanceof Response) return resource;
6108
- if (resource instanceof URL) return fetch(resource);
6109
- return resource;
5996
+ const numberOfAdjacentSpineItemToPreLoad$ = settings.values$.pipe(
5997
+ rxjs.map(
5998
+ ({ numberOfAdjacentSpineItemToPreLoad }) => numberOfAdjacentSpineItemToPreLoad
5999
+ ),
6000
+ rxjs.skip(1),
6001
+ rxjs.distinctUntilChanged()
6002
+ );
6003
+ const loadSpineItems$ = rxjs.merge(
6004
+ navigationUpdate$,
6005
+ layoutHasChanged$,
6006
+ numberOfAdjacentSpineItemToPreLoad$
6007
+ ).pipe(
6008
+ // this can be changed by whatever we want and SHOULD not break navigation.
6009
+ // Ideally loading faster is better but loading too close to user navigating can
6010
+ // be dangerous.
6011
+ rxjs.debounceTime(100, rxjs.animationFrameScheduler),
6012
+ waitForSwitch(this.context.bridgeEvent.viewportFree$),
6013
+ rxjs.withLatestFrom(this.context.bridgeEvent.navigation$),
6014
+ rxjs.map(([, navigation]) => navigation.position),
6015
+ mapToItemsToLoad({ spineLocator }),
6016
+ loadItems({ spineItemsManager, settings })
6017
+ );
6018
+ loadSpineItems$.pipe(rxjs.takeUntil(this.destroy$)).subscribe();
6110
6019
  }
6111
6020
  }
6112
- class SpineItem {
6113
- constructor(item, parentElement, context, settings, hookManager, index) {
6114
- var _a, _b;
6115
- this.item = item;
6116
- this.parentElement = parentElement;
6117
- this.context = context;
6118
- this.settings = settings;
6119
- this.hookManager = hookManager;
6120
- this.index = index;
6121
- this.adjustPositionOfElement = ({
6122
- right,
6123
- left,
6124
- top
6125
- }) => {
6126
- if (right !== void 0) {
6127
- this.containerElement.style.right = `${right}px`;
6128
- } else {
6129
- this.containerElement.style.removeProperty(`right`);
6130
- }
6131
- if (left !== void 0) {
6132
- this.containerElement.style.left = `${left}px`;
6133
- } else {
6134
- this.containerElement.style.removeProperty(`left`);
6135
- }
6136
- if (top !== void 0) {
6137
- this.containerElement.style.top = `${top}px`;
6138
- } else {
6139
- this.containerElement.style.removeProperty(`top`);
6140
- }
6141
- };
6142
- this.getBoundingRectOfElementFromSelector = (selector) => {
6143
- var _a2, _b2, _c, _d, _e;
6144
- const frameElement = (_a2 = this.renderer.layers[0]) == null ? void 0 : _a2.element;
6145
- if (frameElement && frameElement instanceof HTMLIFrameElement && selector) {
6146
- if (selector.startsWith(`#`)) {
6147
- return (_c = (_b2 = frameElement.contentDocument) == null ? void 0 : _b2.getElementById(selector.replace(`#`, ``))) == null ? void 0 : _c.getBoundingClientRect();
6148
- }
6149
- return (_e = (_d = frameElement.contentDocument) == null ? void 0 : _d.querySelector(selector)) == null ? void 0 : _e.getBoundingClientRect();
6150
- }
6151
- };
6152
- this.layout = ({
6153
- blankPagePosition,
6154
- minimumWidth,
6155
- spreadPosition
6156
- }) => {
6157
- this.hookManager.execute(`item.onBeforeLayout`, void 0, {
6158
- blankPagePosition,
6159
- item: this.item,
6160
- minimumWidth
6161
- });
6162
- const { height, width } = this.renderer.layout({
6163
- blankPagePosition,
6164
- minPageSpread: minimumWidth / this.context.getPageSize().width,
6165
- spreadPosition
6166
- }) ?? { width: 0, height: 0 };
6167
- const minHeight = Math.max(height, this.context.getPageSize().height);
6168
- const minWidth = Math.max(width, minimumWidth);
6169
- this.containerElement.style.width = `${minWidth}px`;
6170
- this.containerElement.style.height = `${minHeight}px`;
6171
- this.hookManager.execute(`item.onAfterLayout`, void 0, {
6172
- blankPagePosition,
6173
- item: this.item,
6174
- minimumWidth
6175
- });
6176
- return { width: minWidth, height: minHeight };
6177
- };
6178
- this.load = () => this.renderer.load();
6179
- this.unload = () => {
6180
- this.renderer.unload();
6181
- };
6182
- this.getElementDimensions = () => {
6183
- const rect = this.containerElement.getBoundingClientRect();
6184
- const normalizedValues = {
6185
- ...rect,
6186
- // we want to round to first decimal because it's possible to have half pixel
6187
- // however browser engine can also gives back x.yyyy based on their precision
6188
- width: Math.round(rect.width * 10) / 10,
6189
- height: Math.round(rect.height * 10) / 10
6190
- };
6191
- return normalizedValues;
6192
- };
6193
- this.destroy = () => {
6194
- this.destroySubject$.next();
6195
- this.containerElement.remove();
6196
- this.destroySubject$.complete();
6197
- this.renderer.destroy();
6198
- };
6199
- this.isUsingVerticalWriting = () => {
6200
- var _a2;
6201
- return !!((_a2 = this.renderer.writingMode) == null ? void 0 : _a2.startsWith(`vertical`));
6202
- };
6203
- this.destroySubject$ = new rxjs.Subject();
6204
- this.containerElement = createContainerElement(
6205
- parentElement,
6206
- item,
6207
- hookManager
6021
+ class SpineItemsObserver extends DestroyableClass {
6022
+ constructor(spineItemsManager, spineLocator) {
6023
+ super();
6024
+ this.spineItemsManager = spineItemsManager;
6025
+ this.spineLocator = spineLocator;
6026
+ this.itemIsReady$ = this.spineItemsManager.items$.pipe(
6027
+ rxjs.switchMap((items) => {
6028
+ const itemsIsReady$ = items.map(
6029
+ (item) => item.isReady$.pipe(rxjs.map((isReady) => ({ item, isReady })))
6030
+ );
6031
+ return rxjs.merge(...itemsIsReady$);
6032
+ }),
6033
+ rxjs.share()
6208
6034
  );
6209
- parentElement.appendChild(this.containerElement);
6210
- const RendererClass = ((_b = (_a = this.settings.values).getRenderer) == null ? void 0 : _b.call(_a, item)) ?? DefaultRenderer;
6211
- this.resourcesHandler = new ResourceHandler(item, this.settings);
6212
- this.renderer = new RendererClass(
6213
- context,
6214
- settings,
6215
- hookManager,
6216
- item,
6217
- this.containerElement,
6218
- this.resourcesHandler
6035
+ this.itemResize$ = this.spineItemsManager.items$.pipe(
6036
+ rxjs.switchMap((items) => {
6037
+ const resize$ = items.map(
6038
+ (item) => observeResize(item.element).pipe(
6039
+ rxjs.map((entries) => ({ entries, item }))
6040
+ )
6041
+ );
6042
+ return rxjs.merge(...resize$);
6043
+ }),
6044
+ rxjs.share()
6219
6045
  );
6220
- const contentLayoutChange$ = rxjs.merge(
6221
- this.unloaded$.pipe(operators.map(() => ({ isFirstLayout: false }))),
6222
- this.ready$.pipe(operators.map(() => ({ isFirstLayout: true })))
6046
+ this.itemLoaded$ = this.spineItemsManager.items$.pipe(
6047
+ rxjs.switchMap((items) => {
6048
+ const itemsIsReady$ = items.map(
6049
+ (item) => item.loaded$.pipe(rxjs.map(() => item))
6050
+ );
6051
+ return rxjs.merge(...itemsIsReady$);
6052
+ })
6223
6053
  );
6224
- this.contentLayout$ = contentLayoutChange$.pipe(
6225
- operators.withLatestFrom(this.isReady$),
6226
- operators.map(([data, isReady]) => ({
6227
- isFirstLayout: data.isFirstLayout,
6228
- isReady
6229
- }))
6054
+ }
6055
+ }
6056
+ const convertSpinePositionToLayoutPosition = ({
6057
+ position,
6058
+ pageSize
6059
+ }) => {
6060
+ return {
6061
+ x: position.x,
6062
+ y: position.y,
6063
+ left: position.x,
6064
+ top: position.y,
6065
+ width: pageSize.width,
6066
+ height: pageSize.height,
6067
+ bottom: position.y + pageSize.height,
6068
+ right: position.x + pageSize.width
6069
+ };
6070
+ };
6071
+ const NAMESPACE = `SpineLayout`;
6072
+ class SpineLayout extends DestroyableClass {
6073
+ constructor(spineItemsManager, context, settings) {
6074
+ super();
6075
+ this.spineItemsManager = spineItemsManager;
6076
+ this.context = context;
6077
+ this.settings = settings;
6078
+ this.itemLayoutInformation = [];
6079
+ this.layoutSubject = new rxjs.Subject();
6080
+ spineItemsManager.items$.pipe(
6081
+ rxjs.switchMap((items) => {
6082
+ const itemsLayout$ = items.map(
6083
+ (spineItem) => spineItem.contentLayout$.pipe(
6084
+ rxjs.tap(() => {
6085
+ this.layout();
6086
+ })
6087
+ )
6088
+ );
6089
+ const writingModeUpdate$ = items.map(
6090
+ (spineItem) => spineItem.loaded$.pipe(
6091
+ rxjs.tap(() => {
6092
+ if (spineItem.isUsingVerticalWriting()) {
6093
+ this.context.update({
6094
+ hasVerticalWriting: true
6095
+ });
6096
+ } else {
6097
+ this.context.update({
6098
+ hasVerticalWriting: false
6099
+ });
6100
+ }
6101
+ })
6102
+ )
6103
+ );
6104
+ return rxjs.merge(...itemsLayout$, ...writingModeUpdate$);
6105
+ })
6106
+ ).pipe(rxjs.takeUntil(this.destroy$)).subscribe();
6107
+ this.layout$ = this.layoutSubject.pipe(
6108
+ rxjs.map((hasChanged) => ({ hasChanged })),
6109
+ rxjs.startWith({ hasChanged: true }),
6110
+ rxjs.map(({ hasChanged }) => {
6111
+ const items = spineItemsManager.items;
6112
+ const spineItemsPagesAbsolutePositions = items.map((item) => {
6113
+ const itemLayout = this.getAbsolutePositionOf(item);
6114
+ const numberOfPages = getSpineItemNumberOfPages({
6115
+ isUsingVerticalWriting: !!item.isUsingVerticalWriting(),
6116
+ itemHeight: itemLayout.height,
6117
+ itemWidth: itemLayout.width,
6118
+ context,
6119
+ settings
6120
+ });
6121
+ const pages2 = new Array(numberOfPages).fill(void 0);
6122
+ return pages2.map(
6123
+ (_, pageIndex) => convertSpinePositionToLayoutPosition({
6124
+ pageSize: this.context.getPageSize(),
6125
+ position: getSpinePositionFromSpineItemPosition({
6126
+ itemLayout,
6127
+ spineItemPosition: getSpineItemPositionFromPageIndex({
6128
+ isUsingVerticalWriting: !!item.isUsingVerticalWriting(),
6129
+ itemLayout,
6130
+ pageIndex,
6131
+ context
6132
+ })
6133
+ })
6134
+ })
6135
+ );
6136
+ });
6137
+ const pages = spineItemsPagesAbsolutePositions.reduce(
6138
+ (acc, itemPages, itemIndex) => {
6139
+ const itemPagesInfo = itemPages.map(
6140
+ (absolutePosition, pageIndex) => ({
6141
+ itemIndex,
6142
+ absolutePageIndex: itemIndex + pageIndex,
6143
+ absolutePosition
6144
+ })
6145
+ );
6146
+ return [...acc, ...itemPagesInfo];
6147
+ },
6148
+ []
6149
+ );
6150
+ return {
6151
+ hasChanged,
6152
+ spineItemsAbsolutePositions: items.map(
6153
+ (item) => this.getAbsolutePositionOf(item)
6154
+ ),
6155
+ spineItemsPagesAbsolutePositions,
6156
+ pages
6157
+ };
6158
+ }),
6159
+ rxjs.shareReplay(1)
6230
6160
  );
6231
6161
  }
6232
- get element() {
6233
- return this.containerElement;
6234
- }
6235
6162
  /**
6236
- * @important
6237
- * Do not use this value for layout and navigation. It will be in possible conflict
6238
- * with the global reading direction. A book should not mix them anyway. A page can have
6239
- * a different reading direction for style reason but it should be in theory pre-paginated.
6240
- * For example an english page in a japanese manga. That's expected and will
6241
- * be confined to a single page.
6163
+ * @todo
6164
+ * move this logic to the spine
6165
+ *
6166
+ * @todo
6167
+ * make sure to check how many times it is being called and try to reduce number of layouts
6168
+ * it is called eery time an item is being unload (which can adds up quickly for big books)
6242
6169
  */
6243
- get readingDirection() {
6244
- return this.renderer.readingDirection;
6245
- }
6246
- get isReady() {
6247
- return this.renderer.state$.getValue() === "ready";
6248
- }
6249
- get ready$() {
6250
- return this.renderer.state$.pipe(
6251
- operators.distinctUntilChanged(),
6252
- operators.filter((state) => state === "ready")
6253
- );
6254
- }
6255
- get loaded$() {
6256
- return this.renderer.state$.pipe(
6257
- operators.distinctUntilChanged(),
6258
- operators.filter((state) => state === "loaded")
6170
+ layout() {
6171
+ const manifest = this.context.manifest;
6172
+ const newItemLayoutInformation = [];
6173
+ const isGloballyPrePaginated = (manifest == null ? void 0 : manifest.renditionLayout) === `pre-paginated`;
6174
+ this.spineItemsManager.items.reduce(
6175
+ ({ horizontalOffset, verticalOffset }, item, index) => {
6176
+ let minimumWidth = this.context.getPageSize().width;
6177
+ let blankPagePosition = `none`;
6178
+ const itemStartOnNewScreen = horizontalOffset % this.context.state.visibleAreaRect.width === 0;
6179
+ const isLastItem = index === this.spineItemsManager.items.length - 1;
6180
+ if (this.context.state.isUsingSpreadMode) {
6181
+ if (!isGloballyPrePaginated && item.item.renditionLayout === `reflowable` && !isLastItem) {
6182
+ minimumWidth = this.context.getPageSize().width * 2;
6183
+ }
6184
+ if (!isGloballyPrePaginated && item.item.renditionLayout === `reflowable` && isLastItem && itemStartOnNewScreen) {
6185
+ minimumWidth = this.context.getPageSize().width * 2;
6186
+ }
6187
+ const lastItemStartOnNewScreenInAPrepaginatedBook = itemStartOnNewScreen && isLastItem && isGloballyPrePaginated;
6188
+ if (item.item.pageSpreadRight && itemStartOnNewScreen && !this.context.isRTL()) {
6189
+ blankPagePosition = `before`;
6190
+ minimumWidth = this.context.getPageSize().width * 2;
6191
+ } else if (item.item.pageSpreadLeft && itemStartOnNewScreen && this.context.isRTL()) {
6192
+ blankPagePosition = `before`;
6193
+ minimumWidth = this.context.getPageSize().width * 2;
6194
+ } else if (lastItemStartOnNewScreenInAPrepaginatedBook) {
6195
+ if (this.context.isRTL()) {
6196
+ blankPagePosition = `before`;
6197
+ } else {
6198
+ blankPagePosition = `after`;
6199
+ }
6200
+ minimumWidth = this.context.getPageSize().width * 2;
6201
+ }
6202
+ }
6203
+ const { width, height } = item.layout({
6204
+ minimumWidth,
6205
+ blankPagePosition,
6206
+ spreadPosition: this.context.state.isUsingSpreadMode ? itemStartOnNewScreen ? this.context.isRTL() ? `right` : `left` : this.context.isRTL() ? `left` : `right` : `none`
6207
+ });
6208
+ if (this.settings.values.computedPageTurnDirection === `vertical`) {
6209
+ const currentValidEdgeYForVerticalPositioning = itemStartOnNewScreen ? verticalOffset : verticalOffset - this.context.state.visibleAreaRect.height;
6210
+ const currentValidEdgeXForVerticalPositioning = itemStartOnNewScreen ? 0 : horizontalOffset;
6211
+ if (this.context.isRTL()) {
6212
+ item.adjustPositionOfElement({
6213
+ top: currentValidEdgeYForVerticalPositioning,
6214
+ left: currentValidEdgeXForVerticalPositioning
6215
+ });
6216
+ } else {
6217
+ item.adjustPositionOfElement({
6218
+ top: currentValidEdgeYForVerticalPositioning,
6219
+ left: currentValidEdgeXForVerticalPositioning
6220
+ });
6221
+ }
6222
+ const newEdgeX = width + currentValidEdgeXForVerticalPositioning;
6223
+ const newEdgeY = height + currentValidEdgeYForVerticalPositioning;
6224
+ newItemLayoutInformation.push({
6225
+ left: currentValidEdgeXForVerticalPositioning,
6226
+ right: newEdgeX,
6227
+ top: currentValidEdgeYForVerticalPositioning,
6228
+ bottom: newEdgeY,
6229
+ height,
6230
+ width,
6231
+ x: currentValidEdgeXForVerticalPositioning,
6232
+ y: currentValidEdgeYForVerticalPositioning
6233
+ });
6234
+ return {
6235
+ horizontalOffset: newEdgeX,
6236
+ verticalOffset: newEdgeY
6237
+ };
6238
+ }
6239
+ item.adjustPositionOfElement(
6240
+ this.context.isRTL() ? { right: horizontalOffset, top: 0 } : { left: horizontalOffset, top: 0 }
6241
+ );
6242
+ const left = this.context.isRTL() ? this.context.state.visibleAreaRect.width - horizontalOffset - width : horizontalOffset;
6243
+ newItemLayoutInformation.push({
6244
+ right: this.context.isRTL() ? this.context.state.visibleAreaRect.width - horizontalOffset : horizontalOffset + width,
6245
+ left,
6246
+ x: left,
6247
+ top: verticalOffset,
6248
+ bottom: height,
6249
+ height,
6250
+ width,
6251
+ y: verticalOffset
6252
+ });
6253
+ return {
6254
+ horizontalOffset: horizontalOffset + width,
6255
+ verticalOffset: 0
6256
+ };
6257
+ },
6258
+ { horizontalOffset: 0, verticalOffset: 0 }
6259
6259
  );
6260
- }
6261
- get unloaded$() {
6262
- return this.renderer.state$.pipe(
6263
- operators.distinctUntilChanged(),
6264
- operators.filter((state) => state !== "idle"),
6265
- operators.switchMap(
6266
- () => this.renderer.state$.pipe(
6267
- operators.filter((state) => state === `idle`),
6268
- operators.first()
6269
- )
6270
- )
6260
+ const hasLayoutChanges = this.itemLayoutInformation.length !== newItemLayoutInformation.length || this.itemLayoutInformation.some(
6261
+ (old, index) => !shared.isShallowEqual(old, newItemLayoutInformation[index])
6271
6262
  );
6272
- }
6273
- get isReady$() {
6274
- return this.renderer.state$.pipe(operators.map((state) => state === "ready"));
6263
+ this.itemLayoutInformation = newItemLayoutInformation;
6264
+ Report.log(NAMESPACE, `layout`, {
6265
+ hasLayoutChanges,
6266
+ itemLayoutInformation: this.itemLayoutInformation
6267
+ });
6268
+ this.layoutSubject.next(hasLayoutChanges);
6275
6269
  }
6276
6270
  /**
6277
- * Helper that will inject CSS into the document frame.
6278
- *
6279
- * @important
6280
- * The document needs to be detected as a frame.
6271
+ * It's important to not use x,y since we need the absolute position of each element. Otherwise x,y would be relative to
6272
+ * current window (viewport).
6281
6273
  */
6282
- upsertCSS(id, style, prepend) {
6283
- this.renderer.layers.forEach((layer) => {
6284
- if (layer.element instanceof HTMLIFrameElement) {
6285
- upsertCSS(layer.element, id, style, prepend);
6286
- }
6287
- });
6274
+ getAbsolutePositionOf(spineItemOrIndex) {
6275
+ const fallback = {
6276
+ left: 0,
6277
+ right: 0,
6278
+ top: 0,
6279
+ bottom: 0,
6280
+ width: 0,
6281
+ height: 0,
6282
+ x: 0,
6283
+ y: 0
6284
+ };
6285
+ const spineItem = this.spineItemsManager.get(spineItemOrIndex);
6286
+ const indexOfItem = spineItem ? this.spineItemsManager.items.indexOf(spineItem) : -1;
6287
+ const layoutInformation = this.itemLayoutInformation[indexOfItem];
6288
+ return layoutInformation || fallback;
6289
+ }
6290
+ destroy() {
6291
+ super.destroy();
6292
+ this.layoutSubject.complete();
6288
6293
  }
6289
6294
  }
6290
- const createContainerElement = (containerElement, item, hookManager) => {
6291
- const element = containerElement.ownerDocument.createElement(`div`);
6292
- element.classList.add(`spineItem`);
6293
- element.classList.add(`spineItem-${item.renditionLayout}`);
6294
- element.style.cssText = `
6295
- position: absolute;
6296
- overflow: hidden;
6297
- `;
6298
- hookManager.execute("item.onBeforeContainerCreated", void 0, { element });
6299
- return element;
6300
- };
6301
6295
  class Spine extends DestroyableClass {
6302
6296
  constructor(parentElement$, context, pagination, spineItemsManager, spineItemLocator, settings, hookManager) {
6303
6297
  super();
@@ -6375,6 +6369,17 @@
6375
6369
  this.elementSubject.complete();
6376
6370
  }
6377
6371
  }
6372
+ const generateCfiFromSelection = ({
6373
+ item,
6374
+ selection
6375
+ }) => {
6376
+ const anchorCfi = selection.anchorNode ? generateCfi(selection.anchorNode, selection.anchorOffset, item) : void 0;
6377
+ const focusCfi = selection.focusNode ? generateCfi(selection.focusNode, selection.focusOffset, item) : void 0;
6378
+ return {
6379
+ anchorCfi,
6380
+ focusCfi
6381
+ };
6382
+ };
6378
6383
  const createReader = (inputSettings) => {
6379
6384
  const stateSubject$ = new rxjs.BehaviorSubject({
6380
6385
  supportedPageTurnAnimation: [`fade`, `none`, `slide`],
@@ -6523,6 +6528,8 @@
6523
6528
  hookManager,
6524
6529
  cfi: {
6525
6530
  generateCfiFromRange,
6531
+ generateCfiFromSelection,
6532
+ parseCfi,
6526
6533
  generateCfiForSpineItemPage: (params) => generateCfiForSpineItemPage({
6527
6534
  ...params,
6528
6535
  spineItemLocator
@@ -6530,7 +6537,6 @@
6530
6537
  resolveCfi: (params) => resolveCfi({ ...params, spineItemsManager })
6531
6538
  },
6532
6539
  navigation: {
6533
- viewportFree$: context.bridgeEvent.viewportFree$,
6534
6540
  viewportBusy$: context.bridgeEvent.viewportBusy$,
6535
6541
  getViewportPosition: () => navigator2.viewportNavigator.viewportPosition,
6536
6542
  getNavigation: navigator2.getNavigation.bind(navigator2),
@@ -6552,6 +6558,7 @@
6552
6558
  element$,
6553
6559
  layout$: spine.spineLayout.layout$,
6554
6560
  viewportState$: context.bridgeEvent.viewportState$,
6561
+ viewportFree$: context.bridgeEvent.viewportFree$,
6555
6562
  /**
6556
6563
  * Dispatched when the reader has loaded a book and is rendering a book.
6557
6564
  * Using navigation API and getting information about current content will
@@ -8111,24 +8118,53 @@
8111
8118
  });
8112
8119
  return reader;
8113
8120
  };
8114
- const generateCfis = ({
8115
- itemId,
8116
- selection
8121
+ const getRangeFromSelection = (anchor, focus) => {
8122
+ var _a;
8123
+ const range = (_a = anchor.node.ownerDocument) == null ? void 0 : _a.createRange();
8124
+ const comparison = anchor.node.compareDocumentPosition(focus.node);
8125
+ if (!range) return void 0;
8126
+ try {
8127
+ if (comparison & Node.DOCUMENT_POSITION_PRECEDING) {
8128
+ range.setStart(focus.node, focus.offset || 0);
8129
+ range.setEnd(anchor.node, anchor.offset || 0);
8130
+ } else if (comparison & Node.DOCUMENT_POSITION_FOLLOWING) {
8131
+ range.setStart(anchor.node, anchor.offset || 0);
8132
+ range.setEnd(focus.node, focus.offset || 0);
8133
+ } else {
8134
+ const startOffset = Math.min(anchor.offset || 0, focus.offset || 0);
8135
+ const endOffset = Math.max(anchor.offset || 0, focus.offset || 0);
8136
+ range.setStart(anchor.node, startOffset);
8137
+ range.setEnd(anchor.node, endOffset);
8138
+ }
8139
+ } catch (e) {
8140
+ Report.warn("Failed to create range from selection", e, {
8141
+ anchor,
8142
+ focus
8143
+ });
8144
+ }
8145
+ return range;
8146
+ };
8147
+ const createRangeFromSelection = ({
8148
+ selection,
8149
+ spineItem
8117
8150
  }) => {
8118
- const anchorCfi = selection.anchorNode ? CfiHandler.generate(
8119
- selection.anchorNode,
8120
- selection.anchorOffset,
8121
- `|[prose~anchor~${encodeURIComponent(itemId)}]`
8122
- ) : void 0;
8123
- const focusCfi = selection.focusNode ? CfiHandler.generate(
8124
- selection.focusNode,
8125
- selection.focusOffset,
8126
- `|[prose~anchor~${encodeURIComponent(itemId)}]`
8127
- ) : void 0;
8128
- return {
8129
- anchorCfi,
8130
- focusCfi
8131
- };
8151
+ if (!spineItem.isReady) return void 0;
8152
+ const { anchorNode, anchorOffset, focusNode, focusOffset } = selection;
8153
+ if (!anchorNode || !focusNode) {
8154
+ return void 0;
8155
+ }
8156
+ try {
8157
+ return getRangeFromSelection(
8158
+ { node: anchorNode, offset: anchorOffset },
8159
+ { node: focusNode, offset: focusOffset }
8160
+ );
8161
+ } catch (e) {
8162
+ Report.warn("Failed to create range from selection", e, {
8163
+ selection,
8164
+ spineItem
8165
+ });
8166
+ return void 0;
8167
+ }
8132
8168
  };
8133
8169
  class SelectionTracker extends DestroyableClass {
8134
8170
  constructor(document2) {
@@ -8161,6 +8197,7 @@
8161
8197
  ({ itemId, layers, destroy$, destroy }) => {
8162
8198
  var _a, _b;
8163
8199
  const frame = (_a = layers[0]) == null ? void 0 : _a.element;
8200
+ const itemIndex = reader.spineItemsManager.getSpineItemIndex(itemId) ?? 0;
8164
8201
  if (frame instanceof HTMLIFrameElement) {
8165
8202
  const frameDoc = frame.contentDocument || ((_b = frame.contentWindow) == null ? void 0 : _b.document);
8166
8203
  if (frameDoc) {
@@ -8172,7 +8209,7 @@
8172
8209
  selectionSubject.next({
8173
8210
  document: frameDoc,
8174
8211
  selection,
8175
- itemId
8212
+ itemIndex
8176
8213
  });
8177
8214
  } else {
8178
8215
  selectionSubject.next(void 0);
@@ -8186,7 +8223,7 @@
8186
8223
  {
8187
8224
  document: frameDoc,
8188
8225
  selection,
8189
- itemId
8226
+ itemIndex
8190
8227
  }
8191
8228
  ]);
8192
8229
  })
@@ -8221,7 +8258,9 @@
8221
8258
  rxjs.switchMap((container) => rxjs.fromEvent(container, "pointerdown")),
8222
8259
  rxjs.withLatestFrom(selection$),
8223
8260
  rxjs.map(([, selection]) => selection),
8224
- rxjs.startWith(void 0)
8261
+ rxjs.startWith(void 0),
8262
+ rxjs.shareReplay(1),
8263
+ rxjs.takeUntil(reader.$.destroy$)
8225
8264
  );
8226
8265
  return {
8227
8266
  ...reader,
@@ -8231,7 +8270,8 @@
8231
8270
  selectionEnd$,
8232
8271
  selectionAfterPointerUp$,
8233
8272
  lastSelectionOnPointerdown$,
8234
- generateCfis
8273
+ getSelection: () => selectionSubject.getValue(),
8274
+ createRangeFromSelection
8235
8275
  },
8236
8276
  destroy: () => {
8237
8277
  selectionSubject.complete();