@openpageflip/core 0.1.0 → 0.2.1

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) {
@@ -1233,7 +1248,8 @@ var DomRenderer = class {
1233
1248
  if (flip.shadow === null) this.hideShadows();
1234
1249
  else if (flippingHard) {
1235
1250
  this.hideSoftShadows();
1236
- this.drawHardShadows(flip.shadow, rect);
1251
+ const landing = flip.direction === FlipDirection.forward ? frame.left : frame.right;
1252
+ this.drawHardShadows(flip.shadow, rect, landing !== null);
1237
1253
  } else {
1238
1254
  this.hideHardShadows();
1239
1255
  this.drawSoftShadows(flip.shadow, flip.fold.rect, rect);
@@ -1243,6 +1259,9 @@ var DomRenderer = class {
1243
1259
  const page = this.pages[index];
1244
1260
  if (page === void 0) return null;
1245
1261
  const el = asClone ? this.cloneOf(page.element) : page.element;
1262
+ el.classList.add(CLASS.page);
1263
+ el.classList.toggle(CLASS.hard, page.drawingDensity === PageDensity.hard);
1264
+ el.classList.toggle(CLASS.soft, page.drawingDensity === PageDensity.soft);
1246
1265
  el.classList.toggle(CLASS.left, side === "left");
1247
1266
  el.classList.toggle(CLASS.right, side === "right");
1248
1267
  return el;
@@ -1251,6 +1270,7 @@ var DomRenderer = class {
1251
1270
  if (this.clone?.source === source) return this.clone.element;
1252
1271
  this.dropClone();
1253
1272
  const element = source.cloneNode(true);
1273
+ if (!(element instanceof HTMLElement)) throw new TypeError("@openpageflip/core: a page clone is not an element");
1254
1274
  element.removeAttribute("id");
1255
1275
  for (const el of element.querySelectorAll("[id]")) el.removeAttribute("id");
1256
1276
  element.setAttribute("aria-hidden", "true");
@@ -1379,14 +1399,23 @@ var DomRenderer = class {
1379
1399
  ], innerTranslate);
1380
1400
  this.shadows.inner.style.cssText = `display: block; z-index: ${Z.shadow}; width: ${innerWidth}px; height: ${rect.height * 2}px; background: linear-gradient(${forward ? "to left" : "to right"}, rgba(0, 0, 0, ${shadow.opacity}) 5%, rgba(0, 0, 0, 0.05) 15%, rgba(0, 0, 0, ${shadow.opacity}) 35%, rgba(0, 0, 0, 0) 100%); transform-origin: ${innerTranslate}px 100px; transform: translate3d(${at.x - innerTranslate}px, ${at.y - 100}px, 0) rotate(${angle}rad); clip-path: polygon(${innerClip});`;
1381
1401
  }
1382
- drawHardShadows(shadow, rect) {
1402
+ /**
1403
+ * Two gradients at the spine. Until the page passes the spine, the inner one lies on the
1404
+ * landing side and darkens as the page comes down, and the outer one lies under the lifting
1405
+ * page; past the spine they trade places. `landingHasPage` is false when the landing side is
1406
+ * empty, and the gradient that would sit there stays hidden.
1407
+ */
1408
+ drawHardShadows(shadow, rect, landingHasPage) {
1383
1409
  const progress = shadow.progress > 100 ? 200 - shadow.progress : shadow.progress;
1384
1410
  const size = Math.min(rect.pageWidth, (100 - progress) * (2.5 * rect.pageWidth) / 100 + 20);
1385
1411
  const spine = rect.left + rect.width / 2;
1386
1412
  const flipped = shadow.direction === FlipDirection.forward && shadow.progress > 100 || shadow.direction === FlipDirection.back && shadow.progress <= 100;
1413
+ const pastSpine = shadow.progress > 100;
1414
+ const showInner = landingHasPage || pastSpine;
1415
+ const showOuter = landingHasPage || !pastSpine;
1387
1416
  const common = `display: block; width: ${size}px; height: ${rect.height}px; left: ${spine}px; top: ${rect.top}px; transform-origin: 0 0;`;
1388
- this.shadows.hardInner.style.cssText = `${common} z-index: ${Z.hardInnerShadow}; background: linear-gradient(to right, rgba(0, 0, 0, ${shadow.opacity * progress / 100}) 5%, rgba(0, 0, 0, 0) 100%); transform: translate3d(0, 0, 0)${flipped ? "" : " rotateY(180deg)"};`;
1389
- this.shadows.hardOuter.style.cssText = `${common} z-index: ${Z.hardShadow}; background: linear-gradient(to left, rgba(0, 0, 0, ${shadow.opacity}) 5%, rgba(0, 0, 0, 0) 100%); transform: translate3d(0, 0, 0)${flipped ? " rotateY(180deg)" : ""};`;
1417
+ this.shadows.hardInner.style.cssText = showInner ? `${common} z-index: ${Z.hardInnerShadow}; background: linear-gradient(to right, rgba(0, 0, 0, ${shadow.opacity * progress / 100}) 5%, rgba(0, 0, 0, 0) 100%); transform: translate3d(0, 0, 0)${flipped ? "" : " rotateY(180deg)"};` : "display: none";
1418
+ this.shadows.hardOuter.style.cssText = showOuter ? `${common} z-index: ${Z.hardShadow}; background: linear-gradient(to left, rgba(0, 0, 0, ${shadow.opacity}) 5%, rgba(0, 0, 0, 0) 100%); transform: translate3d(0, 0, 0)${flipped ? " rotateY(180deg)" : ""};` : "display: none";
1390
1419
  }
1391
1420
  hideSoftShadows() {
1392
1421
  this.shadows.outer.style.cssText = "display: none";
@@ -1410,6 +1439,7 @@ var DomRenderer = class {
1410
1439
  /** Put the container and every page back the way they were found. */
1411
1440
  destroy() {
1412
1441
  this.dropClone();
1442
+ this.hidden.clear();
1413
1443
  for (const page of this.pages) this.restore(page.element);
1414
1444
  this.pages = [];
1415
1445
  for (const el of Object.values(this.shadows)) el.remove();
@@ -1465,7 +1495,7 @@ function createBook(container, userOptions) {
1465
1495
  let pendingRelayout = null;
1466
1496
  const observer = new ResizeObserver(() => {
1467
1497
  if (pendingRelayout !== null) return;
1468
- pendingRelayout = requestAnimationFrame(() => {
1498
+ pendingRelayout = clock.requestFrame(() => {
1469
1499
  pendingRelayout = null;
1470
1500
  const size = {
1471
1501
  width: container.clientWidth,
@@ -1519,9 +1549,10 @@ function createBook(container, userOptions) {
1519
1549
  });
1520
1550
  },
1521
1551
  update: relayout,
1552
+ redraw: () => controller.redraw(),
1522
1553
  destroy() {
1523
1554
  observer.disconnect();
1524
- if (pendingRelayout !== null) cancelAnimationFrame(pendingRelayout);
1555
+ if (pendingRelayout !== null) clock.cancelFrame(pendingRelayout);
1525
1556
  detachInput();
1526
1557
  controller.destroy();
1527
1558
  renderer.destroy();
@@ -1530,6 +1561,6 @@ function createBook(container, userOptions) {
1530
1561
  };
1531
1562
  }
1532
1563
  //#endregion
1533
- export { ClickMode, Direction, FlipCorner, FlipDirection, FlipState, Layout, Orientation, PageDensity, SizeMode, computeFold, computeLayout, createBook };
1564
+ export { ClickMode, FlipCorner, FlipDirection, FlipState, Layout, Orientation, PageDensity, SizeMode, computeFold, computeLayout, createBook };
1534
1565
 
1535
1566
  //# sourceMappingURL=index.js.map