Skip to content

Commit 25c1375

Browse files
committed
UX: Refactor Lightbox close event handling photoprism#5258 photoprism#5260
Signed-off-by: Michael Mayer <michael@photoprism.app>
1 parent 74abddb commit 25c1375

File tree

5 files changed

+177
-58
lines changed

5 files changed

+177
-58
lines changed

frontend/src/common/captions.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77

88
const defaultOptions = {
99
captionContent: ".pswp-caption-content",
10-
type: "auto",
10+
type: "below",
1111
horizontalEdgeThreshold: 20,
1212
mobileCaptionOverlapRatio: 0.3,
13-
mobileLayoutBreakpoint: 600,
13+
mobileLayoutBreakpoint: 1024,
1414
verticallyCenterImage: false,
1515
};
1616

frontend/src/component/lightbox.vue

Lines changed: 151 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,13 @@
1515
@keydown.space.exact="onKeyDown"
1616
@keydown.left.exact="onKeyDown"
1717
@keydown.right.exact="onKeyDown"
18+
@click.capture="captureDialogClick"
19+
@pointerdown.capture="captureDialogPointerDown"
20+
@mousedown.stop.prevent
21+
@pointerdown.stop.prevent
1822
>
1923
<div class="p-lightbox__underlay"></div>
20-
<div
21-
ref="container"
22-
class="p-lightbox__container"
23-
@click.capture="onContainerClick"
24-
@pointerdown.capture="onContainerPointerDown"
25-
>
24+
<div ref="container" class="p-lightbox__container">
2625
<div
2726
ref="content"
2827
tabindex="1"
@@ -160,6 +159,7 @@ export default {
160159
trace,
161160
visible: false,
162161
busy: false,
162+
closing: false,
163163
info: localStorage.getItem("lightbox.info") === "true",
164164
menuElement: null,
165165
menuBgColor: "#252525",
@@ -286,6 +286,7 @@ export default {
286286
showDialog() {
287287
this.$view.enter(this, this.$refs?.content);
288288
this.busy = true;
289+
this.closing = false;
289290
this.visible = true;
290291
this.wasFullscreen = $fullscreen.isEnabled();
291292
this.info = localStorage.getItem("lightbox.info") === "true";
@@ -307,6 +308,7 @@ export default {
307308
}
308309
309310
this.busy = false;
311+
this.closing = false;
310312
311313
// Publish event to be consumed by other components.
312314
this.$event.publish("lightbox.closed");
@@ -328,7 +330,7 @@ export default {
328330
// Traps the focus inside the lightbox dialog.
329331
onFocusOut(ev) {
330332
if (this.debug) {
331-
this.log(`dialog.${ev.type}`, ev);
333+
this.log(`dialog.${ev.type}`, { ev });
332334
}
333335
334336
if (!this.$view.isActive(this)) {
@@ -451,7 +453,8 @@ export default {
451453
this.busy = false;
452454
})
453455
.catch(() => {
454-
this.hideDialog();
456+
this.busy = false;
457+
this.close();
455458
});
456459
});
457460
@@ -1041,7 +1044,7 @@ export default {
10411044
// action when events are triggered on an HTMLMediaElement.
10421045
this.lightbox.on("pointerUp", this.lightboxPointerListener);
10431046
this.lightbox.on("pointerDown", this.lightboxPointerListener);
1044-
this.lightbox.on("pointerMove", this.lightboxPointerListener);
1047+
// this.lightbox.on("pointerMove", this.lightboxPointerListener);
10451048
10461049
// Add PhotoSwipe lightbox controls,
10471050
// see https://photoswipe.com/adding-ui-elements/.
@@ -1150,6 +1153,7 @@ export default {
11501153
11511154
// Show first image.
11521155
this.lightbox.loadAndOpen(this.index);
1156+
this.busy = false;
11531157
11541158
return Promise.resolve();
11551159
},
@@ -1179,9 +1183,11 @@ export default {
11791183
},
11801184
onClick: (ev) =>
11811185
this.onControlClick(ev, () => {
1182-
if (lightbox && lightbox.pswp) {
1183-
lightbox.pswp.close();
1186+
if (this.debug) {
1187+
this.log("lightbox.close.click", ev);
11841188
}
1189+
1190+
this.close();
11851191
}),
11861192
});
11871193
@@ -1410,22 +1416,28 @@ export default {
14101416
onHideMenu() {
14111417
this.menuVisible = false;
14121418
},
1413-
closeLightbox() {
1414-
if (this.isBusy("close lightbox")) {
1415-
return Promise.reject();
1419+
close() {
1420+
if (this.closing) {
1421+
return new Promise((resolve) => {
1422+
this.$event.subscribeOnce("lightbox.leave", resolve);
1423+
});
14161424
}
14171425
1418-
const pswp = this.pswp();
1426+
this.closing = true;
14191427
1420-
if (pswp) {
1421-
this.busy = true;
1428+
if (this.lightbox) {
14221429
return new Promise((resolve) => {
14231430
this.$event.subscribeOnce("lightbox.leave", resolve);
1424-
this.destroyLightbox();
1431+
setTimeout(() => {
1432+
this.destroyLightbox();
1433+
}, 150);
14251434
});
14261435
}
14271436
1428-
return this.hideDialog();
1437+
return new Promise((resolve) => {
1438+
this.$event.subscribeOnce("lightbox.leave", resolve);
1439+
this.hideDialog();
1440+
});
14291441
},
14301442
onLightboxOpened() {
14311443
this.addEventListeners();
@@ -1437,20 +1449,23 @@ export default {
14371449
},
14381450
// Destroys the PhotoSwipe lightbox instance after use, see onClose().
14391451
destroyLightbox() {
1440-
if (this.lightbox) {
1441-
this.lightbox.destroy();
1442-
this.$event.publish("lightbox.destroy");
1443-
return;
1444-
}
1452+
this.$nextTick(() => {
1453+
if (this.lightbox) {
1454+
this.lightbox.destroy();
1455+
return;
1456+
}
14451457
1446-
this.hideDialog();
1458+
this.hideDialog();
1459+
});
14471460
},
14481461
onLightboxDestroyed() {
14491462
// Remove lightbox reference.
14501463
this.lightbox = null;
14511464
14521465
// Hide lightbox and sidebar.
1453-
this.hideDialog();
1466+
this.$nextTick(() => {
1467+
this.hideDialog();
1468+
});
14541469
},
14551470
// Returns the picture (model) caption as sanitized HTML, if any.
14561471
formatCaption(model) {
@@ -1499,6 +1514,7 @@ export default {
14991514
15001515
this.clearTimeouts();
15011516
this.removeEventListeners();
1517+
this.closing = true;
15021518
},
15031519
// Resets the component state after closing the lightbox.
15041520
onReset() {
@@ -1562,19 +1578,98 @@ export default {
15621578
return;
15631579
}
15641580
1581+
if (this.debug) {
1582+
this.log(`background.event.${ev?.type}`, { ev });
1583+
}
1584+
15651585
if (this.controlsVisible()) {
1566-
this.closeLightbox();
1586+
this.close();
15671587
} else {
15681588
this.showControls();
15691589
}
15701590
},
1571-
// Called when the lightbox receives a pointer move, down or up event.
1572-
onLightboxPointerEvent(ev) {
1573-
if (ev && ev.originalEvent.target.closest(".pswp__dynamic-caption")) {
1591+
// Returns the type of control if the event originates
1592+
// from a PhotoSwipe UI control, like the close button.
1593+
pswpControl(ev) {
1594+
if (!ev) {
1595+
return false;
1596+
}
1597+
1598+
let target;
1599+
1600+
if (ev.originalEvent?.target) {
1601+
target = ev.originalEvent.target;
1602+
} else if (ev.target) {
1603+
target = ev.target;
1604+
} else {
1605+
return false;
1606+
}
1607+
1608+
if (typeof target.closest === "function") {
1609+
if (target.closest(".pswp__button--close-button")) {
1610+
if (this.debug) {
1611+
this.log(`pswp.${ev?.type} on close`, { ev });
1612+
}
1613+
1614+
return "close";
1615+
}
1616+
1617+
if (target.closest(".pswp__button")) {
1618+
if (this.debug) {
1619+
this.log(`pswp.${ev?.type} on button`, { ev });
1620+
}
1621+
1622+
return "button";
1623+
}
1624+
1625+
if (target.closest(".pswp__top-bar")) {
1626+
if (this.debug) {
1627+
this.log(`pswp.${ev?.type} on top-bar`, { ev });
1628+
}
1629+
1630+
return "top-bar";
1631+
}
1632+
}
1633+
1634+
return false;
1635+
},
1636+
// Called when the lightbox receives a pointer down or up event.
1637+
// Move events are ignored for now.
1638+
onLightboxPointerEvent(ev, action) {
1639+
if (!ev || !ev.originalEvent?.target) {
1640+
return;
1641+
}
1642+
1643+
const target = ev.originalEvent.target;
1644+
1645+
if (this.debug) {
1646+
this.log(`lightbox.event.${ev.type}`, { ev, target, action });
1647+
}
1648+
1649+
// Close the lightbox when the user clicks the close button if it is visible.
1650+
const pswpControl = this.pswpControl(ev);
1651+
if (pswpControl === "close") {
1652+
if (this.controlsVisible()) {
1653+
ev.preventDefault();
1654+
this.close();
1655+
}
1656+
return;
1657+
}
1658+
1659+
if (target.closest(".pswp__dynamic-caption")) {
15741660
ev.preventDefault();
15751661
}
15761662
},
1663+
// Handle user clicks on a control. Does not reliably work for the close button.
15771664
onControlClick(ev, action) {
1665+
if (!ev) {
1666+
return;
1667+
}
1668+
1669+
if (this.debug) {
1670+
this.log(`control.event.${ev.type}`, { ev, action });
1671+
}
1672+
15781673
if (ev && ev.cancelable) {
15791674
ev.stopPropagation();
15801675
ev.preventDefault();
@@ -1593,11 +1688,16 @@ export default {
15931688
15941689
return false;
15951690
},
1596-
onContainerClick(ev) {
1691+
// Capture click events on the dialog component.
1692+
captureDialogClick(ev) {
15971693
if (!ev) {
15981694
return;
15991695
}
16001696
1697+
if (this.debug) {
1698+
this.log(`dialog.capture.${ev.type}`, { ev, target: ev.target });
1699+
}
1700+
16011701
// Reveal the controls when the user clicks or touches the top of the screen,
16021702
// where they are located when visible.
16031703
if (ev.y <= 128) {
@@ -1612,12 +1712,16 @@ export default {
16121712
ev.preventDefault();
16131713
}
16141714
},
1615-
// Called when a pointer down (click, touch) event is captured by the lightbox container.
1616-
onContainerPointerDown(ev) {
1715+
// Capture pointer down events on the dialog component.
1716+
captureDialogPointerDown(ev) {
16171717
if (!ev) {
16181718
return;
16191719
}
16201720
1721+
if (this.debug) {
1722+
this.log(`dialog.capture.${ev.type}`, { ev, target: ev.target });
1723+
}
1724+
16211725
// Handle the click and touch events on custom content.
16221726
if (
16231727
ev.target instanceof HTMLMediaElement ||
@@ -1652,12 +1756,16 @@ export default {
16521756
this.toggleVideo();
16531757
}
16541758
},
1655-
// Called when the user clicks on an image slide in the lightbox.
1759+
// Handle user clicks on an image slide in the lightbox.
16561760
onContentClick(ev) {
16571761
if (!ev) {
16581762
return;
16591763
}
16601764
1765+
if (this.debug) {
1766+
this.log(`content.event.${ev.type}`, { ev, target: ev.target, originalTarget: ev.originalEvent?.target });
1767+
}
1768+
16611769
if (this.slideshow.active) {
16621770
this.pauseSlideshow();
16631771
}
@@ -1670,12 +1778,16 @@ export default {
16701778
pswp.currSlide.toggleZoom();
16711779
}
16721780
},
1673-
// Called when the user taps on an image slide in the lightbox.
1781+
// Handle user taps on an image slide in the lightbox.
16741782
onContentTap(ev) {
16751783
if (!ev) {
16761784
return;
16771785
}
16781786
1787+
if (this.debug) {
1788+
this.log(`content.event.${ev.type}`, { ev, target: ev.target, originalTarget: ev.originalEvent?.target });
1789+
}
1790+
16791791
if (ev.target instanceof HTMLMediaElement) {
16801792
// Do nothing.
16811793
} else {
@@ -1806,7 +1918,7 @@ export default {
18061918
18071919
switch (ev.code) {
18081920
case "Escape":
1809-
this.closeLightbox();
1921+
this.close();
18101922
return true;
18111923
case "Period":
18121924
this.onShowMenu();
@@ -2224,7 +2336,7 @@ export default {
22242336
let album = null;
22252337
22262338
// Close lightbox and open edit dialog when closed.
2227-
this.closeLightbox().then(() => {
2339+
this.close().then(() => {
22282340
this.$event.publish("dialog.edit", { selection, album, index });
22292341
});
22302342
},
@@ -2530,7 +2642,7 @@ export default {
25302642
data.loading = false;
25312643
25322644
if (this.trace) {
2533-
this.log(`image.${ev.type}`, [ev, ev.target]);
2645+
this.log(`image.event.${ev.type}`, { ev, target: ev.target });
25342646
}
25352647
25362648
// Abort if image URL is empty or the current slide is undefined.
@@ -2569,7 +2681,7 @@ export default {
25692681
// Set thumbnail src to load the new image.
25702682
image.src = thumb.src;
25712683
} catch (err) {
2572-
this.log(`failed to load image size ${thumb.size}`, err);
2684+
this.log(`failed to load image size ${thumb.size}`, { err });
25732685
data.loading = false;
25742686
}
25752687
},

0 commit comments

Comments
 (0)