@openpageflip/core 0.1.0 → 0.2.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.
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ function startTween(clock, spec) {
20
20
  const tick = (time) => {
21
21
  handle = null;
22
22
  if (done) return;
23
- const t = spec.duration <= 0 ? 1 : Math.min(1, (time - startedAt) / spec.duration);
23
+ const t = spec.duration <= 0 ? 1 : Math.min(1, Math.max(0, (time - startedAt) / spec.duration));
24
24
  if (t >= 1) {
25
25
  end();
26
26
  return;
@@ -51,11 +51,6 @@ const Layout = {
51
51
  single: "single",
52
52
  spread: "spread"
53
53
  };
54
- /** Reading direction. `rtl` flips the spine to the right for manga and Hebrew/Arabic books. */
55
- const Direction = {
56
- ltr: "ltr",
57
- rtl: "rtl"
58
- };
59
54
  /** Which corner a programmatic flip lifts. */
60
55
  const FlipCorner = {
61
56
  top: "top",
@@ -120,7 +115,7 @@ const DEFAULTS = {
120
115
  ignoreDragOn: "a, button, input, textarea, select, [data-opf-no-flip]"
121
116
  };
122
117
  function isOneOf(vocabulary, value) {
123
- return Object.values(vocabulary).includes(value);
118
+ return Object.values(vocabulary).some((allowed) => allowed === value);
124
119
  }
125
120
  /** Fill in defaults and reject options that could only produce a broken book. */
126
121
  function resolveOptions(user) {
@@ -144,6 +139,11 @@ function resolveOptions(user) {
144
139
  if (!isOneOf(ClickMode, options.click)) throw new TypeError(`@openpageflip/core: unknown "click" ${String(options.click)}`);
145
140
  if (!(options.shadowOpacity >= 0 && options.shadowOpacity <= 1)) throw new TypeError(`@openpageflip/core: "shadowOpacity" must be within 0..1, got ${options.shadowOpacity}`);
146
141
  if (!Number.isInteger(options.startPage) || options.startPage < 0) throw new TypeError(`@openpageflip/core: "startPage" must be a non-negative integer, got ${options.startPage}`);
142
+ if (options.ignoreDragOn !== false) try {
143
+ document.createElement("div").matches(options.ignoreDragOn);
144
+ } catch {
145
+ throw new TypeError(`@openpageflip/core: "ignoreDragOn" is not a valid selector: ${options.ignoreDragOn}`);
146
+ }
147
147
  return options;
148
148
  }
149
149
  //#endregion
@@ -600,8 +600,10 @@ var FlipController = class {
600
600
  }
601
601
  /** Returns true when the orientation changed, which re-paginates the book. */
602
602
  setLayout(layout) {
603
+ const resized = layout.rect.pageWidth !== this.rect.pageWidth || layout.rect.height !== this.rect.height;
603
604
  this.rect = layout.rect;
604
605
  const orientationChanged = layout.orientation !== this.orientation;
606
+ if (resized && !orientationChanged) this.endSession();
605
607
  if (orientationChanged) {
606
608
  this.endSession();
607
609
  this.orientation = layout.orientation;
@@ -620,7 +622,7 @@ var FlipController = class {
620
622
  }
621
623
  showPage(page) {
622
624
  const index = spreadIndexOfPage(this.spreads, page);
623
- if (index === null) return;
625
+ if (index === null) throw new RangeError(`@openpageflip/core: page ${page} is out of range (0..${this.pages.length - 1})`);
624
626
  this.spreadIndex = index;
625
627
  this.showSpread();
626
628
  }
@@ -664,6 +666,7 @@ var FlipController = class {
664
666
  * The static pages keep showing the current spread until that turn lands.
665
667
  */
666
668
  flipTo(page, corner) {
669
+ this.tween?.finish();
667
670
  const target = spreadIndexOfPage(this.spreads, page);
668
671
  if (target === null || target === this.spreadIndex) return Promise.resolve(false);
669
672
  if (target > this.spreadIndex) {
@@ -790,14 +793,16 @@ var FlipController = class {
790
793
  this.release();
791
794
  }
792
795
  pointerDown(containerPos) {
796
+ if (this.state === FlipState.flipping) this.tween?.finish();
793
797
  this.pressStart = containerPos;
794
798
  this.dragged = false;
795
799
  }
796
800
  /** A pressed pointer moved. Starts a drag once it travels past the click threshold. */
797
801
  pointerDrag(containerPos) {
798
- if (this.pressStart === null || !this.options.drag) return;
802
+ if (this.pressStart === null) return;
799
803
  if (!this.dragged && distance(this.pressStart, containerPos) <= DRAG_THRESHOLD) return;
800
804
  this.dragged = true;
805
+ if (!this.options.drag) return;
801
806
  const session = this.session ?? this.start(this.pressStart);
802
807
  if (session === null) return;
803
808
  this.setState(FlipState.userFold);
@@ -937,6 +942,10 @@ var FlipController = class {
937
942
  } : null
938
943
  };
939
944
  }
945
+ /** Hand the current frame to the renderer again, after something else touched the DOM. */
946
+ redraw() {
947
+ this.render();
948
+ }
940
949
  render() {
941
950
  this.hooks.onFrame(this.frame());
942
951
  }
@@ -959,8 +968,9 @@ function createEmitter() {
959
968
  set = /* @__PURE__ */ new Set();
960
969
  listeners.set(name, set);
961
970
  }
962
- set.add(listener);
963
971
  const unsubscribe = () => off(name, listener);
972
+ if (options?.signal?.aborted) return unsubscribe;
973
+ set.add(listener);
964
974
  options?.signal?.addEventListener("abort", unsubscribe, { once: true });
965
975
  return unsubscribe;
966
976
  },
@@ -991,7 +1001,7 @@ function attachInput(container, controller, options) {
991
1001
  const onDown = (event) => {
992
1002
  if (press !== null) return;
993
1003
  if (event.pointerType === "mouse" && event.button !== 0) return;
994
- if (event.target instanceof Element && event.target.closest(options.ignoreDragOn) !== null) return;
1004
+ if (options.ignoreDragOn !== false && event.target instanceof Element && event.target.closest(options.ignoreDragOn) !== null) return;
995
1005
  const start = local(event);
996
1006
  press = {
997
1007
  id: event.pointerId,
@@ -1148,6 +1158,8 @@ var DomRenderer = class {
1148
1158
  * over it. The copy is inert, has no ids, and lives only for the duration of the flip.
1149
1159
  */
1150
1160
  clone = null;
1161
+ /** Pages currently hidden inline. A page is hidden once when it leaves the stage, not every frame. */
1162
+ hidden = /* @__PURE__ */ new Set();
1151
1163
  container;
1152
1164
  options;
1153
1165
  constructor(container, options) {
@@ -1172,6 +1184,7 @@ var DomRenderer = class {
1172
1184
  setPages(pages) {
1173
1185
  for (const page of this.pages) if (!pages.some((next) => next.element === page.element)) this.restore(page.element);
1174
1186
  this.pages = pages;
1187
+ this.hidden.clear();
1175
1188
  for (const page of pages) {
1176
1189
  if (this.saved.has(page.element)) continue;
1177
1190
  this.saved.set(page.element, {
@@ -1186,25 +1199,27 @@ var DomRenderer = class {
1186
1199
  applyContainerSizing(orientation = Orientation.landscape) {
1187
1200
  const { autoSize, size, width, height, minWidth, maxWidth, layout } = this.options;
1188
1201
  if (!autoSize) return;
1189
- const pagesAcross = layout === "spread" ? 2 : 1;
1202
+ const minAcross = layout === Layout.spread ? 2 : 1;
1203
+ const maxAcross = layout === Layout.single ? 1 : 2;
1190
1204
  const style = this.container.style;
1191
1205
  style.width = "100%";
1192
- style.minWidth = `${(size === "fixed" ? width : minWidth) * pagesAcross}px`;
1193
- style.maxWidth = `${(size === "fixed" ? width : maxWidth) * 2}px`;
1206
+ style.minWidth = `${(size === SizeMode.fixed ? width : minWidth) * minAcross}px`;
1207
+ style.maxWidth = `${(size === SizeMode.fixed ? width : maxWidth) * maxAcross}px`;
1194
1208
  style.aspectRatio = orientation === Orientation.portrait ? `${width} / ${height}` : `${width * 2} / ${height}`;
1195
1209
  }
1196
1210
  render(frame) {
1197
1211
  const { rect, flip } = frame;
1198
- const active = /* @__PURE__ */ new Set([
1212
+ const active = /* @__PURE__ */ new Set();
1213
+ for (const index of [
1199
1214
  frame.left,
1200
1215
  frame.right,
1201
1216
  flip?.flipping,
1202
1217
  flip?.bottom
1203
- ]);
1204
- for (const [index, page] of this.pages.entries()) {
1205
- if (!active.has(index)) applyPageStyle(page.element, { display: "none" });
1206
- page.element.classList.toggle(CLASS.hard, page.drawingDensity === PageDensity.hard);
1207
- page.element.classList.toggle(CLASS.soft, page.drawingDensity === PageDensity.soft);
1218
+ ]) if (index !== void 0 && index !== null) active.add(index);
1219
+ for (const [index, page] of this.pages.entries()) if (active.has(index)) this.hidden.delete(index);
1220
+ else if (!this.hidden.has(index)) {
1221
+ applyPageStyle(page.element, { display: "none" });
1222
+ this.hidden.add(index);
1208
1223
  }
1209
1224
  const flippingHard = flip !== null && this.pages[flip.flipping]?.drawingDensity === PageDensity.hard;
1210
1225
  if (frame.orientation !== Orientation.portrait && frame.left !== null) {
@@ -1243,6 +1258,9 @@ var DomRenderer = class {
1243
1258
  const page = this.pages[index];
1244
1259
  if (page === void 0) return null;
1245
1260
  const el = asClone ? this.cloneOf(page.element) : page.element;
1261
+ el.classList.add(CLASS.page);
1262
+ el.classList.toggle(CLASS.hard, page.drawingDensity === PageDensity.hard);
1263
+ el.classList.toggle(CLASS.soft, page.drawingDensity === PageDensity.soft);
1246
1264
  el.classList.toggle(CLASS.left, side === "left");
1247
1265
  el.classList.toggle(CLASS.right, side === "right");
1248
1266
  return el;
@@ -1251,6 +1269,7 @@ var DomRenderer = class {
1251
1269
  if (this.clone?.source === source) return this.clone.element;
1252
1270
  this.dropClone();
1253
1271
  const element = source.cloneNode(true);
1272
+ if (!(element instanceof HTMLElement)) throw new TypeError("@openpageflip/core: a page clone is not an element");
1254
1273
  element.removeAttribute("id");
1255
1274
  for (const el of element.querySelectorAll("[id]")) el.removeAttribute("id");
1256
1275
  element.setAttribute("aria-hidden", "true");
@@ -1410,6 +1429,7 @@ var DomRenderer = class {
1410
1429
  /** Put the container and every page back the way they were found. */
1411
1430
  destroy() {
1412
1431
  this.dropClone();
1432
+ this.hidden.clear();
1413
1433
  for (const page of this.pages) this.restore(page.element);
1414
1434
  this.pages = [];
1415
1435
  for (const el of Object.values(this.shadows)) el.remove();
@@ -1465,7 +1485,7 @@ function createBook(container, userOptions) {
1465
1485
  let pendingRelayout = null;
1466
1486
  const observer = new ResizeObserver(() => {
1467
1487
  if (pendingRelayout !== null) return;
1468
- pendingRelayout = requestAnimationFrame(() => {
1488
+ pendingRelayout = clock.requestFrame(() => {
1469
1489
  pendingRelayout = null;
1470
1490
  const size = {
1471
1491
  width: container.clientWidth,
@@ -1519,9 +1539,10 @@ function createBook(container, userOptions) {
1519
1539
  });
1520
1540
  },
1521
1541
  update: relayout,
1542
+ redraw: () => controller.redraw(),
1522
1543
  destroy() {
1523
1544
  observer.disconnect();
1524
- if (pendingRelayout !== null) cancelAnimationFrame(pendingRelayout);
1545
+ if (pendingRelayout !== null) clock.cancelFrame(pendingRelayout);
1525
1546
  detachInput();
1526
1547
  controller.destroy();
1527
1548
  renderer.destroy();
@@ -1530,6 +1551,6 @@ function createBook(container, userOptions) {
1530
1551
  };
1531
1552
  }
1532
1553
  //#endregion
1533
- export { ClickMode, Direction, FlipCorner, FlipDirection, FlipState, Layout, Orientation, PageDensity, SizeMode, computeFold, computeLayout, createBook };
1554
+ export { ClickMode, FlipCorner, FlipDirection, FlipState, Layout, Orientation, PageDensity, SizeMode, computeFold, computeLayout, createBook };
1534
1555
 
1535
1556
  //# sourceMappingURL=index.js.map