@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/README.md +42 -21
- package/dist/index.d.ts +10 -14
- package/dist/index.iife.js +1 -1
- package/dist/index.iife.js.map +1 -1
- package/dist/index.js +58 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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).
|
|
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)
|
|
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
|
|
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
|
|
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 ===
|
|
1193
|
-
style.maxWidth = `${(size ===
|
|
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
|
-
|
|
1206
|
-
page.element
|
|
1207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
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,
|
|
1564
|
+
export { ClickMode, FlipCorner, FlipDirection, FlipState, Layout, Orientation, PageDensity, SizeMode, computeFold, computeLayout, createBook };
|
|
1534
1565
|
|
|
1535
1566
|
//# sourceMappingURL=index.js.map
|