diff --git a/demo/sites/components/lightbox.html b/demo/sites/components/lightbox.html new file mode 100644 index 000000000..49a67f31b --- /dev/null +++ b/demo/sites/components/lightbox.html @@ -0,0 +1,313 @@ + + + + + + + + Album example · Bootstrap v5.1 + + + + + + + + + + + + + + + + +
+
+
+
+

+ Basic example +

+ +
+
+ Table Full of Spices +
+
+ Winter Landscape +
+
+ View of the City in the Mountains +
+
+
+
+
+

+ Shadows and rounded corners +

+ +
+
+ Table Full of Spices +
+
+ Winter Landscape +
+
+ View of the City in the Mountains +
+
+
+
+
+

+ Different sizes +

+ +
+
+ Table Full of Spices + Coconut with Strawberries +
+
+ Dark Roast Iced Coffee +
+
+
+
+
+

+ Zoom level +

+
+
+ Table Full of Spices +
+
+ Winter Landscape +
+
+ View of the City in the Mountains +
+
+
+
+
+

+ Disabled image +

+
+
+ Table Full of Spices +
+
+ Winter Landscape +
+
+ Disabled image +
+
+ Place Royale Bruxelles +
+
+
+
+
+

+ Captions +

+
+
+ Table Full of Spices +
+
+ Winter Landscape +
+
+ View of the City in the Mountains +
+
+
+
+
+

+ Outside access +

+ +
+ +
+
+
+
+
+ + + + + + + + + + + + + + diff --git a/index.html b/index.html index 040bc248d..842b48b7e 100644 --- a/index.html +++ b/index.html @@ -424,6 +424,11 @@

Your sites:

class="mb-1 mr-1 rounded bg-primary px-6 pb-2 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-[0_4px_9px_-4px_#3b71ca] transition duration-150 ease-in-out hover:bg-primary-600 hover:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:bg-primary-600 focus:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)] focus:outline-none focus:ring-0 active:bg-primary-700 active:shadow-[0_8px_9px_-4px_rgba(59,113,202,0.3),0_4px_18px_0_rgba(59,113,202,0.2)]" >Datatables + Lightbox
diff --git a/site/content/docs/standard/components/lightbox/a-ss.html b/site/content/docs/standard/components/lightbox/a-ss.html new file mode 100644 index 000000000..f5cddf86e --- /dev/null +++ b/site/content/docs/standard/components/lightbox/a-ss.html @@ -0,0 +1,21 @@ +--- +--- + +
  • + Import +
  • +
  • + Usage +
  • +
  • + Options +
  • +
  • + Classes +
  • +
  • + Methods +
  • +
  • + Events +
  • diff --git a/site/content/docs/standard/components/lightbox/a.html b/site/content/docs/standard/components/lightbox/a.html new file mode 100644 index 000000000..b9736c077 --- /dev/null +++ b/site/content/docs/standard/components/lightbox/a.html @@ -0,0 +1,1197 @@ +--- +--- + + +
    + +
    + +

    + Import +

    + +

    + Importing components depends on how your application works. If you intend + to use the Tailwind Elements ES format, you must first import + the component and then initialize it with the initTE method. + If you are going to use the UMD format, just import the + tw-elements package. +

    + + + {{< twsnippet/no-demo id="api-example1" >}} + + +
    + + +
    + + +
    + +

    + Usage +

    + +

    Via data attributes

    + +

    + Using the Lightbox component doesn't require any additional JavaScript + code - simply add a div wrapper with a + data-te-lightbox-init attribute and use other data attributes + to set all options. For ES format, you must first import and + call the initTE method. +

    + + + {{< twsnippet/no-demo id="api-example2" >}} + + + +

    Via JavaScript

    + + + {{< twsnippet/no-demo id="api-example3" >}} + + + +
    + +
    + + +
    + +

    + Options +

    + +

    + Options can be passed via data attributes or JavaScript. For data + attributes, append the option name to data-te-, as in + data-te-caption="". +

    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Type + + Default + Description
    + caption + + String + + - + + Defines caption of the image. If not set, the value of + alt attribute is taken. +
    + closeIcon + + String + + <svg xmlns="http://www.w3.org/2000/svg" fill="none" + viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" + class="w-4 h-4"> <path stroke-linecap="round" + stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /> + </svg> + + Defines close icon template of the lightbox. +
    + container + + String + + "body" + + Allows to set the parent element for the lightbox. +
    + hideFullscreenIconTemplate + + String + + <svg xmlns="http://www.w3.org/2000/svg" fill="none" + viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" + class="w-4 h-4"> <path stroke-linecap="round" + stroke-linejoin="round" d="M9 9V4.5M9 9H4.5M9 9L3.75 + 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 + 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5l5.25 5.25" + /> </svg> + + Defines hide fullscreen icon template of the lightbox. +
    + img + + String + + - + + Defines regular image to load when gallery is open. If not + set, the value of src attribute is taken. +
    + nextIconTemplate + + String + + <svg xmlns="http://www.w3.org/2000/svg" fill="none" + viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" + class="w-4 h-4"> <path stroke-linecap="round" + stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 + 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 + 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 + 0v-4.5m0 4.5L15 15" /> </svg> + + Defines next icon template of the lightbox. +
    + prevIconTemplate + + String + + <svg xmlns="http://www.w3.org/2000/svg" + fill="currentColor" viewBox="0 0 24 24" stroke-width="3" + stroke="currentColor" class="w-4 h-4"> <path + stroke-linecap="round" stroke-linejoin="round" d="M19.5 + 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" /> </svg> + + Defines prev icon template of the lightbox. +
    + showFullscreenIconTemplate + + String + + <svg xmlns="http://www.w3.org/2000/svg" fill="none" + viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" + class="w-4 h-4"> <path stroke-linecap="round" + stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 + 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 + 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 + 0v-4.5m0 4.5L15 15" /> </svg> + + Defines show fullscreen icon template of the lightbox. +
    + spinnerContent + + String + + "Loading..." + + Defines the text of a spinner +
    + zoomLevel + + Number/String + + 1 + + Defines zoom level while zooming in or out. +
    + zoomInIconTemplate + + String + + <svg xmlns="http://www.w3.org/2000/svg" fill="none" + viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" + class="w-4 h-4"> <path stroke-linecap="round" + stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 + 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 + 7.5v6m3-3h-6" /> </svg> + + Defines zoom in icon template of the lightbox. +
    + zoomOutIconTemplate + + String + + <svg xmlns="http://www.w3.org/2000/svg" fill="none" + viewBox="0 0 24 24" stroke-width="3" stroke="currentColor" + class="w-4 h-4"> <path stroke-linecap="round" + stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 + 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM13.5 10.5h-6" + /> </svg> + + Defines zoom out icon template of the lightbox. +
    +
    +
    +
    +
    +
    + + +
    + + +
    + +

    + Classes +

    + +

    + Custom classes can be passed via data attributes or JavaScript. For data + attributes, append the class name to + data-te-class, as in data-te-class-caption="". +

    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Name + + Default + Description
    + caption + + text-white text-ellipsis overflow-hidden whitespace-nowrap + mx-[10px] text-center + + Defines caption classes of the lightbox. +
    + captionWrapper + + fixed left-0 bottom-0 w-full h-[50px] flex justify-center + items-center + + Defines caption wrapper classes of the lightbox. +
    + closeBtn + + border-none bg-transparent w-[50px] h-[50px] px-4 + text-[#b3b3b3] transition-colors duration-200 ease-in-out + hover:text-white focus:text-white + motion-reduce:transition-none outline-none + + Defines close button classes of the lightbox. +
    + fullscreenBtn + + border-none bg-transparent w-[50px] h-[50px] px-4 + text-[#b3b3b3] transition-colors duration-200 ease-in-out + hover:text-white focus:text-white + motion-reduce:transition-none outline-none + + Defines fullscreen button classes of the lightbox. +
    + gallery + + invisible fixed left-0 top-0 w-full h-full z-[1100] + pointer-events-none opacity-0 bg-[#000000e6] transition-all + duration-[400ms] motion-reduce:transition-none + + Defines gallery classes of the lightbox. +
    + galleryContent + + fixed top-[50px] left-[50px] w-[calc(100%-100px)] + h-[calc(100%-100px)] + + Defines gallery content classes of the lightbox. +
    + galleryCounter + + flex justify-center items-center px-[10px] mb-0 h-full + text-[#b3b3b3] + + Defines gallery counter classes of the lightbox. +
    + img + + absolute left-0 top-0 w-full max-h-full h-auto + cursor-pointer pointer-events-auto + + Defines image classes of the lightbox. +
    + imgWrapper + + absolute top-0 left-0 w-full h-full opacity-0 transform + scale-[0.25] transition-all duration-[400ms] ease-out + pointer-events-none motion-reduce:transition-none + motion-reduce:transform-none + + Defines image wrapper classes of the lightbox. +
    + leftTools + + float-left h-full + + Defines left tools classes of the lightbox. +
    + loader + + fixed left-0 top-0 z-[2] w-full h-full text-neutral-50 + opacity-1 flex justify-center items-center + pointer-events-none transition-opacity duration-[1000ms] + motion-reduce:transition-none + + Defines loader classes of the lightbox. +
    + nextBtn + + border-none bg-transparent w-full h-[50px] flex + justify-center items-center text-[#b3b3b3] transition-colors + duration-200 ease-in-out hover:text-white focus:text-white + motion-reduce:transition-none outline-none + + Defines next button classes of the lightbox. +
    + nextBtnWrapper + + fixed right-0 top-0 w-[50px] h-full flex justify-center + items-center transition-opacity duration-[400ms] + motion-reduce:transition-none + + Defines next button wrapper classes of the lightbox. +
    + prevBtn + + border-none bg-transparent w-full h-[50px] flex + justify-center items-center text-[#b3b3b3] transition-colors + duration-200 ease-in-out hover:text-white focus:text-white + motion-reduce:transition-none outline-none + + Defines prev button classes of the lightbox. +
    + nextBtnWrapper + + fixed left-0 top-0 w-[50px] h-full flex justify-center + items-center transition-opacity duration-[400ms] + motion-reduce:transition-none + + Defines prev button wrapper classes of the lightbox. +
    + rightTools + + float-right + + Defines right tools classes of the lightbox. +
    + spinner + + inline-block h-8 w-8 + animate-[spinner-grow_0.75s_linear_infinite] rounded-full + bg-current align-[-0.125em] + motion-reduce:animate-[spinner-grow_1.5s_linear_infinite] + + Defines spinner classes of the lightbox. Spinner is visible + when image is loading. +
    + spinnerContent + + !absolute !-m-px !h-px !w-px !overflow-hidden + !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)] + + Defines spinner content classes of the lightbox. +
    + toolbar + + absolute top-0 left-0 w-full h-[50px] z-20 + transition-opacity duration-[400ms] + motion-reduce:transition-none + + Defines toolbar classes of the lightbox. +
    + vertical + + h-full max-h-full w-auto + + Defines vertical classes of the lightbox. Vertical class is + added when image height is greater than image width. +
    + zoomBtn + + border-none bg-transparent w-[50px] h-[50px] px-4 + text-[#b3b3b3] transition-colors duration-200 ease-in-out + hover:text-white focus:text-white + motion-reduce:transition-none outline-none + + Defines zoom button classes of the lightbox. +
    +
    +
    +
    +
    +
    + + +
    + + +
    + +

    + Methods +

    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Method + + Description + Example
    + close + + Closes the gallery. + + myLightbox.close() +
    + dispose + + Removes the Lightbox instance. + + myLightbox.dispose() +
    + getOrCreateInstance + + Static method which returns the lightbox instance associated + to a DOM element or create a new one in case it wasn't + initialized. + + myLightbox.getOrCreateInstance(element) +
    + getInstance + + Static method which allows to get the lightbox instance + associated with a DOM element. + + myLightbox.getInstance(element) +
    + open + + Opens the gallery and activates image set by the target key. + Default is the first image. + + myLightbox.open(1) +
    + reset + + Resets changed parameters such as position, zoom or + fullscreen. + + myLightbox.reset() +
    + slide + + Slides an image in the gallery. Available options: + left, right, first, + last. Dafault is right direction. + + myLightbox.slide('last') +
    + toggleFullscreen + + Toggles fullscreen. + + myLightbox.toggleFullscreen() +
    + zoomIn + + Zooms in active image by one level. + + myLightbox.zoomIn() +
    + zoomout + + Zooms out active image by one level. + + myLightbox.zoomOut() +
    +
    +
    +
    +
    + + + {{< twsnippet/no-demo id="api-example4" >}} + + +
    + + +
    + + +
    + +

    + Events +

    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Event type + Description
    + open.te.lightbox + + Emitted when a lightbox starts opening. +
    + opened.te.lightbox + + Emitted when a lightbox is opened. +
    + slide.te.lightbox + + Emitted when a lightbox starts sliding. +
    + slided.te.lightbox + + Emitted after an image slide. +
    + zoomIn.te.lightbox + + Emitted when an image starts zooming in. +
    + zoomedIn.te.lightbox + + Emitted after an image is zoomed in. +
    + zoomOut.te.lightbox + + Emitted when an image starts zooming out. +
    + zoomedOut.te.lightbox + + Emitted after an image is zoomed out. +
    + close.te.lightbox + + Emitted when a lightbox starts closing. +
    + closed.te.lightbox + + Emitted when a lightbox is closed. +
    +
    +
    +
    +
    + + + {{< twsnippet/no-demo id="api-example5" >}} + + +
    + +
    diff --git a/site/content/docs/standard/components/lightbox/index-js.html b/site/content/docs/standard/components/lightbox/index-js.html new file mode 100644 index 000000000..3d57930f4 --- /dev/null +++ b/site/content/docs/standard/components/lightbox/index-js.html @@ -0,0 +1,13 @@ +--- +--- + + + diff --git a/site/content/docs/standard/components/lightbox/index-ss.html b/site/content/docs/standard/components/lightbox/index-ss.html new file mode 100644 index 000000000..d925b4607 --- /dev/null +++ b/site/content/docs/standard/components/lightbox/index-ss.html @@ -0,0 +1,30 @@ +--- +--- + +
  • + Basic example +
  • +
  • + Image optimization +
  • +
  • + Shadows and rounded corners +
  • +
  • + Different sizes +
  • +
  • + Zoom level +
  • +
  • + Disabled image +
  • +
  • + Captions +
  • +
  • + Outside access +
  • +
  • + Related resources +
  • diff --git a/site/content/docs/standard/components/lightbox/index.html b/site/content/docs/standard/components/lightbox/index.html new file mode 100644 index 000000000..5a29e3cf5 --- /dev/null +++ b/site/content/docs/standard/components/lightbox/index.html @@ -0,0 +1,1125 @@ +--- +title: "Lightbox" +date: 2023-03-01T16:00:58+02:00 +draft: false +main_title: "Lightbox" +subheading: "Tailwind CSS Lightbox" +seo_title: "Tailwind CSS Lightbox - Free Examples & Tutorial" +description: "Responsive lightbox built with the latest Tailwind CSS. Lightbox is a responsive gallery with the option to enlarge selected photos." +image: "" +video: "https://www.youtube.com/watch?v=-GmnyjgI4Jc&ab_channel=Keepcoding" +url: "docs/standard/components/lightbox/" +menu: + components: + name: "Lightbox" +autoinits: "Lightbox" +--- + + +
    + +

    + Basic example +

    + + +

    + Use these default button styles with multiple colors to indicate an action + or link within your website. +

    + + + {{< twsnippet/demo id="example1" >}} +
    +
    +
    + Table Full of Spices +
    +
    + Winter Landscape +
    +
    + View of the City in the Mountains +
    +
    +
    + {{< /twsnippet/demo >}} + + + +
    + + + +
    + +
    + + + +
    + +

    + Image optimization +

    + + +

    + To ensure the proper performance of the page, it is recommended to include + thumbnails of images in the src attribute. Then in the + data-te-img + attribute add the path to the image with higher resolution. If the + data-te-img attribute is omitted, the lightbox will display the + same image as in the reduced size. +

    + + + {{< twsnippet/no-demo id="api-example2" >}} + + +
    + + + +
    + +

    + Shadows and rounded corners +

    + + +

    + You can improve the look of the images by adding + shadows + and rounded corners. +

    + + + {{< twsnippet/demo id="example3" >}} +
    +
    +
    + Table Full of Spices +
    +
    + Winter Landscape +
    +
    + View of the City in the Mountains +
    +
    +
    + {{< /twsnippet/demo >}} + + + +
    + + + +
    + +

    + Different sizes +

    + + +

    + Loaded images can have any aspect ratio. Their size will be automatically + adjusted to the window when you open the lightbox. +

    + + + {{< twsnippet/demo id="example4" >}} +
    +
    +
    + Table Full of Spices + Coconut with Strawberries +
    +
    + Dark Roast Iced Coffee +
    +
    +
    + {{< /twsnippet/demo >}} + + + +
    + + + +
    + +

    + Zoom level +

    + + +

    + The data-te-zoom-level attribute allows you to define the size + of a single zoom in / zoom out. +

    + + + {{< twsnippet/demo id="example5" >}} +
    +
    +
    + Table Full of Spices +
    +
    + Winter Landscape +
    +
    + View of the City in the Mountains +
    +
    +
    + {{< /twsnippet/demo >}} + + + +
    + + + +
    + +

    + Disabled image +

    + + +

    + By adding a data-te-lightbox-disabled attribute to any image, + you can exclude it from your lightbox. In the example below, the third image + has been disabled. +

    + + + {{< twsnippet/demo id="example6" >}} +
    +
    +
    + Table Full of Spices +
    +
    + Winter Landscape +
    +
    + Disabled image +
    +
    + Place Royale Bruxelles +
    +
    +
    + {{< /twsnippet/demo >}} + + + +
    + + + +
    + +

    + Captions +

    + + +

    + Image captions can be added using the + data-te-caption attribute. If it is not set, the data is taken + from the alt attribute. When you want to disable caption, leave + the data-te-caption empty. +

    + + + {{< twsnippet/demo id="example7" >}} +
    +
    +
    + Table Full of Spices +
    +
    + Winter Landscape +
    +
    + View of the City in the Mountains +
    +
    +
    + {{< /twsnippet/demo >}} + + + +
    + + + +
    + +

    + Outside access +

    + + +

    + The lightbox can be successfully operated via JavaScript. There are many + public methods available such as open, slide, + zoomIn, zoomOut, or close. All are + described in the API tab. +

    + + + {{< twsnippet/demo id="example8" >}} +
    + +
    + +
    +
    + {{< /twsnippet/demo >}} + + + +
    + + +
    + + + + + +

    + If you are looking for more advanced options, try + Bootstrap Lightbox + from MDBootstrap. +

    diff --git a/site/content/docs/standard/data/datatables/a.html b/site/content/docs/standard/data/datatables/a.html index 6911437ff..b0a59e474 100644 --- a/site/content/docs/standard/data/datatables/a.html +++ b/site/content/docs/standard/data/datatables/a.html @@ -28,8 +28,8 @@ {{< twsnippet/wrapper "javascript,umd" >}} {{< twsnippet/code active=true lang="js" >}} - import { Datatable, Select, initTE } from "tw-elements"; - initTE({ Datatable, Select }); + import { Datatable, initTE } from "tw-elements"; + initTE({ Datatable }); {{< /twsnippet/code >}} {{< twsnippet/code lang="js" >}} diff --git a/site/static/search.json b/site/static/search.json index 9fa9b1919..ced2f86a7 100644 --- a/site/static/search.json +++ b/site/static/search.json @@ -433,6 +433,15 @@ "jumbotron": 1 } }, + { + "href": "/docs/standard/components/lightbox/", + "name": "Lightbox", + "keywords": ["lightbox", "images", "gallery"], + "category": "Components", + "priority": { + "lightbox": 1 + } + }, { "href": "/docs/standard/components/link/", "name": "Link", diff --git a/src/js/autoinit/autoinitCallbacks.js b/src/js/autoinit/autoinitCallbacks.js index cb224b95f..6271af77f 100644 --- a/src/js/autoinit/autoinitCallbacks.js +++ b/src/js/autoinit/autoinitCallbacks.js @@ -181,6 +181,18 @@ const popoverCallback = (component, initSelector) => { }); }; +const lightboxCallback = (component, initSelector) => { + SelectorEngine.find(initSelector).forEach((element) => { + new component(element); + }); + EventHandler.on( + document, + `click.te.${component.NAME}.data-api`, + `${initSelector} img:not([data-te-lightbox-disabled])`, + component.toggle() + ); +}; + export { dropdownCallback, tabCallback, @@ -191,4 +203,5 @@ export { collapseCallback, tooltipsCallback, popoverCallback, + lightboxCallback, }; diff --git a/src/js/autoinit/index.js b/src/js/autoinit/index.js index 62a524854..0c4db9c4c 100644 --- a/src/js/autoinit/index.js +++ b/src/js/autoinit/index.js @@ -11,6 +11,7 @@ import { collapseCallback, tooltipsCallback, popoverCallback, + lightboxCallback, } from "./autoinitCallbacks"; import { chartsCallback } from "./chartsInit"; @@ -170,6 +171,12 @@ const defaultInitSelectors = { isToggler: true, callback: popoverCallback, }, + lightbox: { + name: "Lightbox", + selector: "[data-te-lightbox-init]", + isToggler: true, + callback: lightboxCallback, + }, }; const getComponentData = (component) => { diff --git a/src/js/components/lightbox.js b/src/js/components/lightbox.js new file mode 100644 index 000000000..9c36f6a8b --- /dev/null +++ b/src/js/components/lightbox.js @@ -0,0 +1,1304 @@ +/* +-------------------------------------------------------------------------- +Tailwind Elements is an open-source UI kit of advanced components for TailwindCSS. +Copyright © 2023 MDBootstrap.com + +Unless a custom, individually assigned license has been granted, this program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. +In addition, a custom license may be available upon request, subject to the terms and conditions of that license. Please contact tailwind@mdbootstrap.com for more information on obtaining a custom license. +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +-------------------------------------------------------------------------- +*/ + +import { element, typeCheckConfig } from "../util/index"; +import Data from "../dom/data"; +import EventHandler from "../dom/event-handler"; +import Manipulator from "../dom/manipulator"; +import SelectorEngine from "../dom/selector-engine"; + +/** + * ------------------------------------------------------------------------ + * Constants + * ------------------------------------------------------------------------ + */ + +const NAME = "lightbox"; +const DATA_KEY = "te.lightbox"; +const EVENT_KEY = `.${DATA_KEY}`; +const DATA_API_KEY = ".data-api"; +const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`; + +const SELECTOR_ATTR_LIGHTBOX = "[data-te-lightbox-init]"; +const SELECTOR_ATTR_TOGGLE = `${SELECTOR_ATTR_LIGHTBOX} img:not([data-te-lightbox-disabled])`; +const ATTR_LIGHTBOX_CAPTION = `data-te-lightbox-caption`; +const ATTR_LIGHTBOX_DISABLED = `data-te-lightbox-disabled`; +const ATTR_STATE_ACTIVE = `data-te-lightbox-active`; + +const prevIconTemplate = ` + + +`; +const nextIconTemplate = ` + + +`; +const showFullscreenIconTemplate = ` + + +`; +const hideFullscreenIconTemplate = ` + + +`; +const zoomInIconTemplate = ` + + +`; +const zoomOutIconTemplate = ` + + +`; +const closeIconTemplate = ` + + +`; + +const OPTIONS_TYPE = { + container: "string", + zoomLevel: "(number|string)", + prevIconTemplate: "string", + nextIconTemplate: "string", + showFullscreenIconTemplate: "string", + hideFullscreenIconTemplate: "string", + zoomInIconTemplate: "string", + closeIconTemplate: "string", + zoomOutIconTemplate: "string", + spinnerContent: "string", +}; + +const DEFAULT_OPTIONS = { + container: "body", + zoomLevel: 1, + prevIconTemplate: prevIconTemplate, + nextIconTemplate: nextIconTemplate, + showFullscreenIconTemplate: showFullscreenIconTemplate, + hideFullscreenIconTemplate: hideFullscreenIconTemplate, + zoomInIconTemplate: zoomInIconTemplate, + zoomOutIconTemplate: zoomOutIconTemplate, + closeIconTemplate: closeIconTemplate, + spinnerContent: "Loading...", +}; + +const DefaultClasses = { + caption: + "text-white text-ellipsis overflow-hidden whitespace-nowrap mx-[10px] text-center", + captionWrapper: + "fixed left-0 bottom-0 w-full h-[50px] flex justify-center items-center", + closeBtn: + "border-none bg-transparent w-[50px] h-[50px] px-4 text-[#b3b3b3] transition-colors duration-200 ease-in-out hover:text-white focus:text-white motion-reduce:transition-none outline-none", + fullscreenBtn: + "border-none bg-transparent w-[50px] h-[50px] px-4 text-[#b3b3b3] transition-colors duration-200 ease-in-out hover:text-white focus:text-white motion-reduce:transition-none outline-none", + gallery: + "invisible fixed left-0 top-0 w-full h-full z-[1100] pointer-events-none opacity-0 bg-[#000000e6] transition-all duration-[400ms] motion-reduce:transition-none", + galleryContent: + "fixed top-[50px] left-[50px] w-[calc(100%-100px)] h-[calc(100%-100px)]", + galleryCounter: + "flex justify-center items-center px-[10px] mb-0 h-full text-[#b3b3b3]", + img: "absolute left-0 top-0 w-full max-h-full h-auto cursor-pointer pointer-events-auto", + imgWrapper: + "absolute top-0 left-0 w-full h-full opacity-0 transform scale-[0.25] transition-all duration-[400ms] ease-out pointer-events-none motion-reduce:transition-none motion-reduce:transform-none", + leftTools: "float-left h-full", + loader: + "fixed left-0 top-0 z-[2] w-full h-full text-neutral-50 opacity-1 flex justify-center items-center pointer-events-none transition-opacity duration-[1000ms] motion-reduce:transition-none", + nextBtn: + "border-none bg-transparent w-full h-[50px] flex justify-center items-center text-[#b3b3b3] transition-colors duration-200 ease-in-out hover:text-white focus:text-white motion-reduce:transition-none outline-none", + nextBtnWrapper: + "fixed right-0 top-0 w-[50px] h-full flex justify-center items-center transition-opacity duration-[400ms] motion-reduce:transition-none", + prevBtn: + "border-none bg-transparent w-full h-[50px] flex justify-center items-center text-[#b3b3b3] transition-colors duration-200 ease-in-out hover:text-white focus:text-white motion-reduce:transition-none outline-none", + prevBtnWrapper: + "fixed left-0 top-0 w-[50px] h-full flex justify-center items-center transition-opacity duration-[400ms] motion-reduce:transition-none", + rightTools: "float-right", + spinner: + "inline-block h-8 w-8 animate-[spinner-grow_0.75s_linear_infinite] rounded-full bg-current align-[-0.125em] motion-reduce:animate-[spinner-grow_1.5s_linear_infinite]", + spinnerContent: + "!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]", + toolbar: + "absolute top-0 left-0 w-full h-[50px] z-20 transition-opacity duration-[400ms] motion-reduce:transition-none", + vertical: "h-full max-h-full w-auto", + zoomBtn: + "border-none bg-transparent w-[50px] h-[50px] px-4 text-[#b3b3b3] transition-colors duration-200 ease-in-out hover:text-white focus:text-white motion-reduce:transition-none outline-none", +}; + +const DefaultClassesType = { + caption: "string", + captionWrapper: "string", + closeBtn: "string", + fullscreenBtn: "string", + gallery: "string", + galleryContent: "string", + galleryCounter: "string", + img: "string", + imgWrapper: "string", + leftTools: "string", + loader: "string", + nextBtn: "string", + nextBtnWrapper: "string", + prevBtn: "string", + prevBtnWrapper: "string", + rightTools: "string", + spinner: "string", + spinnerContent: "string", + toolbar: "string", + vertical: "string", + zoomBtn: "string", +}; + +/** + * ------------------------------------------------------------------------ + * Class Definition + * ------------------------------------------------------------------------ + */ + +class Lightbox { + constructor(element, options = {}, classes) { + this._element = element; + this._options = options; + this._classes = this._getClasses(classes); + this._getContainer(); + + this._id = `lightbox-${Math.random().toString(36).substr(2, 9)}`; + this._activeImg = 0; + this._images = []; + this._zoom = 1; + this._gallery = null; + this._galleryToolbar = null; + this._galleryContent = null; + this._loader = null; + this._imgCounter = null; + this._animating = false; + this._fullscreen = false; + this._zoomBtn = null; + this._fullscreenBtn = null; + this._toolsToggleTimer = 0; + this._mousedown = false; + this._mousedownPositionX = 0; + this._mousedownPositionY = 0; + this._originalPositionX = 0; + this._originalPositionY = 0; + this._positionX = 0; + this._positionY = 0; + this._zoomTimer = 0; + this._tapCounter = 0; + this._tapTime = 0; + this._rightArrow = null; + this._leftArrowWrapper = null; + this._rightArrowWrapper = null; + this._initiated = false; + this._multitouch = false; + this._touchZoomPosition = []; + + if (this._element) { + Data.setData(element, DATA_KEY, this); + this.init(); + } + } + + // Getters + static get NAME() { + return NAME; + } + + get activeImg() { + return this._activeImg; + } + + get currentImg() { + return SelectorEngine.findOne( + `[${ATTR_STATE_ACTIVE}]`, + this._galleryContent + ); + } + + get options() { + const config = { + ...DEFAULT_OPTIONS, + ...Manipulator.getDataAttributes(this._element), + ...this._options, + }; + + typeCheckConfig(NAME, config, OPTIONS_TYPE); + + return config; + } + + // Public + init() { + if (this._initiated) { + return; + } + + this._appendTemplate(); + this._initiated = true; + } + + open(target = 0) { + this._getImages(); + this._setActiveImg(target); + this._sortImages(); + this._triggerEvents("open", "opened"); + + this._loadImages().then((images) => { + this._resizeImages(images); + this._toggleTemplate(); + this._addEvents(); + this._focusFullscreenBtn(); + }); + } + + close() { + this.reset(); + this._removeEvents(); + this._toggleTemplate(); + this._triggerEvents("close", "closed"); + } + + slide(target = "right") { + if (this._animating === true || this._images.length <= 1) return; + this._triggerEvents("slide", "slided"); + + this._beforeSlideEvents(); + if (target === "right") this._slideHorizontally(target); + if (target === "left") this._slideHorizontally(target); + if (target === "first") this._slideToTarget(target); + if (target === "last") this._slideToTarget(target); + this._afterSlideEvents(); + } + + zoomIn() { + if (this._zoom >= 3) return; + this._triggerEvents("zoomIn", "zoomedIn"); + this._zoom += parseFloat(this.options.zoomLevel); + Manipulator.style(this.currentImg.parentNode, { + transform: `scale(${this._zoom})`, + }); + + this._updateZoomBtn(); + } + + zoomOut() { + if (this._zoom <= 1) return; + this._triggerEvents("zoomOut", "zoomedOut"); + this._zoom -= parseFloat(this.options.zoomLevel); + Manipulator.style(this.currentImg.parentNode, { + transform: `scale(${this._zoom})`, + }); + + this._updateZoomBtn(); + this._updateImgPosition(); + } + + toggleFullscreen() { + if (this._fullscreen === false) { + this._fullscreenBtn.setAttribute(ATTR_STATE_ACTIVE, ""); + this._fullscreenBtn.innerHTML = this.options.hideFullscreenIconTemplate; + if (this._gallery.requestFullscreen) { + this._gallery.requestFullscreen(); + } + + this._fullscreen = true; + } else { + this._fullscreenBtn.removeAttribute(ATTR_STATE_ACTIVE); + if (document.exitFullscreen) { + document.exitFullscreen(); + } + + this._fullscreen = false; + } + } + + reset() { + this._restoreDefaultFullscreen(); + this._restoreDefaultPosition(); + this._restoreDefaultZoom(); + clearTimeout(this._toolsToggleTimer); + clearTimeout(this._doubleTapTimer); + } + + dispose() { + EventHandler.off( + document, + EVENT_CLICK_DATA_API, + SELECTOR_ATTR_TOGGLE, + this.toggle + ); + if (this._galleryContent) this._removeEvents(); + if (this._gallery) this._gallery.remove(); + + Data.removeData(this._element, DATA_KEY); + this._element = null; + } + + // Private + _getClasses(classes) { + const dataAttributes = Manipulator.getDataClassAttributes(this._element); + + classes = { + ...DefaultClasses, + ...dataAttributes, + ...classes, + }; + + typeCheckConfig(NAME, classes, DefaultClassesType); + + return classes; + } + + _getImages() { + const allImages = SelectorEngine.find("img", this._element); + const lightboxImages = allImages.filter( + (image) => !image.hasAttribute(ATTR_LIGHTBOX_DISABLED) + ); + this._images = lightboxImages; + } + + _getContainer() { + this._container = SelectorEngine.findOne(this.options.container); + } + + _setActiveImg(target) { + this._activeImg = + typeof target === "number" ? target : this._images.indexOf(target.target); + } + + _appendTemplate() { + this._gallery = element("div"); + Manipulator.addClass(this._gallery, `${this._classes.gallery}`); + this._element.dataset.id = this._id; + this._gallery.id = this._id; + + this._appendLoader(); + this._appendToolbar(); + this._appendContent(); + this._appendArrows(); + this._appendCaption(); + this._container.append(this._gallery); + } + + _appendToolbar() { + this._galleryToolbar = element("div"); + this._imgCounter = element("p"); + this._fullscreenBtn = element("button"); + this._zoomBtn = element("button"); + const closeBtn = element("button"); + const leftTools = element("div"); + const rightTools = element("div"); + + Manipulator.addClass(this._galleryToolbar, `${this._classes.toolbar}`); + Manipulator.addClass(this._imgCounter, `${this._classes.galleryCounter}`); + Manipulator.addClass(this._fullscreenBtn, `${this._classes.fullscreenBtn}`); + Manipulator.addClass(this._zoomBtn, `${this._classes.zoomInBtn}`); + Manipulator.addClass(this._zoomBtn, this._classes.zoomBtn); + Manipulator.addClass(leftTools, `${this._classes.leftTools}`); + Manipulator.addClass(rightTools, `${this._classes.rightTools}`); + Manipulator.addClass(closeBtn, `${this._classes.closeBtn}`); + + this._fullscreenBtn.innerHTML = this.options.showFullscreenIconTemplate; + closeBtn.innerHTML = this.options.closeIconTemplate; + this._zoomBtn.innerHTML = this.options.zoomInIconTemplate; + this._fullscreenBtn.setAttribute("aria-label", "Toggle fullscreen"); + this._zoomBtn.setAttribute("aria-label", "Zoom in"); + closeBtn.setAttribute("aria-label", "Close"); + + EventHandler.on(this._fullscreenBtn, EVENT_CLICK_DATA_API, () => + this.toggleFullscreen() + ); + EventHandler.on(this._zoomBtn, EVENT_CLICK_DATA_API, () => + this._toggleZoom() + ); + EventHandler.on(closeBtn, EVENT_CLICK_DATA_API, () => this.close()); + + leftTools.append(this._imgCounter); + rightTools.append(this._fullscreenBtn); + rightTools.append(this._zoomBtn); + rightTools.append(closeBtn); + + this._galleryToolbar.append(leftTools); + this._galleryToolbar.append(rightTools); + this._gallery.append(this._galleryToolbar); + } + + _appendContent() { + this._galleryContent = element("div"); + Manipulator.addClass( + this._galleryContent, + `${this._classes.galleryContent}` + ); + this._gallery.append(this._galleryContent); + } + + _appendLoader() { + this._loader = element("div"); + const spinner = element("div"); + const spinnerContent = element("span"); + + Manipulator.addClass(this._loader, `${this._classes.loader}`); + Manipulator.addClass(spinner, `${this._classes.spinner}`); + Manipulator.addClass(spinnerContent, `${this._classes.spinnerContent}`); + + spinner.setAttribute("role", "status"); + spinnerContent.innerHTML = this.options.spinnerContent; + + spinner.append(spinnerContent); + this._loader.append(spinner); + this._gallery.append(this._loader); + } + + _appendArrows() { + this._leftArrowWrapper = element("div"); + Manipulator.addClass( + this._leftArrowWrapper, + `${this._classes.prevBtnWrapper}` + ); + const leftArrow = element("button"); + leftArrow.setAttribute("aria-label", "Previous"); + Manipulator.addClass(leftArrow, `${this._classes.prevBtn}`); + EventHandler.on(leftArrow, EVENT_CLICK_DATA_API, () => this.slide("left")); + this._leftArrowWrapper.append(leftArrow); + + this._rightArrowWrapper = element("div"); + Manipulator.addClass( + this._rightArrowWrapper, + `${this._classes.nextBtnWrapper}` + ); + this._rightArrow = element("button"); + this._rightArrow.setAttribute("aria-label", "Next"); + Manipulator.addClass(this._rightArrow, `${this._classes.nextBtn}`); + EventHandler.on(this._rightArrow, EVENT_CLICK_DATA_API, () => this.slide()); + this._rightArrowWrapper.append(this._rightArrow); + + this._rightArrow.innerHTML = this.options.nextIconTemplate; + leftArrow.innerHTML = this.options.prevIconTemplate; + + this._getImages(); + if (this._images.length <= 1) return; + this._gallery.append(this._leftArrowWrapper); + this._gallery.append(this._rightArrowWrapper); + } + + _appendCaption() { + const captionWrapper = element("div"); + const caption = element("p"); + + caption.setAttribute(ATTR_LIGHTBOX_CAPTION, ""); + Manipulator.addClass(captionWrapper, `${this._classes.captionWrapper}`); + Manipulator.addClass(caption, `${this._classes.caption}`); + + captionWrapper.append(caption); + this._gallery.append(captionWrapper); + } + + _sortImages() { + for (let i = 0; i < this._activeImg; i++) { + this._images.push(this._images.shift()); + } + } + + async _loadImages() { + const promiseArray = []; + const imageArray = []; + + this._galleryContent.innerHTML = ""; + let positionLeft = 0; + + this._images.forEach((img, key) => { + promiseArray.push( + new Promise((resolve) => { + const newImg = new Image(); + const newImgWrapper = element("div"); + Manipulator.addClass(newImgWrapper, `${this._classes.imgWrapper}`); + Manipulator.addClass(newImg, `${this._classes.img}`); + + this._addImgStyles(newImg, newImgWrapper, positionLeft, key, img); + + newImgWrapper.append(newImg); + this._galleryContent.append(newImgWrapper); + + newImg.onload = resolve; + newImg.src = img.dataset.teImg || img.src; + imageArray.push(newImg); + + positionLeft += 100; + }) + ); + }); + + await Promise.all(promiseArray); + return imageArray; + } + + _addImgStyles(newImg, newImgWrapper, positionLeft, key, img) { + newImg.alt = img.alt; + newImg.draggable = false; + + Manipulator.style(newImgWrapper, { + position: "absolute", + left: `${positionLeft}%`, + top: 0, + }); + if (img.dataset.teCaption || img.dataset.teCaption === "") { + newImg.dataset.caption = img.dataset.teCaption; + } + + if (positionLeft === 0) { + if (img.width < img.height) { + Manipulator.addClass(newImg, `${this._classes.vertical}`); + } + Manipulator.style(newImgWrapper, { opacity: 1 }); + newImg.setAttribute(ATTR_STATE_ACTIVE, ""); + } else { + newImg.removeAttribute(ATTR_STATE_ACTIVE); + } + + if (key === this._images.length - 1 && this._images.length > 1) { + Manipulator.style(newImgWrapper, { left: "-100%" }); + } + } + + _resizeImages(images) { + images.forEach((img) => { + this._calculateImgSize(img); + }); + } + + _calculateImgSize(img) { + if (img.width >= img.height) { + img.style.width = "100%"; + img.style.maxWidth = "100%"; + img.style.height = "auto"; + img.style.top = `${(img.parentNode.offsetHeight - img.height) / 2}px`; + img.style.left = 0; + } else { + img.style.height = "100%"; + img.style.maxHeight = "100%"; + img.style.width = "auto"; + img.style.left = `${(img.parentNode.offsetWidth - img.width) / 2}px`; + img.style.top = 0; + } + + if (img.width >= img.parentNode.offsetWidth) { + img.style.width = `${img.parentNode.offsetWidth}px`; + img.style.height = "auto"; + img.style.left = 0; + img.style.top = `${(img.parentNode.offsetHeight - img.height) / 2}px`; + } + if (img.height >= img.parentNode.offsetHeight) { + img.style.height = `${img.parentNode.offsetHeight}px`; + img.style.width = "auto"; + img.style.top = 0; + img.style.left = `${(img.parentNode.offsetWidth - img.width) / 2}px`; + } + + this._positionX = parseFloat(img.style.left) || 0; + this._positionY = parseFloat(img.style.top) || 0; + } + + _onResize() { + this._images = SelectorEngine.find("img", this._galleryContent); + this._images.forEach((img) => { + this._calculateImgSize(img); + }); + } + + _onFullscreenChange() { + const isFullscreenEnabled = + document.webkitIsFullScreen || + document.mozFullScreen || + document.msFullscreenElement; + if (isFullscreenEnabled === undefined) { + this._fullscreen = false; + this._fullscreenBtn.innerHTML = this.options.showFullscreenIconTemplate; + this._fullscreenBtn.removeAttribute(ATTR_STATE_ACTIVE); + } + } + + _beforeSlideEvents() { + this._animationStart(); + this._restoreDefaultZoom(); + this._restoreDefaultPosition(); + this._resetDoubleTap(); + } + + _slideHorizontally(direction) { + this._images = SelectorEngine.find("img", this._galleryContent); + this._images.forEach((img) => { + let newPositionLeft; + + if (direction === "right") { + newPositionLeft = parseInt(img.parentNode.style.left, 10) - 100; + if (newPositionLeft < -100) + newPositionLeft = (this._images.length - 2) * 100; + } else { + newPositionLeft = parseInt(img.parentNode.style.left, 10) + 100; + if (newPositionLeft === (this._images.length - 1) * 100) + newPositionLeft = -100; + } + + this._slideImg(img, newPositionLeft); + }); + + this._updateActiveImg(direction); + } + + _slideImg(img, position) { + if (position === 0) { + img.setAttribute(ATTR_STATE_ACTIVE, ""); + Manipulator.style(img.parentNode, { opacity: 1, transform: "scale(1)" }); + } else { + img.removeAttribute(ATTR_STATE_ACTIVE); + Manipulator.style(img.parentNode, { + opacity: 0, + transform: "scale(0.25)", + }); + } + img.parentNode.style.left = `${position}%`; + } + + _slideToTarget(target) { + if (target === "first" && this._activeImg === 0) return; + if (target === "last" && this._activeImg === this._images.length - 1) + return; + + this.reset(); + this._removeEvents(); + this._showLoader(); + + this._getImages(); + this._activeImg = target === "first" ? 0 : this._images.length - 1; + this._sortImages(); + + Manipulator.style(this.currentImg.parentNode, { + transform: "scale(0.25)", + opacity: 0, + }); + + setTimeout(() => { + this._loadImages().then((images) => { + this._resizeImages(images); + this._addEvents(); + this._updateCaption(); + this._hideLoader(); + setTimeout(() => { + Manipulator.style(this.currentImg.parentNode, { + transform: "scale(1)", + opacity: 1, + }); + }, 10); + }); + }, 400); + } + + _updateActiveImg(direction) { + if (direction === "right") { + if (this._activeImg === this._images.length - 1) { + this._activeImg = 0; + } else { + this._activeImg++; + } + } + + if (direction === "left") { + if (this._activeImg === 0) { + this._activeImg = this._images.length - 1; + } else { + this._activeImg--; + } + } + } + + _afterSlideEvents() { + this._updateCounter(); + this._updateCaption(); + } + + _updateCounter() { + if (this._images.length <= 1) return; + setTimeout(() => { + this._imgCounter.innerHTML = `${this._activeImg + 1} / ${ + this._images.length + }`; + }, 200); + } + + _updateCaption() { + setTimeout(() => { + let caption = this.currentImg.alt; + if ( + this.currentImg.dataset.caption || + this.currentImg.dataset.caption === "" + ) { + caption = this.currentImg.dataset.caption; + } + SelectorEngine.findOne( + `[${ATTR_LIGHTBOX_CAPTION}]`, + this._gallery + ).innerHTML = caption; + }, 200); + } + + _toggleTemplate() { + if (this._gallery.style.visibility === "visible") { + Manipulator.style(this.currentImg.parentNode, { + transform: "scale(0.25)", + }); + + setTimeout(() => { + this._hideGallery(); + this._enableScroll(); + this._showLoader(); + }, 100); + } else { + this._showGallery(); + this._disableScroll(); + this._updateCounter(); + this._updateCaption(); + this._setToolsToggleTimout(); + this._hideLoader(); + } + } + + _showLoader() { + Manipulator.style(this._loader, { opacity: 1 }); + } + + _hideLoader() { + Manipulator.style(this._loader, { opacity: 0 }); + } + + _hideGallery() { + Manipulator.style(this._gallery, { + opacity: 0, + pointerEvents: "none", + visibility: "hidden", + }); + } + + _showGallery() { + Manipulator.style(this._gallery, { + opacity: 1, + pointerEvents: "initial", + visibility: "visible", + }); + setTimeout(() => { + Manipulator.style(this.currentImg.parentNode, { transform: "scale(1)" }); + }, 50); + } + + _toggleZoom() { + if (this._zoom !== 1) { + this.zoomOut(); + } else { + this.zoomIn(); + } + } + + _updateZoomBtn() { + if (this._zoom > 1) { + this._zoomBtn.setAttribute(ATTR_STATE_ACTIVE, ""); + this._zoomBtn.setAttribute("aria-label", "Zoom out"); + this._zoomBtn.innerHTML = this.options.zoomOutIconTemplate; + } else { + this._zoomBtn.removeAttribute(ATTR_STATE_ACTIVE); + this._zoomBtn.setAttribute("aria-label", "Zoom in"); + this._zoomBtn.innerHTML = this.options.zoomInIconTemplate; + } + } + + _updateImgPosition() { + if (this._zoom === 1) { + this._restoreDefaultPosition(); + } + } + + _addEvents() { + const images = SelectorEngine.find("img", this._galleryContent); + + this._onWindowTouchmove = this._onWindowTouchmove.bind(this); + this._onWindowTouchstart = this._onWindowTouchstart.bind(this); + this._onImgMousedown = this._onMousedown.bind(this); + this._onImgMousemove = this._onMousemove.bind(this); + this._onImgWheel = this._onZoom.bind(this); + this._onImgMouseup = this._onMouseup.bind(this); + this._onImgTouchend = this._onTouchend.bind(this); + this._onImgDoubleClick = this._onDoubleClick.bind(this); + this._onWindowResize = this._onResize.bind(this); + this._onWindowFullscreenChange = this._onFullscreenChange.bind(this); + this._onAnyImgAction = this._resetToolsToggler.bind(this); + this._onGalleryClick = this._onBackdropClick.bind(this); + this._onKeyupEvent = this._onKeyup.bind(this); + this._onRightArrowKeydownEvent = this._onRightArrowKeydown.bind(this); + this._onFullscreenBtnKeydownEvent = this._onFullscreenBtnKeydown.bind(this); + + images.forEach((img) => { + EventHandler.on(img, "mousedown", this._onImgMousedown, { + passive: true, + }); + EventHandler.on(img, "touchstart", this._onImgMousedown, { + passive: true, + }); + EventHandler.on(img, "mousemove", this._onImgMousemove, { + passive: true, + }); + EventHandler.on(img, "touchmove", this._onImgMousemove, { + passive: true, + }); + EventHandler.on(img, "wheel", this._onImgWheel, { passive: true }); + EventHandler.on(img, "dblclick", this._onImgDoubleClick, { + passive: true, + }); + }); + + document.addEventListener("touchmove", this._onWindowTouchmove, { + passive: false, + }); + EventHandler.on(window, "touchstart", this._onWindowTouchstart); + EventHandler.on(window, "mouseup", this._onImgMouseup); + EventHandler.on(window, "touchend", this._onImgTouchend); + EventHandler.on(window, "resize", this._onWindowResize); + EventHandler.on(window, "orientationchange", this._onWindowResize); + EventHandler.on(window, "keyup", this._onKeyupEvent); + EventHandler.on(window, "fullscreenchange", this._onWindowFullscreenChange); + EventHandler.on(this._gallery, "mousemove", this._onAnyImgAction); + EventHandler.on(this._gallery, "click", this._onGalleryClick); + EventHandler.on( + this._rightArrow, + "keydown", + this._onRightArrowKeydownEvent + ); + EventHandler.on( + this._fullscreenBtn, + "keydown", + this._onFullscreenBtnKeydownEvent + ); + } + + _removeEvents() { + const images = SelectorEngine.find("img", this._galleryContent); + + images.forEach((img) => { + EventHandler.off(img, "mousedown", this._onImgMousedown); + EventHandler.off(img, "touchstart", this._onImgMousedown); + EventHandler.off(img, "mousemove", this._onImgMousemove); + EventHandler.off(img, "touchmove", this._onImgMousemove); + EventHandler.off(img, "wheel", this._onImgWheel); + EventHandler.off(img, "dblclick", this._onImgDoubleClick); + }); + + document.removeEventListener("touchmove", this._onWindowTouchmove, { + passive: false, + }); + EventHandler.off(window, "touchstart", this._onWindowTouchstart); + EventHandler.off(window, "mouseup", this._onImgMouseup); + EventHandler.off(window, "touchend", this._onImgTouchend); + EventHandler.off(window, "resize", this._onWindowResize); + EventHandler.off(window, "orientationchange", this._onWindowResize); + EventHandler.off(window, "keyup", this._onKeyupEvent); + EventHandler.off( + window, + "fullscreenchange", + this._onWindowFullscreenChange + ); + EventHandler.off(this._gallery, "mousemove", this._onAnyImgAction); + EventHandler.off(this._gallery, "click", this._onGalleryClick); + EventHandler.off( + this._rightArrow, + "keydown", + this._onRightArrowKeydownEvent + ); + EventHandler.off( + this._fullscreenBtn, + "keydown", + this._onFullscreenBtnKeydownEvent + ); + } + + _onMousedown(e) { + const touch = e.touches; + const x = touch ? touch[0].clientX : e.clientX; + const y = touch ? touch[0].clientY : e.clientY; + + this._originalPositionX = parseFloat(this.currentImg.style.left) || 0; + this._originalPositionY = parseFloat(this.currentImg.style.top) || 0; + this._positionX = this._originalPositionX; + this._positionY = this._originalPositionY; + this._mousedownPositionX = x * (1 / this._zoom) - this._positionX; + this._mousedownPositionY = y * (1 / this._zoom) - this._positionY; + this._mousedown = true; + + if (e.type === "touchstart") { + if (e.touches.length > 1) { + this._multitouch = true; + this._touchZoomPosition = e.touches; + } + } + } + + _onMousemove(e) { + if (!this._mousedown) return; + + const touch = e.touches; + const x = touch ? touch[0].clientX : e.clientX; + const y = touch ? touch[0].clientY : e.clientY; + + if (touch) this._resetToolsToggler(); + + if (!this._multitouch) { + if (this._zoom !== 1) { + this._positionX = x * (1 / this._zoom) - this._mousedownPositionX; + this._positionY = y * (1 / this._zoom) - this._mousedownPositionY; + Manipulator.style(this.currentImg, { + left: `${this._positionX}px`, + top: `${this._positionY}px`, + }); + } else { + if (this._images.length <= 1) return; + this._positionX = x * (1 / this._zoom) - this._mousedownPositionX; + Manipulator.style(this.currentImg, { left: `${this._positionX}px` }); + } + } + } + + _onMouseup(e) { + this._mousedown = false; + this._moveImg(e.target); + } + + _onTouchend(e) { + this._mousedown = false; + + if (this._multitouch) { + if (e.targetTouches.length === 0) { + this._multitouch = false; + this._touchZoomPosition = []; + } + } else if (!this._multitouch) { + this._checkDoubleTap(e); + this._moveImg(e.target); + } + } + + _calculateTouchZoom(e) { + const initialDistance = Math.hypot( + this._touchZoomPosition[1].pageX - this._touchZoomPosition[0].pageX, + this._touchZoomPosition[1].pageY - this._touchZoomPosition[0].pageY + ); + const finalDistance = Math.hypot( + e.touches[1].pageX - e.touches[0].pageX, + e.touches[1].pageY - e.touches[0].pageY + ); + const distanceChange = Math.abs(initialDistance - finalDistance); + const screenWidth = e.view.screen.width; + if (distanceChange > screenWidth * 0.03) { + if (initialDistance <= finalDistance) { + this.zoomIn(); + } else { + this.zoomOut(); + } + this._touchZoomPosition = e.touches; + } + } + + _onWindowTouchstart(e) { + if (e.touches.length > 1) { + this._multitouch = true; + this._touchZoomPosition = e.touches; + } + } + + _onWindowTouchmove(e) { + e.preventDefault(); + if (e.type === "touchmove" && e.targetTouches.length > 1) { + this._calculateTouchZoom(e); + } + } + + _onRightArrowKeydown(e) { + switch (e.keyCode) { + case 9: + if (e.shiftKey) break; + e.preventDefault(); + this._focusFullscreenBtn(); + break; + default: + break; + } + } + + _onFullscreenBtnKeydown(e) { + switch (e.keyCode) { + case 9: + if (!e.shiftKey) break; + e.preventDefault(); + this._focusRightArrow(); + break; + default: + break; + } + } + + _onKeyup(e) { + this._resetToolsToggler(); + switch (e.keyCode) { + case 39: + this.slide(); + break; + case 37: + this.slide("left"); + break; + case 27: + this.close(); + break; + case 36: + this.slide("first"); + break; + case 35: + this.slide("last"); + break; + case 38: + this.zoomIn(); + break; + case 40: + this.zoomOut(); + break; + default: + break; + } + } + + _focusFullscreenBtn() { + setTimeout(() => { + this._fullscreenBtn.focus(); + }, 100); + } + + _focusRightArrow() { + this._rightArrow.focus(); + } + + _moveImg(target) { + if (this._multitouch) return; + if ( + this._zoom !== 1 || + target !== this.currentImg || + this._images.length <= 1 + ) + return; + + const movement = this._positionX - this._originalPositionX; + if (movement > 0) { + this.slide("left"); + } else if (movement < 0) { + this.slide(); + } + } + + _checkDoubleTap(e) { + clearTimeout(this._doubleTapTimer); + const currentTime = new Date().getTime(); + const tapLength = currentTime - this._tapTime; + + if (this._tapCounter > 0 && tapLength < 500) { + this._onDoubleClick(e); + this._doubleTapTimer = setTimeout(() => { + this._tapTime = new Date().getTime(); + this._tapCounter = 0; + }, 300); + } else { + this._tapCounter++; + this._tapTime = new Date().getTime(); + } + } + + _resetDoubleTap() { + this._tapTime = 0; + this._tapCounter = 0; + clearTimeout(this._doubleTapTimer); + } + + _onDoubleClick(e) { + if (this._multitouch) return; + + if (!e.touches) this._setNewPositionOnZoomIn(e); + if (this._zoom !== 1) { + this._restoreDefaultZoom(); + } else { + this.zoomIn(); + } + } + + _onZoom(e) { + if (e.deltaY > 0) { + this.zoomOut(); + } else { + if (this._zoom >= 3) return; + this._setNewPositionOnZoomIn(e); + this.zoomIn(); + } + } + + _onBackdropClick(e) { + this._resetToolsToggler(); + + if (e.target.tagName !== "DIV") return; + this.close(); + } + + _setNewPositionOnZoomIn(e) { + clearTimeout(this._zoomTimer); + this._positionX = window.innerWidth / 2 - e.offsetX - 50; + this._positionY = window.innerHeight / 2 - e.offsetY - 50; + this.currentImg.style.transition = "all 0.5s ease-out"; + this.currentImg.style.left = `${this._positionX}px`; + this.currentImg.style.top = `${this._positionY}px`; + + this._zoomTimer = setTimeout(() => { + this.currentImg.style.transition = "none"; + }, 500); + } + + _resetToolsToggler() { + this._showTools(); + clearTimeout(this._toolsToggleTimer); + this._setToolsToggleTimout(); + } + + _setToolsToggleTimout() { + this._toolsToggleTimer = setTimeout(() => { + this._hideTools(); + clearTimeout(this._toolsToggleTimer); + }, 4000); + } + + _hideTools() { + Manipulator.style(this._galleryToolbar, { opacity: 0 }); + Manipulator.style(this._leftArrowWrapper, { opacity: 0 }); + Manipulator.style(this._rightArrowWrapper, { opacity: 0 }); + } + + _showTools() { + Manipulator.style(this._galleryToolbar, { opacity: 1 }); + Manipulator.style(this._leftArrowWrapper, { opacity: 1 }); + Manipulator.style(this._rightArrowWrapper, { opacity: 1 }); + } + + _disableScroll() { + Manipulator.addClass(document.body, `overflow-y-hidden relative`); + + if ( + document.documentElement.scrollHeight > + document.documentElement.clientHeight + ) { + Manipulator.addClass(document.body, `md:pr-[17px]`); + } + } + + _enableScroll() { + setTimeout(() => { + Manipulator.removeClass(document.body, `overflow-y-hidden relative`); + Manipulator.removeClass(document.body, `md:pr-[17px]`); + }, 300); + } + + _animationStart() { + this._animating = true; + setTimeout(() => { + this._animating = false; + }, 400); + } + + _restoreDefaultZoom() { + if (this._zoom !== 1) { + this._zoom = 1; + Manipulator.style(this.currentImg.parentNode, { + transform: `scale(${this._zoom})`, + }); + + this._updateZoomBtn(); + this._updateImgPosition(); + } + } + + _restoreDefaultFullscreen() { + if (this._fullscreen) this.toggleFullscreen(); + } + + _restoreDefaultPosition() { + clearTimeout(this._zoomTimer); + const currentImg = this.currentImg; + + Manipulator.style(this.currentImg.parentNode, { left: 0, top: 0 }); + Manipulator.style(this.currentImg, { + transition: "all 0.5s ease-out", + left: 0, + top: 0, + }); + + this._calculateImgSize(currentImg); + + setTimeout(() => { + Manipulator.style(this.currentImg, { transition: "none" }); + }, 500); + } + + async _triggerEvents(startEvent, completeEvent) { + EventHandler.trigger(this._element, `${startEvent}.te.lightbox`); + + if (completeEvent) { + await setTimeout(() => { + EventHandler.trigger(this._element, `${completeEvent}.te.lightbox`); + }, 505); + } + } + + static getInstance(element) { + return Data.getData(element, DATA_KEY); + } + + static getOrCreateInstance(element, config = {}) { + return ( + this.getInstance(element) || + new this(element, typeof config === "object" ? config : null) + ); + } + + static toggle() { + return function (event) { + const lightbox = SelectorEngine.closest( + event.target, + `${SELECTOR_ATTR_LIGHTBOX}` + ); + const instance = Lightbox.getInstance(lightbox) || new Lightbox(lightbox); + instance.open(event); + }; + } + + static jQueryInterface(config, options) { + return this.each(function () { + let data = Data.getData(this, DATA_KEY); + const _config = typeof config === "object" && config; + if (!data && /dispose/.test(config)) { + return; + } + if (!data) { + data = new Lightbox(this, _config); + } + if (typeof config === "string") { + if (typeof data[config] === "undefined") { + throw new TypeError(`No method named "${config}"`); + } + data[config](options); + } + }); + } +} + +export default Lightbox; diff --git a/src/js/index.es.js b/src/js/index.es.js index 95bc023de..9aabaf6cd 100644 --- a/src/js/index.es.js +++ b/src/js/index.es.js @@ -35,6 +35,7 @@ import Chart from "./data/chart/charts"; import PerfectScrollbar from "./methods/perfect-scrollbar"; import Datatable from "./data/datatables/index"; import Rating from "./components/rating"; +import Lightbox from "./components/lightbox"; import initTE from "./autoinit/index"; export { @@ -64,5 +65,6 @@ export { PerfectScrollbar, Datatable, Rating, + Lightbox, initTE, }; diff --git a/src/js/index.umd.js b/src/js/index.umd.js index 17a407e8a..8046f1f36 100644 --- a/src/js/index.umd.js +++ b/src/js/index.umd.js @@ -35,6 +35,7 @@ import Chart from "./data/chart/charts"; import PerfectScrollbar from "./methods/perfect-scrollbar"; import Datatable from "./data/datatables/index"; import Rating from "./components/rating"; +import Lightbox from "./components/lightbox"; import initTE from "./autoinit/index"; const te = { @@ -64,6 +65,7 @@ const te = { Rating, Chart, Datatable, + Lightbox, }; initTE(te); @@ -95,5 +97,6 @@ export { PerfectScrollbar, Datatable, Rating, + Lightbox, initTE, };