FEATURE: New Discourse Lightbox using Glimmer (#19798)
Introduces new lightbox as a step to migrate away from Magnific Popup. Please see https://meta.discourse.org/t/migrating-away-from-magnific-popup/251505 for more details Co-authored-by: Nat <natalie.tay@discourse.org> Co-authored-by: David Battersby <info@davidbattersby.com>
This commit is contained in:
parent
f933c9fcd9
commit
82c03127df
|
@ -27,7 +27,13 @@
|
|||
{{~d-icon "spinner" class="loading-icon"~}}
|
||||
{{else}}
|
||||
{{#if @icon}}
|
||||
{{~d-icon @icon~}}
|
||||
{{#if @ariaHidden}}
|
||||
<span aria-hidden="true">
|
||||
{{~d-icon @icon~}}
|
||||
</span>
|
||||
{{else}}
|
||||
{{~d-icon @icon~}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
<DSection
|
||||
tabIndex="-1"
|
||||
id={{this.elementId}}
|
||||
aria-hidden={{not this.isVisible}}
|
||||
class={{this.HTMLClassList}}
|
||||
@scrollTop={{false}}
|
||||
{{did-insert this.registerAppEventListeners}}
|
||||
{{will-destroy this.deregisterAppEventListners}}
|
||||
>
|
||||
{{#if this.isVisible}}
|
||||
<div
|
||||
aria-hidden="false"
|
||||
tabindex="0"
|
||||
role="document"
|
||||
class="d-lightbox__content"
|
||||
style={{this.CSSVars}}
|
||||
aria-labelledby={{this.titleElementId}}
|
||||
{{on "keyup" this.onKeyup passive=true capture=true}}
|
||||
>
|
||||
<div class="d-lightbox__focus-trap" tabindex="0"></div>
|
||||
<DLightbox::Header
|
||||
@canDownload={{this.canDownload}}
|
||||
@canFullscreen={{this.canFullscreen}}
|
||||
@canNavigate={{this.canNavigate}}
|
||||
@close={{this.close}}
|
||||
@openInNewTab={{this.openInNewTab}}
|
||||
@toggleCarousel={{this.toggleCarousel}}
|
||||
@toggleFullScreen={{this.toggleFullScreen}}
|
||||
@totalItemCount={{this.totalItemCount}}
|
||||
@counterIndex={{this.counterIndex}}
|
||||
/>
|
||||
<DLightbox::Body
|
||||
@close={{this.close}}
|
||||
@centerZoomedBackgroundPosition={{this.centerZoomedBackgroundPosition}}
|
||||
@currentItem={{this.currentItem}}
|
||||
@hasLoadingError={{this.hasLoadingError}}
|
||||
@isLoading={{this.isLoading}}
|
||||
@isZoomed={{this.isZoomed}}
|
||||
@nextButtonIcon={{this.nextButtonIcon}}
|
||||
@onTouchend={{this.onTouchend}}
|
||||
@onTouchstart={{this.onTouchstart}}
|
||||
@previousButtonIcon={{this.previousButtonIcon}}
|
||||
@reloadImage={{this.reloadImage}}
|
||||
@shouldDisplayMainImageArrows={{this.shouldDisplayMainImageArrows}}
|
||||
@showNextItem={{this.showNextItem}}
|
||||
@showPreviousItem={{this.showPreviousItem}}
|
||||
@toggleZoom={{this.toggleZoom}}
|
||||
@zoomOnMouseover={{this.zoomOnMouseover}}
|
||||
/>
|
||||
{{#if this.shouldDisplayCarousel}}
|
||||
<DLightbox::Carousel
|
||||
@currentIndex={{this.currentIndex}}
|
||||
@items={{this.items}}
|
||||
@nextButtonIcon={{this.nextButtonIcon}}
|
||||
@previousButtonIcon={{this.previousButtonIcon}}
|
||||
@showNextItem={{this.showNextItem}}
|
||||
@showPreviousItem={{this.showPreviousItem}}
|
||||
@showSelectedImage={{this.showSelectedImage}}
|
||||
@shouldDisplayCarouselArrows={{this.shouldDisplayCarouselArrows}}
|
||||
/>
|
||||
{{/if}}
|
||||
<DLightbox::Footer
|
||||
@canDownload={{this.canDownload}}
|
||||
@canRotate={{this.canRotate}}
|
||||
@canZoom={{this.canZoom}}
|
||||
@currentItem={{this.currentItem}}
|
||||
@downloadImage={{this.downloadImage}}
|
||||
@rotateImage={{this.rotateImage}}
|
||||
@shouldDisplayTitle={{this.shouldDisplayTitle}}
|
||||
@toggleExpandTitle={{this.toggleExpandTitle}}
|
||||
@toggleZoom={{this.toggleZoom}}
|
||||
@zoomButtonIcon={{this.zoomButtonIcon}}
|
||||
/>
|
||||
<DLightbox::ScreenReaderAnnouncer
|
||||
@currentItem={{this.currentItem}}
|
||||
@counterIndex={{this.counterIndex}}
|
||||
@totalItemCount={{this.totalItemCount}}
|
||||
@titleElementId={{this.titleElementId}}
|
||||
/>
|
||||
<div class="d-lightbox__focus-trap" tabindex="0"></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</DSection>
|
|
@ -0,0 +1,470 @@
|
|||
import {
|
||||
ANIMATION_DURATION,
|
||||
KEYBOARD_SHORTCUTS,
|
||||
LAYOUT_TYPES,
|
||||
LIGHTBOX_APP_EVENT_NAMES,
|
||||
LIGHTBOX_ELEMENT_ID,
|
||||
SWIPE_DIRECTIONS,
|
||||
TITLE_ELEMENT_ID,
|
||||
} from "discourse/lib/lightbox/constants";
|
||||
import {
|
||||
createDownloadLink,
|
||||
getSwipeDirection,
|
||||
openImageInNewTab,
|
||||
preloadItemImages,
|
||||
scrollParentToElementCenter,
|
||||
setCarouselScrollPosition,
|
||||
setSiteThemeColor,
|
||||
} from "discourse/lib/lightbox/helpers";
|
||||
|
||||
import Component from "@glimmer/component";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
export default class DLightbox extends Component {
|
||||
@service appEvents;
|
||||
|
||||
@tracked isVisible = false;
|
||||
@tracked isLoading = false;
|
||||
@tracked currentIndex = 0;
|
||||
@tracked currentItem = {};
|
||||
|
||||
@tracked isZoomed = false;
|
||||
@tracked isRotated = false;
|
||||
@tracked isFullScreen = false;
|
||||
@tracked rotationAmount = 0;
|
||||
|
||||
@tracked hasCarousel = false;
|
||||
@tracked hasExpandedTitle = false;
|
||||
|
||||
elementId = LIGHTBOX_ELEMENT_ID;
|
||||
titleElementId = TITLE_ELEMENT_ID;
|
||||
animationDuration = ANIMATION_DURATION;
|
||||
|
||||
get layoutType() {
|
||||
return window.innerWidth > window.innerHeight
|
||||
? LAYOUT_TYPES.HORIZONTAL
|
||||
: LAYOUT_TYPES.VERTICAL;
|
||||
}
|
||||
|
||||
get CSSVars() {
|
||||
const base = "--d-lightbox-image";
|
||||
|
||||
const variables = [
|
||||
`${base}-animation-duration: ${this.animationDuration}ms;`,
|
||||
];
|
||||
|
||||
if (!this.currentItem) {
|
||||
return htmlSafe(variables.join(""));
|
||||
}
|
||||
|
||||
const { width, height, aspectRatio, dominantColor, fullsizeURL, smallURL } =
|
||||
this.currentItem;
|
||||
|
||||
variables.push(
|
||||
`${base}-rotation: ${this.rotationAmount}deg`,
|
||||
`${base}-width: ${width}px`,
|
||||
`${base}-height: ${height}px`,
|
||||
`${base}-aspect-ratio: ${aspectRatio}`,
|
||||
`${base}-dominant-color: #${dominantColor}`,
|
||||
`${base}-full-size-url: url(${encodeURI(fullsizeURL)})`,
|
||||
`${base}-small-url: url(${encodeURI(smallURL)})`
|
||||
);
|
||||
|
||||
return htmlSafe(variables.filter(Boolean).join(";"));
|
||||
}
|
||||
|
||||
get HTMLClassList() {
|
||||
const base = "d-lightbox";
|
||||
|
||||
const classNames = [base];
|
||||
|
||||
if (!this.isVisible) {
|
||||
return classNames.join("");
|
||||
}
|
||||
|
||||
classNames.push(
|
||||
this.layoutType && `is-${this.layoutType}`,
|
||||
this.isVisible && `is-visible`,
|
||||
this.isLoading ? `is-loading` : `is-finished-loading`,
|
||||
this.isFullScreen && `is-fullscreen`,
|
||||
this.isZoomed && `is-zoomed`,
|
||||
this.isRotated && `is-rotated`,
|
||||
this.canZoom && `can-zoom`,
|
||||
this.hasExpandedTitle && `has-expanded-title`,
|
||||
this.hasCarousel && `has-carousel`,
|
||||
this.hasLoadingError && `has-loading-error`,
|
||||
this.willClose && `will-close`,
|
||||
this.isRotated &&
|
||||
this.rotationAmount &&
|
||||
`is-rotated-${this.rotationAmount}`
|
||||
);
|
||||
|
||||
return classNames.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
get shouldDisplayMainImageArrows() {
|
||||
return (
|
||||
!this.options.isMobile &&
|
||||
this.canNavigate &&
|
||||
!this.hasCarousel &&
|
||||
!this.isZoomed &&
|
||||
!this.isRotated
|
||||
);
|
||||
}
|
||||
|
||||
get shouldDisplayCarousel() {
|
||||
return this.hasCarousel && !this.isZoomed && !this.isRotated;
|
||||
}
|
||||
|
||||
get shouldDisplayCarouselArrows() {
|
||||
return (
|
||||
!this.options.isMobile &&
|
||||
this.totalItemCount >= this.options.minCarosuelArrowItemCount
|
||||
);
|
||||
}
|
||||
|
||||
get shouldDisplayTitle() {
|
||||
return !this.hasLoadingError && !this.isZoomed && !this.isRotated;
|
||||
}
|
||||
|
||||
get totalItemCount() {
|
||||
return this.items?.length || 0;
|
||||
}
|
||||
|
||||
get counterIndex() {
|
||||
return this.currentIndex ? this.currentIndex + 1 : 1;
|
||||
}
|
||||
|
||||
get canNavigate() {
|
||||
return this.items?.length > 1;
|
||||
}
|
||||
|
||||
get canZoom() {
|
||||
return !this.hasLoadingError && this.currentItem?.canZoom;
|
||||
}
|
||||
|
||||
get canRotate() {
|
||||
return !this.hasLoadingError;
|
||||
}
|
||||
|
||||
get canDownload() {
|
||||
return !this.hasLoadingError && this.options.canDownload;
|
||||
}
|
||||
|
||||
get canFullscreen() {
|
||||
return !this.hasLoadingError;
|
||||
}
|
||||
|
||||
get hasLoadingError() {
|
||||
return this.currentItem?.hasLoadingError;
|
||||
}
|
||||
|
||||
get nextButtonIcon() {
|
||||
return this.options.isRTL ? "chevron-left" : "chevron-right";
|
||||
}
|
||||
|
||||
get previousButtonIcon() {
|
||||
return this.options.isRTL ? "chevron-right" : "chevron-left";
|
||||
}
|
||||
|
||||
get zoomButtonIcon() {
|
||||
return this.isZoomed ? "search-minus" : "search-plus";
|
||||
}
|
||||
|
||||
@bind
|
||||
registerAppEventListeners() {
|
||||
this.appEvents.on(LIGHTBOX_APP_EVENT_NAMES.OPEN, this.open);
|
||||
this.appEvents.on(LIGHTBOX_APP_EVENT_NAMES.CLOSE, this.close);
|
||||
}
|
||||
|
||||
@bind
|
||||
deregisterAppEventListners() {
|
||||
this.appEvents.off(LIGHTBOX_APP_EVENT_NAMES.OPEN, this.open);
|
||||
this.appEvents.off(LIGHTBOX_APP_EVENT_NAMES.CLOSE, this.close);
|
||||
}
|
||||
|
||||
@bind
|
||||
async open({ items, startingIndex, callbacks, options }) {
|
||||
this.options = options;
|
||||
|
||||
this.items = items;
|
||||
this.currentIndex = startingIndex;
|
||||
this.callbacks = callbacks;
|
||||
|
||||
this.isLoading = true;
|
||||
this.isVisible = true;
|
||||
|
||||
await this.#setCurrentItem(this.currentIndex);
|
||||
|
||||
if (
|
||||
this.options.zoomOnOpen &&
|
||||
this.currentItem?.canZoom &&
|
||||
!this.currentItem?.isZoomed
|
||||
) {
|
||||
this.toggleZoom();
|
||||
}
|
||||
|
||||
this.callbacks.onOpen?.({
|
||||
items: this.items,
|
||||
currentItem: this.currentItem,
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
close() {
|
||||
this.willClose = true;
|
||||
|
||||
discourseLater(this.cleanup, this.animationDuration);
|
||||
|
||||
this.callbacks.onClose?.();
|
||||
}
|
||||
|
||||
async #setCurrentItem(index) {
|
||||
this.#onBeforeItemChange();
|
||||
|
||||
this.currentIndex = (index + this.totalItemCount) % this.totalItemCount;
|
||||
this.currentItem = await preloadItemImages(this.items[this.currentIndex]);
|
||||
|
||||
this.#onAfterItemChange();
|
||||
}
|
||||
|
||||
#onBeforeItemChange() {
|
||||
this.callbacks.onItemWillChange?.({
|
||||
currentItem: this.currentItem,
|
||||
});
|
||||
|
||||
this.isLoading = true;
|
||||
this.isZoomed = false;
|
||||
this.isRotated = false;
|
||||
}
|
||||
|
||||
#onAfterItemChange() {
|
||||
this.isLoading = false;
|
||||
|
||||
setSiteThemeColor(this.currentItem.dominantColor);
|
||||
|
||||
setCarouselScrollPosition({
|
||||
behavior: "smooth",
|
||||
});
|
||||
|
||||
this.callbacks.onItemDidChange?.({
|
||||
currentItem: this.currentItem,
|
||||
});
|
||||
|
||||
const nextItem = this.items[this.currentIndex + 1];
|
||||
return nextItem ? preloadItemImages(nextItem) : false;
|
||||
}
|
||||
|
||||
@bind
|
||||
centerZoomedBackgroundPosition(zoomedImageContainer) {
|
||||
return this.options.isMobile
|
||||
? scrollParentToElementCenter({
|
||||
element: zoomedImageContainer,
|
||||
isRTL: this.options.isRTL,
|
||||
})
|
||||
: false;
|
||||
}
|
||||
|
||||
zoomOnMouseover(event) {
|
||||
const zoomedImageContainer = event.target;
|
||||
|
||||
const offsetX = event.offsetX;
|
||||
const offsetY = event.offsetY;
|
||||
|
||||
const x = (offsetX / zoomedImageContainer.offsetWidth) * 100;
|
||||
const y = (offsetY / zoomedImageContainer.offsetHeight) * 100;
|
||||
|
||||
zoomedImageContainer.style.backgroundPosition = x + "% " + y + "%";
|
||||
}
|
||||
|
||||
@bind
|
||||
toggleZoom() {
|
||||
if (this.isLoading || !this.canZoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isZoomed = !this.isZoomed;
|
||||
document.querySelector(".d-lightbox__close-button")?.focus();
|
||||
}
|
||||
|
||||
@bind
|
||||
rotateImage() {
|
||||
this.rotationAmount = (this.rotationAmount + 90) % 360;
|
||||
this.isRotated = this.rotationAmount !== 0;
|
||||
}
|
||||
|
||||
@bind
|
||||
toggleFullScreen() {
|
||||
this.isFullScreen = !this.isFullScreen;
|
||||
|
||||
return this.isFullScreen
|
||||
? document.documentElement.requestFullscreen()
|
||||
: document.exitFullscreen();
|
||||
}
|
||||
|
||||
@bind
|
||||
downloadImage() {
|
||||
return createDownloadLink(this.currentItem);
|
||||
}
|
||||
|
||||
@bind
|
||||
openInNewTab() {
|
||||
return openImageInNewTab(this.currentItem);
|
||||
}
|
||||
|
||||
@bind
|
||||
reloadImage() {
|
||||
this.#setCurrentItem(this.currentIndex);
|
||||
}
|
||||
|
||||
@bind
|
||||
toggleCarousel() {
|
||||
this.hasCarousel = !this.hasCarousel;
|
||||
|
||||
requestAnimationFrame(setCarouselScrollPosition);
|
||||
}
|
||||
|
||||
@bind
|
||||
showNextItem() {
|
||||
this.#setCurrentItem(this.currentIndex + 1);
|
||||
}
|
||||
|
||||
@bind
|
||||
showPreviousItem() {
|
||||
this.#setCurrentItem(this.currentIndex - 1);
|
||||
}
|
||||
|
||||
@bind
|
||||
showSelectedImage(event) {
|
||||
const targetIndex = event.target.dataset?.lightboxItemIndex;
|
||||
return targetIndex ? this.#setCurrentItem(Number(targetIndex)) : false;
|
||||
}
|
||||
|
||||
@bind
|
||||
toggleExpandTitle() {
|
||||
this.hasExpandedTitle = !this.hasExpandedTitle;
|
||||
}
|
||||
|
||||
@bind
|
||||
onKeyup({ key }) {
|
||||
if (KEYBOARD_SHORTCUTS.PREVIOUS.includes(key)) {
|
||||
return this.showPreviousItem();
|
||||
}
|
||||
|
||||
if (KEYBOARD_SHORTCUTS.NEXT.includes(key)) {
|
||||
return this.showNextItem();
|
||||
}
|
||||
|
||||
if (key === KEYBOARD_SHORTCUTS.CLOSE) {
|
||||
return this.close();
|
||||
}
|
||||
|
||||
if (key === KEYBOARD_SHORTCUTS.ZOOM) {
|
||||
return this.toggleZoom();
|
||||
}
|
||||
|
||||
if (key === KEYBOARD_SHORTCUTS.FULLSCREEN) {
|
||||
return this.toggleFullScreen();
|
||||
}
|
||||
|
||||
if (key === KEYBOARD_SHORTCUTS.ROTATE) {
|
||||
return this.rotateImage();
|
||||
}
|
||||
|
||||
if (key === KEYBOARD_SHORTCUTS.DOWNLOAD) {
|
||||
return this.downloadImage();
|
||||
}
|
||||
|
||||
if (key === KEYBOARD_SHORTCUTS.CAROUSEL) {
|
||||
return this.toggleCarousel();
|
||||
}
|
||||
|
||||
if (key === KEYBOARD_SHORTCUTS.TITLE) {
|
||||
return this.toggleExpandTitle();
|
||||
}
|
||||
|
||||
if (key === KEYBOARD_SHORTCUTS.NEWTAB) {
|
||||
return this.openInNewTab();
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
onTouchstart(event = Event) {
|
||||
if (this.isZoomed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.touchstartX = event.changedTouches[0].screenX;
|
||||
this.touchstartY = event.changedTouches[0].screenY;
|
||||
}
|
||||
|
||||
@bind
|
||||
async onTouchend(event) {
|
||||
if (this.isZoomed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
|
||||
const touchendY = event.changedTouches[0].screenY;
|
||||
const touchendX = event.changedTouches[0].screenX;
|
||||
|
||||
const swipeDirection = await getSwipeDirection({
|
||||
touchstartX: this.touchstartX,
|
||||
touchstartY: this.touchstartY,
|
||||
touchendX,
|
||||
touchendY,
|
||||
});
|
||||
|
||||
switch (swipeDirection) {
|
||||
case SWIPE_DIRECTIONS.LEFT:
|
||||
this.options.isRTL ? this.showNextItem() : this.showPreviousItem();
|
||||
break;
|
||||
case SWIPE_DIRECTIONS.RIGHT:
|
||||
this.options.isRTL ? this.showPreviousItem() : this.showNextItem();
|
||||
break;
|
||||
case SWIPE_DIRECTIONS.UP:
|
||||
this.close();
|
||||
break;
|
||||
case SWIPE_DIRECTIONS.DOWN:
|
||||
this.toggleCarousel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
cleanup() {
|
||||
if (this.isVisible) {
|
||||
this.hasCarousel = !!document.querySelector(".d-lightbox.has-carousel");
|
||||
this.hasExpandedTitle = false;
|
||||
this.isLoading = false;
|
||||
this.items = [];
|
||||
this.currentIndex = 0;
|
||||
this.isZoomed = false;
|
||||
this.isRotated = false;
|
||||
this.rotationAmount = 0;
|
||||
|
||||
if (this.isFullScreen) {
|
||||
this.toggleFullScreen();
|
||||
this.isFullScreen = false;
|
||||
}
|
||||
|
||||
this.isVisible = false;
|
||||
this.willClose = false;
|
||||
|
||||
this.callbacks.onCleanup?.();
|
||||
|
||||
this.callbacks = {};
|
||||
this.options = {};
|
||||
}
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
this.cleanup();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<div
|
||||
aria-hidden="true"
|
||||
class="d-lightbox__backdrop"
|
||||
tabindex="-1"
|
||||
{{on "click" @close passive=true capture=true}}
|
||||
></div>
|
|
@ -0,0 +1,59 @@
|
|||
<div
|
||||
class="d-lightbox__body"
|
||||
tabindex="-1"
|
||||
{{on "touchstart" @onTouchstart passive=true capture=true}}
|
||||
{{on "touchend" @onTouchend passive=true capture=true}}
|
||||
{{on "click" @toggleZoom passive=true}}
|
||||
>
|
||||
{{#if @shouldDisplayMainImageArrows}}
|
||||
<DButton
|
||||
@class="d-lightbox__previous-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.previous"
|
||||
@icon={{@previousButtonIcon}}
|
||||
@ariaHidden="true"
|
||||
{{on "click" @showPreviousItem passive=true capture=true}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @isLoading}}
|
||||
<span class="d-lightbox__loading-spinner">
|
||||
{{loading-spinner size="large"}}
|
||||
</span>
|
||||
{{else if @hasLoadingError}}
|
||||
<span class="d-lightbox__error-message">
|
||||
<DButton
|
||||
@class="d-lightbox__retry-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.redo"
|
||||
@icon="redo"
|
||||
{{on "click" @reloadImage passive=true capture=true}}
|
||||
/>
|
||||
<span>{{i18n "experimental_lightbox.image_load_error"}}</span>
|
||||
</span>
|
||||
{{else if @isZoomed}}
|
||||
<div
|
||||
class="d-lightbox__zoomed-image-container"
|
||||
tabindex="-1"
|
||||
{{did-insert @centerZoomedBackgroundPosition}}
|
||||
{{on "mousemove" @zoomOnMouseover passive=true capture=true}}
|
||||
></div>
|
||||
{{else}}
|
||||
<DLightbox::Backdrop @close={{@close}} />
|
||||
<img
|
||||
aria-hidden="true"
|
||||
draggable="false"
|
||||
fetchPriority="high"
|
||||
decoding="async"
|
||||
tabindex="-1"
|
||||
class="d-lightbox__main-image"
|
||||
src={{@currentItem.fullsizeURL}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @shouldDisplayMainImageArrows}}
|
||||
<DButton
|
||||
@class="d-lightbox__next-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.next"
|
||||
@icon={{@nextButtonIcon}}
|
||||
@ariaHidden="true"
|
||||
{{on "click" @showNextItem passive=true capture=true}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -0,0 +1,49 @@
|
|||
<div class="d-lightbox__carousel">
|
||||
{{#if @shouldDisplayCarouselArrows}}
|
||||
<DButton
|
||||
@class="d-lightbox__carousel-previous-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.previous"
|
||||
@icon={{@previousButtonIcon}}
|
||||
@ariaHidden="true"
|
||||
{{on "click" @showPreviousItem passive=true capture=true}}
|
||||
/>
|
||||
{{/if}}
|
||||
<div
|
||||
class="d-lightbox__carousel-items"
|
||||
tabindex="-1"
|
||||
role="list"
|
||||
aria-hidden="true"
|
||||
{{on "click" @showSelectedImage passive=true capture=true}}
|
||||
{{on "focus" @showSelectedImage passive=true capture=true}}
|
||||
>
|
||||
{{#each @items as |item|}}
|
||||
<img
|
||||
data-lightbox-carousel-item={{if
|
||||
(eq item.index @currentIndex)
|
||||
"current"
|
||||
""
|
||||
}}
|
||||
fetchPriority="low"
|
||||
decoding="async"
|
||||
loading="lazy"
|
||||
tabindex="-1"
|
||||
src={{item.smallURL}}
|
||||
data-lightbox-item-index={{item.index}}
|
||||
style={{item.cssVars}}
|
||||
class={{concat
|
||||
"d-lightbox__carousel-item"
|
||||
(if (eq item.index @currentIndex) " is-current")
|
||||
}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if @shouldDisplayCarouselArrows}}
|
||||
<DButton
|
||||
@class="d-lightbox__carousel-next-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.next"
|
||||
@icon={{@nextButtonIcon}}
|
||||
@ariaHidden="true"
|
||||
{{on "click" @showNextItem passive=true capture=true}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -0,0 +1,48 @@
|
|||
<div class="d-lightbox__footer">
|
||||
{{#if @shouldDisplayTitle}}
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="d-lightbox__main-title"
|
||||
tabindex="0"
|
||||
{{on "click" @toggleExpandTitle passive=true capture=true}}
|
||||
>
|
||||
<span class="d-lightbox__item-title">
|
||||
{{~@currentItem.title~}}
|
||||
</span>
|
||||
<span class="d-lightbox__item-file-details">
|
||||
{{~@currentItem.fileDetails~}}
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="d-lightbox__footer-buttons">
|
||||
{{#if @canZoom}}
|
||||
<DButton
|
||||
@class="d-lightbox__zoom-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.zoom"
|
||||
@icon={{@zoomButtonIcon}}
|
||||
@ariaHidden="true"
|
||||
{{on "click" @toggleZoom passive=true capture=true}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @canRotate}}
|
||||
<DButton
|
||||
@class="d-lightbox__rotate-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.rotate"
|
||||
@icon="redo"
|
||||
@ariaHidden="true"
|
||||
{{on "click" @rotateImage passive=true capture=true}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @canDownload}}
|
||||
<DButton
|
||||
@class="d-lightbox__download-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.download"
|
||||
@icon="download"
|
||||
@ariaHidden="true"
|
||||
{{on "click" @downloadImage passive=true capture=true}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,54 @@
|
|||
<div class="d-lightbox__header">
|
||||
{{#if @canNavigate}}
|
||||
<div aria-hidden="true" class="d-lightbox__multi-item-controls">
|
||||
<DButton
|
||||
@class="d-lightbox__carousel-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.carousel"
|
||||
@icon="images"
|
||||
@ariaHidden="true"
|
||||
{{on "click" @toggleCarousel passive=true capture=true}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="d-lightbox__counters">
|
||||
<span class="d-lightbox__counter-current">
|
||||
{{~@counterIndex~}}
|
||||
</span>
|
||||
<span class="d-lightbox__counter-separator">
|
||||
<span>/</span>
|
||||
</span>
|
||||
<span class="d-lightbox__counter-total">
|
||||
{{~@totalItemCount~}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="d-lightbox__header-buttons">
|
||||
{{#if @canDownload}}
|
||||
<DButton
|
||||
@class="d-lightbox__new-tab-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.newtab"
|
||||
@icon="external-link-alt"
|
||||
@ariaHidden="true"
|
||||
{{on "click" @openInNewTab passive=true capture=true}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{/if}}
|
||||
{{#if @canFullscreen}}
|
||||
<DButton
|
||||
@class="d-lightbox__full-screen-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.fullscreen"
|
||||
@icon="discourse-expand"
|
||||
@ariaHidden="true"
|
||||
{{on "click" @toggleFullScreen passive=true capture=true}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{{/if}}
|
||||
<DButton
|
||||
@class="d-lightbox__close-button btn-flat"
|
||||
@title="experimental_lightbox.buttons.close"
|
||||
@icon="times"
|
||||
@ariaHidden="true"
|
||||
{{on "click" @close passive=true capture=true}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,16 @@
|
|||
<div class="d-lightbox__screen-reader-announcer" tabindex="-1">
|
||||
<h2
|
||||
aria-live="polite"
|
||||
aria-level="2"
|
||||
aria-atomic="true"
|
||||
class="d-lightbox__screen-reader-title"
|
||||
id={{@titleElementId}}
|
||||
>
|
||||
{{i18n
|
||||
"experimental_lightbox.screen_reader_image_title"
|
||||
current=@counterIndex
|
||||
total=@totalItemCount
|
||||
title=@currentItem.title
|
||||
}}
|
||||
</h2>
|
||||
</div>
|
|
@ -25,14 +25,14 @@
|
|||
@icon="far-trash-alt"
|
||||
@type="button"
|
||||
/>
|
||||
|
||||
<DButton
|
||||
@icon="discourse-expand"
|
||||
@title="expand"
|
||||
@type="button"
|
||||
@class="image-uploader-lightbox-btn no-text"
|
||||
@action={{action "toggleLightbox"}}
|
||||
@disabled={{this.loadingLightbox}}
|
||||
@action={{unless this.experimentalLightboxEnabled this.toggleLightbox}}
|
||||
data-lightbox-trigger={{if this.experimentalLightboxEnabled "true"}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { or } from "@ember/object/computed";
|
||||
import UppyUploadMixin from "discourse/mixins/uppy-upload";
|
||||
import discourseComputed, { on } from "discourse-common/utils/decorators";
|
||||
import { getURLWithCDN } from "discourse-common/lib/get-url";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import lightbox from "discourse/lib/lightbox";
|
||||
import {
|
||||
cleanupLightboxes,
|
||||
default as lightbox,
|
||||
setupLightboxes,
|
||||
} from "discourse/lib/lightbox";
|
||||
import { next } from "@ember/runloop";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { authorizesOneOrMoreExtensions } from "discourse/lib/uploads";
|
||||
|
@ -14,6 +19,11 @@ export default Component.extend(UppyUploadMixin, {
|
|||
classNames: ["image-uploader"],
|
||||
disabled: or("notAllowed", "uploading", "processing"),
|
||||
|
||||
@discourseComputed("siteSettings.enable_experimental_lightbox")
|
||||
experimentalLightboxEnabled(experimentalLightboxEnabled) {
|
||||
return experimentalLightboxEnabled;
|
||||
},
|
||||
|
||||
@discourseComputed("disabled", "notAllowed")
|
||||
disabledReason(disabled, notAllowed) {
|
||||
if (disabled && notAllowed) {
|
||||
|
@ -97,21 +107,33 @@ export default Component.extend(UppyUploadMixin, {
|
|||
|
||||
@on("didRender")
|
||||
_applyLightbox() {
|
||||
next(() => lightbox(this.element, this.siteSettings));
|
||||
if (this.experimentalLightboxEnabled) {
|
||||
setupLightboxes({
|
||||
container: this.element,
|
||||
selector: ".lightbox",
|
||||
});
|
||||
} else {
|
||||
next(() => lightbox(this.element, this.siteSettings));
|
||||
}
|
||||
},
|
||||
|
||||
@on("willDestroyElement")
|
||||
_closeOnRemoval() {
|
||||
if ($.magnificPopup?.instance) {
|
||||
$.magnificPopup.instance.close();
|
||||
if (this.experimentalLightboxEnabled) {
|
||||
cleanupLightboxes();
|
||||
} else {
|
||||
if ($.magnificPopup?.instance) {
|
||||
$.magnificPopup.instance.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleLightbox() {
|
||||
$(this.element.querySelector("a.lightbox"))?.magnificPopup("open");
|
||||
},
|
||||
@action
|
||||
toggleLightbox() {
|
||||
$(this.element.querySelector("a.lightbox"))?.magnificPopup("open");
|
||||
},
|
||||
|
||||
actions: {
|
||||
trash() {
|
||||
// uppy needs to be reset to allow for more uploads
|
||||
this._reset();
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
export default {
|
||||
async initialize(owner) {
|
||||
const siteSettings = owner.lookup("service:site-settings");
|
||||
|
||||
if (siteSettings.enable_experimental_lightbox) {
|
||||
const viewportWidth = window.innerWidth;
|
||||
const bodyRects = document.body.getBoundingClientRect();
|
||||
let scrollbarWidth = viewportWidth - bodyRects.width;
|
||||
scrollbarWidth = scrollbarWidth = Math.round(scrollbarWidth * 100) / 100;
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
"--document-scrollbar-width",
|
||||
`${scrollbarWidth}px`
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -10,6 +10,7 @@ import { nativeLazyLoading } from "discourse/lib/lazy-load-images";
|
|||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { create } from "virtual-dom";
|
||||
import FullscreenTableModal from "discourse/components/modal/fullscreen-table";
|
||||
import { SELECTORS } from "discourse/lib/lightbox/constants";
|
||||
|
||||
export default {
|
||||
initialize(owner) {
|
||||
|
@ -18,6 +19,8 @@ export default {
|
|||
const session = owner.lookup("service:session");
|
||||
const site = owner.lookup("service:site");
|
||||
const modal = owner.lookup("service:modal");
|
||||
// will eventually just be called lightbox
|
||||
const lightboxService = owner.lookup("service:lightbox");
|
||||
api.decorateCookedElement(
|
||||
(elem) => {
|
||||
return highlightSyntax(elem, siteSettings, session);
|
||||
|
@ -27,12 +30,32 @@ export default {
|
|||
}
|
||||
);
|
||||
|
||||
api.decorateCookedElement(
|
||||
(elem) => {
|
||||
return lightbox(elem, siteSettings);
|
||||
},
|
||||
{ id: "discourse-lightbox" }
|
||||
);
|
||||
if (siteSettings.enable_experimental_lightbox) {
|
||||
api.decorateCookedElement(
|
||||
(element, helper) => {
|
||||
return helper &&
|
||||
element.querySelector(SELECTORS.DEFAULT_ITEM_SELECTOR)
|
||||
? lightboxService.setupLightboxes({
|
||||
container: element,
|
||||
selector: SELECTORS.DEFAULT_ITEM_SELECTOR,
|
||||
})
|
||||
: null;
|
||||
},
|
||||
{
|
||||
id: "experimental-discourse-lightbox",
|
||||
onlyStream: true,
|
||||
}
|
||||
);
|
||||
|
||||
api.cleanupStream(lightboxService.cleanupLightboxes);
|
||||
} else {
|
||||
api.decorateCookedElement(
|
||||
(elem) => {
|
||||
return lightbox(elem, siteSettings);
|
||||
},
|
||||
{ id: "discourse-lightbox" }
|
||||
);
|
||||
}
|
||||
|
||||
api.decorateCookedElement(
|
||||
(elem) => {
|
||||
|
|
|
@ -2,22 +2,50 @@ import {
|
|||
escapeExpression,
|
||||
postRNWebviewMessage,
|
||||
} from "discourse/lib/utilities";
|
||||
|
||||
import I18n from "I18n";
|
||||
import User from "discourse/models/user";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
import { helperContext } from "discourse-common/lib/helpers";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import { renderIcon } from "discourse-common/lib/icon-library";
|
||||
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
||||
import { helperContext } from "discourse-common/lib/helpers";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import { SELECTORS } from "discourse/lib/lightbox/constants";
|
||||
|
||||
export async function setupLightboxes({ container, selector }) {
|
||||
const lightboxService = getOwner(this).lookup("service:lightbox");
|
||||
lightboxService.setupLightboxes({ container, selector });
|
||||
}
|
||||
|
||||
export function cleanupLightboxes() {
|
||||
const lightboxService = getOwner(this).lookup("service:lightbox");
|
||||
return lightboxService.cleanupLightboxes();
|
||||
}
|
||||
|
||||
export default function lightbox(elem, siteSettings) {
|
||||
if (siteSettings.enable_experimental_lightbox) {
|
||||
deprecated(
|
||||
"Accessing the default `lightbox` export is deprecated. Import setupLightboxes and cleanupLightboxes from `discourse/lib/lightbox` instead.",
|
||||
{
|
||||
since: "3.0.0.beta16",
|
||||
dropFrom: "3.2.0",
|
||||
id: "discourse.lightbox.default-export",
|
||||
}
|
||||
);
|
||||
|
||||
return setupLightboxes({
|
||||
container: elem,
|
||||
selector: SELECTORS.DEFAULT_ITEM_SELECTOR,
|
||||
});
|
||||
}
|
||||
|
||||
export default function (elem, siteSettings) {
|
||||
if (!elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lightboxes = elem.querySelectorAll(
|
||||
"*:not(.spoiler):not(.spoiled) a.lightbox"
|
||||
);
|
||||
const lightboxes = elem.querySelectorAll(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
if (!lightboxes.length) {
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import { isTesting } from "discourse-common/config/environment";
|
||||
|
||||
export const ANIMATION_DURATION =
|
||||
isTesting() || window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
? 0
|
||||
: 150;
|
||||
|
||||
export const MIN_CAROUSEL_ARROW_ITEM_COUNT = 5;
|
||||
|
||||
export const SWIPE_THRESHOLD = 50;
|
||||
|
||||
export const SWIPE_DIRECTIONS = {
|
||||
DOWN: "down",
|
||||
LEFT: "left",
|
||||
RIGHT: "right",
|
||||
UP: "up",
|
||||
};
|
||||
|
||||
export const DOCUMENT_ELEMENT_LIGHTBOX_OPEN_CLASS = "has-lightbox";
|
||||
export const LIGHTBOX_ELEMENT_ID = "discourse-lightbox";
|
||||
export const TITLE_ELEMENT_ID = "d-lightbox-image-title";
|
||||
|
||||
export const SELECTORS = {
|
||||
ACTIVE_CAROUSEL_ITEM: "[data-lightbox-carousel-item='current']",
|
||||
DEFAULT_ITEM_SELECTOR: "*:not(.spoiler):not(.spoiled) a.lightbox",
|
||||
FILE_DETAILS_CONTAINER: ".informations",
|
||||
LIGHTBOX_CONTAINER: ".d-lightbox",
|
||||
LIGHTBOX_CONTENT: ".d-lightbox__content",
|
||||
LIGHTBOX_BODY: ".d-lightbox__body",
|
||||
FOCUS_TRAP: ".d-lightbox__focus-trap",
|
||||
MAIN_IMAGE: ".d-lightbox__main-image",
|
||||
MULTI_BUTTONS: ".d-lightbox__multi-item-controls",
|
||||
CAROUSEL_BUTTON: ".d-lightbox__carousel-button",
|
||||
PREV_BUTTON: ".d-lightbox__previous-button",
|
||||
NEXT_BUTTON: ".d-lightbox__next-button",
|
||||
CLOSE_BUTTON: ".d-lightbox__close-button",
|
||||
FULL_SCREEN_BUTTON: ".d-lightbox__full-screen-button",
|
||||
TAB_BUTTON: ".d-lightbox__new-tab-button",
|
||||
ROTATE_BUTTON: ".d-lightbox__rotate-button",
|
||||
ZOOM_BUTTON: ".d-lightbox__zoom-button",
|
||||
DOWNLOAD_BUTTON: ".d-lightbox__download-button",
|
||||
COUNTERS: ".d-lightbox__counters",
|
||||
COUNTER_CURRENT: ".d-lightbox__counter-current",
|
||||
COUNTER_TOTAL: ".d-lightbox__counter-total",
|
||||
IMAGE_TITLE: ".d-lightbox__image-title",
|
||||
ACTIVE_ITEM_TITLE: ".d-lightbox__item-title",
|
||||
ACTIVE_ITEM_FILE_DETAILS: ".d-lightbox__item-file-details",
|
||||
CAROUSEL: ".d-lightbox__carousel",
|
||||
CAROUSEL_ITEM: ".d-lightbox__carousel-item",
|
||||
CAROUSEL_PREV_BUTTON: ".d-lightbox__carousel-previous-button",
|
||||
CAROUSEL_NEXT_BUTTON: ".d-lightbox__carousel-next-button",
|
||||
};
|
||||
|
||||
export const LIGHTBOX_APP_EVENT_NAMES = {
|
||||
// this cannot use dom:clean else #cleanupLightboxes will be called after #setupLighboxes
|
||||
CLEAN: "lightbox:clean",
|
||||
CLOSE: "lightbox:close",
|
||||
CLOSED: "lightbox:closed",
|
||||
ITEM_DID_CHANGE: "lightbox:item-did-change",
|
||||
ITEM_WILL_CHANGE: "lightbox:item-will-change",
|
||||
OPEN: "lightbox:open",
|
||||
OPENED: "lightbox:opened",
|
||||
};
|
||||
|
||||
export const LAYOUT_TYPES = {
|
||||
HORIZONTAL: "horizontal",
|
||||
VERTICAL: "vertical",
|
||||
};
|
||||
|
||||
export const KEYBOARD_SHORTCUTS = {
|
||||
CAROUSEL: "a",
|
||||
CLOSE: "Escape",
|
||||
DOWNLOAD: "d",
|
||||
FULLSCREEN: "m",
|
||||
NEWTAB: "n",
|
||||
NEXT: ["ArrowRight", "ArrowDown"],
|
||||
PREVIOUS: ["ArrowLeft", "ArrowUp"],
|
||||
ROTATE: "r",
|
||||
TITLE: "t",
|
||||
ZOOM: "z",
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
export {
|
||||
setSiteThemeColor,
|
||||
getSiteThemeColor,
|
||||
} from "./helpers/site-theme-color";
|
||||
|
||||
export { getSwipeDirection } from "./helpers/get-swipe-direction";
|
||||
|
||||
export { preloadItemImages } from "./helpers/preload-item-images";
|
||||
|
||||
export { createDownloadLink } from "./helpers/create-download-link";
|
||||
|
||||
export { scrollParentToElementCenter } from "./helpers/scroll-parent-to-element-center";
|
||||
|
||||
export { openImageInNewTab } from "./helpers/open-image-in-new-tab";
|
||||
|
||||
export { setCarouselScrollPosition } from "./helpers/set-carousel-scroll-position";
|
||||
|
||||
export { findNearestSharedParent } from "./helpers/find-nearest-shared-parent";
|
|
@ -0,0 +1,11 @@
|
|||
export async function createDownloadLink(lightboxItem) {
|
||||
try {
|
||||
const link = document.createElement("a");
|
||||
link.href = lightboxItem.downloadURL;
|
||||
link.download = lightboxItem.title;
|
||||
link.click();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
export function findNearestSharedParent(items) {
|
||||
const ancestors = [];
|
||||
for (const item of items) {
|
||||
let ancestor = item;
|
||||
while (ancestor) {
|
||||
ancestors.push(ancestor);
|
||||
ancestor = ancestor.parentElement;
|
||||
}
|
||||
}
|
||||
return ancestors.filter(
|
||||
(ancestor) =>
|
||||
ancestors.indexOf(ancestor) !== ancestors.lastIndexOf(ancestor)
|
||||
)[0];
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { SWIPE_DIRECTIONS, SWIPE_THRESHOLD } from "../constants";
|
||||
|
||||
export function getSwipeDirection({
|
||||
touchstartX,
|
||||
touchstartY,
|
||||
touchendX,
|
||||
touchendY,
|
||||
}) {
|
||||
const diffX = touchstartX - touchendX;
|
||||
const absDiffX = Math.abs(diffX);
|
||||
|
||||
const diffY = touchstartY - touchendY;
|
||||
const absDiffY = Math.abs(diffY);
|
||||
|
||||
if (absDiffX > SWIPE_THRESHOLD) {
|
||||
return Math.sign(diffX) > 0
|
||||
? SWIPE_DIRECTIONS.RIGHT
|
||||
: SWIPE_DIRECTIONS.LEFT;
|
||||
}
|
||||
|
||||
if (absDiffY > SWIPE_THRESHOLD) {
|
||||
return Math.sign(diffY) > 0 ? SWIPE_DIRECTIONS.UP : SWIPE_DIRECTIONS.DOWN;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export async function openImageInNewTab(lightboxItem) {
|
||||
try {
|
||||
window.open(lightboxItem.fullsizeURL, "_blank");
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
export async function preloadItemImages(lightboxItem) {
|
||||
if (!lightboxItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lightboxItem.isLoaded && !lightboxItem.hasLoadingError) {
|
||||
return lightboxItem;
|
||||
}
|
||||
|
||||
const fullsizeImage = new Image();
|
||||
const smallImage = new Image();
|
||||
|
||||
const fullsizeImagePromise = new Promise((resolve, reject) => {
|
||||
fullsizeImage.onload = resolve;
|
||||
fullsizeImage.onerror = reject;
|
||||
fullsizeImage.src = lightboxItem.fullsizeURL;
|
||||
});
|
||||
|
||||
const smallImagePromise = new Promise((resolve, reject) => {
|
||||
smallImage.onload = resolve;
|
||||
smallImage.onerror = reject;
|
||||
smallImage.src = lightboxItem.smallURL;
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all([fullsizeImagePromise, smallImagePromise]);
|
||||
|
||||
lightboxItem = {
|
||||
...lightboxItem,
|
||||
isLoaded: true,
|
||||
hasLoadingError: false,
|
||||
width: fullsizeImage.naturalWidth,
|
||||
height: fullsizeImage.naturalHeight,
|
||||
aspectRatio:
|
||||
lightboxItem.aspectRatio ||
|
||||
`${smallImage.naturalWidth} / ${smallImage.naturalHeight}`,
|
||||
canZoom:
|
||||
fullsizeImage.naturalWidth > window.innerWidth ||
|
||||
fullsizeImage.naturalHeight > window.innerHeight,
|
||||
};
|
||||
} catch (error) {
|
||||
lightboxItem.hasLoadingError = true;
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Failed to load lightbox image ${lightboxItem.index}: ${lightboxItem.fullsizeURL}`
|
||||
);
|
||||
}
|
||||
|
||||
return lightboxItem;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
export async function scrollParentToElementCenter({ element, isRTL }) {
|
||||
const {
|
||||
offsetWidth: width,
|
||||
offsetHeight: height,
|
||||
parentElement: parent,
|
||||
} = element;
|
||||
|
||||
// if isRTL, make it relative to the right side of the viewport
|
||||
const modifier = isRTL ? -1 : 1;
|
||||
|
||||
const x = ((width - parent.offsetWidth) / 2) * modifier;
|
||||
const y = (height - parent.offsetHeight) / 2;
|
||||
|
||||
parent.scrollLeft = parseInt(x, 10);
|
||||
parent.scrollTop = parseInt(y, 10);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { SELECTORS } from "../constants";
|
||||
|
||||
export async function setCarouselScrollPosition({ behavior = "instant" } = {}) {
|
||||
const carouselItem = document.querySelector(SELECTORS.ACTIVE_CAROUSEL_ITEM);
|
||||
|
||||
if (!carouselItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const left =
|
||||
carouselItem.offsetLeft -
|
||||
carouselItem.offsetWidth -
|
||||
carouselItem.offsetWidth / 2;
|
||||
|
||||
const top =
|
||||
carouselItem.offsetTop -
|
||||
carouselItem.offsetHeight -
|
||||
carouselItem.offsetHeight / 2;
|
||||
|
||||
carouselItem.parentElement.scrollTo({
|
||||
behavior,
|
||||
left,
|
||||
top,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { postRNWebviewMessage } from "discourse/lib/utilities";
|
||||
|
||||
export async function getSiteThemeColor() {
|
||||
const siteThemeColor = document.querySelector('meta[name="theme-color"]');
|
||||
return siteThemeColor?.content;
|
||||
}
|
||||
|
||||
export async function setSiteThemeColor(color = "000000") {
|
||||
const _color = `#${color}`;
|
||||
|
||||
const siteThemeColor = document.querySelector('meta[name="theme-color"]');
|
||||
|
||||
if (siteThemeColor) {
|
||||
siteThemeColor.content = _color;
|
||||
}
|
||||
|
||||
postRNWebviewMessage?.("headerBg", _color);
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
import { SELECTORS } from "./constants";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
|
||||
export async function processHTML({ container, selector }) {
|
||||
selector ??= SELECTORS.DEFAULT_ITEM_SELECTOR;
|
||||
|
||||
const items = [...container.querySelectorAll(selector)];
|
||||
|
||||
let _startingIndex = items.findIndex(
|
||||
(item) => item === document.activeElement
|
||||
);
|
||||
|
||||
if (_startingIndex === -1) {
|
||||
_startingIndex = 0;
|
||||
}
|
||||
|
||||
const backgroundImageRegex = /url\((['"])?(.*?)\1\)/gi;
|
||||
|
||||
const _processedItems = items.map((item, index) => {
|
||||
try {
|
||||
const innerImage = item.querySelector("img") || {};
|
||||
|
||||
const _backgroundImage =
|
||||
item.style?.backgroundImage ||
|
||||
item.parentElement?.style?.backgroundImage ||
|
||||
null;
|
||||
|
||||
const _fullsizeURL = item.href || item.src || innerImage.src || null;
|
||||
|
||||
const _smallURL =
|
||||
innerImage.currentSrc ||
|
||||
item.src ||
|
||||
innerImage.src ||
|
||||
_backgroundImage?.replace(backgroundImageRegex, "$2") ||
|
||||
null;
|
||||
|
||||
const _downloadURL =
|
||||
item.dataset?.downloadHref ||
|
||||
item.href ||
|
||||
item.src ||
|
||||
innerImage.src ||
|
||||
null;
|
||||
|
||||
const _title =
|
||||
item.title || item.alt || innerImage.title || innerImage.alt || null;
|
||||
|
||||
const _aspectRatio =
|
||||
item.dataset?.aspectRatio ||
|
||||
innerImage.dataset?.aspectRatio ||
|
||||
item.style?.aspectRatio ||
|
||||
innerImage.style?.aspectRatio ||
|
||||
null;
|
||||
|
||||
const _fileDetails =
|
||||
item
|
||||
.querySelector(SELECTORS.FILE_DETAILS_CONTAINER)
|
||||
?.innerText.trim() || null;
|
||||
|
||||
const _dominantColor = innerImage.dataset?.dominantColor || null;
|
||||
|
||||
const _cssVars = [
|
||||
_dominantColor && `--dominant-color: #${_dominantColor};`,
|
||||
_aspectRatio && `--aspect-ratio: ${_aspectRatio};`,
|
||||
_smallURL && `--small-url: url(${encodeURI(_smallURL)});`,
|
||||
].join("");
|
||||
|
||||
return {
|
||||
fullsizeURL: encodeURI(_fullsizeURL),
|
||||
smallURL: encodeURI(_smallURL),
|
||||
downloadURL: encodeURI(_downloadURL),
|
||||
title: escapeExpression(_title),
|
||||
fileDetails: _fileDetails,
|
||||
dominantColor: _dominantColor,
|
||||
aspectRatio: _aspectRatio,
|
||||
index,
|
||||
cssVars: htmlSafe(_cssVars),
|
||||
};
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Error processing lightbox item ${index}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
items: _processedItems,
|
||||
startingIndex: _startingIndex,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
import {
|
||||
DOCUMENT_ELEMENT_LIGHTBOX_OPEN_CLASS,
|
||||
LIGHTBOX_APP_EVENT_NAMES,
|
||||
MIN_CAROUSEL_ARROW_ITEM_COUNT,
|
||||
SELECTORS,
|
||||
} from "discourse/lib/lightbox/constants";
|
||||
import Service, { inject as service } from "@ember/service";
|
||||
import {
|
||||
getSiteThemeColor,
|
||||
setSiteThemeColor,
|
||||
} from "discourse/lib/lightbox/helpers";
|
||||
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import { isDocumentRTL } from "discourse/lib/text-direction";
|
||||
import { processHTML } from "discourse/lib/lightbox/process-html";
|
||||
|
||||
export default class LightboxService extends Service {
|
||||
@service appEvents;
|
||||
@service site;
|
||||
|
||||
lightboxIsOpen = false;
|
||||
lightboxClickElements = [];
|
||||
lastFocusedElement = null;
|
||||
originalSiteThemeColor = null;
|
||||
onFocus = null;
|
||||
selector = null;
|
||||
|
||||
callbacks = {};
|
||||
options = {};
|
||||
|
||||
async init() {
|
||||
super.init(...arguments);
|
||||
|
||||
this.callbacks = {
|
||||
onOpen: this.onLightboxOpened,
|
||||
onClose: this.onLightboxClosed,
|
||||
onWillChange: this.onLightboxItemWillChange,
|
||||
onItemDidChange: this.onLightboxItemDidChange,
|
||||
onCleanUp: this.onLightboxCleanedUp,
|
||||
};
|
||||
|
||||
this.options = {
|
||||
isMobile: this.site.mobileView,
|
||||
isRTL: isDocumentRTL(),
|
||||
minCarosuelArrowItemCount: MIN_CAROUSEL_ARROW_ITEM_COUNT,
|
||||
zoomOnOpen: false,
|
||||
canDownload:
|
||||
this.currentUser ||
|
||||
!this.siteSettings.prevent_anons_from_downloading_files,
|
||||
};
|
||||
|
||||
this.appEvents.on(
|
||||
LIGHTBOX_APP_EVENT_NAMES.CLEAN,
|
||||
this,
|
||||
this.cleanupLightboxes
|
||||
);
|
||||
}
|
||||
|
||||
@bind
|
||||
async onLightboxOpened({ items, currentItem }) {
|
||||
this.originalSiteThemeColor = await getSiteThemeColor();
|
||||
|
||||
document.documentElement.classList.add(
|
||||
DOCUMENT_ELEMENT_LIGHTBOX_OPEN_CLASS
|
||||
);
|
||||
|
||||
this.#setupDocumentFocus();
|
||||
|
||||
this.appEvents.trigger(LIGHTBOX_APP_EVENT_NAMES.OPENED, {
|
||||
items,
|
||||
currentItem,
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
async onLightboxItemWillChange({ currentItem }) {
|
||||
this.appEvents.trigger(LIGHTBOX_APP_EVENT_NAMES.ITEM_WILL_CHANGE, {
|
||||
currentItem,
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
async onLightboxItemDidChange({ currentItem }) {
|
||||
this.appEvents.trigger(LIGHTBOX_APP_EVENT_NAMES.ITEM_DID_CHANGE, {
|
||||
currentItem,
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
async onLightboxClosed() {
|
||||
document.documentElement.classList.remove(
|
||||
DOCUMENT_ELEMENT_LIGHTBOX_OPEN_CLASS
|
||||
);
|
||||
|
||||
setSiteThemeColor(this.originalSiteThemeColor);
|
||||
this.#restoreDocumentFocus();
|
||||
|
||||
this.originalSiteThemeColor = "";
|
||||
this.lightboxIsOpen = false;
|
||||
|
||||
this.appEvents.trigger(LIGHTBOX_APP_EVENT_NAMES.CLOSED);
|
||||
}
|
||||
|
||||
@bind
|
||||
onLightboxCleanedUp() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@bind
|
||||
handleEvent(event) {
|
||||
const isLightboxClick = event
|
||||
.composedPath()
|
||||
.find(
|
||||
(element) =>
|
||||
element.matches &&
|
||||
(element.matches(this.selector) ||
|
||||
element.matches("[data-lightbox-trigger]"))
|
||||
);
|
||||
|
||||
if (!isLightboxClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this.openLightbox({
|
||||
container: event.currentTarget,
|
||||
selector: this.selector,
|
||||
});
|
||||
|
||||
event.target.toggleAttribute(SELECTORS.DOCUMENT_LAST_FOCUSED_ELEMENT);
|
||||
}
|
||||
|
||||
@bind
|
||||
async openLightbox({ container, selector }) {
|
||||
const { items, startingIndex } = await processHTML({ container, selector });
|
||||
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.appEvents.trigger(LIGHTBOX_APP_EVENT_NAMES.OPEN, {
|
||||
items,
|
||||
startingIndex,
|
||||
callbacks: { ...this.callbacks },
|
||||
options: { ...this.options },
|
||||
});
|
||||
|
||||
this.lightboxIsOpen = true;
|
||||
}
|
||||
|
||||
@bind
|
||||
async closeLightbox() {
|
||||
if (this.lightboxIsOpen) {
|
||||
this.appEvents.trigger(LIGHTBOX_APP_EVENT_NAMES.CLOSE);
|
||||
this.lightboxIsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
async #setupLightboxes({ container, selector }) {
|
||||
if (!container) {
|
||||
throw new Error("Lightboxes require a container to be passed in");
|
||||
}
|
||||
|
||||
this.selector = selector;
|
||||
const hasLightboxes = container.querySelector(selector);
|
||||
|
||||
if (!hasLightboxes) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlerOptions = { capture: true };
|
||||
|
||||
container.addEventListener("click", this, handlerOptions);
|
||||
|
||||
this.lightboxClickElements.push({ container, handlerOptions });
|
||||
}
|
||||
|
||||
@bind
|
||||
async setupLightboxes({ container, selector }) {
|
||||
this.#setupLightboxes({ container, selector });
|
||||
}
|
||||
|
||||
async #cleanupLightboxes() {
|
||||
this.closeLightbox();
|
||||
|
||||
this.lightboxClickElements.forEach(({ container, handlerOptions }) => {
|
||||
container.removeEventListener("click", this, handlerOptions);
|
||||
});
|
||||
|
||||
this.lightboxClickElements = [];
|
||||
}
|
||||
|
||||
@bind
|
||||
async cleanupLightboxes() {
|
||||
this.#cleanupLightboxes();
|
||||
}
|
||||
|
||||
async #setupDocumentFocus() {
|
||||
if (!this.lightboxIsOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastFocusedElement = document.activeElement;
|
||||
document.activeElement.blur();
|
||||
document.querySelector(SELECTORS.CLOSE_BUTTON)?.focus();
|
||||
|
||||
const focusableElements = document.querySelectorAll(
|
||||
SELECTORS.LIGHTBOX_CONTAINER + " button"
|
||||
);
|
||||
|
||||
const firstFocusableElement = focusableElements[0];
|
||||
const lastFocusableElement =
|
||||
focusableElements[focusableElements.length - 1];
|
||||
|
||||
const focusTraps = document.querySelectorAll(SELECTORS.FOCUS_TRAP);
|
||||
|
||||
const firstfocusTrap = focusTraps[0];
|
||||
const lastfocusTrap = focusTraps[focusTraps.length - 1];
|
||||
|
||||
this.onFocus = ({ target }) => {
|
||||
if (target === firstfocusTrap) {
|
||||
lastFocusableElement.focus();
|
||||
} else if (target === lastfocusTrap) {
|
||||
firstFocusableElement.focus();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("focus", this.onFocus, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
}
|
||||
|
||||
async #restoreDocumentFocus() {
|
||||
document.removeEventListener("focus", this.onFocus, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
|
||||
document.activeElement.blur();
|
||||
|
||||
this.lastFocusedElement?.focus();
|
||||
}
|
||||
|
||||
async #reset() {
|
||||
this.appEvents.off(
|
||||
LIGHTBOX_APP_EVENT_NAMES.CLEAN,
|
||||
this,
|
||||
this.cleanupLightboxes
|
||||
);
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
this.#reset();
|
||||
}
|
||||
}
|
|
@ -96,6 +96,7 @@
|
|||
@outletArgs={{hash showFooter=this.showFooter}}
|
||||
/>
|
||||
|
||||
<DLightbox />
|
||||
<ModalContainer />
|
||||
<DialogHolder />
|
||||
<TopicEntrance />
|
||||
|
|
|
@ -0,0 +1,950 @@
|
|||
import {
|
||||
LIGHTBOX_IMAGE_FIXTURES,
|
||||
generateLightboxMarkup,
|
||||
} from "discourse/tests/helpers/lightbox-helpers";
|
||||
import {
|
||||
acceptance,
|
||||
chromeTest,
|
||||
queryAll,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import {
|
||||
click,
|
||||
triggerEvent,
|
||||
triggerKeyEvent,
|
||||
visit,
|
||||
waitUntil,
|
||||
} from "@ember/test-helpers";
|
||||
|
||||
import { cloneJSON } from "discourse-common/lib/object";
|
||||
import i18n from "I18n";
|
||||
import sinon from "sinon";
|
||||
import { test } from "qunit";
|
||||
import topicFixtures from "discourse/tests/fixtures/topic";
|
||||
import { SELECTORS } from "discourse/lib/lightbox/constants";
|
||||
|
||||
async function waitForLoad() {
|
||||
return await waitUntil(
|
||||
() => document.querySelector(".d-lightbox.is-finished-loading"),
|
||||
{
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const singleLargeImageMarkup = `
|
||||
${generateLightboxMarkup(LIGHTBOX_IMAGE_FIXTURES.first)}`;
|
||||
|
||||
const singleSmallImageMarkup = `
|
||||
${generateLightboxMarkup(LIGHTBOX_IMAGE_FIXTURES.smallerThanViewPort)}
|
||||
`;
|
||||
|
||||
const multipleLargeImagesMarkup = `
|
||||
${generateLightboxMarkup(LIGHTBOX_IMAGE_FIXTURES.first)}
|
||||
${generateLightboxMarkup(LIGHTBOX_IMAGE_FIXTURES.second)}
|
||||
${generateLightboxMarkup(LIGHTBOX_IMAGE_FIXTURES.third)}
|
||||
`;
|
||||
|
||||
const markupWithInvalidImage = `
|
||||
${generateLightboxMarkup(LIGHTBOX_IMAGE_FIXTURES.first)}
|
||||
${generateLightboxMarkup(LIGHTBOX_IMAGE_FIXTURES.invalidImage)}
|
||||
${generateLightboxMarkup(LIGHTBOX_IMAGE_FIXTURES.second)}`;
|
||||
|
||||
function setupPretender(server, helper, markup) {
|
||||
const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]);
|
||||
topicResponse.post_stream.posts[0].cooked += markup;
|
||||
|
||||
server.get("/t/280.json", () => helper.response(topicResponse));
|
||||
server.get("/t/280/:post_number.json", () => helper.response(topicResponse));
|
||||
}
|
||||
|
||||
acceptance("Experimental Lightbox - site setting", function (needs) {
|
||||
needs.pretender((server, helper) =>
|
||||
setupPretender(server, helper, singleLargeImageMarkup)
|
||||
);
|
||||
|
||||
test("it does not interfere with Magnific when enable_experimental_lightbox is disabled", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
|
||||
await click(".mfp-close");
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Experimental Lightbox - layout single image", function (needs) {
|
||||
needs.settings({ enable_experimental_lightbox: true });
|
||||
|
||||
needs.pretender((server, helper) =>
|
||||
setupPretender(server, helper, singleLargeImageMarkup)
|
||||
);
|
||||
|
||||
test("it shows the correct elements for a single-image lightbox", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
await waitForLoad();
|
||||
|
||||
assert.dom(".d-lightbox__main-title").exists();
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.ACTIVE_ITEM_TITLE)
|
||||
.hasText(LIGHTBOX_IMAGE_FIXTURES.first.title);
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.ACTIVE_ITEM_FILE_DETAILS)
|
||||
.hasText(LIGHTBOX_IMAGE_FIXTURES.first.fileDetails);
|
||||
|
||||
assert.dom(SELECTORS.CLOSE_BUTTON).exists();
|
||||
assert.dom(SELECTORS.CLOSE_BUTTON).isFocused();
|
||||
assert.dom(SELECTORS.TAB_BUTTON).exists();
|
||||
assert.dom(SELECTORS.FULL_SCREEN_BUTTON).exists();
|
||||
assert.dom(SELECTORS.ROTATE_BUTTON).exists();
|
||||
assert.dom(SELECTORS.ZOOM_BUTTON).exists();
|
||||
assert.dom(SELECTORS.DOWNLOAD_BUTTON).exists();
|
||||
assert.dom(SELECTORS.PREV_BUTTON).doesNotExist();
|
||||
assert.dom(SELECTORS.NEXT_BUTTON).doesNotExist();
|
||||
assert.dom(SELECTORS.MULTI_BUTTONS).doesNotExist();
|
||||
assert.dom(SELECTORS.CAROUSEL).doesNotExist();
|
||||
assert.dom(SELECTORS.MAIN_IMAGE).exists();
|
||||
assert.dom(SELECTORS.MAIN_IMAGE).hasAttribute("src");
|
||||
|
||||
assert.dom(".d-lightbox__error-message").doesNotExist();
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL);
|
||||
|
||||
assert.dom(".d-lightbox.is-fullscreen").doesNotExist();
|
||||
assert.dom(".d-lightbox.is-rotated").doesNotExist();
|
||||
assert.dom(".d-lightbox.is-zoomed").doesNotExist();
|
||||
assert.dom(".d-lightbox__backdrop").exists();
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Experimental Lightbox - layout multiple images", function (needs) {
|
||||
needs.settings({ enable_experimental_lightbox: true });
|
||||
|
||||
needs.pretender((server, helper) =>
|
||||
setupPretender(server, helper, multipleLargeImagesMarkup)
|
||||
);
|
||||
|
||||
test("it shows multiple image controls when there's more than one item", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
await waitForLoad();
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
assert.dom(SELECTORS.PREV_BUTTON).exists();
|
||||
assert.dom(SELECTORS.NEXT_BUTTON).exists();
|
||||
assert.dom(SELECTORS.MULTI_BUTTONS).exists();
|
||||
assert.dom(SELECTORS.COUNTERS).exists();
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
assert.dom(SELECTORS.COUNTER_TOTAL).hasText("3");
|
||||
assert.dom(SELECTORS.CAROUSEL).doesNotExist();
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Experimental Lightbox - interaction", function (needs) {
|
||||
needs.settings({ enable_experimental_lightbox: true });
|
||||
|
||||
needs.pretender((server, helper) =>
|
||||
setupPretender(server, helper, multipleLargeImagesMarkup)
|
||||
);
|
||||
|
||||
test("handles zoom", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
await waitForLoad();
|
||||
assert.dom(".d-lightbox.is-zoomed").doesNotExist();
|
||||
|
||||
await click(SELECTORS.ZOOM_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("is-zoomed");
|
||||
assert.dom(SELECTORS.ACTIVE_ITEM_TITLE).doesNotExist();
|
||||
|
||||
await click(SELECTORS.ZOOM_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-zoomed");
|
||||
assert.dom(SELECTORS.ACTIVE_ITEM_TITLE).exists();
|
||||
|
||||
await click(SELECTORS.MAIN_IMAGE);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("is-zoomed");
|
||||
|
||||
await click(".d-lightbox__zoomed-image-container");
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-zoomed");
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
|
||||
test("handles rotation", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).exists();
|
||||
|
||||
await waitForLoad();
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-rotated");
|
||||
|
||||
await click(SELECTORS.ROTATE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("is-rotated");
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("is-rotated-90");
|
||||
|
||||
await click(SELECTORS.ROTATE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("is-rotated-180");
|
||||
|
||||
await click(SELECTORS.ROTATE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("is-rotated-270");
|
||||
|
||||
await click(SELECTORS.ROTATE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-rotated");
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
|
||||
test("handles navigation - next", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
assert.dom(SELECTORS.COUNTER_TOTAL).hasText("3");
|
||||
|
||||
await waitForLoad();
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.NEXT_BUTTON);
|
||||
await waitForLoad();
|
||||
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("2");
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.second.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.NEXT_BUTTON);
|
||||
await waitForLoad();
|
||||
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("3");
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.third.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.NEXT_BUTTON);
|
||||
await waitForLoad();
|
||||
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
|
||||
test("handles navigation - previous", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
assert.dom(SELECTORS.COUNTER_TOTAL).hasText("3");
|
||||
|
||||
await waitForLoad();
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.PREV_BUTTON);
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("3");
|
||||
|
||||
await waitForLoad();
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.third.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.PREV_BUTTON);
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("2");
|
||||
|
||||
await waitForLoad();
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.second.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.PREV_BUTTON);
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
|
||||
await waitForLoad();
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
|
||||
test("handles navigation - opens at the correct index", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
const lightboxes = queryAll(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
await click(lightboxes[1]);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("2");
|
||||
assert.dom(SELECTORS.COUNTER_TOTAL).hasText("3");
|
||||
|
||||
await waitForLoad();
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.second.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
|
||||
await click(lightboxes[2]);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("3");
|
||||
assert.dom(SELECTORS.COUNTER_TOTAL).hasText("3");
|
||||
|
||||
await waitForLoad();
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.third.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
|
||||
test(`handles navigation - prevents document scroll while the lightbox is open`, async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
const classListAddStub = sinon.stub(
|
||||
document.documentElement.classList,
|
||||
"add"
|
||||
);
|
||||
const classListRemoveStub = sinon.stub(
|
||||
document.documentElement.classList,
|
||||
"remove"
|
||||
);
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
await waitForLoad();
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
assert.ok(
|
||||
classListAddStub.calledWith("has-lightbox"),
|
||||
"adds has-lightbox class to document element"
|
||||
);
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
|
||||
assert.ok(
|
||||
classListRemoveStub.calledWith("has-lightbox"),
|
||||
"removes has-lightbox class from document element"
|
||||
);
|
||||
|
||||
classListAddStub.restore();
|
||||
classListRemoveStub.restore();
|
||||
});
|
||||
|
||||
test("handles fullscreen", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-fullscreen");
|
||||
|
||||
const requestFullscreenStub = sinon.stub(
|
||||
document.documentElement,
|
||||
"requestFullscreen"
|
||||
);
|
||||
|
||||
const exitFullscreenStub = sinon.stub(document, "exitFullscreen");
|
||||
|
||||
await click(SELECTORS.FULL_SCREEN_BUTTON);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("is-fullscreen");
|
||||
assert.ok(requestFullscreenStub.calledOnce, "it calls requestFullscreen");
|
||||
|
||||
await click(SELECTORS.FULL_SCREEN_BUTTON);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotHaveClass("is-fullscreen");
|
||||
|
||||
assert.ok(exitFullscreenStub.calledOnce, "it calls exitFullscreen");
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
|
||||
requestFullscreenStub.restore();
|
||||
exitFullscreenStub.restore();
|
||||
});
|
||||
|
||||
test("handles download", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
const clickStub = sinon.stub(HTMLAnchorElement.prototype, "click");
|
||||
|
||||
// appends and clicks <a download="..." href="..."></a>
|
||||
await click(SELECTORS.DOWNLOAD_BUTTON);
|
||||
|
||||
assert.ok(clickStub.called, "The click method was called");
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
|
||||
clickStub.restore();
|
||||
});
|
||||
|
||||
test("handles newtab", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
const openStub = sinon.stub(window, "open");
|
||||
|
||||
await click(SELECTORS.TAB_BUTTON);
|
||||
|
||||
assert.ok(openStub.called, "The open method was called");
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
|
||||
openStub.restore();
|
||||
});
|
||||
|
||||
test("handles close", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
|
||||
test("handles focus", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
await waitForLoad();
|
||||
|
||||
assert.dom(SELECTORS.CLOSE_BUTTON).isFocused();
|
||||
|
||||
// tab forward
|
||||
Array(50)
|
||||
.fill()
|
||||
.forEach(async () => {
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 9);
|
||||
});
|
||||
|
||||
// it keeps focus inside the lightbox when tabbing forward
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
// tab backward
|
||||
Array(50)
|
||||
.fill()
|
||||
.forEach(async () => {
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 9, {
|
||||
shiftKey: true,
|
||||
});
|
||||
});
|
||||
|
||||
// it keeps focus inside the lightbox when tabbing backward
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
|
||||
// it restores focus in the main document when closed
|
||||
assert.dom(SELECTORS.DEFAULT_ITEM_SELECTOR).isFocused();
|
||||
});
|
||||
|
||||
test("navigation - screen reader announcer", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
const firstExpectedTitle = i18n.t(
|
||||
"experimental_lightbox.screen_reader_image_title",
|
||||
{
|
||||
current: 1,
|
||||
total: 3,
|
||||
title: LIGHTBOX_IMAGE_FIXTURES.first.title,
|
||||
}
|
||||
);
|
||||
|
||||
const secondExpectedTitle = i18n.t(
|
||||
"experimental_lightbox.screen_reader_image_title",
|
||||
{
|
||||
current: 2,
|
||||
total: 3,
|
||||
title: LIGHTBOX_IMAGE_FIXTURES.second.title,
|
||||
}
|
||||
);
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
await waitForLoad();
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
assert.dom(".d-lightbox__screen-reader-announcer").exists();
|
||||
|
||||
assert
|
||||
.dom(".d-lightbox__screen-reader-announcer")
|
||||
.hasText(firstExpectedTitle);
|
||||
|
||||
await click(SELECTORS.NEXT_BUTTON);
|
||||
await waitForLoad();
|
||||
|
||||
assert
|
||||
.dom(".d-lightbox__screen-reader-announcer")
|
||||
.hasText(secondExpectedTitle);
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
|
||||
// TODO: this test is flaky on firefox. It runs fine locally and the functionality works in a real session, but fails on CI.
|
||||
chromeTest("handles keyboard shortcuts", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
await waitForLoad();
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", "ArrowRight");
|
||||
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("2");
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", "ArrowLeft");
|
||||
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", "ArrowDown");
|
||||
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("2");
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", "ArrowUp");
|
||||
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-zoomed");
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 90); // 'z' key
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("is-zoomed");
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 90);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-zoomed");
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-rotated");
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 82); // r key
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("is-rotated");
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 82);
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 82);
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 82);
|
||||
|
||||
// back to original rotation
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-rotated");
|
||||
|
||||
assert.dom(SELECTORS.CAROUSEL).doesNotExist();
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 65); // 'a' key
|
||||
|
||||
assert.dom(SELECTORS.CAROUSEL).exists();
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 65);
|
||||
|
||||
assert.dom(SELECTORS.CAROUSEL).doesNotExist();
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.LIGHTBOX_CONTAINER)
|
||||
.doesNotHaveClass("has-expanded-title");
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 84); // 't' key
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("has-expanded-title");
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 84);
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.LIGHTBOX_CONTAINER)
|
||||
.doesNotHaveClass("has-expanded-title");
|
||||
|
||||
const requestFullscreenStub = sinon.stub(
|
||||
document.documentElement,
|
||||
"requestFullscreen"
|
||||
);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-fullscreen");
|
||||
|
||||
const exitFullscreenStub = sinon.stub(document, "exitFullscreen");
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 77); // 'm' key
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("is-fullscreen");
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", 77);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-fullscreen");
|
||||
|
||||
requestFullscreenStub.restore();
|
||||
exitFullscreenStub.restore();
|
||||
|
||||
await triggerKeyEvent(SELECTORS.LIGHTBOX_CONTENT, "keyup", "Escape");
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Experimental Lightbox - carousel", function (needs) {
|
||||
needs.settings({ enable_experimental_lightbox: true });
|
||||
|
||||
needs.pretender((server, helper) =>
|
||||
setupPretender(
|
||||
server,
|
||||
helper,
|
||||
multipleLargeImagesMarkup + multipleLargeImagesMarkup
|
||||
)
|
||||
);
|
||||
|
||||
test("navigation", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
// lightbox opens with the first image
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
|
||||
// carousel is not visible by default
|
||||
assert.dom(SELECTORS.CAROUSEL).doesNotExist();
|
||||
|
||||
await click(SELECTORS.CAROUSEL_BUTTON);
|
||||
|
||||
// carousel opens after clicking the button, and has prev/next buttons
|
||||
assert.dom(SELECTORS.CAROUSEL).exists();
|
||||
assert.dom(SELECTORS.CAROUSEL_PREV_BUTTON).exists();
|
||||
assert.dom(SELECTORS.CAROUSEL_NEXT_BUTTON).exists();
|
||||
|
||||
// carousel has 5 items and an active item
|
||||
assert.dom(SELECTORS.CAROUSEL_ITEM).exists({ count: 6 });
|
||||
assert.dom(SELECTORS.CAROUSEL_ITEM + ".is-current").exists();
|
||||
|
||||
await waitForLoad();
|
||||
|
||||
// carousel current item is the first image
|
||||
assert
|
||||
.dom(SELECTORS.CAROUSEL_ITEM + ".is-current")
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.first.smallURL);
|
||||
|
||||
await click(SELECTORS.CAROUSEL_NEXT_BUTTON);
|
||||
await waitForLoad();
|
||||
|
||||
// carousel next button works and current item is the second image
|
||||
assert
|
||||
.dom(SELECTORS.CAROUSEL_ITEM + ".is-current")
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.second.smallURL);
|
||||
|
||||
await click(SELECTORS.CAROUSEL_PREV_BUTTON);
|
||||
await waitForLoad();
|
||||
|
||||
// carousel previous button works and current item is the first image again
|
||||
assert
|
||||
.dom(SELECTORS.CAROUSEL_ITEM + ".is-current")
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.first.smallURL);
|
||||
|
||||
await click(SELECTORS.CAROUSEL_ITEM + ":nth-child(3)");
|
||||
await waitForLoad();
|
||||
|
||||
// carousel manual item selection works and current item is the third image
|
||||
assert
|
||||
.dom(SELECTORS.CAROUSEL_ITEM + ".is-current")
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.third.smallURL);
|
||||
|
||||
// carousel closes after clicking the carousel button again
|
||||
await click(SELECTORS.CAROUSEL_BUTTON);
|
||||
assert.dom(SELECTORS.CAROUSEL).doesNotExist();
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
|
||||
test("arrows are not shown when there are only a few images", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
const lightboxes = [...queryAll(SELECTORS.DEFAULT_ITEM_SELECTOR)];
|
||||
|
||||
const lastThreeLightboxes = lightboxes.slice(-3);
|
||||
|
||||
lastThreeLightboxes.forEach((lightbox) => {
|
||||
lightbox.remove();
|
||||
});
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).exists();
|
||||
assert.dom(SELECTORS.CAROUSEL).doesNotExist();
|
||||
|
||||
// carousel opens after clicking the button
|
||||
await click(SELECTORS.CAROUSEL_BUTTON);
|
||||
assert.dom(SELECTORS.CAROUSEL).exists();
|
||||
assert.dom(SELECTORS.CAROUSEL_ITEM).exists({ count: 3 });
|
||||
|
||||
// no prev/next buttons when carousel only has a few images
|
||||
assert.dom(SELECTORS.CAROUSEL_PREV_BUTTON).doesNotExist();
|
||||
assert.dom(SELECTORS.CAROUSEL_NEXT_BUTTON).doesNotExist();
|
||||
|
||||
// carousel closes after clicking the button again
|
||||
await click(SELECTORS.CAROUSEL_BUTTON);
|
||||
assert.dom(SELECTORS.CAROUSEL).doesNotExist();
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Experimental Lightbox - mobile", function (needs) {
|
||||
needs.settings({ enable_experimental_lightbox: true });
|
||||
|
||||
needs.pretender((server, helper) =>
|
||||
setupPretender(server, helper, multipleLargeImagesMarkup)
|
||||
);
|
||||
|
||||
test("navigation - swipe navigation LTR", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchstart", {
|
||||
changedTouches: [{ screenX: 0, screenY: 0 }],
|
||||
touches: [{ screenX: 0, screenY: 0 }],
|
||||
});
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchend", {
|
||||
changedTouches: [{ screenX: 150, screenY: 0 }],
|
||||
touches: [{ pageX: 150, pageY: 0 }],
|
||||
});
|
||||
|
||||
// swiping left goes to the previous image
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("3");
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchstart", {
|
||||
changedTouches: [{ screenX: 0, screenY: 0 }],
|
||||
touches: [{ screenX: 0, screenY: 0 }],
|
||||
});
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchend", {
|
||||
changedTouches: [{ screenX: -150, screenY: 0 }],
|
||||
touches: [{ pageX: 150, pageY: 0 }],
|
||||
});
|
||||
|
||||
// swiping right goes to the next image
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
|
||||
test("navigation - swipe navigation RTL", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
|
||||
const containsStub = sinon.stub(
|
||||
document.documentElement.classList,
|
||||
"contains"
|
||||
);
|
||||
|
||||
containsStub.withArgs("rtl").returns(true);
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchstart", {
|
||||
changedTouches: [{ screenX: 0, screenY: 0 }],
|
||||
touches: [{ screenX: 0, screenY: 0 }],
|
||||
});
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchend", {
|
||||
changedTouches: [{ screenX: -150, screenY: 0 }],
|
||||
touches: [{ pageX: 150, pageY: 0 }],
|
||||
});
|
||||
|
||||
// swiping left goes to the next image in RTL
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("2");
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchstart", {
|
||||
changedTouches: [{ screenX: 0, screenY: 0 }],
|
||||
touches: [{ screenX: 0, screenY: 0 }],
|
||||
});
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchend", {
|
||||
changedTouches: [{ screenX: 150, screenY: 0 }],
|
||||
touches: [{ pageX: 150, pageY: 0 }],
|
||||
});
|
||||
|
||||
// swiping right goes to the previous image in RTL
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("1");
|
||||
|
||||
containsStub.restore();
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
|
||||
test("navigation - swipe close", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchstart", {
|
||||
changedTouches: [{ screenX: 0, screenY: 0 }],
|
||||
touches: [{ screenX: 0, screenY: 0 }],
|
||||
});
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchend", {
|
||||
changedTouches: [{ screenX: 0, screenY: -150 }],
|
||||
touches: [{ pageX: 0, pageY: 150 }],
|
||||
});
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
|
||||
test("navigation - swipe carousel", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
assert.dom(SELECTORS.CAROUSEL).doesNotExist();
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchstart", {
|
||||
changedTouches: [{ screenX: 0, screenY: 0 }],
|
||||
touches: [{ screenX: 0, screenY: 0 }],
|
||||
});
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchend", {
|
||||
changedTouches: [{ screenX: 0, screenY: 150 }],
|
||||
touches: [{ pageX: 0, pageY: 150 }],
|
||||
});
|
||||
|
||||
assert.dom(SELECTORS.CAROUSEL).exists(); // opens after swiping down
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchstart", {
|
||||
changedTouches: [{ screenX: 0, screenY: 0 }],
|
||||
touches: [{ screenX: 0, screenY: 0 }],
|
||||
});
|
||||
|
||||
await triggerEvent(SELECTORS.LIGHTBOX_BODY, "touchend", {
|
||||
changedTouches: [{ screenX: 0, screenY: 150 }],
|
||||
touches: [{ pageX: 0, pageY: 150 }],
|
||||
});
|
||||
|
||||
assert.dom(SELECTORS.CAROUSEL).doesNotExist(); // closes after swiping down again
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Experimental Lightbox - loading state", function (needs) {
|
||||
needs.settings({ enable_experimental_lightbox: true });
|
||||
|
||||
needs.pretender((server, helper) =>
|
||||
setupPretender(server, helper, markupWithInvalidImage)
|
||||
);
|
||||
|
||||
test("handles loading errors", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
|
||||
await waitForLoad();
|
||||
|
||||
// the image has the correct src
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.NEXT_BUTTON);
|
||||
|
||||
// does not show an image if it can't be loaded
|
||||
assert.dom(SELECTORS.MAIN_IMAGE).doesNotExist();
|
||||
|
||||
await click(SELECTORS.NEXT_BUTTON);
|
||||
|
||||
// it shows the correct image when navigating after an error
|
||||
assert.dom(SELECTORS.COUNTER_CURRENT).hasText("3");
|
||||
|
||||
await waitForLoad();
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.MAIN_IMAGE)
|
||||
.hasAttribute("src", LIGHTBOX_IMAGE_FIXTURES.second.fullsizeURL);
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Experimental Lightbox - conditional buttons", function (needs) {
|
||||
needs.settings({
|
||||
enable_experimental_lightbox: true,
|
||||
prevent_anons_from_downloading_files: true,
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) =>
|
||||
setupPretender(server, helper, singleSmallImageMarkup)
|
||||
);
|
||||
|
||||
test("it doesn't show the newtab and download buttons to anons if prevent_anons_from_downloading_files is enabled", async function (assert) {
|
||||
this.siteSettings.prevent_anons_from_downloading_files = true;
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
|
||||
// it doesn not show the newtab or download button
|
||||
assert.dom(SELECTORS.TAB_BUTTON).doesNotExist();
|
||||
assert.dom(SELECTORS.DOWNLOAD_BUTTON).doesNotExist();
|
||||
});
|
||||
|
||||
test("it doesn't show the zoom button if the image is smaller than the viewport", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
await click(SELECTORS.DEFAULT_ITEM_SELECTOR);
|
||||
assert.dom(SELECTORS.ZOOM_BUTTON).doesNotExist();
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,176 @@
|
|||
import { htmlSafe } from "@ember/template";
|
||||
|
||||
// we use transparent pngs here to avoid loading actual images in tests. We don't care so much about the content of the image
|
||||
// we only care that the correct loading state is set and the metadata is correct
|
||||
const PNGS = {
|
||||
first: {
|
||||
fullsizeURL:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAC7gAAAAKCAYAAAAkEBP9AAAAqElEQVR42u3aQQEAIAwAIdfN/pVmDO8BOZizdw8AAAAAAAAAAAAAAHw2gjsAAAAAAAAAAAAAAAWCOwAAAAAAAAAAAAAACYI7AAAAAAAAAAAAAAAJgjsAAAAAAAAAAAAAAAmCOwAAAAAAAAAAAAAACYI7AAAAAAAAAAAAAAAJgjsAAAAAAAAAAAAAAAmCOwAAAAAAAAAAAAAACYI7AAAAAAAAAAAAAAAJD2GpFp8NV4+AAAAAAElFTkSuQmCC",
|
||||
smallURL:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfQAAAABCAYAAAAo/lyUAAAAG0lEQVR42mP8z8AARKNgFIyCUTAKRsEoGMoAAJ3mAgDVocSsAAAAAElFTkSuQmCC",
|
||||
},
|
||||
second: {
|
||||
fullsizeURL:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAACCAYAAADLlPadAAAAJ0lEQVR42u3XMQEAAAgDoNk/pzk0xh5owdzmAgAAAFSNoAMAAEDfA6HNBcm32R2bAAAAAElFTkSuQmCC",
|
||||
smallURL:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfQAAAABCAYAAAAo/lyUAAAAHElEQVR42mP8/5ThP8MoGAWjYBSMglEwCoY0AACaegLl/taPAQAAAABJRU5ErkJggg==",
|
||||
},
|
||||
third: {
|
||||
fullsizeURL:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAACCAYAAADLlPadAAAAJklEQVR42u3XMQEAAAgDoPnZv7DG2AMtmNxeAAAAgKoRdAAAAOh7JuQED1zV49EAAAAASUVORK5CYII=",
|
||||
smallURL:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAfQAAAABCAYAAAAo/lyUAAAAHElEQVR42mNk+M/xn2EUjIJRMApGwSgYBUMaAADbVwIINvIVWgAAAABJRU5ErkJggg==",
|
||||
},
|
||||
smallerThanViewPort: {
|
||||
fullsizeURL:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAACCAYAAADirOGHAAAAIUlEQVR42u3UAQ0AAAgDoNvE/iU1xzcIwWTvAlBghAW0eNbwBD9majEtAAAAAElFTkSuQmCC",
|
||||
smallURL:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAAABCAYAAAA8YlcZAAAAE0lEQVR42mNkUPj/n2EUjAIqAwD2IwIg6SI42wAAAABJRU5ErkJggg==",
|
||||
},
|
||||
};
|
||||
|
||||
const cssVars1 = htmlSafe(
|
||||
`--dominant-color: #F0F1F3;--aspect-ratio: 3000 / 10;--small-url: url(${PNGS.first.smallURL});`
|
||||
);
|
||||
const cssVars2 = htmlSafe(
|
||||
`--dominant-color: #F0F1F3;--aspect-ratio: 3000 / 10;--small-url: url(${PNGS.second.smallURL});`
|
||||
);
|
||||
const cssVars3 = htmlSafe(
|
||||
`--dominant-color: #F0F1F3;--aspect-ratio: 3000 / 10;--small-url: url(${PNGS.third.smallURL});`
|
||||
);
|
||||
const cssVars4 = htmlSafe(
|
||||
`--dominant-color: #F0F1F3;--aspect-ratio: 3000 / 10;--small-url: url(${PNGS.smallerThanViewPort.smallURL});`
|
||||
);
|
||||
|
||||
export const LIGHTBOX_IMAGE_FIXTURES = {
|
||||
first: {
|
||||
fullsizeURL: PNGS.first.fullsizeURL,
|
||||
smallURL: PNGS.first.smallURL,
|
||||
downloadURL: PNGS.first.fullsizeURL,
|
||||
fileDetails: "3000×10 221 KB",
|
||||
width: 3000,
|
||||
height: 10,
|
||||
aspectRatio: "3000 / 10",
|
||||
dominantColor: "F0F1F3",
|
||||
index: 0,
|
||||
title: "first image title",
|
||||
alt: "first image alt",
|
||||
cssVars: cssVars1,
|
||||
},
|
||||
second: {
|
||||
fullsizeURL: PNGS.second.fullsizeURL,
|
||||
smallURL: PNGS.second.smallURL,
|
||||
downloadURL: PNGS.second.fullsizeURL,
|
||||
fileDetails: "1000×2 166 KB",
|
||||
width: 1000,
|
||||
height: 2,
|
||||
aspectRatio: "1000 / 2",
|
||||
dominantColor: "F9F5F6",
|
||||
index: 1,
|
||||
title: "second image title",
|
||||
alt: "second image alt",
|
||||
cssVars: cssVars2,
|
||||
},
|
||||
third: {
|
||||
fullsizeURL: PNGS.third.fullsizeURL,
|
||||
smallURL: PNGS.third.smallURL,
|
||||
downloadURL: PNGS.third.fullsizeURL,
|
||||
fileDetails: "1000×2 240 KB",
|
||||
width: 1000,
|
||||
height: 2,
|
||||
aspectRatio: "1000 / 2",
|
||||
dominantColor: "EEF0EE",
|
||||
index: 2,
|
||||
title: "third image title",
|
||||
alt: "third image alt",
|
||||
cssVars: cssVars3,
|
||||
},
|
||||
smallerThanViewPort: {
|
||||
fullsizeURL: PNGS.smallerThanViewPort.fullsizeURL,
|
||||
smallURL: PNGS.smallerThanViewPort.smallURL,
|
||||
downloadURL: PNGS.smallerThanViewPort.fullsizeURL,
|
||||
fileDetails: "300×2 92.3 KB",
|
||||
width: 300,
|
||||
height: 2,
|
||||
aspectRatio: "300 / 2",
|
||||
dominantColor: "F0F0F1",
|
||||
index: 3,
|
||||
title: "fourth image title",
|
||||
alt: "fourth image alt",
|
||||
cssVars: cssVars4,
|
||||
},
|
||||
invalidImage: {
|
||||
fullsizeURL: `https:expected-lightbox-invalid/.image/404.png`,
|
||||
},
|
||||
};
|
||||
|
||||
export function generateLightboxObject() {
|
||||
const trimmedLighboxItem = Object.keys(LIGHTBOX_IMAGE_FIXTURES.first).reduce(
|
||||
(acc, key) => {
|
||||
if (key !== "height" && key !== "width" && key !== "alt") {
|
||||
acc[key] = LIGHTBOX_IMAGE_FIXTURES.first[key];
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return {
|
||||
items: [{ ...trimmedLighboxItem }],
|
||||
startingIndex: 0,
|
||||
callbacks: {},
|
||||
options: {},
|
||||
};
|
||||
}
|
||||
|
||||
export function generateLightboxMarkup(
|
||||
{
|
||||
fullsizeURL,
|
||||
smallURL,
|
||||
downloadURL,
|
||||
title,
|
||||
fileDetails,
|
||||
dominantColor,
|
||||
aspectRatio,
|
||||
alt,
|
||||
height,
|
||||
width,
|
||||
} = { ...LIGHTBOX_IMAGE_FIXTURES.first }
|
||||
) {
|
||||
return `
|
||||
<div class="lightbox-wrapper">
|
||||
<a class="lightbox" href="${fullsizeURL}"
|
||||
data-download-href="${downloadURL}"
|
||||
title="${title}"><img src="${smallURL}" title="${title}" alt="${alt}"
|
||||
width="${width}" height="${height}"
|
||||
data-dominant-color="${dominantColor}" loading="lazy"
|
||||
style="aspect-ratio: ${aspectRatio}" />
|
||||
<div class="meta">
|
||||
<span class="filename">${title}</span><span
|
||||
class="informations">${fileDetails}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function generateImageUploaderMarkup(
|
||||
fullsizeURL = LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL
|
||||
) {
|
||||
return `
|
||||
<div id="profile-background-uploader" class="image-uploader ember-view">
|
||||
<div class="uploaded-image-preview input-xxlarge"
|
||||
style="background-image: url(${fullsizeURL})">
|
||||
<a class="lightbox"
|
||||
href="${fullsizeURL}"
|
||||
rel="nofollow ugc noopener">
|
||||
<div class="meta">
|
||||
<span class="informations">
|
||||
x
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
<meta property="og:url" content="{{rootURL}}" />
|
||||
<meta name="twitter:title" content="Discourse Tests" />
|
||||
<meta name="twitter:url" content="{{rootURL}}" />
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<link rel="canonical" href="{{rootURL}}" />
|
||||
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import { click, render, settled } from "@ember/test-helpers";
|
||||
import { query } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { module, test } from "qunit";
|
||||
|
||||
import domFromString from "discourse-common/lib/dom-from-string";
|
||||
import { generateLightboxMarkup } from "discourse/tests/helpers/lightbox-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { setupLightboxes } from "discourse/lib/lightbox";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { SELECTORS } from "discourse/lib/lightbox/constants";
|
||||
|
||||
module("Integration | Component | d-lightbox", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("it renders according to state", async function (assert) {
|
||||
await render(hbs`<DLightbox />`);
|
||||
|
||||
// lightbox container exists but is not visible
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).exists();
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-visible");
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasAttribute("tabindex", "-1");
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
|
||||
// it is hidden from screen readers
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasAttribute("aria-hidden");
|
||||
|
||||
const container = domFromString(generateLightboxMarkup())[0];
|
||||
await setupLightboxes({
|
||||
container,
|
||||
selector: SELECTORS.DEFAULT_ITEM_SELECTOR,
|
||||
});
|
||||
|
||||
const lightboxedElement = container.querySelector(
|
||||
SELECTORS.DEFAULT_ITEM_SELECTOR
|
||||
);
|
||||
await click(lightboxedElement);
|
||||
|
||||
await settled();
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasClass("is-visible");
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.LIGHTBOX_CONTAINER)
|
||||
.hasClass(/^(is-vertical|is-horizontal)$/);
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-zoomed");
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-rotated");
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-fullscreen");
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).exists();
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveAria("hidden");
|
||||
|
||||
// the content is tabbable
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).hasAttribute("tabindex", "0");
|
||||
|
||||
// the content has a document role
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).hasAttribute("role", "document");
|
||||
|
||||
// the content has an aria-labelledby attribute
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).hasAttribute("aria-labelledby");
|
||||
|
||||
assert.strictEqual(
|
||||
query(SELECTORS.LIGHTBOX_CONTENT)
|
||||
.getAttribute("style")
|
||||
.match(/--d-lightbox/g).length > 0,
|
||||
true,
|
||||
"the content has the correct css variables added"
|
||||
);
|
||||
|
||||
// it has focus traps for keyboard navigation
|
||||
assert.dom(SELECTORS.FOCUS_TRAP).exists();
|
||||
|
||||
await click(SELECTORS.CLOSE_BUTTON);
|
||||
await settled();
|
||||
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).doesNotHaveClass("is-visible");
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTENT).doesNotExist();
|
||||
|
||||
// it is not tabbable
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasAttribute("tabindex", "-1");
|
||||
|
||||
// it is hidden from screen readers
|
||||
assert.dom(SELECTORS.LIGHTBOX_CONTAINER).hasAttribute("aria-hidden");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
import { module, test } from "qunit";
|
||||
|
||||
import { createDownloadLink } from "discourse/lib/lightbox/helpers";
|
||||
import sinon from "sinon";
|
||||
|
||||
module(
|
||||
"Unit | lib | Experimental Lightbox | Helpers | createDownloadLink()",
|
||||
function () {
|
||||
test("creates a download link with the correct href and download attributes", async function (assert) {
|
||||
const lightboxItem = {
|
||||
downloadURL: "http://example.com/download.jpg",
|
||||
title: "image.jpg",
|
||||
};
|
||||
const createElementSpy = sinon.spy(document, "createElement");
|
||||
const clickStub = sinon.stub(HTMLAnchorElement.prototype, "click");
|
||||
|
||||
createDownloadLink(lightboxItem);
|
||||
|
||||
assert.strictEqual(
|
||||
createElementSpy.calledWith("a"),
|
||||
true,
|
||||
"creates an anchor element"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
createElementSpy.returnValues[0].href,
|
||||
"http://example.com/download.jpg",
|
||||
"sets the correct href attribute"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
createElementSpy.returnValues[0].download,
|
||||
"image.jpg",
|
||||
"sets the correct download attribute"
|
||||
);
|
||||
|
||||
assert.strictEqual(clickStub.called, true, "clicks the link element");
|
||||
|
||||
createElementSpy.restore();
|
||||
clickStub.restore();
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,28 @@
|
|||
import { module, test } from "qunit";
|
||||
|
||||
import { findNearestSharedParent } from "discourse/lib/lightbox/helpers";
|
||||
|
||||
module(
|
||||
"Unit | lib | Experimental Lightbox | Helpers | findNearestSharedParent()",
|
||||
function () {
|
||||
test("it returns the nearest shared parent for the elements passed in", async function (assert) {
|
||||
const element0 = document.createElement("div");
|
||||
const element1 = document.createElement("div");
|
||||
const element2 = document.createElement("div");
|
||||
const element3 = document.createElement("div");
|
||||
const element4 = document.createElement("div");
|
||||
|
||||
element1.appendChild(element2);
|
||||
element3.appendChild(element4);
|
||||
|
||||
element0.appendChild(element1);
|
||||
element0.appendChild(element3);
|
||||
|
||||
assert.strictEqual(
|
||||
findNearestSharedParent([element2, element4]),
|
||||
element0,
|
||||
"returns the correct nearest shared parent"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,66 @@
|
|||
import { module, test } from "qunit";
|
||||
|
||||
import { SWIPE_DIRECTIONS } from "discourse/lib/lightbox/constants";
|
||||
import { getSwipeDirection } from "discourse/lib/lightbox/helpers";
|
||||
|
||||
module(
|
||||
"Unit | lib | Experimental Lightbox | Helpers | getSwipeDirection()",
|
||||
function () {
|
||||
test("returns the correct direction based on the difference between touchstart and touchend", function (assert) {
|
||||
assert.strictEqual(
|
||||
getSwipeDirection({
|
||||
touchstartX: 200,
|
||||
touchstartY: 0,
|
||||
touchendX: 50,
|
||||
touchendY: 0,
|
||||
}),
|
||||
SWIPE_DIRECTIONS.RIGHT,
|
||||
"returns 'RIGHT' for swipes with a large negative x-axis difference"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
getSwipeDirection({
|
||||
touchstartX: 50,
|
||||
touchstartY: 0,
|
||||
touchendX: 200,
|
||||
touchendY: 0,
|
||||
}),
|
||||
SWIPE_DIRECTIONS.LEFT,
|
||||
"returns 'LEFT' for swipes with a large positive x-axis difference"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
getSwipeDirection({
|
||||
touchstartX: 0,
|
||||
touchstartY: 200,
|
||||
touchendX: 0,
|
||||
touchendY: 50,
|
||||
}),
|
||||
SWIPE_DIRECTIONS.UP,
|
||||
"returns 'UP' for swipes with a large negative y-axis difference"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
getSwipeDirection({
|
||||
touchstartX: 0,
|
||||
touchstartY: 50,
|
||||
touchendX: 0,
|
||||
touchendY: 200,
|
||||
}),
|
||||
SWIPE_DIRECTIONS.DOWN,
|
||||
"returns 'DOWN' for swipes with a large positive y-axis difference"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
getSwipeDirection({
|
||||
touchstartX: 50,
|
||||
touchstartY: 50,
|
||||
touchendX: 49,
|
||||
touchendY: 49,
|
||||
}),
|
||||
false,
|
||||
"returns 'false' for swipes with a small x-axis difference and a small y-axis difference"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,47 @@
|
|||
import { module, test } from "qunit";
|
||||
|
||||
import { openImageInNewTab } from "discourse/lib/lightbox/helpers";
|
||||
import sinon from "sinon";
|
||||
|
||||
module(
|
||||
"Unit | lib | Experimental Lightbox | Helpers | openImageinNewTab()",
|
||||
function () {
|
||||
test("opens the fullsize URL of the lightbox item in a new tab", async function (assert) {
|
||||
const lightboxItem = {
|
||||
fullsizeURL: "image.jpg",
|
||||
};
|
||||
|
||||
const openStub = sinon.stub(window, "open");
|
||||
|
||||
await openImageInNewTab(lightboxItem);
|
||||
|
||||
assert.strictEqual(
|
||||
openStub.calledWith("image.jpg", "_blank"),
|
||||
true,
|
||||
"calls window.open with the correct arguments"
|
||||
);
|
||||
|
||||
openStub.restore();
|
||||
});
|
||||
|
||||
test("handles errors when trying to open the new tab", async function (assert) {
|
||||
const lightboxItem = {
|
||||
fullsizeURL: "image.jpg",
|
||||
};
|
||||
|
||||
const openStub = sinon.stub(window, "open").throws();
|
||||
const consoleErrorStub = sinon.stub(console, "error");
|
||||
|
||||
await openImageInNewTab(lightboxItem);
|
||||
|
||||
assert.strictEqual(
|
||||
consoleErrorStub.called,
|
||||
true,
|
||||
"logs an error to the console"
|
||||
);
|
||||
|
||||
openStub.restore();
|
||||
consoleErrorStub.restore();
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,78 @@
|
|||
import {
|
||||
LIGHTBOX_IMAGE_FIXTURES,
|
||||
generateLightboxObject,
|
||||
} from "discourse/tests/helpers/lightbox-helpers";
|
||||
import { module, test } from "qunit";
|
||||
|
||||
import { cloneJSON } from "discourse-common/lib/object";
|
||||
import { preloadItemImages } from "discourse/lib/lightbox/helpers";
|
||||
|
||||
module(
|
||||
"Unit | lib | Experimental Lightbox | Helpers | preloadItemImages()",
|
||||
|
||||
function () {
|
||||
const baseLightboxItem = generateLightboxObject().items[0];
|
||||
|
||||
test("returns the correct object", async function (assert) {
|
||||
const lightboxItem = cloneJSON(baseLightboxItem);
|
||||
|
||||
const result = await preloadItemImages(lightboxItem);
|
||||
|
||||
assert.ok(result.isLoaded, "isLoaded should be true");
|
||||
|
||||
assert.ok(!result.hasLoadingError, "hasLoadingError should be false");
|
||||
|
||||
assert.strictEqual(
|
||||
result.width,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.width,
|
||||
"width should be equal to fullsizeImage width"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
result.height,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.height,
|
||||
"height should be equal to fullsizeImage height"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
result.aspectRatio,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.aspectRatio,
|
||||
"aspectRatio should be equal to image width/height"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
result.canZoom,
|
||||
"canZoom should be true if fullsizeImage width or height is greater than window inner width or height"
|
||||
);
|
||||
});
|
||||
|
||||
test("handles errors", async function (assert) {
|
||||
const lightboxItem = cloneJSON(baseLightboxItem);
|
||||
|
||||
lightboxItem.fullsizeURL =
|
||||
LIGHTBOX_IMAGE_FIXTURES.invalidImage.fullsizeURL;
|
||||
|
||||
const result = await preloadItemImages(lightboxItem);
|
||||
|
||||
assert.strictEqual(
|
||||
result.hasLoadingError,
|
||||
true,
|
||||
"sets hasLoadingError to true if there is an error"
|
||||
);
|
||||
});
|
||||
|
||||
test("handles images smaller than the viewport", async function (assert) {
|
||||
const lightboxItem = cloneJSON(baseLightboxItem);
|
||||
|
||||
lightboxItem.fullsizeURL =
|
||||
LIGHTBOX_IMAGE_FIXTURES.smallerThanViewPort.fullsizeURL;
|
||||
|
||||
const result = await preloadItemImages(lightboxItem);
|
||||
|
||||
assert.notOk(
|
||||
result.canZoom,
|
||||
"canZoom should be false if fullsizeImage width or height is smaller than window inner width or height"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,51 @@
|
|||
import { module, test } from "qunit";
|
||||
|
||||
import { scrollParentToElementCenter } from "discourse/lib/lightbox/helpers";
|
||||
|
||||
module(
|
||||
"Unit | lib | Experimental Lightbox | Helpers | scrollParentToElementCenter()",
|
||||
function () {
|
||||
test("scrolls the parent element to the center of the element", async function (assert) {
|
||||
const parent = document.createElement("div");
|
||||
parent.style.width = "200px";
|
||||
parent.style.height = "200px";
|
||||
parent.style.overflow = "scroll";
|
||||
|
||||
const element = document.createElement("div");
|
||||
element.style.width = "400px";
|
||||
element.style.height = "400px";
|
||||
parent.appendChild(element);
|
||||
|
||||
document.body.appendChild(parent);
|
||||
|
||||
const getExpectedX = (element.offsetWidth - parent.offsetWidth) / 2;
|
||||
const expectedY = (element.offsetHeight - parent.offsetHeight) / 2;
|
||||
|
||||
scrollParentToElementCenter({ element, isRTL: false });
|
||||
|
||||
assert.strictEqual(
|
||||
parent.scrollLeft,
|
||||
getExpectedX,
|
||||
"scrolls parent to center of element (LTR - horizontal)"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
parent.scrollTop,
|
||||
expectedY,
|
||||
"scrolls parent to center of viewport (LTR and RTL - vertical)"
|
||||
);
|
||||
|
||||
parent.style.direction = "rtl";
|
||||
|
||||
scrollParentToElementCenter({ element, isRTL: true });
|
||||
|
||||
assert.strictEqual(
|
||||
parent.scrollLeft,
|
||||
getExpectedX * -1,
|
||||
"scrolls parent to center of viewport (RTL - horizontal)"
|
||||
);
|
||||
|
||||
document.body.removeChild(parent);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,76 @@
|
|||
import { module, test } from "qunit";
|
||||
|
||||
import { setCarouselScrollPosition } from "discourse/lib/lightbox/helpers";
|
||||
|
||||
module(
|
||||
"Unit | lib | Experimental Lightbox | Helpers | setCarouselScrollPosition()",
|
||||
function () {
|
||||
const carouselItemSize = 100;
|
||||
const target = 9;
|
||||
|
||||
const carousel = document.createElement("div");
|
||||
carousel.style.cssText = `
|
||||
display: grid;
|
||||
height: 400px;
|
||||
width: 400px;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const carouselItem = document.createElement("div");
|
||||
carouselItem.style.cssText = `
|
||||
width: ${carouselItemSize}px;
|
||||
height: ${carouselItemSize}px;
|
||||
`;
|
||||
|
||||
Array(20)
|
||||
.fill(null)
|
||||
.map((_, index) => {
|
||||
const item = carouselItem.cloneNode(true);
|
||||
if (index === target) {
|
||||
item.dataset.lightboxCarouselItem = "current";
|
||||
}
|
||||
carousel.appendChild(item);
|
||||
});
|
||||
|
||||
const expected =
|
||||
target * carouselItemSize - carouselItemSize - carouselItemSize / 2;
|
||||
|
||||
test("scrolls the carousel to the center of the active item ", async function (assert) {
|
||||
const container = carousel.cloneNode(true);
|
||||
|
||||
container.style.cssText += `
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: repeat(auto, ${carouselItemSize}px);
|
||||
`;
|
||||
|
||||
const fixtureDiv = document.getElementById("qunit-fixture");
|
||||
fixtureDiv.appendChild(container);
|
||||
|
||||
await setCarouselScrollPosition("instant");
|
||||
|
||||
assert.strictEqual(
|
||||
container.scrollLeft,
|
||||
expected,
|
||||
"scrolls carousel to center of active item (horizontal)"
|
||||
);
|
||||
|
||||
container.style.cssText += `
|
||||
grid-auto-flow: row;
|
||||
grid-template-rows: repeat(auto, ${carouselItemSize}px);
|
||||
`;
|
||||
|
||||
await setCarouselScrollPosition("instant");
|
||||
|
||||
assert.strictEqual(
|
||||
container.scrollTop,
|
||||
expected,
|
||||
"scrolls carousel to center of active item (vertical)"
|
||||
);
|
||||
});
|
||||
|
||||
test("test scroll animation", async function (assert) {
|
||||
assert.ok(true);
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,59 @@
|
|||
import {
|
||||
getSiteThemeColor,
|
||||
setSiteThemeColor,
|
||||
} from "discourse/lib/lightbox/helpers";
|
||||
import { module, test } from "qunit";
|
||||
|
||||
import sinon from "sinon";
|
||||
|
||||
module(
|
||||
"Unit | lib | Experimental Lightbox | Helpers | getSiteThemeColor()",
|
||||
function () {
|
||||
test("gets the correct site theme color", async function (assert) {
|
||||
const querySelectorSpy = sinon.spy(document, "querySelector");
|
||||
const MetaSiteColorStub = sinon.stub(
|
||||
HTMLMetaElement.prototype,
|
||||
"content"
|
||||
);
|
||||
|
||||
MetaSiteColorStub.value("#ff0000");
|
||||
|
||||
const themeColor = await getSiteThemeColor();
|
||||
|
||||
assert.strictEqual(
|
||||
querySelectorSpy.calledWith('meta[name="theme-color"]'),
|
||||
true,
|
||||
"Queries the correct element"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
themeColor,
|
||||
"#ff0000",
|
||||
"returns the correct theme color"
|
||||
);
|
||||
|
||||
querySelectorSpy.restore();
|
||||
MetaSiteColorStub.restore();
|
||||
});
|
||||
|
||||
test("sets the site theme color correctly", async function (assert) {
|
||||
const querySelectorSpy = sinon.spy(document, "querySelector");
|
||||
|
||||
await setSiteThemeColor("0000ff");
|
||||
|
||||
assert.strictEqual(
|
||||
querySelectorSpy.calledWith('meta[name="theme-color"]'),
|
||||
true,
|
||||
"queries the correct element"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
querySelectorSpy.returnValues[0].content,
|
||||
"#0000ff",
|
||||
"sets the correct theme color"
|
||||
);
|
||||
|
||||
querySelectorSpy.restore();
|
||||
});
|
||||
}
|
||||
);
|
|
@ -0,0 +1,279 @@
|
|||
import {
|
||||
LIGHTBOX_IMAGE_FIXTURES,
|
||||
generateImageUploaderMarkup,
|
||||
generateLightboxMarkup,
|
||||
} from "discourse/tests/helpers/lightbox-helpers";
|
||||
import { module, test } from "qunit";
|
||||
|
||||
import { SELECTORS } from "discourse/lib/lightbox/constants";
|
||||
import domFromString from "discourse-common/lib/dom-from-string";
|
||||
import { processHTML } from "discourse/lib/lightbox/process-html";
|
||||
|
||||
module("Unit | lib | Experimental lightbox | processHTML()", function () {
|
||||
const wrap = domFromString(generateLightboxMarkup())[0];
|
||||
const imageUploaderWrap = domFromString(generateImageUploaderMarkup())[0];
|
||||
const selector = SELECTORS.DEFAULT_ITEM_SELECTOR;
|
||||
|
||||
test("returns the correct object from the proccessed element", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
const { items, startingIndex } = await processHTML({
|
||||
container,
|
||||
selector,
|
||||
});
|
||||
|
||||
assert.strictEqual(items.length, 1);
|
||||
|
||||
const item = items[0];
|
||||
|
||||
assert.strictEqual(
|
||||
item.fullsizeURL,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL
|
||||
);
|
||||
|
||||
assert.strictEqual(item.smallURL, LIGHTBOX_IMAGE_FIXTURES.first.smallURL);
|
||||
|
||||
assert.strictEqual(
|
||||
item.downloadURL,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.downloadURL
|
||||
);
|
||||
|
||||
assert.strictEqual(items[0].title, LIGHTBOX_IMAGE_FIXTURES.first.title);
|
||||
|
||||
assert.strictEqual(
|
||||
item.fileDetails,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.fileDetails
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
item.dominantColor,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.dominantColor
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
item.aspectRatio,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.aspectRatio
|
||||
);
|
||||
|
||||
assert.strictEqual(item.index, LIGHTBOX_IMAGE_FIXTURES.first.index);
|
||||
|
||||
assert.strictEqual(
|
||||
item.cssVars.string,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.cssVars.string
|
||||
);
|
||||
|
||||
assert.strictEqual(startingIndex, 0);
|
||||
});
|
||||
|
||||
test("returns the correct number of items", async function (assert) {
|
||||
const htmlString = generateLightboxMarkup().repeat(3);
|
||||
const container = domFromString(htmlString);
|
||||
|
||||
const outer = document.createElement("div");
|
||||
outer.append(...container);
|
||||
|
||||
const { items } = await processHTML({
|
||||
container: outer,
|
||||
selector,
|
||||
});
|
||||
|
||||
assert.strictEqual(items.length, 3);
|
||||
});
|
||||
|
||||
test("fallsback to src when no href is defined for fullsizeURL", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
container.querySelector("a").removeAttribute("href");
|
||||
|
||||
const { items } = await processHTML({
|
||||
container,
|
||||
selector,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
items[0].fullsizeURL,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.smallURL
|
||||
);
|
||||
});
|
||||
|
||||
test("handles title fallbacks", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
container.querySelector("a").removeAttribute("title");
|
||||
|
||||
let { items } = await processHTML({
|
||||
container,
|
||||
selector,
|
||||
});
|
||||
|
||||
assert.strictEqual(items[0].title, LIGHTBOX_IMAGE_FIXTURES.first.title);
|
||||
|
||||
container.querySelector("img").removeAttribute("title");
|
||||
|
||||
({ items } = await processHTML({
|
||||
container,
|
||||
selector,
|
||||
}));
|
||||
|
||||
assert.strictEqual(items[0].title, LIGHTBOX_IMAGE_FIXTURES.first.alt);
|
||||
|
||||
container.querySelector("img").removeAttribute("alt");
|
||||
|
||||
({ items } = await processHTML({
|
||||
container,
|
||||
selector,
|
||||
}));
|
||||
|
||||
assert.strictEqual(items[0].title, "");
|
||||
});
|
||||
|
||||
test("correctly escapes the title", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
container
|
||||
.querySelector("a")
|
||||
.setAttribute("title", `"><\x00script>javascript:alert(1)</script>`);
|
||||
|
||||
const { items } = await processHTML({
|
||||
container,
|
||||
selector,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
items[0].title,
|
||||
`"><\x00script>javascript:alert(1)</script>`
|
||||
);
|
||||
});
|
||||
|
||||
test("handles missing aspect ratio", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
container.querySelector("img").style.removeProperty("aspect-ratio");
|
||||
|
||||
const { items } = await processHTML({
|
||||
container,
|
||||
selector,
|
||||
});
|
||||
|
||||
assert.strictEqual(items[0].aspectRatio, null);
|
||||
|
||||
assert.strictEqual(
|
||||
items[0].cssVars.string,
|
||||
`--dominant-color: #${LIGHTBOX_IMAGE_FIXTURES.first.dominantColor};--small-url: url(${LIGHTBOX_IMAGE_FIXTURES.first.smallURL});`
|
||||
);
|
||||
});
|
||||
|
||||
test("handles missing file details", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
container.querySelector(SELECTORS.FILE_DETAILS_CONTAINER).remove();
|
||||
|
||||
const { items } = await processHTML({
|
||||
container,
|
||||
selector,
|
||||
});
|
||||
|
||||
assert.strictEqual(items[0].fileDetails, null);
|
||||
});
|
||||
|
||||
test("handles missing dominant color", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
container.querySelector("img").removeAttribute("data-dominant-color");
|
||||
|
||||
const { items } = await processHTML({
|
||||
container,
|
||||
selector,
|
||||
});
|
||||
|
||||
assert.strictEqual(items[0].dominantColor, null);
|
||||
|
||||
assert.strictEqual(
|
||||
items[0].cssVars.string,
|
||||
`--aspect-ratio: ${LIGHTBOX_IMAGE_FIXTURES.first.aspectRatio};--small-url: url(${LIGHTBOX_IMAGE_FIXTURES.first.smallURL});`
|
||||
);
|
||||
});
|
||||
|
||||
test("falls back to href when data-download is not defined", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
container.querySelector("a").removeAttribute("data-download-href");
|
||||
|
||||
const { items } = await processHTML({
|
||||
container,
|
||||
selector,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
items[0].downloadURL,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL
|
||||
);
|
||||
});
|
||||
|
||||
test("handles missing selector", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
const { items } = await processHTML({
|
||||
container,
|
||||
});
|
||||
|
||||
assert.strictEqual(items.length, 1);
|
||||
});
|
||||
|
||||
test("handles custom selector", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
container.querySelector("a").classList.add("custom-selector");
|
||||
|
||||
const { items } = await processHTML({
|
||||
container,
|
||||
selector: ".custom-selector",
|
||||
});
|
||||
|
||||
assert.strictEqual(items.length, 1);
|
||||
});
|
||||
|
||||
test("returns the correct object for image uploader components", async function (assert) {
|
||||
const container = imageUploaderWrap.cloneNode(true);
|
||||
|
||||
const { items } = await processHTML({
|
||||
container,
|
||||
selector,
|
||||
});
|
||||
|
||||
const item = items[0];
|
||||
|
||||
assert.strictEqual(items.length, 1);
|
||||
|
||||
assert.strictEqual(item.title, "");
|
||||
|
||||
assert.strictEqual(item.aspectRatio, null);
|
||||
|
||||
assert.strictEqual(item.dominantColor, null);
|
||||
|
||||
assert.strictEqual(item.fileDetails, "x");
|
||||
|
||||
assert.strictEqual(
|
||||
item.downloadURL,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
item.smallURL,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
item.fullsizeURL,
|
||||
LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
item.cssVars.string,
|
||||
`--small-url: url(${LIGHTBOX_IMAGE_FIXTURES.first.fullsizeURL});`
|
||||
);
|
||||
});
|
||||
|
||||
test("throws missing container error when no container / nodelist is passed", async function (assert) {
|
||||
assert.rejects(processHTML({ selector }));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,187 @@
|
|||
import {
|
||||
generateLightboxMarkup,
|
||||
generateLightboxObject,
|
||||
} from "discourse/tests/helpers/lightbox-helpers";
|
||||
import { module, test } from "qunit";
|
||||
import { click } from "@ember/test-helpers";
|
||||
import { LIGHTBOX_APP_EVENT_NAMES } from "discourse/lib/lightbox/constants";
|
||||
import domFromString from "discourse-common/lib/dom-from-string";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
import { setupTest } from "ember-qunit";
|
||||
import sinon from "sinon";
|
||||
|
||||
module("Unit | Service | Experimental Lightbox", function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
const wrap = domFromString(generateLightboxMarkup())[0];
|
||||
const selector = ".lightbox";
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.lightbox = getOwner(this).lookup("service:lightbox");
|
||||
this.appEvents = getOwner(this).lookup("service:app-events");
|
||||
});
|
||||
|
||||
test("Lightbox Service has appEvents", async function (assert) {
|
||||
assert.ok(this.lightbox.appEvents);
|
||||
});
|
||||
|
||||
test("Does not add event listener if no lightboxes are found", async function (assert) {
|
||||
const container = document.createElement("div");
|
||||
const addEventListenerSpy = sinon.spy(container, "addEventListener");
|
||||
|
||||
await this.lightbox.setupLightboxes({ container, selector });
|
||||
|
||||
assert.strictEqual(
|
||||
addEventListenerSpy.called,
|
||||
false,
|
||||
"does not add event listener"
|
||||
);
|
||||
|
||||
addEventListenerSpy.restore();
|
||||
});
|
||||
|
||||
test("Adds event listener if lightboxes are found", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
const addEventListenerSpy = sinon.spy(container, "addEventListener");
|
||||
|
||||
await this.lightbox.setupLightboxes({ container, selector });
|
||||
|
||||
assert.strictEqual(
|
||||
addEventListenerSpy.calledOnce,
|
||||
true,
|
||||
"adds event listener"
|
||||
);
|
||||
|
||||
addEventListenerSpy.restore();
|
||||
});
|
||||
|
||||
test("Correctly sets event listeners", async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
const openLightboxSpy = sinon.spy(this.lightbox, "openLightbox");
|
||||
const removeEventListenerSpy = sinon.spy(container, "removeEventListener");
|
||||
|
||||
await this.lightbox.setupLightboxes({ container, selector });
|
||||
|
||||
await click(container.querySelector(selector));
|
||||
|
||||
container.appendChild(document.createElement("p"));
|
||||
|
||||
await click(container.querySelector("p"));
|
||||
|
||||
assert.strictEqual(
|
||||
openLightboxSpy.calledWith({ container, selector }),
|
||||
true,
|
||||
"calls openLightbox on lightboxed element click"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
openLightboxSpy.calledOnce,
|
||||
true,
|
||||
"only calls open lightbox when lightboxed element is clicked"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
this.lightbox.lightboxClickElements.length,
|
||||
1,
|
||||
"correctly stores lightbox click elements for cleanup"
|
||||
);
|
||||
|
||||
await this.lightbox.cleanupLightboxes();
|
||||
|
||||
assert.strictEqual(
|
||||
removeEventListenerSpy.calledOnce,
|
||||
true,
|
||||
"removes event listener from element on cleanup"
|
||||
);
|
||||
|
||||
removeEventListenerSpy.restore();
|
||||
|
||||
assert.strictEqual(
|
||||
this.lightbox.lightboxClickElements.length,
|
||||
0,
|
||||
"correctly removes stored entry from lightboxClickElements on cleanup"
|
||||
);
|
||||
|
||||
openLightboxSpy.restore();
|
||||
removeEventListenerSpy.restore();
|
||||
});
|
||||
|
||||
test(`correctly calls the lightbox:open event`, async function (assert) {
|
||||
const done = assert.async();
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
await this.lightbox.setupLightboxes({ container, selector });
|
||||
|
||||
const appEventsTriggerSpy = sinon.spy(this.appEvents, "trigger");
|
||||
|
||||
const expectedObject = {
|
||||
...generateLightboxObject(),
|
||||
options: this.lightbox.options,
|
||||
callbacks: this.lightbox.callbacks,
|
||||
};
|
||||
|
||||
const expectedEvent = LIGHTBOX_APP_EVENT_NAMES.OPEN;
|
||||
|
||||
this.appEvents.on(LIGHTBOX_APP_EVENT_NAMES.OPEN, (args) => {
|
||||
assert.deepEqual(args, expectedObject);
|
||||
done();
|
||||
});
|
||||
|
||||
await click(container.querySelector(selector));
|
||||
|
||||
assert.ok(appEventsTriggerSpy.calledWith(expectedEvent));
|
||||
|
||||
appEventsTriggerSpy.restore();
|
||||
});
|
||||
|
||||
test(`correctly calls the lightbox:close event`, async function (assert) {
|
||||
const done = assert.async();
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
await this.lightbox.setupLightboxes({ container, selector });
|
||||
|
||||
this.appEvents.on(LIGHTBOX_APP_EVENT_NAMES.CLOSE, () => {
|
||||
assert.ok(true);
|
||||
done();
|
||||
});
|
||||
|
||||
await click(container.querySelector(selector));
|
||||
|
||||
await this.lightbox.closeLightbox();
|
||||
});
|
||||
|
||||
test(`correctly responds to the lightbox:clean event`, async function (assert) {
|
||||
const container = wrap.cloneNode(true);
|
||||
|
||||
await this.lightbox.setupLightboxes({ container, selector });
|
||||
|
||||
await click(container.querySelector(".lightbox"));
|
||||
|
||||
assert.strictEqual(
|
||||
this.lightbox.lightboxClickElements.length,
|
||||
1,
|
||||
"correctly stores lightbox click elements for cleanup"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
this.lightbox.lightboxIsOpen,
|
||||
true,
|
||||
"sets lightboxIsOpen to true"
|
||||
);
|
||||
|
||||
this.appEvents.trigger(LIGHTBOX_APP_EVENT_NAMES.CLEAN);
|
||||
|
||||
assert.strictEqual(
|
||||
this.lightbox.lightboxClickElements.length,
|
||||
0,
|
||||
"correctly removes stored entry from lightboxClickElements on cleanup"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
this.lightbox.lightboxIsOpen,
|
||||
false,
|
||||
"sets lightboxIsOpen to false"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -7,6 +7,7 @@
|
|||
@import "char-counter";
|
||||
@import "conditional-loading-section";
|
||||
@import "convert-to-public-topic-modal";
|
||||
@import "d-lightbox";
|
||||
@import "d-tooltip";
|
||||
@import "d-toggle-switch";
|
||||
@import "date-input";
|
||||
|
|
|
@ -0,0 +1,795 @@
|
|||
/* Main document */
|
||||
|
||||
html.has-lightbox {
|
||||
overflow-y: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profiler-results {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#main {
|
||||
padding-right: var(--document-scrollbar-width);
|
||||
}
|
||||
|
||||
--carousel-item-size: clamp(6.5rem, 12vh, 15rem);
|
||||
--d-lightbox-primary: #ffffff;
|
||||
--d-lightbox-secondary: #000000;
|
||||
--d-lightbox-secondary-translucent: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* Lightbox element*/
|
||||
|
||||
// Grid
|
||||
|
||||
.d-lightbox {
|
||||
overscroll-behavior: contain;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&.is-visible {
|
||||
display: grid;
|
||||
place-items: stretch;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: grid;
|
||||
place-items: stretch;
|
||||
}
|
||||
|
||||
&.is-vertical &__content {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr auto auto;
|
||||
|
||||
grid-template-areas:
|
||||
"lightbox-header"
|
||||
"lightbox-body"
|
||||
"lightbox-carousel"
|
||||
"lightbox-footer";
|
||||
}
|
||||
|
||||
&.is-horizontal &__content {
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
grid-template-areas:
|
||||
"lightbox-carousel lightbox-header"
|
||||
"lightbox-carousel lightbox-body"
|
||||
"lightbox-carousel lightbox-footer";
|
||||
}
|
||||
|
||||
&__header {
|
||||
grid-area: lightbox-header;
|
||||
justify-self: stretch;
|
||||
display: grid;
|
||||
place-items: center start;
|
||||
grid-template-areas: "preview-controls lightbox-header-buttons";
|
||||
}
|
||||
|
||||
&__multi-item-controls {
|
||||
grid-area: preview-controls;
|
||||
place-self: center start;
|
||||
display: grid;
|
||||
gap: 0.5em;
|
||||
place-items: center start;
|
||||
grid-auto-flow: column;
|
||||
grid-template-areas: "lightbox-carousel-toggle lightbox-counter";
|
||||
|
||||
.d-lightbox__carousel-button {
|
||||
grid-area: lightbox-carousel-toggle;
|
||||
}
|
||||
}
|
||||
|
||||
&__counters {
|
||||
grid-area: lightbox-counter;
|
||||
}
|
||||
|
||||
&__header-buttons {
|
||||
grid-area: lightbox-header-buttons;
|
||||
place-self: center end;
|
||||
display: grid;
|
||||
gap: 0 0.25em;
|
||||
place-items: center end;
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
&__body {
|
||||
grid-area: lightbox-body;
|
||||
display: grid;
|
||||
place-items: center stretch;
|
||||
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-areas: "lightbox-previous-button lightbox-main-image lightbox-next-button";
|
||||
}
|
||||
|
||||
&__main-image,
|
||||
&__error-message,
|
||||
&__loading-spinner {
|
||||
grid-area: lightbox-main-image;
|
||||
place-self: center;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
&__previous-button {
|
||||
grid-area: lightbox-previous-button;
|
||||
place-self: center start;
|
||||
}
|
||||
|
||||
&__next-button {
|
||||
grid-area: lightbox-next-button;
|
||||
place-self: center end;
|
||||
}
|
||||
|
||||
&.has-carousel &,
|
||||
&.is-rotated &,
|
||||
&.is-zoomed & {
|
||||
&__body {
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas: "lightbox-main-image";
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
grid-area: lightbox-footer;
|
||||
place-self: stretch;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 1.5em;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-areas: "lightbox-title lightbox-footer-buttons";
|
||||
}
|
||||
|
||||
&__main-title {
|
||||
grid-area: lightbox-title;
|
||||
place-self: center start;
|
||||
}
|
||||
|
||||
&__footer-buttons {
|
||||
grid-area: lightbox-footer-buttons;
|
||||
place-self: center end;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 0 0.25em;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
&__carousel {
|
||||
grid-area: lightbox-carousel;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
&__carousel-items {
|
||||
grid-area: lightbox-preview-images;
|
||||
display: grid;
|
||||
gap: 1em;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
&__carousel-previous-button {
|
||||
grid-area: lightbox-previous;
|
||||
}
|
||||
|
||||
&__carousel-next-button {
|
||||
grid-area: lightbox-next;
|
||||
}
|
||||
|
||||
&.is-vertical &__carousel {
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
grid-template-areas: "lightbox-previous lightbox-preview-images lightbox-next";
|
||||
}
|
||||
|
||||
&__carousel-items {
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
&__carousel-item,
|
||||
&__carousel-item.is-current {
|
||||
height: var(--carousel-item-size);
|
||||
}
|
||||
|
||||
&.is-horizontal &__carousel {
|
||||
max-height: 100vh;
|
||||
grid-template-rows: 1fr auto 1fr;
|
||||
place-items: center;
|
||||
grid-template-areas:
|
||||
"lightbox-previous"
|
||||
"lightbox-preview-images"
|
||||
"lightbox-next";
|
||||
}
|
||||
|
||||
&.is-horizontal &__carousel-items {
|
||||
grid-auto-flow: row;
|
||||
}
|
||||
|
||||
&__carousel-item,
|
||||
&__carousel-item.is-current {
|
||||
width: var(--carousel-item-size);
|
||||
}
|
||||
|
||||
&__carousel-previous-button {
|
||||
place-self: self-end center;
|
||||
}
|
||||
|
||||
&__carousel-next-button {
|
||||
place-self: self-start center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
|
||||
.d-lightbox {
|
||||
&.is-visible {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: z("header");
|
||||
}
|
||||
|
||||
&__content {
|
||||
background-color: var(--d-lightbox-secondary);
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-lightbox {
|
||||
&__header {
|
||||
z-index: 1;
|
||||
padding: 0.25em 1px; // 1px for button outlines
|
||||
color: var(--d-lightbox-primary);
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--d-lightbox-secondary),
|
||||
transparent
|
||||
);
|
||||
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.d-lightbox {
|
||||
&__body {
|
||||
@include user-select(none);
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&__loading-spinner,
|
||||
&__error-message,
|
||||
&__main-image {
|
||||
position: absolute;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&__backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
filter: blur(15px) brightness(0.25);
|
||||
background-color: var(--d-lightbox-secondary);
|
||||
background-image: var(--d-lightbox-image-small-url);
|
||||
}
|
||||
|
||||
&__main-image {
|
||||
aspect-ratio: var(--d-lightbox-image-aspect-ratio);
|
||||
background-color: var(--d-lightbox-image-dominant-color);
|
||||
box-sizing: border-box;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
box-shadow: shadow("card");
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
color: var(--d-lightbox-primary);
|
||||
}
|
||||
|
||||
&__previous-button,
|
||||
&__next-button {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
|
||||
.d-icon {
|
||||
font-size: var(--font-up-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-lightbox {
|
||||
&__footer {
|
||||
z-index: 1;
|
||||
padding: 0.25em 1px;
|
||||
box-sizing: border-box;
|
||||
color: var(--d-lightbox-primary);
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
var(--d-lightbox-secondary),
|
||||
transparent
|
||||
);
|
||||
|
||||
position: relative;
|
||||
|
||||
&__main-title {
|
||||
word-break: break-word;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
// right/left padding to align with buttons
|
||||
padding: 0 0.65em;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
border: 1px solid var(--tertiary);
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__item-file-details {
|
||||
font-size: var(--font-down-2);
|
||||
opacity: 0.5;
|
||||
padding: 0.25em 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.d-lightbox {
|
||||
&__carousel {
|
||||
@include user-select(none);
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
color: var(--d-lightbox-primary);
|
||||
}
|
||||
&__carousel-previous-button,
|
||||
&__carousel-next-button {
|
||||
z-index: 2;
|
||||
|
||||
.d-icon {
|
||||
font-size: var(--font-up-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-lightbox {
|
||||
&__carousel-items {
|
||||
@extend %lightbox-scrollable;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
z-index: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__carousel-item,
|
||||
&__carousel-item.is-current {
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 0 2px -1px var(--d-lightbox-primary);
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all var(--d-lightbox-image-animation-duration) ease-in-out;
|
||||
background-color: var(--dominant-color);
|
||||
aspect-ratio: var(--aspect-ratio);
|
||||
filter: saturate(0.5);
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__carousel-item.is-current {
|
||||
border-color: var(--tertiary);
|
||||
pointer-events: none;
|
||||
filter: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.d-lightbox {
|
||||
&.is-vertical &__carousel {
|
||||
position: relative;
|
||||
background-color: var(--d-lightbox-secondary-translucent);
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
height: 100%;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
&:before {
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: var(--d-lightbox-secondary);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
90deg,
|
||||
var(--d-lightbox-secondary),
|
||||
transparent
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
90deg,
|
||||
var(--d-lightbox-secondary),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
&:after {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--d-lightbox-secondary);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
-90deg,
|
||||
var(--d-lightbox-secondary),
|
||||
transparent
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
-90deg,
|
||||
var(--d-lightbox-secondary),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.rtl & {
|
||||
&:before {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-vertical &__carousel-items {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
&__carousel-previous-button,
|
||||
&__carousel-next-button {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.d-lightbox {
|
||||
&.is-horizontal &__carousel {
|
||||
position: relative;
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
height: 20%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:before {
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: var(--d-lightbox-secondary);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
180deg,
|
||||
var(--d-lightbox-secondary),
|
||||
transparent
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
180deg,
|
||||
var(--d-lightbox-secondary) 20%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
&:after {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--d-lightbox-secondary);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
0,
|
||||
var(--d-lightbox-secondary),
|
||||
transparent
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
0,
|
||||
var(--d-lightbox-secondary) 20%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-horizontal &__carousel-items {
|
||||
padding: 50% 1em;
|
||||
}
|
||||
|
||||
&__carousel-previous-button,
|
||||
&__carousel-next-button {
|
||||
transform: rotate(90deg);
|
||||
|
||||
.rtl & {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-lightbox {
|
||||
.btn-flat {
|
||||
.d-icon {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: transparent;
|
||||
outline: none;
|
||||
|
||||
.d-icon {
|
||||
color: var(--d-lightbox-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 1px solid var(--tertiary);
|
||||
border-radius: 1px;
|
||||
.d-icon {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
@include hover {
|
||||
.d-icon {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.d-lightbox {
|
||||
&__focus-trap,
|
||||
&__screen-reader-announcer {
|
||||
position: absolute;
|
||||
left: -100%;
|
||||
top: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* State styles */
|
||||
|
||||
// Carousel
|
||||
.d-lightbox {
|
||||
&.has-carousel {
|
||||
.d-lightbox__content {
|
||||
gap: 2em 0;
|
||||
}
|
||||
|
||||
.btn-flat.d-lightbox__carousel-button .d-icon {
|
||||
color: var(--tertiary);
|
||||
|
||||
@include hover {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// expanded title
|
||||
.d-lightbox {
|
||||
&.has-expanded-title {
|
||||
.d-lightbox__content {
|
||||
gap: 2em;
|
||||
}
|
||||
|
||||
.d-lightbox__main-title {
|
||||
overflow: visible;
|
||||
white-space: normal;
|
||||
text-overflow: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom
|
||||
.d-lightbox {
|
||||
&.can-zoom {
|
||||
.d-lightbox__body {
|
||||
@extend %lightbox-scrollable;
|
||||
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.d-lightbox__main-image {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-zoomed {
|
||||
.d-lightbox__content {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.d-lightbox {
|
||||
&__body {
|
||||
@extend %lightbox-scrollable;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&__zoomed-image-container {
|
||||
cursor: zoom-out;
|
||||
background-image: var(--d-lightbox-image-full-size-url);
|
||||
background-repeat: no-repeat;
|
||||
backface-visibility: hidden;
|
||||
background-position: center;
|
||||
|
||||
.mobile-view & {
|
||||
width: var(--d-lightbox-image-width);
|
||||
height: var(--d-lightbox-image-height);
|
||||
}
|
||||
|
||||
.desktop-view & {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-flat.d-lightbox__zoom-button .d-icon {
|
||||
color: var(--tertiary);
|
||||
|
||||
@include hover {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate
|
||||
.d-lightbox {
|
||||
&.is-rotated {
|
||||
.d-lightbox__body {
|
||||
gap: 0;
|
||||
}
|
||||
.d-lightbox__main-image,
|
||||
.d-lightbox__zoomed-image-container {
|
||||
transform: rotate(var(--d-lightbox-image-rotation));
|
||||
}
|
||||
|
||||
.btn-flat.d-lightbox__rotate-button .d-icon {
|
||||
color: var(--tertiary);
|
||||
transform: rotate(var(--d-lightbox-image-rotation));
|
||||
|
||||
@include hover {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full screen
|
||||
.d-lightbox {
|
||||
&.is-fullscreen {
|
||||
.btn-flat.d-lightbox__full-screen-button .d-icon {
|
||||
color: var(--tertiary);
|
||||
|
||||
@include hover {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
|
||||
.d-lightbox {
|
||||
&.is-visible &__content {
|
||||
@extend %lightbox-animation-base;
|
||||
animation-name: lightbox-fade-in-scale;
|
||||
}
|
||||
|
||||
&__backdrop,
|
||||
&__main-image,
|
||||
&__loading-spinner,
|
||||
&__zoomed-image-container,
|
||||
&__carousel,
|
||||
&__main-title {
|
||||
@extend %lightbox-animation-base;
|
||||
animation-name: lightbox-fade-in;
|
||||
}
|
||||
|
||||
&__loading-spinner {
|
||||
animation-delay: var(--d-lightbox-image-animation-duration);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.will-close &__content {
|
||||
animation-name: lightbox-fade-out;
|
||||
}
|
||||
}
|
||||
|
||||
%lightbox-animation-base {
|
||||
animation-duration: 150ms;
|
||||
animation-fill-mode: forwards;
|
||||
backface-visibility: hidden;
|
||||
will-change: opacity;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
%lightbox-scrollable {
|
||||
backface-visibility: hidden;
|
||||
|
||||
// MOZ
|
||||
scrollbar-width: none;
|
||||
overflow: auto;
|
||||
// Webkit
|
||||
overflow: overlay;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lightbox-fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lightbox-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lightbox-fade-in-scale {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.85);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lightbox-fade-out-scale {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.85);
|
||||
}
|
||||
}
|
|
@ -4040,6 +4040,21 @@ en:
|
|||
content_load_error: '<a href="%url%">The content</a> could not be loaded.'
|
||||
image_load_error: '<a href="%url%">The image</a> could not be loaded.'
|
||||
|
||||
experimental_lightbox:
|
||||
image_load_error: "The image could not be loaded."
|
||||
screen_reader_image_title: "Image %{current} of %{total}: %{title}"
|
||||
buttons:
|
||||
next: "Next (Right or down arrow key)"
|
||||
previous: "Previous (Left or up arrow key)"
|
||||
close: "Close (Esc)"
|
||||
download: "Download image"
|
||||
newtab: "Open image in a new tab"
|
||||
zoom: "Zoom image in/out (Z key)"
|
||||
rotate: "Rotate image (R key)"
|
||||
fullscreen: "Toggle browser full screen mode (M key)"
|
||||
carousel: "Display all images in a carousel (A key)"
|
||||
retry: "Retry loading the image"
|
||||
|
||||
cannot_render_video: This video cannot be rendered because your browser does not support the codec.
|
||||
|
||||
keyboard_shortcuts_help:
|
||||
|
|
|
@ -2439,6 +2439,7 @@ en:
|
|||
enable_custom_sidebar_sections: "EXPERIMENTAL: Enable custom sidebar sections"
|
||||
experimental_topics_filter: "EXPERIMENTAL: Enables the experimental topics filter route at /filter"
|
||||
experimental_search_menu_groups: "EXPERIMENTAL: Enables the new search menu that has been upgraded to use glimmer"
|
||||
enable_experimental_lightbox: "EXPERIMENTAL: Replace the default image lightbox with the revamped design."
|
||||
|
||||
page_loading_indicator: "Configure the loading indicator which appears during page navigations within Discourse. 'Spinner' is a full page indicator. 'Slider' shows a narrow bar at the top of the screen."
|
||||
|
||||
|
|
|
@ -2130,6 +2130,9 @@ developer:
|
|||
default: ""
|
||||
allow_any: false
|
||||
refresh: true
|
||||
enable_experimental_lightbox:
|
||||
default: false
|
||||
client: true
|
||||
experimental_topics_filter:
|
||||
client: true
|
||||
default: false
|
||||
|
|
|
@ -149,6 +149,7 @@ module SvgSprite
|
|||
hourglass-start
|
||||
id-card
|
||||
image
|
||||
images
|
||||
inbox
|
||||
info-circle
|
||||
italic
|
||||
|
@ -187,6 +188,8 @@ module SvgSprite
|
|||
reply
|
||||
rocket
|
||||
search
|
||||
search-plus
|
||||
search-minus
|
||||
share
|
||||
shield-alt
|
||||
sign-in-alt
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
src={{@upload.url}}
|
||||
style={{this.imageStyle}}
|
||||
loading="lazy"
|
||||
tabindex="0"
|
||||
{{on "load" this.imageLoaded}}
|
||||
/>
|
||||
{{else if (eq this.type this.VIDEO_TYPE)}}
|
||||
|
|
|
@ -13,7 +13,9 @@ export default {
|
|||
|
||||
initializeWithPluginApi(api, container) {
|
||||
const siteSettings = container.lookup("service:site-settings");
|
||||
const lightboxService = container.lookup("service:lightbox");
|
||||
const site = container.lookup("service:site");
|
||||
|
||||
api.decorateChatMessage((element) => decorateGithubOneboxBody(element), {
|
||||
id: "onebox-github-body",
|
||||
});
|
||||
|
@ -65,14 +67,27 @@ export default {
|
|||
id: "linksNewTab",
|
||||
});
|
||||
|
||||
api.decorateChatMessage(
|
||||
(element) =>
|
||||
this.lightbox(element.querySelectorAll("img:not(.emoji, .avatar)")),
|
||||
{
|
||||
id: "lightbox",
|
||||
}
|
||||
);
|
||||
|
||||
if (siteSettings.enable_experimental_lightbox) {
|
||||
api.decorateChatMessage(
|
||||
(element) => {
|
||||
lightboxService.setupLightboxes({
|
||||
container: element,
|
||||
selector: "img:not(.emoji, .avatar)",
|
||||
});
|
||||
},
|
||||
{
|
||||
id: "experimental-chat-lightbox",
|
||||
}
|
||||
);
|
||||
} else {
|
||||
api.decorateChatMessage(
|
||||
(element) =>
|
||||
this.lightbox(element.querySelectorAll("img:not(.emoji, .avatar)")),
|
||||
{
|
||||
id: "lightbox",
|
||||
}
|
||||
);
|
||||
}
|
||||
api.decorateChatMessage((element) => decorateHashtags(element, site), {
|
||||
id: "hashtagIcons",
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue