From 4a589c0f9edffd099cb72fbc5090e22db607f301 Mon Sep 17 00:00:00 2001 From: Amr Wagdy Date: Wed, 16 Oct 2024 17:39:04 +0300 Subject: [PATCH 01/31] Refactored existing code to enhance readability, maintainability, and performance. --- .babelrc | 16 +- .prettierrc | 9 + CHANGELOG.md | 72 +- README.md | 448 ++--- config/webpack-build.config.js | 27 +- config/webpack-demo.config.js | 32 +- eslint.config.mjs | 8 + examples/src/constants.js | 48 +- examples/src/controllers.css | 2 +- examples/src/hotspots-config.constant.js | 69 +- examples/src/index.js | 211 +-- examples/src/mouse-direction.js | 42 + examples/src/styles/main.css | 30 +- package.json | 8 +- src/ci360.service.js | 1481 +++++------------ src/ci360.utils.js | 100 +- src/constants/auto-play-behavior.js | 6 - src/constants/falsy-values.js | 10 - src/constants/index.js | 7 - src/constants/orientations.js | 5 - src/constants/regex.js | 8 - src/index.js | 58 +- src/static/css/hotspots.css | 52 +- src/static/css/style.css | 114 +- src/utils/auto-play/get-speed-factor.js | 5 +- src/utils/auto-play/get-throttle-time.js | 18 + src/utils/auto-play/is-completed-one-cycle.js | 36 +- src/utils/auto-play/loop.js | 61 +- src/utils/class-names/remove-class.js | 9 +- .../constants.js} | 22 +- .../create-360-view-circle-icon.js | 3 +- .../create-360-view-icon.js | 8 - src/utils/container-elements/create-canvas.js | 10 +- .../create-close-fullscreen-icon.js | 8 +- .../container-elements/create-close-icon.js | 7 + .../create-fullscreen-icon.js | 7 - .../create-fullscreen-modal.js | 3 +- .../container-elements/create-initial-icon.js | 8 + src/utils/container-elements/create-loader.js | 8 +- src/utils/container-elements/index.js | 8 +- .../remove-child-from-parent.js | 7 - .../remove-element-from-container.js | 7 + src/utils/controls/get-item-skipped.js | 9 +- src/utils/controls/init-controls.js | 41 +- src/utils/delay.js | 9 + .../hotspots/attach-events/hide-popup.js | 2 +- .../hotspots/attach-events/show-popup.js | 2 +- src/utils/hotspots/configs-error-handler.js | 26 +- src/utils/hotspots/create-popper-instace.js | 1 - .../hotspots/elements/create-hotspot-icon.js | 18 +- .../elements/create-hotspot-popup-link.js | 4 +- .../hotspots/elements/create-hotspots.js | 7 +- .../elements/create-images-carousel.js | 1 - .../elements/create-model-elements.js | 8 +- src/utils/hotspots/elements/create-popup.js | 13 +- .../hotspots/elements/create-read-more-btn.js | 2 +- .../hotspots/generate-hotspots-configs.js | 4 +- src/utils/hotspots/generate-popup-config.js | 1 - .../hotspots/prepare-hotspots-positions.js | 33 +- src/utils/hotspots/toggle-popup-events.js | 9 +- .../hotspots/update-hotspot-icon-position.js | 12 +- src/utils/hotspots/update-hotspots.js | 24 +- src/utils/image-src/generate-cdn-path.js | 37 + .../generate-high-preview-cdn-url.js | 3 + src/utils/image-src/generate-images-path.js | 30 - .../image-src/generate-low-preview-cdn-url.js | 10 + .../is-props-change-require-reload.js | 22 +- src/utils/image-src/removeParamByRegex.js | 14 + src/utils/index.js | 11 +- .../load-images/generate-images-cdn-links.js | 4 + .../prepare-images-from-folder.js | 21 - .../prepare-images-from-list.js | 6 +- .../load-images/lazyload/init-lazyload.js | 69 +- .../get-first-cdn-image.js | 9 + .../get-first-cdn-mage-from-list.js | 14 + .../prepare-first-image-from-folder.js | 8 - .../prepare-first-image-from-list.js | 13 - .../load-images/load-image-as-promise.js | 4 +- src/utils/load-images/load-image.js | 19 + .../load-images-relative-to-container-size.js | 18 - src/utils/load-images/load-images.js | 59 + src/utils/load-images/load-original-images.js | 8 +- src/utils/load-images/preload-images.js | 98 +- .../load-images/preload-original-images.js | 24 +- .../magnify/get-current-original-image.js | 20 +- src/utils/magnify/get-cursor-position.js | 19 +- src/utils/magnify/magnify.js | 21 +- src/utils/magnify/move-magnifier.js | 40 +- src/utils/responsive/fit.js | 42 +- .../responsive/get-image-aspect-ratio.js | 11 +- .../get-size-according-to-pixel-ratio.js | 14 +- src/utils/spin-y/get-moving-direction.js | 28 +- src/utils/spin/ease-out.js | 3 + src/utils/spin/get-default-spin-direction.js | 15 + src/utils/spin/is-spin-keys-pressed.js | 11 + .../spin/should-switch-spin-direction.js | 23 + src/utils/spin/switch-spin-direction.js | 3 + src/utils/zoom/generate-zoom-in-steps.js | 17 +- src/utils/zoom/generate-zoom-out-steps.js | 17 +- 99 files changed, 1993 insertions(+), 2116 deletions(-) create mode 100644 .prettierrc create mode 100644 eslint.config.mjs create mode 100644 examples/src/mouse-direction.js delete mode 100644 src/constants/auto-play-behavior.js delete mode 100644 src/constants/falsy-values.js delete mode 100644 src/constants/index.js delete mode 100644 src/constants/orientations.js delete mode 100644 src/constants/regex.js create mode 100644 src/utils/auto-play/get-throttle-time.js rename src/{constants/props-require-reload.js => utils/constants.js} (50%) delete mode 100644 src/utils/container-elements/create-360-view-icon.js create mode 100644 src/utils/container-elements/create-close-icon.js delete mode 100644 src/utils/container-elements/create-fullscreen-icon.js create mode 100644 src/utils/container-elements/create-initial-icon.js delete mode 100644 src/utils/container-elements/remove-child-from-parent.js create mode 100644 src/utils/container-elements/remove-element-from-container.js create mode 100644 src/utils/delay.js create mode 100644 src/utils/image-src/generate-cdn-path.js create mode 100644 src/utils/image-src/generate-high-preview-cdn-url.js delete mode 100644 src/utils/image-src/generate-images-path.js create mode 100644 src/utils/image-src/generate-low-preview-cdn-url.js create mode 100644 src/utils/image-src/removeParamByRegex.js create mode 100644 src/utils/load-images/generate-images-cdn-links.js delete mode 100644 src/utils/load-images/images-from-folder/prepare-images-from-folder.js create mode 100644 src/utils/load-images/lazyload/prepare-first-image/get-first-cdn-image.js create mode 100644 src/utils/load-images/lazyload/prepare-first-image/get-first-cdn-mage-from-list.js delete mode 100644 src/utils/load-images/lazyload/prepare-first-image/prepare-first-image-from-folder.js delete mode 100644 src/utils/load-images/lazyload/prepare-first-image/prepare-first-image-from-list.js create mode 100644 src/utils/load-images/load-image.js delete mode 100644 src/utils/load-images/load-images-relative-to-container-size.js create mode 100644 src/utils/load-images/load-images.js create mode 100644 src/utils/spin/ease-out.js create mode 100644 src/utils/spin/get-default-spin-direction.js create mode 100644 src/utils/spin/is-spin-keys-pressed.js create mode 100644 src/utils/spin/should-switch-spin-direction.js create mode 100644 src/utils/spin/switch-spin-direction.js diff --git a/.babelrc b/.babelrc index 56ce9c8..212a831 100644 --- a/.babelrc +++ b/.babelrc @@ -4,23 +4,15 @@ "@babel/preset-env", { "targets": { - "browsers": [ - "last 2 versions", - "ie 11" - ] + "browsers": ["last 2 versions", "ie 11"] } } ] ], - "plugins": [ - "array-includes", - "@babel/plugin-transform-runtime" - ], + "plugins": ["array-includes", "@babel/plugin-transform-runtime"], "env": { "production": { - "presets": [ - "minify" - ] + "presets": ["minify"] } } -} \ No newline at end of file +} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..eb0950b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "tabWidth": 2, + "useTabs": false, + "printWidth": 110, + "arrowParens": "always" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 293a4a2..03ad4b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ # Changelog + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Types of changes: + - `Added` for new features. - `Changed` for changes in existing functionality. - `Deprecated` for soon-to-be removed features. @@ -17,126 +19,185 @@ Types of changes: > If we have some "Breaking changes" we can mark it in message by `**BREAKING**` preffix, like: > `- **BREAKING**: Some message` -------------- +--- ## TODOs + > Todo list for future - ... -------------- +--- + ## 3.2.0 - 2023-09-25 + ### Added + - Possibility to add hotspots dynamically ## 3.1.1 - 2023-04-19 + ### Fixed + - Remove CVE vulnerabilities ## 3.1.0 - 2023-04-10 + ### Added + - possibility to enable/disable the request of new image on resize using `data-request-responsive-images` ### Fixed + - Page scroll, even spin-y is not active ## 3.0.4 - 2022-10-19 + ### Fixed + - Hotspot icons width ## 3.0.3 - 2022-05-04 + ### Fixed + - Error on loading original images from image list ## 3.0.2 - 2022-05-04 + ### Changed + - documentation ## 3.0.1 - 2022-03-28 + ### Changed + - hotspots icons ### Fixed + - loader is hidden if hide-360-logo is active ## 3.0.0 - 2022-03-25 + ### Added + - possibility to add makers or hotspots to each image - possibility add views after init the plugin - possibility to update views + ### Fixed + - hide 360 logo after play once ## 2.7.12 - 2022-03-19 + ### Changed + - hotspots init method ## 2.7.11 - 2022-03-17 + ### Fixed + - canvas aspect ratio - initialization of lazyloading ## 2.7.10 - 2022-03-01 + ### Fixed + - image quality in fullscreen - resized image loading ## 2.7.9 - 2022-02-27 + ### Fixed + - typo in documentation ## 2.7.8 - 2022-02-27 + ### Added + - possibility to add new view to CI360 views + ### Fixed + - re-render method ## 2.7.7 - 2022-02-24 + ### Fixed + - container width on mobile - re-render method ## 2.7.6 - 2022-02-20 + ### Fixed + - drag speed - responsive canvas width and height ## 2.7.5 - 2022-02-08 + ### Added + - update method to re-render or re-init the plugin + ### Fixed + - drag speed on mobile - error when drag speed is too high - error in dependencies ## 2.7.4 - 2022-01-26 + ### Fixed + - typo in documentation file + ## 2.7.3 - 2022-01-26 + ### Fixed + - typo in documentation file ## 2.7.2 - 2022-01-26 + ### Added + - possibility to fit container relative to its width or height and maintain the aspect ratio - possibility to reverse the directions of the keys on the keyboard + ### Fixed + - pointer zoom behavior - error while loading images from lists - get the active image index ## 2.7.1 - 2021-11-06 + ### Added + - click to reset mouse zoom - show 360 logo after play once + ### Fixed + - set click as default value to start zoom - Sass error + ### Changed + - pointer zoom behavior ## 2.7.0 - 2021-11-04 + ### Added + - play once then stop auto-play - spin in the y-direction - zoom with mouse wheel @@ -144,6 +205,7 @@ Types of changes: - possibility to change icons styles ### Fixed + - hide scrollbar in fullscreen mode - auto-play not working on mobile - error on init the plugin inside a modal @@ -151,25 +213,29 @@ Types of changes: - images are not resized when window size changes ### Changed + - migrate CDN URL to V7 ## 2.6.0 - 2020-09-03 ### Feat + - add ability to specify custom 360 view logo ## 2.5.0 - 2020-07-05 ### Feat + - possibility to hide 360 view icon ## 2.4.1 - 2020-04-11 ### Fixed -- initialize first image using data-image-list #33 +- initialize first image using data-image-list #33 ## 2.4.0 - 2020-04-09 ### Fixed + - problem with first preview image is random #28, #29 diff --git a/README.md b/README.md index 3dd10be..b66df5a 100644 --- a/README.md +++ b/README.md @@ -45,23 +45,22 @@ ## Table of contents -* [Demo](#demo) -* [Step 1: Installation](#installation) -* [Step 2: Initialize](#initialize) -* [Methods](#methods) -* [Customize elements](#customize-elements) -* [Configuration](#configuration) -* [Controls](#controls) -* [Spin in X and Y axis](#spin_x_y) -* [Hotspots or Markers](#hotspots) -* [Cloudimage responsive integration](#cloudimage-responsive-integration) -* [Lazy loading integration](#lazy-loading) -* [Best practices](#best-practices) -* [Browser support](#browser_support) -* [Filerobot UI Family](#ui_family) -* [Contributing](#contributing) -* [License](#license) - +- [Demo](#demo) +- [Step 1: Installation](#installation) +- [Step 2: Initialize](#initialize) +- [Methods](#methods) +- [Customize elements](#customize-elements) +- [Configuration](#configuration) +- [Controls](#controls) +- [Spin in X and Y axis](#spin_x_y) +- [Hotspots or Markers](#hotspots) +- [Cloudimage responsive integration](#cloudimage-responsive-integration) +- [Lazy loading integration](#lazy-loading) +- [Best practices](#best-practices) +- [Browser support](#browser_support) +- [Filerobot UI Family](#ui_family) +- [Contributing](#contributing) +- [License](#license) ## Demo @@ -83,11 +82,11 @@ After adding the js-cloudimage-360-view lib, simply initialize it with **class n ```html
``` @@ -106,6 +105,7 @@ window.CI360.init(); ``` > NOTE: initialization of the plugin runs on the script load. In case you need to postpone the initialization of the plugin you can disable it with **notInitOnLoad** param +> > ```javascript > > @@ -119,8 +119,11 @@ window.CI360.init(); ```javascript window.CI360.add(idOftheView: string); ``` + lazy init cloudimage-360 view by id. + ###### arguments + `idOftheView`: string The id of the new view @@ -129,13 +132,17 @@ The id of the new view ### update ###### Type: **Function** + ```javascript window.CI360.update(idOftheView, forceUpdate); ``` + Update cloudimage 360 viewer instance.
For any update in source attributes after plugin initialization (e.g. `data-folder`, `data-filename-x`, `data-amount-y`), the plugin will re-init. + ###### arguments + `idOftheView`: string The id of the new view @@ -144,13 +151,14 @@ Force the view to reinitialize. ```html
``` + ```javascript window.CI360.update('gurkha-suv'); ``` @@ -170,6 +178,7 @@ Destroying a cloudimage 360 viewer instance will reset the HTML to its original ```javascript window.CI360.destroy(); ``` + edit in codesandbox @@ -182,7 +191,9 @@ Get the {index} of the image that is being viewed. ```javascript window.CI360.getActiveIndexByID(idOfInstance: string, oriantation: string); ``` + ###### arguments + `idOfInstance`: string The id of the instance @@ -194,37 +205,41 @@ The oriantation of the active index You can customize elements by adding the following classes: ### Example CSS + ```css .cloudimage-360-icons-container { - top: 5px; - right: 5px; + top: 5px; + right: 5px; } .cloudimage-360-fullscreen-modal { - top: 0; - bottom: 0; + top: 0; + bottom: 0; } .cloudimage-360-magnifier-icon { - background: url(https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/loupe.svg) 50% 50% / cover no-repeat; + background: url(https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/loupe.svg) + 50% 50% / cover no-repeat; } -.cloudimage-360-close-fullscreen-icon { - background: url(https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/cross.svg) 50% 50% / cover no-repeat; +.cloudimage-360-close-icon { + background: url(https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/cross.svg) + 50% 50% / cover no-repeat; } .cloudimage-360-view-360-circle { - margin: auto; + margin: auto; } .cloudimage-360-loader { - margin: auto; + margin: auto; } -.cloudimage-360-view-360-icon { - background: url(https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/360_view.svg) 50% 50% / cover no-repeat; +.cloudimage-initial-icon { + background: url(https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/360_view.svg) + 50% 50% / cover no-repeat; } .cloudimage-360-box-shadow { - top: 0; - left: 0; + top: 0; + left: 0; } .cloudimage-360-img-magnifier-glass { - border: 3px solid #000; - border-radius: 50%; + border: 3px solid #000; + border-radius: 50%; } ``` @@ -241,6 +256,7 @@ The selector for js-cloudimage-360-view lib. ###### Type: **String(url)** | _required_ Your images folder on server. + ### data-api-version (or api-version) ###### Type: **String** |Default: **'v7'** | _optional_ @@ -248,13 +264,17 @@ Your images folder on server. Allow to use a specific version of API. - set a specific version of API + ```javascript data-api-version="v7" ``` + - disable API version + ```javascript data-api-version="null" ``` + ### data-filename-x (or filename-x) ###### Type: **String** | Default: **image-{index}.jpg** | _optional_ @@ -264,9 +284,11 @@ The filename pattern for your 360 image. Must include {index}, which the library ### data-filename-y (or filename-y) ###### Type: **String** | Default: **image-y-{index}.jpg** | _optional_ + The same for [data-amount-x](#data-amount-x) but for images set in Y-axis. ### data-amount-x (or amount-x) + ###### Type: **Number** | Default: **36** | _optional_ Amount of images to load in X-axis for 360 view . @@ -310,6 +332,7 @@ Changing autoplay behavior Available behaviors (spin-x, spin-y, spin-xy, spin-yx) ### data-fullscreen (or fullscreen) + ###### Type: **Bool** | Default: **false** | _optional_ Open 360 spin view in full screen modal. @@ -321,23 +344,28 @@ Open 360 spin view in full screen modal. Magnifier to zoom image. ### data-ratio (or ratio) + ###### Type: **Number** (width / height) or JSON object | Default: **none** | _optional_ + #### `ratio`: string Setting the height relative to the container width according to the provided ratio
```html
``` + edit in codesandbox + #### `ratio`: JSON + Setting the height relative to the container width at any window size. In the following example, the height should be 1.3 the container width at window size less than or equal to 567px @@ -345,12 +373,12 @@ and 2.22 at window size less than or equal to 768px. ```html
``` + edit in codesandbox ### data-autoplay-reverse (or autoplay-reverse) @@ -403,11 +432,6 @@ Apply box shadow for container. Display 360 view line at the bottom of container. -### data-hide-360-logo (or hide-360-logo) - -###### Type: **Bool** | Default: **false** | _optional_ - -Hide 360 view icon. ### data-control-reverse (or control-reverse) @@ -434,6 +458,7 @@ Bottom offset for 360 view line. Left zero padding on filename. For example: index-zero-base="4" => image index will be "0004" ### data-image-list-x (or data-image-list-x) + ###### Type: **Array** | _optional_ Option to add list of images in x-oriantation instead of `folder` , `filename-x` & `amount-x`. @@ -467,6 +492,7 @@ data-image-list-y='[ "https://scaleflex.cloudimg.io/v7/demo/360-car/iris-30-y.jpeg" ]’ ``` + ### data-pointer-zoom (or pointer-zoom) ###### Type: **Number** | Default: **none** | _optional_ @@ -484,79 +510,89 @@ data-pointer-zoom="3" ###### Type: **Bool** | Default: **false** | _optional_ Only 360 view images close to the client's viewport will be loaded, hence accelerating the page loading time. If set to true, an additional script must be included, see [Lazy loading](#lazy-loading) + ### data-lazyload-selector (or lazyload-selector) ###### Type: **String** | Default: **lazyload** | _optional_ Helper class to apply lazy-loading depending on library you choose, see [Lazy loading](#lazy-loading) - ## Controls -You can add controls by adding elements with the following classes: **cloudimage-360-left**, **cloudimage-360-right**, **cloudimage-360-top**, **cloudimage-360-bottom +You can add controls by adding elements with the following classes: **cloudimage-360-left**, **cloudimage-360-right**, **cloudimage-360-top**, \*\*cloudimage-360-bottom ### Example CSS + ```css -.cloudimage-360 .cloudimage-360-left, .cloudimage-360 .cloudimage-360-right { - padding: 8px; - background: rgba(255, 255, 255, 0.5); - border: none; - border-radius: 4px; +.cloudimage-360 .cloudimage-360-left, +.cloudimage-360 .cloudimage-360-right { + padding: 8px; + background: rgba(255, 255, 255, 0.5); + border: none; + border-radius: 4px; } -.cloudimage-360 .cloudimage-360-left:focus, .cloudimage-360 .cloudimage-360-right:focus { - outline: none; +.cloudimage-360 .cloudimage-360-left:focus, +.cloudimage-360 .cloudimage-360-right:focus { + outline: none; } .cloudimage-360 .cloudimage-360-left { - display: none; - position: absolute; - z-index: 100; - top: calc(50% - 15px); - left: 20px; + display: none; + position: absolute; + z-index: 100; + top: calc(50% - 15px); + left: 20px; } .cloudimage-360 .cloudimage-360-right { - display: none; - position: absolute; - z-index: 100; - top: calc(50% - 15px); - right: 20px; + display: none; + position: absolute; + z-index: 100; + top: calc(50% - 15px); + right: 20px; } -.cloudimage-360 .cloudimage-360-left:before, .cloudimage-360 .cloudimage-360-right:before { - content: ''; - display: block; - width: 30px; - height: 30px; - background: 50% 50% / cover no-repeat; +.cloudimage-360 .cloudimage-360-left:before, +.cloudimage-360 .cloudimage-360-right:before { + content: ''; + display: block; + width: 30px; + height: 30px; + background: 50% 50% / cover no-repeat; } .cloudimage-360 .cloudimage-360-left:before { - background-image: url('https://cdn.scaleflex.it/plugins/js-cloudimage-360-view/assets/img/arrow-left.svg'); + background-image: url('https://cdn.scaleflex.it/plugins/js-cloudimage-360-view/assets/img/arrow-left.svg'); } .cloudimage-360 .cloudimage-360-right:before { - background-image: url('https://cdn.scaleflex.it/plugins/js-cloudimage-360-view/assets/img/arrow-right.svg'); + background-image: url('https://cdn.scaleflex.it/plugins/js-cloudimage-360-view/assets/img/arrow-right.svg'); } -.cloudimage-360 .cloudimage-360-left.not-active, .cloudimage-360 .cloudimage-360-right.not-active { - opacity: 0.4; - cursor: default; +.cloudimage-360 .cloudimage-360-left.not-active, +.cloudimage-360 .cloudimage-360-right.not-active { + opacity: 0.4; + cursor: default; } ``` + ### Example HTML + ```html
- - - - + + + +
``` edit in codesandbox ## Spin in X and Y axes + Allow the view to spin in both X, Y axes + ### Requirements + We need to provide the `file-name` of the y-axis images using data-filename-y Also as we did for the x-axis if we are intializing the view using data-folder and data-filename-y @@ -565,25 +601,31 @@ example: ```javascript
-
+ class="cloudimage-360" + data-folder="https://scaleflex.cloudimg.io/v7/demo/360-nike/" + data-filename-x="nike-{index}.jpg" + data-filename-y="nike-y-{index}.jpg" + data-amount-x="35" + data-amount-y="36" +> ``` + edit in codesandbox
+ > Note: We can initilize the view in x, y axes without providing add `data-folder`, `data-amount-y`, `data-amount-y`.
-Just we need to provide the data-amount-y +> Just we need to provide the data-amount-y + ## Hotspots or Markers + Display information about the product on specific areas. Once a hotspot is created it can be used on more than one image. + ### Requirements + First, we need to set `data-hotspots` attribute to the view we want to add hotspots or markers on it, to prevent the plugin to init the view without hotspots config. Also we need to set an `id` attribute, we will need it to link the view with the hotspots config. ### Create hotspots configuration + The hotspots config should be an array of objects, each object in the array indicates a single hotspot config. For each item in the array, we need to set the positions (X-coord and Y-coord) of the hotspot at every image index we need to show the hotspot on it.
hint: To know the current image index we will need to set `data-info="white || black"` attribute. @@ -591,136 +633,165 @@ example: ```js const HOTSPOTS_CONFIG = [ - { - positions: [ - { imageIndex: 0, xCoord: 527, yCoord: 319 }, - { imageIndex: 1, xCoord: 524 }, - { imageIndex: 2, xCoord: 520 }, - { imageIndex: 3, xCoord: 498 }, - { imageIndex: 4, xCoord: 470 }, - { imageIndex: 5, xCoord: 441 }, - ] - } -] + { + positions: [ + { imageIndex: 0, xCoord: 527, yCoord: 319 }, + { imageIndex: 1, xCoord: 524 }, + { imageIndex: 2, xCoord: 520 }, + { imageIndex: 3, xCoord: 498 }, + { imageIndex: 4, xCoord: 470 }, + { imageIndex: 5, xCoord: 441 }, + ], + }, +]; ``` + In the previous example, we have only set the Ycoord a single time at the image index 0. So if the coord didn't change there's no need to reset it, it will already take the previous value. Now we need to set the hotspot variant, we have three types of hotspots (link, popup, and custom), as it will be explained below. + ## Variant + ### Link + we need to provide the URL of the link and the link title. example: ```js const HOTSPOTS_CONFIG = [ - { - positions, - variant: { - title: 'New Gurkha Technical Specifications', - url: 'https://www.forcegurkha.co.in/specifications/', - newTab: true - } - } -] + { + positions, + variant: { + title: 'New Gurkha Technical Specifications', + url: 'https://www.forcegurkha.co.in/specifications/', + newTab: true, + }, + }, +]; ``` + --- + ### Popup + Only the property inserted will displayed. -| Property |Type | Default | Description | +| Property |Type | Default | Description | | ------------- | ------------- | ------------- |------------- | -| images | Array| [] | To display a carousel of images we need an array of objects, each object should include the src and the alt of each image | -| title |String| null | Display title underneath the images | -| description| String | null | Display description underneath the title | +| images | Array| [] | To display a carousel of images we need an array of objects, each object should include the src and the alt of each image | +| title |String| null | Display title underneath the images | +| description| String | null | Display description underneath the title | | moreDetailsUrl | String | null | Display a button underneath the description to navigate to a provided URL | -| moreDetailsTitle | String | null| Set the title of the more details button | +| moreDetailsTitle | String | null| Set the title of the more details button | + +example: - example: ```js const HOTSPOTS_CONFIG = [ - { - positions, - variant: { - images: [ - { src: 'https://scaleflex.cloudimg.io/v7/demo/360-assets/AIR_SNORKEL_FINAL_JPG.png?vh=88bccb', alt: 'air snorkel' } - ], // optional - title: 'Air Intake Snorkel', // optional - description: 'The snorkel gives the Gurkha an unmatched water-wading ability and ensures ample supply of fresh air for combustion.', // optional - moreDetailsUrl: 'https://forcegurkha.co.in', // optional - moreDetailsTitle: 'Read more' // optional - } - } -] + { + positions, + variant: { + images: [ + { + src: 'https://scaleflex.cloudimg.io/v7/demo/360-assets/AIR_SNORKEL_FINAL_JPG.png?vh=88bccb', + alt: 'air snorkel', + }, + ], // optional + title: 'Air Intake Snorkel', // optional + description: + 'The snorkel gives the Gurkha an unmatched water-wading ability and ensures ample supply of fresh air for combustion.', // optional + moreDetailsUrl: 'https://forcegurkha.co.in', // optional + moreDetailsTitle: 'Read more', // optional + }, + }, +]; ``` + --- + ### Custom + Display any element in the DOM in a popup and link it with the hotspot.
We will need to set the variant property value to the id of the element. example: + ```js const HOTSPOTS_CONFIG = [ - { - positions, - variant: 'gurkha-suv' - } -] + { + positions, + variant: 'gurkha-suv', + }, +]; ``` + ## PopupProps + Options to customize the hotspot popup. + ### Properties -| Property | Type | Defaullt |Description | -| ------------- | ------------- | ------------- | ------------- | -| popupSelector |String| null |Set className to the popup wrapper | -| open |Boolean | false |Open the popup | -| arrow |Boolean| true |Dipslay an arrow that points toward the hotspot element| -| offset|Array | [0, 0] |Set a distance between the hotspot element and the popup | -| placement|String| Auto|- we can adjust the position of the hotspot popup relative to the hotspot element. (top - bottom - left - right)| +| Property | Type | Defaullt | Description | +| ------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------- | +| popupSelector | String | null | Set className to the popup wrapper | +| open | Boolean | false | Open the popup | +| arrow | Boolean | true | Dipslay an arrow that points toward the hotspot element | +| offset | Array | [0, 0] | Set a distance between the hotspot element and the popup | +| placement | String | Auto | - we can adjust the position of the hotspot popup relative to the hotspot element. (top - bottom - left - right) | example: + ```js const HOTSPOTS_CONFIG = [ - { - positions, - variant, - popupProps: { - popupSelector: 'air-intake-popup', // optional - offset: [20, 5], // optional - arrow: false, // optional - placement: 'bottom' // optional + { + positions, + variant, + popupProps: { + popupSelector: 'air-intake-popup', // optional + offset: [20, 5], // optional + arrow: false, // optional + placement: 'bottom', // optional + }, + indicatorSelector: 'first-hotspot-icon', // optional }, - indicatorSelector: 'first-hotspot-icon' // optional - } -] +]; ``` + ## Responsive hotspots + Now we need to make our hotspots responsive to have an accurate positioning in different screens. we have to set `initialDimensions` property to every hotspot config. which indicates the dimension of the cloudimage-360 view.
hint: `data-info` can be used to get view size. example: + ```js const HOTSPOTS_CONFIG = [ - { - positions, - variant, - popupProps, - indicatorSelector, - initialDimensions: [ 1170, 662 ] - } -] + { + positions, + variant, + popupProps, + indicatorSelector, + initialDimensions: [1170, 662], + }, +]; ``` + ## Add Hotspots + we need this function to link the created config with the 360-view. + ```js window.CI360.addHotspots(idOftheView, hotspotsConfig); ``` + example: + ```js -window.CI360.addHotspots("gurkha-suv", HOTSPOTS_CONFIG); +window.CI360.addHotspots('gurkha-suv', HOTSPOTS_CONFIG); ``` +
edit in codesandbox ### data-responsive (or responsive) @@ -759,7 +830,7 @@ Request new images on resize, based on the container width. ###### Type: **String** | Default: **none** | _optional_ Applies Cloudimage resize operations to your image, e.g. width, height, crop, face crop, rotate, prevent enlargement... -Multiple transformation operations can be applied to your image, separated by "```&```" (Ampersand). +Multiple transformation operations can be applied to your image, separated by "`&`" (Ampersand). example: ```html @@ -773,7 +844,7 @@ data-transformation="w=400&h=200&func=fit" ###### Type: **String** | Default: **none** | _optional_ Applies Cloudimage filters to your image, e.g. brightness, contrast, greyscale, blur, Sharpen... -Multiple filters can be applied, separated by "```,```" (comma). +Multiple filters can be applied, separated by "`,`" (comma). example: ```html @@ -794,11 +865,11 @@ Lazy loading is not included into js-cloudimage-360-view by default. There are w ## Best practices -* In order to use cloudimage responsive with 360 view, your original (master) images should be stored on a server -or storage bucket (S3, Google Cloud, Azure Blob...) reachable over -HTTP or HTTPS by Cloudimage. If you want to upload your master images to -Cloudimage, contact us at -[hello@cloudimage.io](mailto:hello@cloudimage.io). +- In order to use cloudimage responsive with 360 view, your original (master) images should be stored on a server + or storage bucket (S3, Google Cloud, Azure Blob...) reachable over + HTTP or HTTPS by Cloudimage. If you want to upload your master images to + Cloudimage, contact us at + [hello@cloudimage.io](mailto:hello@cloudimage.io). ## Browser support @@ -806,15 +877,16 @@ Tested in all modern browsers and IE 11, 10, 9. ## Filerobot UI Familiy -* [JS Cloudimage Responsive](https://github.com/scaleflex/js-cloudimage-responsive) -* [React Cloudimage Responsive](https://github.com/scaleflex/react-cloudimage-responsive) -* [Angular Cloudimage Responsive](https://github.com/scaleflex/ng-cloudimage-responsive) -* [Image Editor](https://github.com/scaleflex/filerobot-image-editor) -* [Uploader](https://github.com/scaleflex/filerobot-uploader) +- [JS Cloudimage Responsive](https://github.com/scaleflex/js-cloudimage-responsive) +- [React Cloudimage Responsive](https://github.com/scaleflex/react-cloudimage-responsive) +- [Angular Cloudimage Responsive](https://github.com/scaleflex/ng-cloudimage-responsive) +- [Image Editor](https://github.com/scaleflex/filerobot-image-editor) +- [Uploader](https://github.com/scaleflex/filerobot-uploader) ## Contributing! All contributions are super welcome! ## License + JS Cloudimage 360 View is provided under the [MIT License](https://opensource.org/licenses/MIT) diff --git a/config/webpack-build.config.js b/config/webpack-build.config.js index f4a1ae2..dda9125 100644 --- a/config/webpack-build.config.js +++ b/config/webpack-build.config.js @@ -13,30 +13,27 @@ const banner = ` Date: ${now.toISOString()} `; - module.exports = { - entry: path.join(__dirname, "../src/index.js"), + entry: path.join(__dirname, '../src/index.js'), output: { - path: path.join(__dirname, "../build"), - filename: `${pkg.name}.min.js` + path: path.join(__dirname, '../build'), + filename: `${pkg.name}.min.js`, }, module: { rules: [ { test: /\.(js|jsx)$/, - use: "babel-loader", - exclude: /node_modules/ + use: 'babel-loader', + exclude: /node_modules/, }, { test: /\.css$/, - use: ["style-loader", "css-loader"] - } - ] + use: ['style-loader', 'css-loader'], + }, + ], }, - plugins: [ - new webpack.BannerPlugin(banner), - ], + plugins: [new webpack.BannerPlugin(banner)], resolve: { - extensions: [".js", ".jsx"] - } -}; \ No newline at end of file + extensions: ['.js', '.jsx'], + }, +}; diff --git a/config/webpack-demo.config.js b/config/webpack-demo.config.js index e9f2e35..2fbd23b 100644 --- a/config/webpack-demo.config.js +++ b/config/webpack-demo.config.js @@ -1,35 +1,35 @@ const path = require('path'); -const HtmlWebpackPlugin = require("html-webpack-plugin"); -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const htmlWebpackPlugin = new HtmlWebpackPlugin({ - template: path.join(__dirname, "../examples/src/index.html"), - filename: "./index.html" + template: path.join(__dirname, '../examples/src/index.html'), + filename: './index.html', }); module.exports = { - entry: path.join(__dirname, "../examples/src/index.js"), + entry: path.join(__dirname, '../examples/src/index.js'), output: { - path: path.join(__dirname, "../examples/dist"), - filename: "bundle[hash].js" + path: path.join(__dirname, '../examples/dist'), + filename: 'bundle[hash].js', }, module: { rules: [ { test: /\.(js|jsx)$/, - use: "babel-loader", - exclude: /node_modules/ + use: 'babel-loader', + exclude: /node_modules/, }, { test: /\.css$/, - use: [MiniCssExtractPlugin.loader, "css-loader"] - } - ] + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], }, plugins: [htmlWebpackPlugin, new MiniCssExtractPlugin()], resolve: { - extensions: [".js", ".jsx"] + extensions: ['.js', '.jsx'], }, devServer: { - port: 3001 + port: 3001, }, - devtool: 'source-map' -}; \ No newline at end of file + devtool: 'source-map', +}; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..2edbef8 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,8 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; + + +export default [ + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, +]; \ No newline at end of file diff --git a/examples/src/constants.js b/examples/src/constants.js index e231662..d3d9023 100644 --- a/examples/src/constants.js +++ b/examples/src/constants.js @@ -1,46 +1,46 @@ import { GURKHA_SUV_HOTSPOTS_CONFIG } from './hotspots-config.constant'; const NIKE_PLUGIN = { - "data-folder": "https://scaleflex.cloudimg.io/v7/demo/360-nike/", - "data-filename-x": "nike-{index}.jpg", - "data-filename-y": "nike-y-{index}.jpg", - "data-amount-x": "35", - "data-amount-y": "36", - "data-autoplay-behavior": "spin-xy", + 'data-folder': 'https://scaleflex.cloudimg.io/v7/demo/360-nike/', + 'data-filename-x': 'nike-{index}.jpg', + 'data-filename-y': 'nike-y-{index}.jpg', + 'data-amount-x': '35', + 'data-amount-y': '36', + 'data-autoplay-behavior': 'spin-xy', }; const EARBUDS_PLUGIN = { - "data-folder": "https://scaleflex.cloudimg.io/v7/demo/earbuds/", - "data-filename-x": "{index}.jpg", - "data-amount-x": "233", + 'data-folder': 'https://scaleflex.cloudimg.io/v7/demo/earbuds/', + 'data-filename-x': '{index}.jpg', + 'data-amount-x': '233', }; const SPIN_DIRECTION_PROPS = [ 'data-filename-y', 'data-amount-y', - 'data-autoplay-behavior' + 'data-autoplay-behavior', ]; const PLUGIN_PROPS = { - class: { value: "cloudimage-360", isRequired: true }, - "data-folder": { - value: "https://scaleflex.cloudimg.io/v7/demo/earbuds/", + class: { value: 'cloudimage-360', isRequired: true }, + 'data-folder': { + value: 'https://scaleflex.cloudimg.io/v7/demo/earbuds/', isRequired: true, - isUrl: true + isUrl: true, }, - "data-filename-x": { value: "{index}.jpg", isRequired: true }, - "data-amount-x": { value: 233, isRequired: true }, - "data-speed": { value: 100, isRequired: true }, - "data-drag-speed": { value: 120, isRequired: true }, - "data-autoplay": { isRequired: true } + 'data-filename-x': { value: '{index}.jpg', isRequired: true }, + 'data-amount-x': { value: 233, isRequired: true }, + 'data-speed': { value: 100, isRequired: true }, + 'data-drag-speed': { value: 120, isRequired: true }, + 'data-autoplay': { isRequired: true }, }; const PROPERTIES_COLORS = { - NAME: "#7B9200", - URL: "#2D88CB" + NAME: '#7B9200', + URL: '#2D88CB', }; -const URL_PROPERTIES = ["data-folder", "class"]; +const URL_PROPERTIES = ['data-folder', 'class']; export { NIKE_PLUGIN, @@ -49,5 +49,5 @@ export { PLUGIN_PROPS, PROPERTIES_COLORS, URL_PROPERTIES, - GURKHA_SUV_HOTSPOTS_CONFIG -} \ No newline at end of file + GURKHA_SUV_HOTSPOTS_CONFIG, +}; diff --git a/examples/src/controllers.css b/examples/src/controllers.css index 9f77a2f..f3010f6 100644 --- a/examples/src/controllers.css +++ b/examples/src/controllers.css @@ -103,4 +103,4 @@ .cloudimage-360 .cloudimage-360-bottom.not-active { opacity: 0.4; cursor: default; -} \ No newline at end of file +} diff --git a/examples/src/hotspots-config.constant.js b/examples/src/hotspots-config.constant.js index 4311de1..db5c2a1 100644 --- a/examples/src/hotspots-config.constant.js +++ b/examples/src/hotspots-config.constant.js @@ -4,11 +4,12 @@ const GURKHA_SUV_HOTSPOTS_CONFIG = [ images: [ { src: 'https://scaleflex.cloudimg.io/v7/demo/360-assets/AIR_SNORKEL_FINAL_JPG.png?vh=88bccb', - alt: 'air snorkel' - } + alt: 'air snorkel', + }, ], title: 'Air Intake Snorkel', - description: 'The snorkel gives the Gurkha an unmatched water-wading ability and ensures ample supply of fresh air for combustion.', + description: + 'The snorkel gives the Gurkha an unmatched water-wading ability and ensures ample supply of fresh air for combustion.', moreDetailsUrl: 'https://forcegurkha.co.in', }, popupProps: { popupSelector: 'air-intake-popup' }, @@ -37,10 +38,10 @@ const GURKHA_SUV_HOTSPOTS_CONFIG = [ variant: { title: 'New Gurkha Technical Specifications', url: 'https://www.forcegurkha.co.in/specifications/', - newTab: true + newTab: true, }, - initialDimensions: [ 1170, 662 ], - popupProps: { popupSelector: 'popup-link'}, + initialDimensions: [1170, 662], + popupProps: { popupSelector: 'popup-link' }, positions: [ { imageIndex: 73, xCoord: 355, yCoord: 474 }, { imageIndex: 74, xCoord: 355 }, @@ -62,23 +63,25 @@ const GURKHA_SUV_HOTSPOTS_CONFIG = [ images: [ { src: 'https://scaleflex.cloudimg.io/v7/demo/360-assets/transparent+%281%29.jpg', - alt: 'car accessory' + alt: 'car accessory', }, { src: 'https://scaleflex.cloudimg.io/v7/demo/360-assets/transparent+%282%29.jpg', - alt: 'spoiler' + alt: 'spoiler', }, { src: 'https://scaleflex.cloudimg.io/v7/demo/360-assets/transparent.jpg', - alt: 'car accessory' - } + alt: 'car accessory', + }, ], - description: 'The All New Force Gurkha ensures best-in-class safety for passengers and security for the vehicle with its full metal body structure.', + description: + 'The All New Force Gurkha ensures best-in-class safety for passengers and security for the vehicle with its full metal body structure.', moreDetailsTitle: 'Download accessories brochure', - moreDetailsUrl: 'https://www.forcegurkha.co.in/wp-content/themes/force-motors/assets/pdf/Force-Gurkha-accessories-E-brochure.pdf' + moreDetailsUrl: + 'https://www.forcegurkha.co.in/wp-content/themes/force-motors/assets/pdf/Force-Gurkha-accessories-E-brochure.pdf', }, popupProps: { popupSelector: 'accessories-popup' }, - initialDimensions: [ 1170, 662 ], + initialDimensions: [1170, 662], positions: [ { imageIndex: 11, xCoord: 683, yCoord: 151 }, { imageIndex: 12, xCoord: 683 }, @@ -93,15 +96,15 @@ const GURKHA_SUV_HOTSPOTS_CONFIG = [ { imageIndex: 21, xCoord: 656, yCoord: 168 }, { imageIndex: 22, xCoord: 650, yCoord: 171 }, { imageIndex: 23, xCoord: 643, yCoord: 176 }, - { imageIndex: 24, xCoord:635, yCoord: 178 }, - { imageIndex: 25, xCoord:628, yCoord: 181 }, - { imageIndex: 26, xCoord:621 }, - { imageIndex: 27, xCoord:610 }, - { imageIndex: 28, xCoord:598 }, - { imageIndex: 29, xCoord:588 }, - { imageIndex: 30, xCoord:578 }, - { imageIndex: 31, xCoord:570, yCoord: 176 }, - { imageIndex: 32, xCoord:560, yCoord: 173 }, + { imageIndex: 24, xCoord: 635, yCoord: 178 }, + { imageIndex: 25, xCoord: 628, yCoord: 181 }, + { imageIndex: 26, xCoord: 621 }, + { imageIndex: 27, xCoord: 610 }, + { imageIndex: 28, xCoord: 598 }, + { imageIndex: 29, xCoord: 588 }, + { imageIndex: 30, xCoord: 578 }, + { imageIndex: 31, xCoord: 570, yCoord: 176 }, + { imageIndex: 32, xCoord: 560, yCoord: 173 }, ], }, { @@ -109,21 +112,23 @@ const GURKHA_SUV_HOTSPOTS_CONFIG = [ images: [ { src: 'https://scaleflex.cloudimg.io/v7/demo/360-assets/feature-5.png', - alt: 'car top' + alt: 'car top', }, { src: 'https://scaleflex.cloudimg.io/v7/demo/360-assets/ALL_AROUND_VISIBILITY_UPDATED.png', - alt: 'car inside' + alt: 'car inside', }, { src: 'https://scaleflex.cloudimg.io/v7/demo/360-assets/feature-2.png', - alt: 'transmission' - } + alt: 'transmission', + }, ], - description:'All four captain seats with ample head room, leg room and shoulder room.Easy ingress and egress from the rear door into the widest gangway in the category that makes space for all your luggage', - moreDetailsUrl: 'https://www.forcegurkha.co.in/wp-content/themes/force-motors/assets/pdf/Force-Gurkha-accessories-E-brochure.pdf', + description: + 'All four captain seats with ample head room, leg room and shoulder room.Easy ingress and egress from the rear door into the widest gangway in the category that makes space for all your luggage', + moreDetailsUrl: + 'https://www.forcegurkha.co.in/wp-content/themes/force-motors/assets/pdf/Force-Gurkha-accessories-E-brochure.pdf', }, - initialDimensions: [ 1170, 662 ], + initialDimensions: [1170, 662], popupProps: { popupSelector: 'car-cabin' }, positions: [ { imageIndex: 6, xCoord: 607, yCoord: 246 }, @@ -133,8 +138,6 @@ const GURKHA_SUV_HOTSPOTS_CONFIG = [ { imageIndex: 10, xCoord: 642 }, ], }, -] +]; -export { - GURKHA_SUV_HOTSPOTS_CONFIG -} \ No newline at end of file +export { GURKHA_SUV_HOTSPOTS_CONFIG }; diff --git a/examples/src/index.js b/examples/src/index.js index aa4be33..9e14dc9 100644 --- a/examples/src/index.js +++ b/examples/src/index.js @@ -9,53 +9,55 @@ import { PROPERTIES_COLORS, URL_PROPERTIES, GURKHA_SUV_HOTSPOTS_CONFIG, -} from "./constants"; - -const spinDirections = document.getElementById("spin-directions"); -const imagesY = document.getElementById("images-y"); -const responsive = document.getElementById("responsive-checkbox"); -const boxShadow = document.getElementById("box-shadow"); -const imageXSelector = document.getElementById("x-images-selector"); -const nikeXSelector = document.getElementById("nike-x-images"); -const copyText = document.getElementById("copy-text"); -const codeBlock = document.getElementById("code-block"); -const codeWrapper = document.getElementById("code-wrapper"); -const controlOption = document.getElementById("control-option"); -const autoPlayBehavior = document.getElementById("auto-play-behavior"); -let container = document.getElementById("demo-generator"); - -const copyButton = document.querySelector(".copy-button"); -const outputCode = document.querySelector(".output-code"); -const pointerZoomCheckbox = document.querySelector("[data-checkbox]"); - -const accordions = document.querySelectorAll("[data-accordion]"); -const pluginCheckboxOptions = document.querySelectorAll(".plugin-option"); -const pluginInputs = document.querySelectorAll("[plugin-input]"); -let controlButtons = Array.from(document.querySelectorAll("control-buttons")); +} from './constants'; + +const spinDirections = document.getElementById('spin-directions'); +const imagesY = document.getElementById('images-y'); +const responsive = document.getElementById('responsive-checkbox'); +const boxShadow = document.getElementById('box-shadow'); +const imageXSelector = document.getElementById('x-images-selector'); +const nikeXSelector = document.getElementById('nike-x-images'); +const copyText = document.getElementById('copy-text'); +const codeBlock = document.getElementById('code-block'); +const codeWrapper = document.getElementById('code-wrapper'); +const controlOption = document.getElementById('control-option'); +const autoPlayBehavior = document.getElementById('auto-play-behavior'); +let container = document.getElementById('demo-generator'); + +const copyButton = document.querySelector('.copy-button'); +const outputCode = document.querySelector('.output-code'); +const pointerZoomCheckbox = document.querySelector('[data-checkbox]'); + +const accordions = document.querySelectorAll('[data-accordion]'); +const pluginCheckboxOptions = document.querySelectorAll('.plugin-option'); +const pluginInputs = document.querySelectorAll('[plugin-input]'); +let controlButtons = Array.from(document.querySelectorAll('control-buttons')); let isSpinY = false; const CLOUDIMAGE_360 = window.CI360; function updateContainer() { - container = document.getElementById("demo-generator"); + container = document.getElementById('demo-generator'); } function updateControlButtons() { - controlButtons = Array.from(document.querySelectorAll(".control-buttons")); + controlButtons = Array.from(document.querySelectorAll('.control-buttons')); } function toggleControlButtons() { - const yButtonsIds = ["control-up-button", "control-down-button"]; + const yButtonsIds = ['control-up-button', 'control-down-button']; controlButtons.forEach((button) => { if (isSpinY) { - button.style.visibility = controlOption.checked ? "visible" : "hidden"; + button.style.visibility = controlOption.checked ? 'visible' : 'hidden'; } else { - button.style.visibility = "hidden"; + button.style.visibility = 'hidden'; button.style.visibility = - controlOption.checked && !yButtonsIds.includes(button.id) ? "visible" : "hidden"; + controlOption.checked && !yButtonsIds.includes(button.id) + ? 'visible' + : 'hidden'; } }); } @@ -64,21 +66,21 @@ function changeSpinDirectionHandler(event) { const spinDirection = event.target.value; const earbudsPlugin = Object.entries(EARBUDS_PLUGIN); const nikePlugin = Object.entries(NIKE_PLUGIN); - const isSpinYDirection = spinDirection === "Y"; + const isSpinYDirection = spinDirection === 'Y'; nikePlugin.forEach(([key, value]) => { updatePluginValues(key, { value }, null, !isSpinYDirection); - isSpinYDirection ? - container.setAttribute(key, value) : - container.removeAttribute(key); + isSpinYDirection + ? container.setAttribute(key, value) + : container.removeAttribute(key); }); if (isSpinYDirection) { isSpinY = true; - imageXSelector.style.display = "none"; - nikeXSelector.style.display = "block"; - imagesY.style.display = "flex"; + imageXSelector.style.display = 'none'; + nikeXSelector.style.display = 'block'; + imagesY.style.display = 'flex'; } else { isSpinY = false; @@ -87,14 +89,14 @@ function changeSpinDirectionHandler(event) { updatePluginValues(key, { value }); }); - imageXSelector.style.display = "block"; - nikeXSelector.style.display = "none"; - imagesY.style.display = "none"; + imageXSelector.style.display = 'block'; + nikeXSelector.style.display = 'none'; + imagesY.style.display = 'none'; } - autoPlayBehavior.disabled = !autoPlayBehavior.disabled + autoPlayBehavior.disabled = !autoPlayBehavior.disabled; - CLOUDIMAGE_360.update("demo-generator"); + CLOUDIMAGE_360.update('demo-generator'); updateContainer(); updateControlButtons(); toggleControlButtons(); @@ -102,103 +104,102 @@ function changeSpinDirectionHandler(event) { function changePointerZoomHandler(event) { const ispluginCheckboxChecked = event.target.checked; - const nextCheckbox = event.target.getAttribute("data-checkbox"); + const nextCheckbox = event.target.getAttribute('data-checkbox'); const pluginInput = document.querySelector(`[data-input=${nextCheckbox}]`); - const pluginAttribute = event.target.getAttribute("data-plugin-value"); - const value = pluginInput[pluginInput.type === "checkbox" ? "checked" : "value"]; + const pluginAttribute = event.target.getAttribute('data-plugin-value'); + const value = + pluginInput[pluginInput.type === 'checkbox' ? 'checked' : 'value']; if (ispluginCheckboxChecked) { container.setAttribute(pluginAttribute, value); - } else { + } else { container.removeAttribute(pluginAttribute); } - pluginInput.disabled=!pluginInput.disabled + pluginInput.disabled = !pluginInput.disabled; - CLOUDIMAGE_360.update("demo-generator"); + CLOUDIMAGE_360.update('demo-generator'); updateContainer(); - updatePluginValues( - pluginAttribute, - { value: value }, - event.target.type, - ); + updatePluginValues(pluginAttribute, { value: value }, event.target.type); } function changeResponsiveOptionHandler(event) { - const allPluginInput = document.querySelectorAll("[responsive-option]"); + const allPluginInput = document.querySelectorAll('[responsive-option]'); const ispluginCheckboxChecked = event.target.checked; allPluginInput.forEach((input) => { - const pluginAttribute = input.getAttribute("data-plugin-value"); + const pluginAttribute = input.getAttribute('data-plugin-value'); if (ispluginCheckboxChecked) { container.setAttribute(pluginAttribute, input.value); updatePluginValues( pluginAttribute, { value: input.value }, - event.target.type, + event.target.type ); } else { container.removeAttribute(pluginAttribute); - updatePluginValues(pluginAttribute, { value: "" }, event.target.type); + updatePluginValues(pluginAttribute, { value: '' }, event.target.type); } - input.disabled=!input.disabled + input.disabled = !input.disabled; }); - CLOUDIMAGE_360.update("demo-generator", true); + CLOUDIMAGE_360.update('demo-generator', true); updateContainer(); } function changeBoxShadowOptionHandler(event) { - const shadowBoxInput = document.querySelector("[box-shadow-option]"); + const shadowBoxInput = document.querySelector('[box-shadow-option]'); const ispluginCheckboxChecked = event.target.checked; - const pluginAttribute = shadowBoxInput.getAttribute("data-plugin-value"); + const pluginAttribute = shadowBoxInput.getAttribute('data-plugin-value'); - if (ispluginCheckboxChecked) { - container.setAttribute(pluginAttribute, shadowBoxInput.value); + if (ispluginCheckboxChecked) { + container.setAttribute(pluginAttribute, shadowBoxInput.value); - updatePluginValues( - pluginAttribute, - { value: shadowBoxInput.value }, - event.target.type, - ); - } else { - container.removeAttribute(pluginAttribute); - updatePluginValues(pluginAttribute, { value: "" }, event.target.type); - } + updatePluginValues( + pluginAttribute, + { value: shadowBoxInput.value }, + event.target.type + ); + } else { + container.removeAttribute(pluginAttribute); + updatePluginValues(pluginAttribute, { value: '' }, event.target.type); + } - shadowBoxInput.disabled=!shadowBoxInput.disabled + shadowBoxInput.disabled = !shadowBoxInput.disabled; - CLOUDIMAGE_360.update("demo-generator", true); + CLOUDIMAGE_360.update('demo-generator', true); updateContainer(); } function showAccordionContent(event) { - const contentID = event.target.getAttribute("data-accordion"); - const accordionContent = ( - document.querySelector(`[data-accordion-content="${contentID}"]`) + const contentID = event.target.getAttribute('data-accordion'); + const accordionContent = document.querySelector( + `[data-accordion-content="${contentID}"]` ); - accordionContent.style.display = !accordionContent.offsetWidth ? "block": "none"; + accordionContent.style.display = !accordionContent.offsetWidth + ? 'block' + : 'none'; } function copyCodeHandler() { navigator.clipboard.writeText(outputCode.innerText); - copyText.innerHTML = "Copied"; + copyText.innerHTML = 'Copied'; setTimeout(() => { - copyText.innerHTML = "Copy"; + copyText.innerHTML = 'Copy'; }, 500); } function pluginCheckboxOptionsHandler(event) { const ispluginCheckboxChecked = event.target.checked; - const pluginAttribute = event.target.getAttribute("data-plugin-value"); - const defaultValue = event.target.value - const value = defaultValue === "on" ? "" : defaultValue + const pluginAttribute = event.target.getAttribute('data-plugin-value'); + const defaultValue = event.target.value; + const value = defaultValue === 'on' ? '' : defaultValue; if (ispluginCheckboxChecked) { container.setAttribute(pluginAttribute, value); @@ -206,14 +207,14 @@ function pluginCheckboxOptionsHandler(event) { container.removeAttribute(pluginAttribute); } - CLOUDIMAGE_360.update("demo-generator"); + CLOUDIMAGE_360.update('demo-generator'); updateContainer(); updatePluginValues(pluginAttribute, { value: value }, event.target.type); } function changePluginInputsHandler(event) { const value = event.target.value; - const pluginAttribute = event.target.getAttribute("data-plugin-value"); + const pluginAttribute = event.target.getAttribute('data-plugin-value'); if (value) { container.setAttribute(pluginAttribute, value); @@ -221,18 +222,18 @@ function changePluginInputsHandler(event) { container.removeAttribute(pluginAttribute); } - CLOUDIMAGE_360.update("demo-generator"); + CLOUDIMAGE_360.update('demo-generator'); updateContainer(); updatePluginValues(pluginAttribute, { value }, event.target.type); } function updatePluginValues(key, properties = {}, inputType, removeProp) { - const isCheckbox = inputType === "checkbox"; + const isCheckbox = inputType === 'checkbox'; const isEmptyInput = !isCheckbox && !properties.value; if ( - Object.keys(PLUGIN_PROPS).includes(key) - && (isCheckbox || isEmptyInput || removeProp) + Object.keys(PLUGIN_PROPS).includes(key) && + (isCheckbox || isEmptyInput || removeProp) ) { delete PLUGIN_PROPS[key]; } else if (key) { @@ -244,12 +245,12 @@ function updatePluginValues(key, properties = {}, inputType, removeProp) { } function updateCodeBlock() { - codeBlock.innerText = ""; + codeBlock.innerText = ''; Object.entries(PLUGIN_PROPS).forEach(([key, properties]) => { - const propertyWrapper = document.createElement("div"); - const propertyName = document.createElement("span"); - const propertyValue = document.createElement("span"); + const propertyWrapper = document.createElement('div'); + const propertyName = document.createElement('span'); + const propertyValue = document.createElement('span'); propertyName.innerText = key; @@ -264,7 +265,7 @@ function updateCodeBlock() { if (properties?.value) { propertyValue.innerText = `"${properties?.value}"`; - propertyWrapper.innerHTML += "="; + propertyWrapper.innerHTML += '='; propertyWrapper.appendChild(propertyValue); } @@ -274,20 +275,20 @@ function updateCodeBlock() { } updatePluginValues(); -window.CI360.addHotspots("gurkha-suv", GURKHA_SUV_HOTSPOTS_CONFIG); - -spinDirections.addEventListener("change", changeSpinDirectionHandler); -copyButton.addEventListener("click", copyCodeHandler); -controlOption.addEventListener("change", toggleControlButtons); -responsive.addEventListener("change", changeResponsiveOptionHandler); -boxShadow.addEventListener("change", changeBoxShadowOptionHandler); -pointerZoomCheckbox.addEventListener("change", changePointerZoomHandler); +window.CI360.addHotspots('gurkha-suv', GURKHA_SUV_HOTSPOTS_CONFIG); + +spinDirections.addEventListener('change', changeSpinDirectionHandler); +copyButton.addEventListener('click', copyCodeHandler); +controlOption.addEventListener('change', toggleControlButtons); +responsive.addEventListener('change', changeResponsiveOptionHandler); +boxShadow.addEventListener('change', changeBoxShadowOptionHandler); +pointerZoomCheckbox.addEventListener('change', changePointerZoomHandler); accordions.forEach((accordion) => { - accordion.addEventListener("click", showAccordionContent) + accordion.addEventListener('click', showAccordionContent); }); pluginCheckboxOptions.forEach((option) => { - option.addEventListener("change", pluginCheckboxOptionsHandler) + option.addEventListener('change', pluginCheckboxOptionsHandler); }); pluginInputs.forEach((input) => { - input.addEventListener("change", changePluginInputsHandler) -}); \ No newline at end of file + input.addEventListener('change', changePluginInputsHandler); +}); diff --git a/examples/src/mouse-direction.js b/examples/src/mouse-direction.js new file mode 100644 index 0000000..6f66d5b --- /dev/null +++ b/examples/src/mouse-direction.js @@ -0,0 +1,42 @@ +import throttle from 'lodash.throttle'; + +export class MouseDirection { + constructor(canvas, threshold = 5, isClicked) { + this.canvas = canvas; + this.previousX = 0; + this.previousY = 0; + this.threshold = threshold; + this.direction = ''; + + this.canvas.addEventListener('mousemove', throttle(this.handleMouseMove.bind(this), 1000)); + } + + handleMouseMove(event) { + const rect = this.canvas.getBoundingClientRect(); + const mouseX = event.clientX - rect.left; // Mouse X relative to canvas + const mouseY = event.clientY - rect.top; // Mouse Y relative to canvas + + const deltaX = mouseX - this.previousX; + const deltaY = mouseY - this.previousY; + + let direction = ''; + + // Determine dominant axis of movement + if (Math.abs(deltaX) > Math.abs(deltaY)) { + // Horizontal movement is dominant + direction = deltaX > 0 ? 'right' : 'left'; + } else { + // Vertical movement is dominant + direction = deltaY > 0 ? 'down' : 'up'; + } + + // Log the direction if it has changed significantly + if (direction) { + this.direction = direction; + } + + // Update the previous mouse position for the next calculation + this.previousX = mouseX; + this.previousY = mouseY; + } +} diff --git a/examples/src/styles/main.css b/examples/src/styles/main.css index 27d35aa..6b676a9 100644 --- a/examples/src/styles/main.css +++ b/examples/src/styles/main.css @@ -5,7 +5,7 @@ } body { - font-family: "Inter"; + font-family: 'Inter'; overflow-x: hidden; } @@ -166,14 +166,14 @@ body { color: #fff7f4; } -.cloudimage-360-modal-description{ +.cloudimage-360-modal-description { font-size: 11px; } .popup-link a { font-size: 11px; line-height: 16px; - color: #0C6DC7; + color: #0c6dc7; font-weight: 700; } @@ -472,7 +472,7 @@ body { } .demo-usage .plugin-selector { - background: url("https://scaleflex.cloudimg.io/v7/demo/360-assets/icons/input-arrow.svg"); + background: url('https://scaleflex.cloudimg.io/v7/demo/360-assets/icons/input-arrow.svg'); background-repeat: no-repeat; background-position-x: calc(100% - 20px); background-position-y: 17.12px; @@ -503,7 +503,9 @@ body { width: 8px; } -.demo-usage-wrapper .demo-usage-content .customize-wrapper::-webkit-scrollbar-thumb { +.demo-usage-wrapper + .demo-usage-content + .customize-wrapper::-webkit-scrollbar-thumb { background: #dbdddf; } @@ -609,7 +611,7 @@ body { /* Create the checkmark/indicator (hidden when not checked) */ .checkmark:after { - content: ""; + content: ''; position: absolute; display: none; } @@ -625,7 +627,7 @@ body { top: 4px; width: 19px; height: 13px; - background-image: url("https://scaleflex.cloudimg.io/v7/demo/360-assets/icons/check-icon.svg"); + background-image: url('https://scaleflex.cloudimg.io/v7/demo/360-assets/icons/check-icon.svg'); background-repeat: no-repeat; } @@ -901,17 +903,13 @@ body { color: #4d5c74; } -.cloudimage-360-loader { - opacity: 0; -} - .transition-outline-button { transition: all 300ms ease-in-out; } .transition-outline-button:hover { background-color: #3daba4; - color: #FFF !important; + color: #fff !important; } .transition-filled-button { @@ -945,7 +943,9 @@ body { width: 8px; } - .demo-usage .demo-usage-content .plugin-options-wrapper::-webkit-scrollbar-thumb { + .demo-usage + .demo-usage-content + .plugin-options-wrapper::-webkit-scrollbar-thumb { background: #dbdddf; } @@ -1018,7 +1018,7 @@ body { } } -@media(max-width: 768px){ +@media (max-width: 768px) { .content-wrapper .header { display: flex; padding: 23.5px 16px 0 16px; @@ -1175,7 +1175,7 @@ body { } .gallery-section { - padding: 0 16px; + padding: 0 16px; } .demo-usage { diff --git a/package.json b/package.json index c5963b1..49f1d63 100644 --- a/package.json +++ b/package.json @@ -36,13 +36,16 @@ "dependencies": { "@babel/runtime": "^7.17.0", "@popperjs/core": "^2.11.2", - "core-js": "^3.0.0" + "core-js": "^3.0.0", + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1" }, "devDependencies": { "@babel/cli": "^7.16.7", "@babel/core": "^7.16.7", "@babel/plugin-transform-runtime": "^7.16.5", "@babel/preset-env": "^7.16.5", + "@eslint/js": "^9.12.0", "babel-loader": "^8.2.3", "babel-plugin-array-includes": "^2.0.3", "babel-preset-env": "^1.7.0", @@ -51,14 +54,17 @@ "babel-preset-stage-0": "^6.24.1", "css-loader": "^2.1.1", "dotenv": "^16.0.3", + "eslint": "^9.12.0", "form-data": "^4.0.0", "fs": "^0.0.1-security", "gh-pages": "^2.0.1", + "globals": "^15.11.0", "highlight.js": "^10.4.1", "html-webpack-plugin": "^5.5.1", "isomorphic-fetch": "^3.0.0", "mini-css-extract-plugin": "^0.9.0", "mobile-detect": "^1.4.3", + "prettier": "3.3.3", "style-loader": "^3.3.2", "webpack": "^5.79.0", "webpack-cli": "^5.0.1", diff --git a/src/ci360.service.js b/src/ci360.service.js index 2b82cee..d2bf3bc 100644 --- a/src/ci360.service.js +++ b/src/ci360.service.js @@ -1,19 +1,12 @@ -import { - get360ViewProps, - setView360Icon, -} from './ci360.utils'; -import { - ORIENTATIONS, - AUTOPLAY_BEHAVIOR, -} from './constants/'; +import throttle from 'lodash.throttle'; + +import { get360ViewProps } from './ci360.utils'; import './static/css/style.css'; import './static/css/hotspots.css'; import { - generateImagesPath, + generateCdnPath, preloadImages, - preloadOriginalImages, - create360ViewIcon, - createCloseFullscreenIcon, + createCloseIcon, createFullscreenIcon, createMagnifierIcon, createLoader, @@ -22,423 +15,174 @@ import { createCanvas, create360ViewCircleIcon, createFullscreenModal, - contain, - getCurrentOriginalImage, magnify, - createBoxShadow, - getSpeedFactor, isCompletedOneCycle, getMovingDirection, - applyStylesToContainer, initControls, - addClass, - removeClass, - getItemSkipped, loop, - generateZoomInSteps, - generateZoomOutSteps, - updateHotspots, - createHotspots, - generateHotspotsConfigs, - isMouseOnHotspot, - hideHotspotsIcons, - isPropsChangeRequireReload, - getImageAspectRatio, - removeChildFromParent, initLazyload, + createInitialIcon, + removeElementFromContainer, + generateHighPreviewCdnUrl, + loadImage, + delay, + shouldSwitchSpinDirection, + switchSpinDirection, } from './utils'; -import { togglePopupEvents } from './utils/hotspots/toggle-popup-events'; +import { ORIENTATIONS, THROTTLE_TIME } from './utils/constants'; +import { getDefaultSpinDirection } from './utils/spin/get-default-spin-direction'; +import { isSpinKeysPressed } from './utils/spin/is-spin-keys-pressed'; class CI360Viewer { - constructor(container, fullscreen, hotspotsConfigs) { + constructor(container, fullscreen) { this.container = container; - this.movementStart = { x: 0, y: 0 }; - this.isStartSpin = false; - this.movingDirection = ORIENTATIONS.CENTER; this.isClicked = false; - this.loadedImagesX = 0; - this.loadedImagesY = 0; - this.imagesLoaded = false; - this.reversed = false; this.fullscreenView = !!fullscreen; this.imagesX = []; this.imagesY = []; - this.originalImagesX = []; - this.originalImagesY = []; - this.resizedImagesX = []; - this.resizedImagesY = []; this.devicePixelRatio = Math.round(window.devicePixelRatio || 1); - this.isMobile = !!('ontouchstart' in window || navigator.msMaxTouchPoints); this.id = container.id; - this.hotspotsConfigs = hotspotsConfigs && generateHotspotsConfigs(hotspotsConfigs); - this.isMagnifyOpen = false; - this.isDragged = false; - this.startPointerZoom = false; - this.zoomIntensity = 0; - this.mouseTracked = false; - this.intialPositions = { x: 0, y: 0 }; - this.pointerCurrentPosition = { x: 0, y: 0 }; - this.isStartedLoadOriginalImages = false; - this.init(container); - } - - isReady() { - const totalAmount = this.amountX + this.amountY; + this.movementStart = { x: 0, y: 0 }; + this.draggingDirection = null; + this.isReady = false; - return (this.imagesX.length + this.imagesY.length) === totalAmount; + this.init(container); } mouseDown(event) { - if (!this.imagesLoaded) return; + if (!this.isReady) return; - const isMouseOnHotspotElement = isMouseOnHotspot(); const { pageX, pageY } = event; - this.hideInitialIcons(); - if (this.autoplay || this.loopTimeoutId) { - this.stop(); + this.stopAutoplay(); this.autoplay = false; - this.isZoomReady = true; } - this.intialPositions = { x: pageX, y: pageY }; + this.hideAllIcons(); this.movementStart = { x: pageX, y: pageY }; this.isClicked = true; - this.isDragged = false; - - if (this.hotspotsConfigs) { - togglePopupEvents(this.hotspotsConfigs, event, true); - } - - if (isMouseOnHotspotElement) { - this.isClicked = false; - } - - if (this.hotspotsConfigs) { - updateHotspots( - this.container, - this.hotspotsConfigs, - this.activeImageX, - this.activeImageY, - this.movingDirection - ); - } } mouseUp() { - if (!this.imagesLoaded || !this.isClicked) return; + if (!this.isReady) return; + this.showAllIcons(); this.movementStart = { x: 0, y: 0 }; - this.isStartSpin = false; this.isClicked = false; + this.innerBox.style.cursor = 'grab'; + } - if (this.bottomCircle && !this.mouseTracked) { - this.show360ViewCircleIcon(); - } + drag(pageX, pageY) { + if (!this.isReady || !this.isClicked) return; - if (this.hotspotsConfigs) { - togglePopupEvents(this.hotspotsConfigs); - } + const deltaX = pageX - this.movementStart.x; + const deltaY = pageY - this.movementStart.y; - if (this.pointerZoom && !this.fullscreenView) { - setTimeout(() => { - this.isZoomReady = true; - }, 50); + this.draggingDirection = getMovingDirection(deltaX, deltaY, this.allowSpinY) || this.draggingDirection; - if (this.mouseTracked) { - this.container.style.cursor = 'zoom-out'; - } else { - this.container.style.cursor = 'zoom-in'; - } - } else { - this.container.style.cursor = 'grab'; - } - } - - mouseClick(event) { - if (!this.pointerZoom || this.fullscreenView) return; + const container = this.fullscreenView ? document.body : this.container; + const dragFactor = this.dragSpeed / 50; + const speedFactorX = dragFactor * (this.amountX / container.offsetWidth); + const speedFactorY = dragFactor * (this.amountY / container.offsetHeight); - this.setCursorPosition(event); - this.hideInitialIcons(); + const itemsSkippedX = Math.round(deltaX * speedFactorX); + const itemsSkippedY = Math.round(deltaY * speedFactorY); - if (!this.isStartedLoadOriginalImages && !this.isDragged && this.isZoomReady) { - this.prepareOriginalImages(event); + if (itemsSkippedX !== 0 || (this.allowSpinY && itemsSkippedY !== 0)) { + this.onMoveHandler(this.draggingDirection, itemsSkippedX, itemsSkippedY); + this.movementStart = { x: pageX, y: pageY }; } - - if (this.isAllOriginalImagesLoaded && !this.isDragged && this.isZoomReady) { - this.togglePointerZoom(event); - }; } mouseMove(event) { - if (!this.imagesLoaded) return; - - const { pageX, pageY } = event; - - if (this.mouseTracked) { - this.setCursorPosition(event); - - if (!this.isClicked) { - this.update(); - } - } - - if (this.isClicked) { - const nextPositions = { x: pageX, y: pageY }; - - this.container.style.cursor = 'grabbing'; - this.isDragged = true; - this.movingDirection = getMovingDirection( - this.isStartSpin, - this.allowSpinY, - this.intialPositions, - nextPositions, - this.movingDirection - ); - - this.onMoveHandler(event); - } + this.drag(event.pageX, event.pageY); } - mouseLeave() { - if (!this.imagesLoaded) return; - - if (this.pointerZoom && this.mouseTracked) { - this.togglePointerZoom(); - } - - if (this.isMagnifyOpen) { - this.closeMagnifier(); - } + mouseClick() { + // !TODO: rework } - togglePointerZoom() { - if (this.autoplay || this.loopTimeoutId) { - this.stop(); - this.autoplay = false; - } - - if (this.mouseTracked) { - const zoomSteps = generateZoomOutSteps(this.pointerZoom); - this.container.style.cursor = 'zoom-in'; - - zoomSteps.forEach((step, index) => { - setTimeout(() => { - this.zoomIntensity = step; - this.update(); - - const isReachedIntialScale = index === (zoomSteps.length - 1); - - if (isReachedIntialScale) { - this.mouseTracked = false; - this.update(); - }; - }, (this.pointerZoom - step) * 200); - }) - } else { - if (this.bottomCircle) this.hide360ViewCircleIcon(); - - const zoomSteps = generateZoomInSteps(this.pointerZoom); - - if (this.hotspotsConfigs) { - hideHotspotsIcons(); - } - - zoomSteps.forEach((step) => { - setTimeout(() => { - this.zoomIntensity = step; - this.update(); - }, step * 200); - }) - - this.mouseTracked = true; - this.container.style.cursor = 'zoom-out'; - } - } - - onOriginalImageLoad(orientation, event, image, index) { - if (orientation === ORIENTATIONS.Y) { - this.originalImagesY[index] = image; - } else { - this.originalImagesX[index] = image; - } - - const loadedOriginalXImages = this.originalImagesX - .filter(image => image); - const loadedOriginalYImages = this.originalImagesY - .filter(image => image); - - const totalAmount = this.amountX + this.amountY; - const totalLoadedImages = loadedOriginalXImages.length + loadedOriginalYImages.length; - - const isAllImagesLoaded = ( - loadedOriginalXImages.length + loadedOriginalYImages.length === this.amountX + this.amountY - ); - - const percentage = Math.round(totalLoadedImages / totalAmount * 100); - - this.updatePercentageInLoader(percentage); - - if (isAllImagesLoaded) { - this.removeLoader(); - this.togglePointerZoom(event); - - this.mouseTracked = true; - this.isAllOriginalImagesLoaded = true; - } - } + touchOutside(event) { + if (!this.glass) return; - prepareOriginalImages(event) { - const srcX = generateImagesPath(this.srcXConfig); + const isOutside = !this.canvas.contains(event.target); - this.isStartedLoadOriginalImages = true; - this.loader = createLoader(this.innerBox); - this.container.style.cursor = 'wait'; - - preloadOriginalImages( - this.srcXConfig, - srcX, - this.onOriginalImageLoad.bind(this, ORIENTATIONS.X, event) - ); - - if (this.allowSpinY) { - const srcY = generateImagesPath(this.srcYConfig); - - preloadOriginalImages( - this.srcYConfig, - srcY, - this.onOriginalImageLoad.bind(this, ORIENTATIONS.Y, event) - ); + if (isOutside) { + this.removeGlass(); } } touchStart(event) { - if (!this.imagesLoaded) return; - - const isMouseOnHotspotElement = isMouseOnHotspot(); + if (!this.isReady || event.touches.length > 1 || this.glass) return; - this.hideInitialIcons(); + const { pageX, pageY } = event.touches[0]; if (this.autoplay || this.loopTimeoutId) { - this.stop(); + this.stopAutoplay(); this.autoplay = false; } - this.intialPositions = { x: event.touches[0].clientX, y: event.touches[0].clientY }; - this.movementStart = { x: event.touches[0].clientX, y: event.touches[0].clientY }; + this.hideAllIcons(); + this.movementStart = { x: pageX, y: pageY }; this.isClicked = true; - - if (isMouseOnHotspotElement) { - this.isClicked = false; - } } touchEnd() { - if (!this.imagesLoaded) return; - - if (this.bottomCircle) this.show360ViewCircleIcon(); + if (!this.isReady) return; + this.showAllIcons(); this.movementStart = { x: 0, y: 0 }; - this.isStartSpin = false; this.isClicked = false; } touchMove(event) { - if (!this.isClicked || !this.imagesLoaded) return; + if (!this.isReady || !this.isClicked || this.glass) return; + const { pageX, pageY } = event.touches[0]; + event.preventDefault(); - if (event.cancelable && this.allowSpinY) { - event.preventDefault(); - } - - const nextPositions = { x: event.touches[0].clientX, y: event.touches[0].clientY }; - - this.movingDirection = getMovingDirection( - this.isStartSpin, - this.allowSpinY, - this.intialPositions, - nextPositions, - this.movingDirection - ); - - this.onMoveHandler(event); - } - - keyDownGeneral(event) { - if (!this.imagesLoaded) return; - - if (this.glass) { - this.closeMagnifier(); - } - - if (event.keyCode === 27) { //ESC - if (this.mouseTracked) { - this.togglePointerZoom(); - } - } - } - - hideInitialIcons() { - if (this.glass) { - this.closeMagnifier(); - } - - if (this.view360Icon) { - this.remove360ViewIcon(); - } - } - - setCursorPosition(event) { - this.mousePositions = { - x: event.clientX, - y: event.clientY - }; - } - - getCursorPositionInCanvas() { - const canvasRect = this.canvas.getBoundingClientRect(); - - this.pointerCurrentPosition = { - x: this.mousePositions.x - canvasRect.left, - y: this.mousePositions.y - canvasRect.top - }; - - return this.pointerCurrentPosition; + this.drag(pageX, pageY); } keyDown(event) { - if (!this.imagesLoaded) return; - - if (this.glass) { - this.closeMagnifier(); - } - - if (event.keyCode === 37) { // left - this.keysReverse ? this.left() : this.right(); - - this.onSpin(); - } - - if (event.keyCode === 39) { // right - this.keysReverse ? this.right() : this.left(); - - this.onSpin(); + if (!this.isReady) return; + + const { keyCode } = event; + const isReverse = this.keysReverse; + + if (isSpinKeysPressed(keyCode, this.allowSpinY)) { + this.hideAllIcons(); + } + + switch (keyCode) { + case 37: // left arrow + isReverse ? this.moveLeft() : this.moveRight(); + break; + case 39: // right arrow + isReverse ? this.moveRight() : this.moveLeft(); + break; + case 38: // up arrow + if (this.allowSpinY) { + event.preventDefault(); + isReverse ? this.moveTop() : this.moveBottom(); + } + break; + case 40: // down arrow + if (this.allowSpinY) { + event.preventDefault(); + isReverse ? this.moveBottom() : this.moveTop(); + } + break; + default: + break; } + } - if (this.allowSpinY) { - event.preventDefault(); - - if (event.keyCode === 38) { // up - this.keysReverse ? this.top() : this.bottom(); - - this.onSpin(); - } - - if (event.keyCode === 40) { // down - this.keysReverse ? this.bottom() : this.top(); + keyUp(event) { + const { keyCode } = event; - this.onSpin(); - } + if (isSpinKeysPressed(keyCode, this.allowSpinY)) { + this.showAllIcons(); } } @@ -447,624 +191,266 @@ class CI360Viewer { this.hide360ViewCircleIcon(); } - if (this.view360Icon) { + if (this.initialIcon) { this.remove360ViewIcon(); } if (this.autoplay || this.loopTimeoutId) { - this.stop(); + this.stopAutoplay(); this.autoplay = false; } } - keyUp(event) { - if (!this.imagesLoaded) return; - - if ([37, 39].includes(event.keyCode)) { - this.onFinishSpin(); - } - } - onFinishSpin() { if (this.bottomCircle) this.show360ViewCircleIcon(); } - moveActiveIndexUp(itemsSkipped) { - const isReverse = this.controlReverse ? !this.spinReverse : this.spinReverse; - - if (this.stopAtEdges) { - const isReachedTheEdge = this.activeImageX + itemsSkipped >= this.amountX; - - if (isReachedTheEdge) { - this.activeImageX = this.amountX; - - if (isReverse ? this.leftElem : this.rightElem) { - addClass(isReverse ? this.leftElem : this.rightElem, 'not-active'); - } - } else { - this.activeImageX += itemsSkipped; - - if (this.rightElem) removeClass(this.rightElem, 'not-active'); - - if (this.leftElem) removeClass(this.leftElem, 'not-active'); - } - } else { - this.activeImageX = (this.activeImageX + itemsSkipped) % this.amountX || this.amountX; - - if (this.activeImageX === this.amountX && this.allowSpinY) this.spinY = true; - } + moveActiveXIndexUp(itemsSkipped) { + this.activeImageX = (this.activeImageX + itemsSkipped) % this.amountX; } - moveActiveIndexDown(itemsSkipped) { - const isReverse = this.controlReverse ? !this.spinReverse : this.spinReverse; - - if (this.stopAtEdges) { - const isReachedTheEdge = this.activeImageX - itemsSkipped <= 1; - - if (isReachedTheEdge) { - this.activeImageX = 1; - - if (isReverse ? this.rightElem : this.leftElem) { - addClass(isReverse ? this.rightElem : this.leftElem, 'not-active'); - } - } else { - this.activeImageX -= itemsSkipped; - - if (this.leftElem) removeClass(this.leftElem, 'not-active'); - - if (this.rightElem) removeClass(this.rightElem, 'not-active'); - } - } else { - if (this.activeImageX - itemsSkipped < 1) { - this.activeImageX = this.amountX + (this.activeImageX - itemsSkipped); - this.spinY = true; - } else { - this.activeImageX -= itemsSkipped; - } - } + moveActiveXIndexDown(itemsSkipped) { + this.activeImageX = (this.activeImageX - itemsSkipped + this.amountX) % this.amountX; } moveActiveYIndexUp(itemsSkipped) { - const isReverse = this.controlReverse ? !this.spinReverse : this.spinReverse; - - if (this.stopAtEdges) { - const isReachedTheEdge = this.activeImageY + itemsSkipped >= this.amountY; - - if (isReachedTheEdge) { - this.activeImageY = this.amountY; - - if (isReverse ? this.bottomElem : this.topElem) { - addClass(isReverse ? this.bottomElem : this.topElem, 'not-active'); - } - } else { - this.activeImageY += itemsSkipped; - - if (this.topElem) removeClass(this.topElem, 'not-active'); - - if (this.bottomElem) removeClass(this.bottomElem, 'not-active'); - } - } else { - this.activeImageY = (this.activeImageY + itemsSkipped) % this.amountY || this.amountY; - - if (this.activeImageY === this.amountY) this.spinY = false; - } + this.activeImageY = (this.activeImageY + itemsSkipped) % this.amountY; } moveActiveYIndexDown(itemsSkipped) { - const isReverse = this.controlReverse ? !this.spinReverse : this.spinReverse; - - if (this.stopAtEdges) { - const isReachedTheEdge = this.activeImageY - itemsSkipped <= 1; - - if (isReachedTheEdge) { - this.activeImageY = 1; - - if (isReverse ? this.topElem : this.bottomElem) { - addClass(isReverse ? this.topElem : this.bottomElem, 'not-active'); - } - } else { - this.activeImageY -= itemsSkipped; - - if (this.bottomElem) removeClass(this.bottomElem, 'not-active'); - if (this.topElem) removeClass(this.topElem, 'not-active'); - } - } else { - if (this.activeImageY - itemsSkipped < 1) { - this.activeImageY = this.amountY + (this.activeImageY - itemsSkipped); - this.spinY = false; - } else { - this.activeImageY -= itemsSkipped; - } - } - } - - moveRight(currentPositionX) { - const itemsSkippedRight = getItemSkipped(currentPositionX, this.movementStart.x, this.speedFactor); - - this.spinReverse ? this.moveActiveIndexDown(itemsSkippedRight) - : this.moveActiveIndexUp(itemsSkippedRight); - - this.movementStart.x = currentPositionX; - this.activeImageY = 1; - this.update(); + this.activeImageY = (this.activeImageY - itemsSkipped + this.amountY) % this.amountY; } - moveLeft(currentPositionX) { - const itemsSkippedLeft = getItemSkipped(this.movementStart.x, currentPositionX, this.speedFactor); + moveRight(stopAtEdges) { + if (stopAtEdges && this.activeImageX >= this.imagesX.length - 1) return; - this.spinReverse ? this.moveActiveIndexUp(itemsSkippedLeft) - : this.moveActiveIndexDown(itemsSkippedLeft); - - this.activeImageY = 1; - this.movementStart.x = currentPositionX; - this.update(); + this.moveActiveXIndexUp(1); + this.update(ORIENTATIONS.X); } - moveTop(currentPositionY) { - const itemsSkippedTop = getItemSkipped(this.movementStart.y, currentPositionY, this.speedFactor); - - this.spinReverse ? this.moveActiveYIndexUp(itemsSkippedTop) - : this.moveActiveYIndexDown(itemsSkippedTop); + moveLeft(stopAtEdges) { + if (stopAtEdges && this.activeImageX <= 0) return; - this.activeImageX = 1; - this.movementStart.y = currentPositionY; - this.update(); + this.moveActiveXIndexDown(1); + this.update(ORIENTATIONS.X); } - moveBottom(currentPositionY) { - const itemsSkippedBottom = getItemSkipped(currentPositionY, this.movementStart.y, this.speedFactor); - - this.spinReverse ? this.moveActiveYIndexDown(itemsSkippedBottom) - : this.moveActiveYIndexUp(itemsSkippedBottom); - - this.activeImageX = 1; - this.movementStart.y = currentPositionY; - this.update(); - } - - onMoveHandler(event) { - const currentPositionX = this.isMobile ? event.touches[0].clientX : event.pageX; - const currentPositionY = this.isMobile ? event.touches[0].clientY : event.pageY; - - const isMoveRight = currentPositionX - this.movementStart.x >= this.speedFactor; - const isMoveLeft = this.movementStart.x - currentPositionX >= this.speedFactor; - const isMoveTop = this.movementStart.y - currentPositionY >= this.speedFactor; - const isMoveBottom = currentPositionY - this.movementStart.y >= this.speedFactor; - - if (this.bottomCircle) this.hide360ViewCircleIcon(); - - if (isMoveRight && this.movingDirection === ORIENTATIONS.X) { - this.moveRight(currentPositionX); - - this.isStartSpin = true; - } else if (isMoveLeft && this.movingDirection === ORIENTATIONS.X) { - this.moveLeft(currentPositionX); - - this.isStartSpin = true; - } else if (isMoveTop && this.movingDirection === ORIENTATIONS.Y) { - this.moveTop(currentPositionY); - - this.isStartSpin = true; - } else if (isMoveBottom && this.movingDirection === ORIENTATIONS.Y) { - this.moveBottom(currentPositionY); - - this.isStartSpin = true; - } - } - - left() { - this.movingDirection = ORIENTATIONS.X; - this.activeImageY = this.reversed ? this.amountY : 1; - - this.moveActiveIndexDown(1); - this.update(); - } - - right() { - this.movingDirection = ORIENTATIONS.X; - this.activeImageY = this.reversed ? this.amountY : 1; - - this.moveActiveIndexUp(1); - this.update(); - } - - top() { - this.movingDirection = ORIENTATIONS.Y; - this.activeImageX = this.reversed ? this.amountX : 1; + moveTop(stopAtEdges) { + if (stopAtEdges && this.activeImageY >= this.imagesY.length - 1) return; this.moveActiveYIndexUp(1); - this.update(); + this.update(ORIENTATIONS.Y); } - bottom() { - this.movingDirection = ORIENTATIONS.Y; - this.activeImageX = this.reversed ? this.amountX : 1; + moveBottom(stopAtEdges) { + if (stopAtEdges && this.activeImageY <= 0) return; this.moveActiveYIndexDown(1); - this.update(); + this.update(ORIENTATIONS.Y); } - loop(reversed) { - const loopTriggers = { - left: this.left.bind(this), - right: this.right.bind(this), - top: this.top.bind(this), - bottom: this.bottom.bind(this) + onMoveHandler(movingDirection, itemsSkippedX, itemsSkippedY) { + if (movingDirection === 'right') { + this.moveRight(this.stopAtEdges, itemsSkippedX); + } else if (movingDirection === 'left') { + this.moveLeft(this.stopAtEdges, itemsSkippedX); + } else if (movingDirection === 'up') { + this.moveTop(this.stopAtEdges, itemsSkippedY); + } else if (movingDirection === 'down') { + this.moveBottom(this.stopAtEdges, itemsSkippedY); } - - loop(this.autoplayBehavior, this.spinY, reversed, loopTriggers); } - updateContainerAndCanvasSize(image) { - const imageAspectRatio = getImageAspectRatio(image, this.ratio); - - if (this.fullscreenView) { - this.container.width = window.innerWidth * this.devicePixelRatio; - this.container.style.width = window.innerWidth + 'px'; - this.container.height = window.innerHeight * this.devicePixelRatio; - this.container.style.height = window.innerHeight + 'px'; - this.container.style.maxWidth = 'unset'; - - this.canvas.width = window.innerWidth * this.devicePixelRatio; - this.canvas.style.width = window.innerWidth + 'px'; - this.canvas.height = window.innerHeight * this.devicePixelRatio; - this.canvas.style.height = window.innerHeight + 'px'; + update(orientation) { + const image = + orientation === ORIENTATIONS.X ? this.imagesX[this.activeImageX] : this.imagesY[this.activeImageY]; - return; - } - - this.canvas.width = this.container.offsetWidth * this.devicePixelRatio; - this.canvas.style.width = this.container.offsetWidth + 'px'; - - this.canvas.height = (this.container.offsetWidth / imageAspectRatio) * this.devicePixelRatio; - this.canvas.style.height = (this.container.offsetWidth / imageAspectRatio) + 'px'; - } - - onResizedImageLoad(orientation, image, index) { - if (orientation === ORIENTATIONS.Y) { - this.resizedImagesY[index] = image; - } else { - this.resizedImagesX[index] = image; - } - - const isAllImagesLoaded = ( - this.resizedImagesX.length + this.resizedImagesY.length === this.amountX + this.amountY - ); - - if (isAllImagesLoaded) { - this.imagesX = this.resizedImagesX; - this.imagesY = this.resizedImagesY; - - this.update(); - } + this.drawImageOnCanvas(image); } - showImageInfo(ctx) { - ctx.font = `${this.fullscreenView ? 28 : 14}px serif`; - ctx.fillStyle = (this.info === 'white' ? '#FFF' : '#000'); - - const imageDimension = `image-dimension: ${this.container.offsetWidth}x${this.container.offsetHeight}px`; - - const currentXImage = 'active-index-x: ' + this.activeImageX; - const currentYImage = 'active-index-y: ' + this.activeImageY; - - const imageIndex = [currentXImage, currentYImage].join(' | '); + updatePercentageInLoader(percentage = 0) { + if (!this.loader) return; - ctx.fillText(imageDimension, 20, this.container.offsetHeight - 35); - ctx.fillText(imageIndex, 20, this.container.offsetHeight - 10); + this.loader.innerText = percentage + '%'; } - requestResizedImages() { - if (!this.isReady()) return; - - const responsive = this.ciParams.ciToken; - const firstImage = this.imagesX[0]; + drawImageOnCanvas(image) { + if (!this.canvas || !image) return; - this.update(); + const ctx = this.canvas.getContext('2d'); - if (!responsive || !this.requestResponsiveImages || (this.container.offsetWidth < firstImage.width * 1.5)) return; + // Handle fullscreen mode: use the full screen dimensions if fullscreen is active + const containerWidth = this.fullscreenView ? window.innerWidth : this.canvas.clientWidth; + const containerHeight = this.fullscreenView ? window.innerHeight : this.canvas.clientHeight; - this.speedFactor = getSpeedFactor(this.dragSpeed, this.amountX, this.container.offsetWidth); - const srcX = generateImagesPath(this.srcXConfig); + // Set canvas resolution to match container size, with device pixel ratio for sharper rendering + this.canvas.width = containerWidth * this.devicePixelRatio; + this.canvas.height = containerHeight * this.devicePixelRatio; - preloadImages( - this.srcXConfig, - srcX, - this.onResizedImageLoad.bind(this, ORIENTATIONS.X), - ) + // Scale the context to handle high DPI screens + ctx.scale(this.devicePixelRatio, this.devicePixelRatio); - if (this.allowSpinY) { - const srcY = generateImagesPath(this.srcYConfig); + // Enable image smoothing for higher quality + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; - preloadImages( - this.srcYConfig, - srcY, - this.onResizedImageLoad.bind(this, ORIENTATIONS.Y), - ) - } - } + // Calculate the aspect ratios + const imageAspectRatio = image.naturalWidth / image.naturalHeight; + const containerAspectRatio = containerWidth / containerHeight; - update() { - let image = this.imagesX[this.activeImageX - 1]; + let drawWidth, drawHeight, offsetX, offsetY; - if (this.movingDirection === ORIENTATIONS.Y) { - image = this.imagesY[this.activeImageY - 1]; + // Adjust image scaling logic based on aspect ratios + if (imageAspectRatio > containerAspectRatio) { + // Image is wider than container + drawWidth = containerWidth; + drawHeight = containerWidth / imageAspectRatio; + offsetX = 0; + offsetY = (containerHeight - drawHeight) / 2; + } else { + // Image is taller than container + drawHeight = containerHeight; + drawWidth = containerHeight * imageAspectRatio; + offsetX = (containerWidth - drawWidth) / 2; + offsetY = 0; } - if (!image) return; - - const ctx = this.canvas.getContext("2d"); - ctx.scale(this.devicePixelRatio, this.devicePixelRatio); + // Clear the canvas before redrawing + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.updateContainerAndCanvasSize(image); + // Draw the image on the canvas, scaling and positioning it as needed + ctx.drawImage(image, offsetX, offsetY, drawWidth, drawHeight); - if (this.fullscreenView) { - const { width, height, offsetX, offsetY } = - contain(this.canvas.width, this.canvas.height, image.width, image.height); + return ctx; + } - ctx.drawImage(image, offsetX, offsetY, width, height); + pushImageToSet(image, index, orientation) { + if (orientation === ORIENTATIONS.X) { + this.imagesX[index] = image; } else { - if (this.mouseTracked) { - this.updateImageScale(ctx); - } else { - if (this.hotspotsConfigs && !this.autoplay) { - updateHotspots( - this.container, - this.hotspotsConfigs, - this.activeImageX, - this.activeImageY, - this.movingDirection, - this.isClicked - ); - } - - ctx.drawImage(image, 0, 0, this.canvas.width, this.canvas.height); - } - } - - if (this.info) { - this.showImageInfo(ctx); + this.imagesY[index] = image; } } - updateImageScale(ctx) { - let image = this.originalImagesX[this.activeImageX - 1]; - - if (this.movingDirection === ORIENTATIONS.Y) { - image = this.originalImagesY[this.activeImageY - 1]; - } - - const position = this.getCursorPositionInCanvas(); - const imageWidth = this.canvas.width; - const imageHeight = this.canvas.height; - - const width = (this.canvas.width * this.zoomIntensity); - const height = (this.canvas.height * this.zoomIntensity); - - const pointX = 0 - ((position.x / imageWidth) * (width - this.canvas.width)); - const pointY = 0 - ((position.y / imageHeight) * (height - this.canvas.height)); - - ctx.drawImage(image, pointX, pointY, width, height); + calculatePercentage() { + const totalAmount = this.amountX + this.amountY; + const totalLoadedImages = this.imagesX.length + this.imagesY.length; + return Math.round((totalLoadedImages / totalAmount) * 100); } - updatePercentageInLoader(percentage) { - if (this.loader) { - this.loader.style.width = percentage + '%'; - } - - if (this.view360Icon) { - this.view360Icon.innerText = percentage + '%'; - } + onImageLoad(image, index, orientation) { + this.pushImageToSet(image, index, orientation); + this.updatePercentageInLoader(this.calculatePercentage()); } - onFirstImageLoaded(image) { - this.add360ViewIcon(); - - const ctx = this.canvas.getContext("2d"); - ctx.scale(this.devicePixelRatio, this.devicePixelRatio); - this.updateContainerAndCanvasSize(image); - - if (this.fullscreenView) { - const { offsetX, offsetY, width, height } = - contain(this.canvas.width, this.canvas.height, image.width, image.height); - - this.offset = { x: offsetX, y: offsetY }; - - this.addCloseFullscreenView(); - - ctx.drawImage(image, offsetX, offsetY, width, height); - } else { - ctx.drawImage(image, 0, 0, this.canvas.width, this.canvas.height); - } - - if (this.info) { - this.showImageInfo(ctx); - } - - if (this.magnifier) { - this.addMagnifier(); - } - - if (this.boxShadow && !this.fullscreenView) { - this.boxShadowEl = createBoxShadow(this.boxShadow, this.innerBox); - } - - if (this.bottomCircle && !this.fullscreenView) { - this.add360ViewCircleIcon(); - } - - if (this.fullscreen && !this.fullscreenView) { - this.addFullscreenIcon(); - } + onFirstImageLoaded(event, image) { + this.createContainers(event); + this.drawImageOnCanvas(image); } onAllImagesLoaded() { - this.removeLoader(); - this.imagesLoaded = true; - - if (this.autoplay && this.pointerZoom) { - this.container.style.cursor = 'zoom-in'; - } else { - this.container.style.cursor = 'grab'; - } - - this.speedFactor = getSpeedFactor(this.dragSpeed, this.amountX, this.container.offsetWidth); + this.addAllIcons(); + this.isReady = true; + this.amountX = this.imagesX.length - 1; + this.amountY = this.imagesY.length - 1; if (this.autoplay) { - this.play(); - } + this.hideAllIcons(); + const delayedPlay = delay(this.play.bind(this)); - if (this.disableDrag) { - this.container.style.cursor = 'default'; + delayedPlay(); } - - if (this.view360Icon) { - if (this.hide360Logo) return this.remove360ViewIcon(); - - this.view360Icon.innerText = ''; - //TODO [deprecated]: remove setView360Icon in the upcoming versions - if (this.logoSrc) setView360Icon(this.view360Icon, this.logoSrc); - } - - this.initControls(); } magnify(event) { event.stopPropagation(); - if (this.mouseTracked) this.togglePointerZoom(); - - const currentOriginalImage = getCurrentOriginalImage( - this.movingDirection, - this.imagesX, - this.imagesY, - this.activeImageX, - this.activeImageY - ); - - this.isMagnifyOpen = true; + const highPreviewCdnUrl = generateHighPreviewCdnUrl(this.imagesX[this.activeImageX].src); + this.createGlass(); - currentOriginalImage.onload = () => { - if (this.glass) { - this.glass.style.cursor = 'none'; - } + const onLoadImage = (image) => { + magnify(event, this.innerBox, this.offset, image, this.glass, this.magnifier); }; - this.glass = document.createElement('div'); - this.container.style.overflow = 'hidden'; - - magnify( - this.container, - this.offset, - currentOriginalImage, - this.glass, - this.magnifier || 3 - ); - } - - closeMagnifier() { - if (!this.glass) return; - - this.container.style.overflow = 'visible'; - this.container.removeChild(this.glass); - this.glass = null; - this.isMagnifyOpen = false; + loadImage(highPreviewCdnUrl, onLoadImage); } openFullscreenModal(event) { event.stopPropagation(); - if (this.mouseTracked) this.togglePointerZoom(); - const fullscreenContainer = createFullscreenModal(this.container); - new CI360Viewer(fullscreenContainer, true, this.hotspotsConfigs); + new CI360Viewer(fullscreenContainer, true); } - setFullscreenEvents(_, event) { - if (event.type === 'click') return this.closeFullscreenModal(event); - if (event.key === 'Escape' && this.container.parentNode.parentNode === document.body) { - this.closeFullscreenModalOnEsc(event); - } - } + closeFullscreenModal(event) { + event.stopPropagation(); - closeFullscreenModalOnEsc(event) { - this.closeFullscreenModal(event); + document.body.removeChild(this.container.parentNode); + window.document.body.style.overflow = 'visible'; } play() { - if (this.bottomCircle) this.hide360ViewCircleIcon(); + this.hide360ViewCircleIcon(); - this.remove360ViewIcon(); + const loopTriggers = { + left: this.moveLeft.bind(this), + right: this.moveRight.bind(this), + top: this.moveTop.bind(this), + bottom: this.moveBottom.bind(this), + }; this.loopTimeoutId = window.setInterval(() => { - this.loop(this.reversed); - - const isPlayedOnce = isCompletedOneCycle( - this.autoplayBehavior, - this.activeImageX, - this.activeImageY, - this.amountX, - this.amountY, - this.reversed - ); - - if (this.playOnce && isPlayedOnce) { - window.clearTimeout(this.loopTimeoutId); - this.autoplay = false; - - if (this.hotspotsConfigs) { - updateHotspots( - this.container, - this.hotspotsConfigs, - this.activeImageX, - this.activeImageY, - this.movingDirection, - this.isClicked - ); - } + const completedOneCycle = + this.playOnce && + isCompletedOneCycle({ + autoplayBehavior: this.autoplayBehavior, + activeImageX: this.activeImageX, + activeImageY: this.activeImageY, + amountX: this.amountX, + amountY: this.amountY, + autoplayReverse: this.autoplayReverse, + }); + + if (completedOneCycle) { + this.stopAutoplay(); + return; } - }, this.autoplaySpeed); - } - - stop() { - if (this.bottomCircle) this.show360ViewCircleIcon(); - window.clearTimeout(this.loopTimeoutId); - } - - updateView(forceUpdate, viewers, hotspotConfigs) { - let container = this.container; - - const imageProps = get360ViewProps(container); - const srcPropsChanged = isPropsChangeRequireReload(this, imageProps); - const reInitView = srcPropsChanged || forceUpdate; - - if (reInitView) { - const oldElement = this.container; - const viewIndex = viewers.findIndex(view => view.id === this.container.id); - - container.removeChild(this.innerBox); - container = container.cloneNode(true); - - container.className = container.className.replace(' initialized', ''); + const shouldSwitch = shouldSwitchSpinDirection({ + autoplayBehavior: this.autoplayBehavior, + activeImageX: this.activeImageX, + activeImageY: this.activeImageY, + amountX: this.amountX, + amountY: this.amountY, + autoplayReverse: this.autoplayReverse, + spinDirection: this.spinDirection, + }); + + if (shouldSwitch) { + this.spinDirection = switchSpinDirection(this.spinDirection); + } - oldElement.parentNode.replaceChild(container, oldElement); + const spinY = this.spinDirection === 'y'; - return viewers.splice(viewIndex, 1, new CI360Viewer(container)); - } + loop({ + autoplayBehavior: this.autoplayBehavior, + spinY, + reversed: this.autoplayReverse, + loopTriggers, + }); + }, this.autoplaySpeed); + } - container.style.position = 'relative'; - container.style.width = '100%'; - container.style.cursor = 'default'; - container.setAttribute('draggable', 'false'); + stopAutoplay() { + this.showAllIcons(); + this.autoplay = false; - this.stop(); - this.init(container, true, hotspotConfigs); + window.clearTimeout(this.loopTimeoutId); } destroy() { - this.stop(); + this.stopAutoplay(); const oldElement = this.container; const newElement = oldElement.cloneNode(true); @@ -1080,66 +466,83 @@ class CI360Viewer { oldElement.parentNode.replaceChild(newElement, oldElement); } - addCloseFullscreenView(event) { - const closeFullscreenIcon = createCloseFullscreenIcon(); - - closeFullscreenIcon.onclick = this.setFullscreenEvents.bind(this, event); - window.onkeyup = this.setFullscreenEvents.bind(this, event); - - this.iconsContainer.appendChild(closeFullscreenIcon); - } - - add360ViewIcon() { - this.view360Icon = create360ViewIcon(); - this.innerBox.appendChild(this.view360Icon); + addInitialIcon() { + this.initialIcon = createInitialIcon(); + this.innerBox.appendChild(this.initialIcon); } - addFullscreenIcon() { - this.fullscreenIcon = createFullscreenIcon(); - this.fullscreenIcon.onclick = this.openFullscreenModal.bind(this); + showInitialIcon() { + if (!this.initialIcon) return; - this.iconsContainer.appendChild(this.fullscreenIcon); + this.initialIcon.style.opacity = 1; } - showFullscreenIcon() { - if (!this.fullscreenIcon) return; + hideInitialIcon() { + if (!this.initialIcon) return; - this.fullscreenIcon.style.display = 'block'; - this.fullscreenIcon.style.pointerEvents = 'auto'; + this.initialIcon.style.opacity = 0; } - hideFullscreenIcon() { - if (!this.fullscreenIcon) return; + createGlass() { + this.hideAllIcons(); + this.glass = document.createElement('div'); + this.innerBox.appendChild(this.glass); + this.innerBox.style.cursor = 'default'; + } - this.fullscreenIcon.style.display = 'none'; - this.fullscreenIcon.style.pointerEvents = 'none'; + removeGlass() { + this.showAllIcons(); + this.innerBox.removeChild(this.glass); + this.glass = null; } - addMagnifier() { + addMagnifierIcon() { + if (!this.magnifier) return; + this.magnifierIcon = createMagnifierIcon(); this.magnifierIcon.onclick = this.magnify.bind(this); this.iconsContainer.appendChild(this.magnifierIcon); } - enableMagnifierIcon() { + showMagnifierIcon() { if (!this.magnifierIcon) return; - this.magnifierIcon.style.display = 'block'; - this.magnifierIcon.style.pointerEvents = 'auto'; + this.magnifierIcon.style.visibility = 'visible'; // !TODO: Rework + this.magnifierIcon.style.opacity = 1; } - disableMagnifierIcon() { + hideMagnifierIcon() { if (!this.magnifierIcon) return; - this.magnifierIcon.style.display = 'none'; - this.magnifierIcon.style.pointerEvents = 'none'; + this.magnifierIcon.style.visibility = 'hidden'; + this.magnifierIcon.style.opacity = 0; } - closeFullscreenModal(event) { - event.stopPropagation(); - document.body.removeChild(this.container.parentNode); - window.document.body.style.overflow = 'visible'; + addFullscreenIcon() { + this.fullscreenIcon = createFullscreenIcon(); + this.fullscreenIcon.onclick = this.openFullscreenModal.bind(this); + + this.iconsContainer.appendChild(this.fullscreenIcon); + } + + addCloseFullscreenIcon() { + this.fullscreenCloseIcon = createCloseIcon(); + this.fullscreenCloseIcon.onclick = this.closeFullscreenModal.bind(this); + + this.iconsContainer.appendChild(this.fullscreenCloseIcon); + } + + showFullscreenIcon() { + if (!this.fullscreenIcon) return; + + this.fullscreenIcon.style.opacity = 1; + } + + hideFullscreenIcon() { + if (!this.fullscreenIcon) return; + + this.fullscreenIcon.style.opacity = 0; } add360ViewCircleIcon() { @@ -1150,13 +553,13 @@ class CI360Viewer { show360ViewCircleIcon() { if (!this.view360CircleIcon) return; - this.view360CircleIcon.style.opacity = '1'; + this.view360CircleIcon.style.opacity = 1; } hide360ViewCircleIcon() { if (!this.view360CircleIcon) return; - this.view360CircleIcon.style.opacity = '0'; + this.view360CircleIcon.style.opacity = 0; } remove360ViewCircleIcon() { @@ -1166,6 +569,31 @@ class CI360Viewer { this.view360CircleIcon = null; } + addAllIcons() { + this.removeLoader(); + + if (!this.fullscreenView) this.addMagnifierIcon(); + if (!this.fullscreenView) this.addFullscreenIcon(); + if (!this.initialIconHidden) this.addInitialIcon(); + if (!this.bottomCircleHidden) this.add360ViewCircleIcon(); + } + + showAllIcons() { + this.showInitialIcon(); + this.show360ViewCircleIcon(); + this.showMagnifierIcon(); + this.showFullscreenIcon(); + } + + hideAllIcons() { + this.hideInitialIcon(); + this.hide360ViewCircleIcon(); + this.hideMagnifierIcon(); + this.hideFullscreenIcon(); + + if (this.glass) this.removeGlass(); + } + removeLoader() { if (!this.loader) return; @@ -1173,15 +601,6 @@ class CI360Viewer { this.loader = null; } - remove360ViewIcon() { - if (!this.view360Icon) return; - - try { - this.innerBox.removeChild(this.view360Icon); - this.view360Icon = null; - } catch { } - } - initControls() { const onLeftStart = (event) => { event.stopPropagation(); @@ -1217,26 +636,20 @@ class CI360Viewer { this.bottom(); this.loopTimeoutId = window.setInterval(this.bottom.bind(this), this.autoplaySpeed); - } - - const onEventEnd = () => { - this.onFinishSpin(); - window.clearTimeout(this.loopTimeoutId); }; const controlsConfig = { container: this.container, controlReverse: this.controlReverse, spinReverse: this.spinReverse, - stopAtEdges: this.stopAtEdges - } + stopAtEdges: this.stopAtEdges, + }; const controlsTriggers = { onLeftStart, onRightStart, onTopStart, onBottomStart, - onEventEnd, }; const controlsElements = initControls(controlsConfig, controlsTriggers); @@ -1248,35 +661,87 @@ class CI360Viewer { } attachEvents(draggable, swipeable, keys) { - window.addEventListener('resize', this.requestResizedImages.bind(this)); - - if ((draggable) && (!this.disableDrag)) { - this.container.addEventListener('click', this.mouseClick.bind(this)); - this.container.addEventListener('mousedown', this.mouseDown.bind(this)); - this.container.addEventListener('mousemove', this.mouseMove.bind(this)); - this.container.addEventListener('mouseleave', this.mouseLeave.bind(this)); - - document.addEventListener('mouseup', this.mouseUp.bind(this)); + if (draggable) { + this.addMouseEvents(); } - if ((swipeable) && (!this.disableDrag)) { - this.container.addEventListener('touchstart', this.touchStart.bind(this), { passive: true }); - this.container.addEventListener('touchend', this.touchEnd.bind(this)); - this.container.addEventListener('touchmove', this.touchMove.bind(this)); + if (swipeable) { + this.addTouchEvents(); } if (keys) { - document.addEventListener('keydown', this.keyDown.bind(this)); - document.addEventListener('keyup', this.keyUp.bind(this)); + this.addKeyboardEvents(); } + } + + addMouseEvents() { + this.innerBox.addEventListener('click', this.mouseClick.bind(this)); + this.innerBox.addEventListener('mousedown', this.mouseDown.bind(this)); + + document.addEventListener('mousemove', throttle(this.mouseMove.bind(this), THROTTLE_TIME)); + document.addEventListener('mouseup', this.mouseUp.bind(this)); + } + + addTouchEvents() { + document.addEventListener('touchstart', this.touchOutside.bind(this)); + + this.container.addEventListener('touchstart', this.touchStart.bind(this)); + this.container.addEventListener('touchend', this.touchEnd.bind(this)); + this.container.addEventListener('touchmove', throttle(this.touchMove.bind(this), THROTTLE_TIME)); + } + + addKeyboardEvents() { + document.addEventListener('keydown', this.keyDown.bind(this)); + document.addEventListener('keyup', this.keyUp.bind(this)); + } + + createContainers(event) { + this.iconsContainer = createIconsContainer(this.container); + this.canvas = createCanvas(this.innerBox, event); + this.loader = createLoader(this.innerBox); + + if (this.fullscreenView) this.addCloseFullscreenIcon(); - document.addEventListener('keydown', this.keyDownGeneral.bind(this)); + removeElementFromContainer(this.innerBox, '.cloudimage-lazy'); } - init(container, update = false, hotspotsConfigs = null) { + init(container) { let { - folder, apiVersion, filenameX, filenameY, imageListX, imageListY, indexZeroBase, amountX, amountY, draggable = true, swipeable = true, keys, keysReverse, bottomCircle, bottomCircleOffset, boxShadow, - autoplay, autoplayBehavior, playOnce, speed, autoplayReverse, disableDrag = true, fullscreen, magnifier, ciToken, ciFilters, ciTransformation, lazyload, lazySelector, spinReverse, dragSpeed, stopAtEdges, controlReverse, hide360Logo, logoSrc, pointerZoom, ratio, imageInfo = 'black', requestResponsiveImages + folder, + apiVersion, + filenameX, + filenameY, + imageListX, + imageListY, + indexZeroBase, + amountX, + amountY, + draggable = true, + swipeable = true, + keys, + keysReverse, + bottomCircle, + bottomCircleOffset, + autoplay, + autoplayBehavior, + playOnce, + speed, + autoplayReverse, + fullscreen, + magnifier, + ciToken, + ciFilters, + ciTransformation, + lazyload, + lazySelector, + dragSpeed, + stopAtEdges, + controlReverse, + logoSrc, + pointerZoom, + imageInfo = 'black', + initialIconHidden, + bottomCircleHidden, } = get360ViewProps(container); const ciParams = { ciToken, ciFilters, ciTransformation }; @@ -1290,28 +755,23 @@ class CI360Viewer { this.indexZeroBase = indexZeroBase; this.amountX = imageListX ? JSON.parse(imageListX).length : amountX; this.amountY = imageListY ? JSON.parse(imageListY).length : amountY; - this.allowSpinY = (!!this.amountY); - this.activeImageX = autoplayReverse ? this.amountX : 1; - this.activeImageY = autoplayReverse ? this.amountY : 1; - this.spinY = (autoplayBehavior === AUTOPLAY_BEHAVIOR.SPIN_YX) ? true : false; + this.allowSpinY = !!this.amountY; + this.activeImageX = autoplayReverse ? this.amountX - 1 : 0; + this.activeImageY = autoplayReverse ? this.amountY - 1 : 0; this.bottomCircle = bottomCircle; this.bottomCircleOffset = bottomCircleOffset; - this.boxShadow = boxShadow; this.autoplay = autoplay; this.autoplayBehavior = autoplayBehavior; this.playOnce = playOnce; this.speed = speed; - this.reversed = autoplayReverse; - this.disableDrag = disableDrag; + this.autoplayReverse = autoplayReverse; this.fullscreen = fullscreen; - this.magnifier = !this.isMobile && magnifier > 1 ? Math.min(magnifier, 5) : 0; + this.magnifier = magnifier > 1 ? Math.min(magnifier, 5) : 0; this.lazySelector = lazySelector; - this.spinReverse = spinReverse; this.controlReverse = controlReverse; this.dragSpeed = Math.max(dragSpeed, 50); - this.autoplaySpeed = this.speed * 36 / this.amountX; + this.autoplaySpeed = (this.speed * 36) / this.amountX; this.stopAtEdges = stopAtEdges; - this.hide360Logo = hide360Logo; this.logoSrc = logoSrc; this.ciParams = ciParams; this.apiVersion = apiVersion; @@ -1319,57 +779,10 @@ class CI360Viewer { this.keysReverse = keysReverse; this.info = imageInfo; this.keys = keys; - this.ratio = ratio && JSON.parse(ratio); - this.requestResponsiveImages = requestResponsiveImages - - if (update) { - removeChildFromParent(this.innerBox, this.iconsContainer); - removeChildFromParent(this.innerBox, this.boxShadowEl); - removeChildFromParent(this.innerBox, this.view360Icon); - this.remove360ViewCircleIcon(); - - this.iconsContainer = createIconsContainer(this.innerBox); - - if (!this.hide360Logo && !this.lazyload && this.logoSrc) { - this.add360ViewIcon(); - setView360Icon(this.view360Icon, this.logoSrc); - } - - if (this.magnifier) { - this.addMagnifier(); - } - - if (this.bottomCircle && !this.fullscreenView) { - this.add360ViewCircleIcon(); - } - - if (this.fullscreen && !this.fullscreenView) { - this.addFullscreenIcon(); - } - - if (this.boxShadow && !this.fullscreenView) { - this.boxShadowEl = createBoxShadow(this.boxShadow, this.innerBox); - } - - - if (hotspotsConfigs && !this.fullscreenView) { - this.hotspotsConfigs = generateHotspotsConfigs(hotspotsConfigs); - createHotspots(container, this.hotspotsConfigs); - } - - return this.onAllImagesLoaded(); - } - this.innerBox = createInnerBox(this.container); - this.iconsContainer = createIconsContainer(this.innerBox); - this.canvas = createCanvas(this.innerBox); - this.loader = createLoader(this.innerBox); - - if (this.hotspotsConfigs && !this.fullscreenView) { - createHotspots(container, this.hotspotsConfigs); - } - - applyStylesToContainer(this.container); + this.initialIconHidden = initialIconHidden; + this.bottomCircleHidden = bottomCircleHidden; + this.spinDirection = getDefaultSpinDirection(this.autoplayBehavior); this.srcXConfig = { folder, @@ -1383,70 +796,32 @@ class CI360Viewer { amount: this.amountX, indexZeroBase, fullscreen: this.fullscreenView, - } + autoplayReverse, + }; this.srcYConfig = { ...this.srcXConfig, filename: filenameY, - orientation: ORIENTATIONS.Y, imageList: imageListY, - amount: this.amountY - } - - const srcX = generateImagesPath(this.srcXConfig); - - const onImageLoad = (orientation, image, index) => { - if (orientation === ORIENTATIONS.X) { - this.imagesX[index] = image; - } else { - this.imagesY[index] = image; - } - - const totalAmount = this.amountX + this.amountY; - const totalLoadedImages = this.imagesX.length + this.imagesY.length; - const isFirstImageLoaded = !index && orientation !== ORIENTATIONS.Y; - const percentage = Math.round(totalLoadedImages / totalAmount * 100); - - this.updatePercentageInLoader(percentage); - if (isFirstImageLoaded) { - this.onFirstImageLoaded(image); - } else if (this.autoplay) { - this.moveRight(index) - } - - if (this.isReady()) { - this.onAllImagesLoaded(); - } - } + orientation: ORIENTATIONS.Y, + amount: this.amountY, + }; - const loadImages = () => { - preloadImages( - this.srcXConfig, - srcX, - (onImageLoad.bind(this, ORIENTATIONS.X)) - ); - - if (this.allowSpinY) { - const srcY = generateImagesPath(this.srcYConfig); - - preloadImages( - this.srcYConfig, - srcY, - onImageLoad.bind(this, ORIENTATIONS.Y) - ); - } - } + const cdnPathX = generateCdnPath(this.srcXConfig, this.fullscreenView); + const cdnPathY = this.allowSpinY ? generateCdnPath(this.srcYConfig, this.fullscreenView) : null; if (lazyload) { - const onFirstImageLoad = (image) => { - this.innerBox.removeChild(image); - - loadImages(); - } - - initLazyload(srcX, this.srcXConfig, onFirstImageLoad); - } else { - loadImages(); + initLazyload(cdnPathX, this.srcXConfig, (event) => { + preloadImages({ + cdnPathX, + cdnPathY, + configX: this.srcXConfig, + configY: this.srcYConfig, + onImageLoad: (image, index, orientation) => this.onImageLoad(image, index, orientation), + onFirstImageLoad: (image) => this.onFirstImageLoaded(event, image), + onAllImagesLoad: this.onAllImagesLoaded.bind(this), + }); + }); } this.attachEvents(draggable, swipeable, keys); diff --git a/src/ci360.utils.js b/src/ci360.utils.js index 7499fa8..a3bd00f 100644 --- a/src/ci360.utils.js +++ b/src/ci360.utils.js @@ -1,56 +1,67 @@ -import { AUTOPLAY_BEHAVIOR } from './constants'; +import { AUTOPLAY_BEHAVIOR } from './utils/constants'; + //TODO [deprecated]: remove filename, amount in the upcoming versions const get360ViewProps = (image) => ({ folder: attr(image, 'folder') || attr(image, 'data-folder') || '/', - apiVersion: attr(image, 'api-version') || attr(image, 'data-api-version') - || attr(image, 'apiVersion') || attr(image, 'data-apiVersion') || "v7", - filenameX: attr(image, 'filename') || attr(image, 'data-filename') - || attr(image, 'filename-x') || attr(image, 'data-filename-x') - || 'image-{index}.jpg', - filenameY: attr(image, 'filename-y') || - attr(image, 'data-filename-y') || 'image-y-{index}.jpg', + apiVersion: + attr(image, 'api-version') || + attr(image, 'data-api-version') || + attr(image, 'apiVersion') || + attr(image, 'data-apiVersion') || + 'v7', + filenameX: + attr(image, 'filename') || + attr(image, 'data-filename') || + attr(image, 'filename-x') || + attr(image, 'data-filename-x') || + 'image-{index}.jpg', + filenameY: attr(image, 'filename-y') || attr(image, 'data-filename-y') || 'image-y-{index}.jpg', imageListX: attr(image, 'image-list-x') || attr(image, 'data-image-list-x') || null, imageListY: attr(image, 'image-list-y') || attr(image, 'data-image-list-y') || null, indexZeroBase: parseInt(attr(image, 'index-zero-base') || attr(image, 'data-index-zero-base') || 0, 10), - amountX: parseInt(attr(image, 'amount') || attr(image, 'data-amount') - || attr(image, 'amount-x') || attr(image, 'data-amount-x') - || 36, 10), - amountY: parseInt(attr(image, 'amount-y') || - attr(image, 'data-amount-y') || 0, 10), + amountX: parseInt( + attr(image, 'amount') || + attr(image, 'data-amount') || + attr(image, 'amount-x') || + attr(image, 'data-amount-x') || + 36, + 10 + ), + amountY: parseInt(attr(image, 'amount-y') || attr(image, 'data-amount-y') || 0, 10), speed: parseInt(attr(image, 'speed') || attr(image, 'data-speed') || 80, 10), dragSpeed: parseInt(attr(image, 'drag-speed') || attr(image, 'data-drag-speed') || 150, 10), keys: isTrue(image, 'keys'), keysReverse: isTrue(image, 'keys-reverse'), boxShadow: attr(image, 'box-shadow') || attr(image, 'data-box-shadow'), autoplay: isTrue(image, 'autoplay'), - autoplayBehavior: attr(image, 'autoplay-behavior') - || attr(image, 'data-autoplay-behavior') - || AUTOPLAY_BEHAVIOR.SPIN_X, + autoplayBehavior: + attr(image, 'autoplay-behavior') || attr(image, 'data-autoplay-behavior') || AUTOPLAY_BEHAVIOR.SPIN_X, playOnce: isTrue(image, 'play-once'), autoplayReverse: isTrue(image, 'autoplay-reverse'), - pointerZoom: parseFloat(attr(image, 'pointer-zoom') || - attr(image, 'data-pointer-zoom') || 0, 10), + pointerZoom: parseFloat(attr(image, 'pointer-zoom') || attr(image, 'data-pointer-zoom') || 0, 10), bottomCircle: isTrue(image, 'bottom-circle'), - disableDrag: isTrue(image, 'disable-drag'), fullscreen: isTrue(image, 'fullscreen') || isTrue(image, 'full-screen'), - magnifier: ((attr(image, 'magnifier') !== null) || (attr(image, 'data-magnifier') !== null)) && + magnifier: + (attr(image, 'magnifier') !== null || attr(image, 'data-magnifier') !== null) && parseFloat(attr(image, 'magnifier') || attr(image, 'data-magnifier'), 10), - bottomCircleOffset: parseInt(attr(image, 'bottom-circle-offset') || attr(image, 'data-bottom-circle-offset') || 5, 10), + bottomCircleOffset: parseInt( + attr(image, 'bottom-circle-offset') || attr(image, 'data-bottom-circle-offset') || 5, + 10 + ), ciToken: attr(image, 'responsive') || attr(image, 'data-responsive'), - ciFilters: attr(image, 'filters') - || attr(image, 'data-filters'), - ciTransformation: attr(image, 'transformation') - || attr(image, 'data-transformation'), + ciFilters: attr(image, 'filters') || attr(image, 'data-filters'), + ciTransformation: attr(image, 'transformation') || attr(image, 'data-transformation'), lazyload: isTrue(image, 'lazyload'), lazySelector: attr(image, 'lazyload-selector') || attr(image, 'data-lazyload-selector') || 'lazyload', spinReverse: isTrue(image, 'spin-reverse'), controlReverse: isTrue(image, 'control-reverse'), stopAtEdges: isTrue(image, 'stop-at-edges'), - hide360Logo: isTrue(image, 'hide-360-logo'), - logoSrc: attr(image, 'logo-src') || 'https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/360_view.svg', - ratio: attr(image, 'ratio') || attr(image, 'data-ratio'), - imageInfo: attr(image, 'info')|| attr(image, 'data-info') || isTrue(image, 'info'), - requestResponsiveImages: isTrue(image, 'request-responsive-images') + logoSrc: + attr(image, 'logo-src') || + 'https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/360_view.svg', + imageInfo: attr(image, 'info') || attr(image, 'data-info') || isTrue(image, 'info'), + initialIconHidden: isFalse(image, 'initial-icon'), + bottomCircleHidden: isFalse(image, 'bottom-circle'), }); const isTrue = (image, type) => { @@ -59,28 +70,13 @@ const isTrue = (image, type) => { return (imgProp !== null && imgProp !== 'false') || (imgDataProp !== null && imgDataProp !== 'false'); }; +const isFalse = (image, type) => { + const imgProp = attr(image, type); + const imgDataProp = attr(image, `data-${type}`); -const attr = (element, attribute) => element.getAttribute(attribute); - -const setView360Icon = (view360Icon, logoSrc) => { - view360Icon.style.background = `rgba(255,255,255,0.8) url('${logoSrc}') 50% 50% / contain no-repeat`; -} - -const debounce = (func, timeout) => { - let timer; - return (...args) => { - clearTimeout(timer); + return imgProp === 'false' || imgDataProp === 'false'; +}; - timer = setTimeout(() => { - func.apply(this, args) - }, timeout) - } -} +const attr = (element, attribute) => element.getAttribute(attribute); -export { - get360ViewProps, - setView360Icon, - attr, - isTrue, - debounce -} \ No newline at end of file +export { get360ViewProps, attr, isTrue }; diff --git a/src/constants/auto-play-behavior.js b/src/constants/auto-play-behavior.js deleted file mode 100644 index 05798e5..0000000 --- a/src/constants/auto-play-behavior.js +++ /dev/null @@ -1,6 +0,0 @@ -export const AUTOPLAY_BEHAVIOR = { - SPIN_X: 'spin-x', - SPIN_Y: 'spin-y', - SPIN_XY: 'spin-xy', - SPIN_YX: 'spin-yx', -}; diff --git a/src/constants/falsy-values.js b/src/constants/falsy-values.js deleted file mode 100644 index 07b2d76..0000000 --- a/src/constants/falsy-values.js +++ /dev/null @@ -1,10 +0,0 @@ -export const FALSY_VALUES = [ - false, - 0, - null, - undefined, - 'false', - '0', - 'null', - 'undefined', -]; diff --git a/src/constants/index.js b/src/constants/index.js deleted file mode 100644 index 2543085..0000000 --- a/src/constants/index.js +++ /dev/null @@ -1,7 +0,0 @@ -export { AUTOPLAY_BEHAVIOR } from './auto-play-behavior'; -export { FALSY_VALUES } from './falsy-values'; -export { ORIENTATIONS } from './orientations'; -export { - ORGINAL_SIZE_REGEX, - AND_SYMBOL_REGEX, -} from './regex'; diff --git a/src/constants/orientations.js b/src/constants/orientations.js deleted file mode 100644 index 4d59489..0000000 --- a/src/constants/orientations.js +++ /dev/null @@ -1,5 +0,0 @@ -export const ORIENTATIONS = { - X: 'x-axis', - Y: 'y-axis', - CENTER: 'center', -}; diff --git a/src/constants/regex.js b/src/constants/regex.js deleted file mode 100644 index 8bba279..0000000 --- a/src/constants/regex.js +++ /dev/null @@ -1,8 +0,0 @@ -const ORGINAL_SIZE_REGEX = /width=\d+|w=\d+|h=\d+|&width=\d+|&w=\d+|&h=\d+|func=\w+|\?$/g; -const AND_SYMBOL_REGEX = /\?&/g; - - -export { - ORGINAL_SIZE_REGEX, - AND_SYMBOL_REGEX, -}; diff --git a/src/index.js b/src/index.js index 18e66b0..cdb0c60 100644 --- a/src/index.js +++ b/src/index.js @@ -19,17 +19,19 @@ function getContainerWithId(container) { function init() { const viewers = []; - const view360Array = document.querySelectorAll('.cloudimage-360:not(.initialized)'); + const view360Array = document.querySelectorAll( + '.cloudimage-360:not(.initialized)' + ); - [].slice.call(view360Array).forEach(_container => { + [].slice.call(view360Array).forEach((_container) => { const container = getContainerWithId(_container); - const isHotspotsEnabled = isTrue(container, 'hotspots') + const isHotspotsEnabled = isTrue(container, 'hotspots'); if (!isHotspotsEnabled) { viewers.push(new CI360Viewer(container)); } - }) + }); window.CI360._viewers = viewers; } @@ -37,7 +39,9 @@ function init() { function destroy() { if (isNoViewers()) return; - window.CI360._viewers.forEach(viewer => { viewer.destroy(); }); + window.CI360._viewers.forEach((viewer) => { + viewer.destroy(); + }); window.CI360._viewers = []; } @@ -45,28 +49,35 @@ function destroy() { function getActiveIndexByID(id, oriantation) { if (isNoViewers()) return; - let currentViewer = window.CI360._viewers.filter(viewer => viewer.id === id)[0]; + let currentViewer = window.CI360._viewers.filter( + (viewer) => viewer.id === id + )[0]; if (oriantation === 'y') { - return currentViewer && (currentViewer.activeImageY - 1); + return currentViewer && currentViewer.activeImageY - 1; } - return currentViewer && (currentViewer.activeImageX - 1); + return currentViewer && currentViewer.activeImageX - 1; } function add(id) { - const view360Array = Array.from(document.querySelectorAll('.cloudimage-360:not(.initialized)')); + const view360Array = Array.from( + document.querySelectorAll('.cloudimage-360:not(.initialized)') + ); if (view360Array.length && id) { - const newViewContainer = view360Array.filter(viewer => viewer.id === id)[0]; + const newViewContainer = view360Array.filter( + (viewer) => viewer.id === id + )[0]; - newViewContainer && window.CI360._viewers.push(new CI360Viewer(newViewContainer)); + newViewContainer && + window.CI360._viewers.push(new CI360Viewer(newViewContainer)); } } function update(id = null, forceUpdate = false, hotspotConfigs = null) { if (id) { - const view = window.CI360._viewers.filter(viewer => viewer.id === id)[0]; + const view = window.CI360._viewers.filter((viewer) => viewer.id === id)[0]; if (hotspotConfigs) { const view360Array = document.querySelectorAll('.cloudimage-360'); @@ -76,8 +87,9 @@ function update(id = null, forceUpdate = false, hotspotConfigs = null) { view.updateView(forceUpdate, window.CI360._viewers, hotspotConfigs); } else { - window.CI360._viewers - .forEach(viewer => { viewer.updateView(forceUpdate, window.CI360._viewers); }); + window.CI360._viewers.forEach((viewer) => { + viewer.updateView(forceUpdate, window.CI360._viewers); + }); } } @@ -86,15 +98,21 @@ function isNoViewers() { } function addHotspots(instanceId, config) { - const view360Array = document.querySelectorAll('.cloudimage-360:not(.initialized)'); - const notInitializedContainer = Array.from(view360Array).find(view => view.id === instanceId); + const view360Array = document.querySelectorAll( + '.cloudimage-360:not(.initialized)' + ); + const notInitializedContainer = Array.from(view360Array).find( + (view) => view.id === instanceId + ); if (notInitializedContainer) { - container.setAttribute('data-hotspots', true) + container.setAttribute('data-hotspots', true); - return window.CI360._viewers.push(new CI360Viewer(container, false, config)) + return window.CI360._viewers.push( + new CI360Viewer(container, false, config) + ); } else { - update(instanceId, false, config) + update(instanceId, false, config); } } @@ -108,4 +126,4 @@ window.CI360.addHotspots = addHotspots; if (!window.CI360.notInitOnLoad) { init(); -} \ No newline at end of file +} diff --git a/src/static/css/hotspots.css b/src/static/css/hotspots.css index b227731..6d4598c 100644 --- a/src/static/css/hotspots.css +++ b/src/static/css/hotspots.css @@ -15,7 +15,7 @@ width: 16px; height: 16px; background-image: url('https://scaleflex.cloudimg.io/v7/plugins/js-cloudimage-360-view/assets/img/link-hotspot.svg'); - background-color: #0C6DC7; + background-color: #0c6dc7; background-repeat: no-repeat; background-size: 9px; background-position: center; @@ -24,15 +24,15 @@ transform: translate(-50%, -50%); box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.25); border-radius: 50%; - animation: pulse 2s infinite; + animation: pulse 2s infinite; cursor: auto; } .cloudimage-360-hotspot-custom-icon { width: 42px; height: 42px; - background: #76AD0133; - border: 1px solid #FFFFFF33; + background: #76ad0133; + border: 1px solid #ffffff33; border-radius: 50%; box-sizing: border-box; transition: opacity 300ms ease-in-out; @@ -45,7 +45,7 @@ width: 16px; height: 16px; background-image: url('https://scaleflex.cloudimg.io/v7/plugins/js-cloudimage-360-view/assets/img/plus.svg'); - background-color: #76AD01; + background-color: #76ad01; background-repeat: no-repeat; background-size: 9px; background-position: center; @@ -54,14 +54,14 @@ transform: translate(-50%, -50%); box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.25); border-radius: 50%; - animation: pulse 2s infinite; + animation: pulse 2s infinite; cursor: auto; } .cloudimage-360-hotspot-popup { visibility: hidden; opacity: 0; - background-color: #FFFFFF; + background-color: #ffffff; padding: 6px; border-radius: 2px; box-shadow: 0px 4px 4px 0px #00000040; @@ -153,7 +153,7 @@ .cloudimage-360-carousel-dot { width: 6px; height: 6px; - background-color: #C9D0DE; + background-color: #c9d0de; border-radius: 50%; cursor: pointer; transition: background-color 0.6s ease; @@ -162,7 +162,7 @@ } .cloudimage-360-carousel-dot.active-dot { - background-color: #76AD01; + background-color: #76ad01; } .cloudimage-360-carousel-dot:focus, @@ -186,8 +186,8 @@ } .cloudimage-360-modal-more-details { - color: #76AD01; - background-color: #ECFAE6; + color: #76ad01; + background-color: #ecfae6; font-size: 10px; line-height: 16px; font-weight: 400; @@ -198,25 +198,29 @@ } .cloudimage-360-modal-more-details:hover { - color: #76AD01; + color: #76ad01; background-color: #e1f5d8; } @keyframes pulse { - 0% { - box-shadow: 0 0 0 0 rgba(99, 99, 99, 0.7); - } + 0% { + box-shadow: 0 0 0 0 rgba(99, 99, 99, 0.7); + } - 70% { - box-shadow: 0 0 0 10px rgba(99, 99, 99, 0); - } + 70% { + box-shadow: 0 0 0 10px rgba(99, 99, 99, 0); + } - 100% { - box-shadow: 0 0 0 0 rgba(99, 99, 99, 0); - } + 100% { + box-shadow: 0 0 0 0 rgba(99, 99, 99, 0); + } } @keyframes fade-active-image { - from { opacity: 0.7 } - to { opacity: 1 } -} \ No newline at end of file + from { + opacity: 0.7; + } + to { + opacity: 1; + } +} diff --git a/src/static/css/style.css b/src/static/css/style.css index 9aa0ccc..d555172 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -1,29 +1,43 @@ +.cloudimage-360 { + width: 100%; + position: relative; +} + +.cloudimage-360-inner-box { + cursor: grab; + width: 100%; + height: 100%; +} + .cloudimage-360-icons-container { position: absolute; display: flex; - top: 5px; + top: 15px; right: 5px; - width: 30px; - height: 95%; + width: 25px; + height: 100%; flex-direction: column; align-items: center; - z-index: 101; + z-index: 100; + gap: 8px; } .cloudimage-360-magnifier-icon { - width: 25px; + width: 100%; height: 25px; - margin-bottom: 5px; cursor: pointer; - background: url('https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/loupe.svg') 50% 50% / cover no-repeat; + transition: all 200ms ease-out; + background: url('https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/loupe.svg') 50% + 50% / cover no-repeat; } .cloudimage-360-fullscreen-icon { - width: 25px; + width: 100%; height: 25px; - margin-bottom: 5px; cursor: pointer; - background: url('https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/full_screen.svg') 50% 50% / cover no-repeat; + transition: all 200ms ease-out; + background: url('https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/full_screen.svg') + 50% 50% / cover no-repeat; } .cloudimage-360-reset-zoom-icon { @@ -32,25 +46,46 @@ height: 30px; margin-top: auto; cursor: pointer; - background: url('https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/ic-resize.svg?vh=248986') 50% 50% / cover no-repeat; + background: url('https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/ic-resize.svg?vh=248986') + 50% 50% / cover no-repeat; } -.cloudimage-360-close-fullscreen-icon { - width: 25px; +.cloudimage-360-close-icon { + width: 100%; height: 25px; cursor: pointer; - background: url('https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/cross.svg') 50% 50% / cover no-repeat; + background: url('https://scaleflex.cloudimg.io/v7/filerobot/js-cloudimage-360-view/cross.svg') 50% + 50% / cover no-repeat; } -.cloudimage-360-loader { +.cloudimage-initial-icon { position: absolute; top: 0; + bottom: 0; left: 0; right: 0; - width: 0%; - height: 8px; - background-color: rgb(165, 175, 184); - z-index: 100; + width: 100px; + height: 100px; + margin: auto; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.5), rgba(200, 200, 200, 0.5)); + backdrop-filter: blur(8px); + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.5); + box-shadow: + 0 8px 12px rgba(0, 0, 0, 0.2), + 0 4px 6px rgba(0, 0, 0, 0.1); + transition: + 200ms all, + transform 0.2s; + color: #4b4b4b; + text-align: center; + z-index: 2; + line-height: 100px; + user-select: none; +} + +.cloudimage-initial-icon:hover { + background-color: rgba(200, 200, 200, 0.2); /* Brighten on hover */ } .cloudimage-360-box-shadow { @@ -62,25 +97,6 @@ z-index: 99; } -.cloudimage-360-view-360-icon { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - width: 100px; - height: 100px; - margin: auto; - background-color: rgba(255, 255, 255, 0.8); - border-radius: 50%; - box-shadow: rgba(255, 255, 255, 0.5) 0px 0px 4px; - transition: 0.5s all; - color: rgb(80, 80, 80); - text-align: center; - line-height: 100px; - z-index: 2; -} - .cloudimage-360-view-360-circle { position: absolute; left: 0; @@ -90,7 +106,7 @@ margin: auto; pointer-events: none; user-select: none; - transition: 0.5s all; + transition: 200ms all; z-index: 2; } @@ -103,16 +119,20 @@ width: 100%; height: 100%; z-index: 999; - background-color: #FFF; + background-color: #fff; } .cloudimage-360-img-magnifier-glass { - background-color: #FFF; + background-color: #fff; + background-image: radial-gradient( + circle at center, + rgba(255, 255, 255, 0.8), + rgba(240, 240, 240, 0.9) + ); background-repeat: no-repeat; position: absolute; - border: 3px solid #000; + border: 2px solid rgba(0, 0, 0, 0.3); /* Softer border */ border-radius: 50%; - cursor: wait; line-height: 200px; text-align: center; z-index: 1000; @@ -120,6 +140,12 @@ height: 250px; top: -75px; right: -85px; + box-shadow: + 0 8px 16px rgba(0, 0, 0, 0.4), + 0 4px 8px rgba(0, 0, 0, 0.2); + transition: box-shadow 200ms ease; + overflow: hidden; + pointer-events: none; } .cloudimage-360 .cloudimage-360-left, @@ -227,4 +253,4 @@ .cloudimage-360 .cloudimage-360-bottom.not-active { opacity: 0.4; cursor: default; -} \ No newline at end of file +} diff --git a/src/utils/auto-play/get-speed-factor.js b/src/utils/auto-play/get-speed-factor.js index b76eb16..2558a8f 100644 --- a/src/utils/auto-play/get-speed-factor.js +++ b/src/utils/auto-play/get-speed-factor.js @@ -1,6 +1,3 @@ export const getSpeedFactor = (dragSpeed, amount, containerOffset) => { - const containerOffsetWidth = Math.max(containerOffset, 600); - const speedFactor = (dragSpeed / 150) * (36 / amount) * 25 * (containerOffsetWidth / 1500) || 1; - - return Math.floor(speedFactor); + return Math.floor(((((dragSpeed / 150) * 36) / amount) * 25 * containerOffset) / 1500) || 1; }; diff --git a/src/utils/auto-play/get-throttle-time.js b/src/utils/auto-play/get-throttle-time.js new file mode 100644 index 0000000..24a7e4c --- /dev/null +++ b/src/utils/auto-play/get-throttle-time.js @@ -0,0 +1,18 @@ +export const getThrottleTime = (dragSpeed, amount, containerOffset) => { + const baseThrottle = 15; // Base throttle time in ms + const minThrottle = 1; // Minimum throttle time + const maxThrottle = 30; // Maximum throttle time + + // Calculate factors based on input parameters + const containerFactor = Math.max(1, containerOffset / 100); // Adjust the scale factor + const amountFactor = Math.max(1, 100 / amount); // Use a higher divisor to adjust sensitivity + const speedFactor = Math.max(1, dragSpeed / 1000); // Higher divisor for drag speed + + // Dynamic throttle time calculation + let throttleTime = baseThrottle + (containerFactor * amountFactor) / speedFactor; + + // Ensure throttle time is within min and max bounds + throttleTime = Math.min(Math.max(throttleTime, minThrottle), maxThrottle); + + return Math.floor(throttleTime); +}; diff --git a/src/utils/auto-play/is-completed-one-cycle.js b/src/utils/auto-play/is-completed-one-cycle.js index 93bff6b..925f8dc 100644 --- a/src/utils/auto-play/is-completed-one-cycle.js +++ b/src/utils/auto-play/is-completed-one-cycle.js @@ -1,26 +1,26 @@ -import { AUTOPLAY_BEHAVIOR } from '../../constants/auto-play-behavior'; +import { AUTOPLAY_BEHAVIOR } from '../constants'; + +export const isCompletedOneCycle = ({ + autoplayBehavior, + activeImageX, + activeImageY, + amountX, + amountY, + autoplayReverse, +}) => { + const checkEdge = (activeImage, amount) => { + const lastIndex = amount - 1; // Calculate the last index + return autoplayReverse ? activeImage === 0 : activeImage === lastIndex; + }; -export const isCompletedOneCycle = (autoplayBehavior, activeImageX, activeImageY, amountX, amountY, isReversed) => { switch (autoplayBehavior) { case AUTOPLAY_BEHAVIOR.SPIN_XY: - case AUTOPLAY_BEHAVIOR.SPIN_Y: { - const isReachedTheEdge = isReversed ? (activeImageY === 1) - : (activeImageY === amountY); - - if (isReachedTheEdge) return true; - - return false; - } + case AUTOPLAY_BEHAVIOR.SPIN_Y: + return checkEdge(activeImageY, amountY); case AUTOPLAY_BEHAVIOR.SPIN_X: case AUTOPLAY_BEHAVIOR.SPIN_YX: - default: { - const isReachedTheEdge = isReversed ? (activeImageX === 1) - : (activeImageX === amountX); - - if (isReachedTheEdge) return true; - - return false; - } + default: + return checkEdge(activeImageX, amountX); } }; diff --git a/src/utils/auto-play/loop.js b/src/utils/auto-play/loop.js index 3b6933b..bf9c7fd 100644 --- a/src/utils/auto-play/loop.js +++ b/src/utils/auto-play/loop.js @@ -1,53 +1,38 @@ -import { AUTOPLAY_BEHAVIOR } from '../../constants/auto-play-behavior'; +import { AUTOPLAY_BEHAVIOR } from '../constants'; -export const loop = (autoplayBehavior, spinY, reversed, loopTriggers) => { - const { - bottom, top, left, right, - } = loopTriggers; +const handleSpinY = (reversed, { bottom, top }) => { + if (reversed) { + bottom(); + } else { + top(); + } +}; - switch (autoplayBehavior) { - case AUTOPLAY_BEHAVIOR.SPIN_Y: - if (reversed) { - bottom(); - } else { - top(); - } - break; +const handleSpinX = (reversed, { left, right }) => { + if (reversed) { + left(); + } else { + right(); + } +}; +export const loop = ({ autoplayBehavior, spinY, reversed, loopTriggers }) => { + switch (autoplayBehavior) { case AUTOPLAY_BEHAVIOR.SPIN_XY: + case AUTOPLAY_BEHAVIOR.SPIN_YX: if (spinY) { - if (reversed) { - bottom(); - } else { - top(); - } - } else if (reversed) { - left(); + handleSpinY(reversed, loopTriggers); } else { - right(); + handleSpinX(reversed, loopTriggers); } break; - case AUTOPLAY_BEHAVIOR.SPIN_YX: - if (spinY) { - if (reversed) { - bottom(); - } else { - top(); - } - } else if (reversed) { - left(); - } else { - right(); - } + case AUTOPLAY_BEHAVIOR.SPIN_Y: + handleSpinY(reversed, loopTriggers); break; case AUTOPLAY_BEHAVIOR.SPIN_X: default: - if (reversed) { - left(); - } else { - right(); - } + handleSpinX(reversed, loopTriggers); } }; diff --git a/src/utils/class-names/remove-class.js b/src/utils/class-names/remove-class.js index a7b9822..c842338 100644 --- a/src/utils/class-names/remove-class.js +++ b/src/utils/class-names/remove-class.js @@ -1,3 +1,10 @@ export const removeClass = (el, className) => { - if (el.classList) { el.classList.remove(className); } else { el.className = el.className.replace(new RegExp(`(^|\\b)${className.split(' ').join('|')}(\\b|$)`, 'gi'), ' '); } + if (el.classList) { + el.classList.remove(className); + } else { + el.className = el.className.replace( + new RegExp(`(^|\\b)${className.split(' ').join('|')}(\\b|$)`, 'gi'), + ' ' + ); + } }; diff --git a/src/constants/props-require-reload.js b/src/utils/constants.js similarity index 50% rename from src/constants/props-require-reload.js rename to src/utils/constants.js index ff992f3..a361f8d 100644 --- a/src/constants/props-require-reload.js +++ b/src/utils/constants.js @@ -1,3 +1,18 @@ +export const AUTOPLAY_BEHAVIOR = { + SPIN_X: 'spin-x', + SPIN_Y: 'spin-y', + SPIN_XY: 'spin-xy', + SPIN_YX: 'spin-yx', +}; + +export const FALSY_VALUES = [false, 0, null, undefined, 'false', '0', 'null', 'undefined']; + +export const ORIENTATIONS = { + X: 'x-axis', + Y: 'y-axis', + CENTER: 'center', +}; + export const PROPS_REQUIRE_RELOAD = [ 'folder', //images source 'filenameX', //images source @@ -12,4 +27,9 @@ export const PROPS_REQUIRE_RELOAD = [ 'disableDrag', // events 'controlReverse', // events 'disableDrag', // events -] \ No newline at end of file +]; + +export const LEFT_RIGHT_KEYS = [37, 39]; +export const UP_DOWN_KEYS = [38, 40]; + +export const THROTTLE_TIME = 10; diff --git a/src/utils/container-elements/create-360-view-circle-icon.js b/src/utils/container-elements/create-360-view-circle-icon.js index 0c32901..c5f8a01 100644 --- a/src/utils/container-elements/create-360-view-circle-icon.js +++ b/src/utils/container-elements/create-360-view-circle-icon.js @@ -1,7 +1,8 @@ export const create360ViewCircleIcon = (circleOffset) => { const view360CircleIcon = new Image(); - view360CircleIcon.src = 'https://scaleflex.cloudimg.io/v7/plugins/js-cloudimage-360-view/assets/img/360.svg'; + view360CircleIcon.src = + 'https://scaleflex.cloudimg.io/v7/plugins/js-cloudimage-360-view/assets/img/360.svg'; view360CircleIcon.style.bottom = `${circleOffset}%`; view360CircleIcon.className = 'cloudimage-360-view-360-circle'; diff --git a/src/utils/container-elements/create-360-view-icon.js b/src/utils/container-elements/create-360-view-icon.js deleted file mode 100644 index 889673b..0000000 --- a/src/utils/container-elements/create-360-view-icon.js +++ /dev/null @@ -1,8 +0,0 @@ -export const create360ViewIcon = () => { - const view360Icon = document.createElement('div'); - - view360Icon.className = 'cloudimage-360-view-360-icon'; - view360Icon.innerText = '0%'; - - return view360Icon; -}; diff --git a/src/utils/container-elements/create-canvas.js b/src/utils/container-elements/create-canvas.js index 475c87d..a19264a 100644 --- a/src/utils/container-elements/create-canvas.js +++ b/src/utils/container-elements/create-canvas.js @@ -1,7 +1,13 @@ -export const createCanvas = (innerBox) => { +export const createCanvas = (innerBox, event) => { + const { width, height } = event; const canvas = document.createElement('canvas'); + + canvas.width = width; + canvas.height = height; + canvas.style.width = '100%'; - canvas.style.fontSize = '0'; + canvas.style.height = 'auto'; + innerBox.appendChild(canvas); return canvas; diff --git a/src/utils/container-elements/create-close-fullscreen-icon.js b/src/utils/container-elements/create-close-fullscreen-icon.js index f06eb17..fe4711f 100644 --- a/src/utils/container-elements/create-close-fullscreen-icon.js +++ b/src/utils/container-elements/create-close-fullscreen-icon.js @@ -1,6 +1,6 @@ -export const createCloseFullscreenIcon = () => { - const closeFullscreenIcon = document.createElement('div'); - closeFullscreenIcon.className = 'cloudimage-360-close-fullscreen-icon'; +export const createFullscreenIcon = () => { + const closeIcon = document.createElement('div'); + closeIcon.className = 'cloudimage-360-fullscreen-icon'; - return closeFullscreenIcon; + return closeIcon; }; diff --git a/src/utils/container-elements/create-close-icon.js b/src/utils/container-elements/create-close-icon.js new file mode 100644 index 0000000..b59e07a --- /dev/null +++ b/src/utils/container-elements/create-close-icon.js @@ -0,0 +1,7 @@ +export const createCloseIcon = () => { + const fullscreenIcon = document.createElement('div'); + + fullscreenIcon.className = 'cloudimage-360-close-icon'; + + return fullscreenIcon; +}; diff --git a/src/utils/container-elements/create-fullscreen-icon.js b/src/utils/container-elements/create-fullscreen-icon.js deleted file mode 100644 index 478aab8..0000000 --- a/src/utils/container-elements/create-fullscreen-icon.js +++ /dev/null @@ -1,7 +0,0 @@ -export const createFullscreenIcon = () => { - const fullscreenIcon = document.createElement('div'); - - fullscreenIcon.className = 'cloudimage-360-fullscreen-icon'; - - return fullscreenIcon; -}; diff --git a/src/utils/container-elements/create-fullscreen-modal.js b/src/utils/container-elements/create-fullscreen-modal.js index ed4933b..41a8b48 100644 --- a/src/utils/container-elements/create-fullscreen-modal.js +++ b/src/utils/container-elements/create-fullscreen-modal.js @@ -5,12 +5,11 @@ export const createFullscreenModal = (container) => { const fullscreenContainer = container.cloneNode(); - fullscreenContainer.style.height = '100%'; + fullscreenContainer.style.height = '100vh'; fullscreenContainer.style.maxHeight = '100%'; fullscreenModal.appendChild(fullscreenContainer); - window.document.body.style.overflow = 'hidden'; window.document.body.appendChild(fullscreenModal); return fullscreenContainer; diff --git a/src/utils/container-elements/create-initial-icon.js b/src/utils/container-elements/create-initial-icon.js new file mode 100644 index 0000000..19a6e6d --- /dev/null +++ b/src/utils/container-elements/create-initial-icon.js @@ -0,0 +1,8 @@ +export const createInitialIcon = () => { + const view360Icon = document.createElement('div'); + + view360Icon.className = 'cloudimage-initial-icon'; + view360Icon.innerText = '360°'; + + return view360Icon; +}; diff --git a/src/utils/container-elements/create-loader.js b/src/utils/container-elements/create-loader.js index f1e82ce..5cfde2c 100644 --- a/src/utils/container-elements/create-loader.js +++ b/src/utils/container-elements/create-loader.js @@ -1,7 +1,13 @@ export const createLoader = (innerBox) => { const loader = document.createElement('div'); - loader.className = 'cloudimage-360-loader'; + loader.className = 'cloudimage-initial-icon'; + // Percentage text element + const percentageText = document.createElement('span'); + percentageText.className = 'percentage'; + percentageText.innerText = '0%'; + + loader.appendChild(percentageText); innerBox.appendChild(loader); return loader; diff --git a/src/utils/container-elements/index.js b/src/utils/container-elements/index.js index 65434d0..9a5dcf1 100644 --- a/src/utils/container-elements/index.js +++ b/src/utils/container-elements/index.js @@ -1,8 +1,8 @@ export { create360ViewCircleIcon } from './create-360-view-circle-icon'; -export { create360ViewIcon } from './create-360-view-icon'; +export { createInitialIcon } from './create-initial-icon'; export { createCanvas } from './create-canvas'; -export { createCloseFullscreenIcon } from './create-close-fullscreen-icon'; -export { createFullscreenIcon } from './create-fullscreen-icon'; +export { createCloseIcon } from './create-close-icon'; +export { createFullscreenIcon } from './create-close-fullscreen-icon'; export { createIconsContainer } from './create-icons-container'; export { createInnerBox } from './create-inner-box'; export { createMagnifierIcon } from './create-magnifier-icon'; @@ -10,4 +10,4 @@ export { createBoxShadow } from './create-box-shadow'; export { createLoader } from './create-loader'; export { applyStylesToContainer } from './apply-styles-to-container'; export { createFullscreenModal } from './create-fullscreen-modal'; -export { removeChildFromParent } from './remove-child-from-parent'; \ No newline at end of file +export { removeElementFromContainer } from './remove-element-from-container'; diff --git a/src/utils/container-elements/remove-child-from-parent.js b/src/utils/container-elements/remove-child-from-parent.js deleted file mode 100644 index 7dd3121..0000000 --- a/src/utils/container-elements/remove-child-from-parent.js +++ /dev/null @@ -1,7 +0,0 @@ -export const removeChildFromParent = (parent, child) => { - if (parent && child) { - try { - parent.removeChild(child); - } catch {} - } -} \ No newline at end of file diff --git a/src/utils/container-elements/remove-element-from-container.js b/src/utils/container-elements/remove-element-from-container.js new file mode 100644 index 0000000..4df69b9 --- /dev/null +++ b/src/utils/container-elements/remove-element-from-container.js @@ -0,0 +1,7 @@ +export const removeElementFromContainer = (container, selector) => { + const element = container.querySelector(selector); + + if (element) { + element.parentNode.removeChild(element); + } +}; diff --git a/src/utils/controls/get-item-skipped.js b/src/utils/controls/get-item-skipped.js index 362cfee..ee5608c 100644 --- a/src/utils/controls/get-item-skipped.js +++ b/src/utils/controls/get-item-skipped.js @@ -1,7 +1,2 @@ -export const getItemSkipped = (currentPosition, movementStart, speedFactor) => { - const itemsSkipped = Math.floor( - (currentPosition - movementStart) / speedFactor, - ) || 1; - - return itemsSkipped; -}; +export const getItemSkipped = (previousPosition, currentPosition, speedFactor = 1) => + Math.abs(Math.floor((currentPosition - previousPosition) / speedFactor)) || 1; diff --git a/src/utils/controls/init-controls.js b/src/utils/controls/init-controls.js index bb95731..bdbd8a8 100644 --- a/src/utils/controls/init-controls.js +++ b/src/utils/controls/init-controls.js @@ -1,21 +1,22 @@ import { addClass } from '../class-names/add-class'; export const initControls = (controlsConfig, controlsTriggers) => { - const { - container, controlReverse, spinReverse, stopAtEdges, - } = controlsConfig; + const { container, controlReverse, spinReverse, stopAtEdges } = + controlsConfig; - const { - onRightStart, onLeftStart, onTopStart, onBottomStart, - onEventEnd, - } = controlsTriggers; + const { onRightStart, onLeftStart, onTopStart, onBottomStart, onEventEnd } = + controlsTriggers; const controlElements = {}; const isReverse = controlReverse ? !spinReverse : spinReverse; - const left = container.querySelectorAll('.cloudimage-360-left, .cloudimage-360-prev')[0]; + const left = container.querySelectorAll( + '.cloudimage-360-left, .cloudimage-360-prev' + )[0]; - const right = container.querySelectorAll('.cloudimage-360-right, .cloudimage-360-next')[0]; + const right = container.querySelectorAll( + '.cloudimage-360-right, .cloudimage-360-next' + )[0]; const top = container.querySelector('.cloudimage-360-top'); @@ -24,7 +25,11 @@ export const initControls = (controlsConfig, controlsTriggers) => { if (left) { left.style.display = 'block'; left.addEventListener('mousedown', isReverse ? onRightStart : onLeftStart); - left.addEventListener('touchstart', isReverse ? onRightStart : onLeftStart, { passive: true }); + left.addEventListener( + 'touchstart', + isReverse ? onRightStart : onLeftStart, + { passive: true } + ); left.addEventListener('mouseup', isReverse ? onEventEnd : onEventEnd); left.addEventListener('touchend', isReverse ? onEventEnd : onEventEnd); @@ -34,7 +39,11 @@ export const initControls = (controlsConfig, controlsTriggers) => { if (right) { right.style.display = 'block'; right.addEventListener('mousedown', isReverse ? onLeftStart : onRightStart); - right.addEventListener('touchstart', isReverse ? onLeftStart : onRightStart, { passive: true }); + right.addEventListener( + 'touchstart', + isReverse ? onLeftStart : onRightStart, + { passive: true } + ); right.addEventListener('mouseup', onEventEnd); right.addEventListener('touchend', onEventEnd); @@ -53,8 +62,14 @@ export const initControls = (controlsConfig, controlsTriggers) => { if (bottom) { bottom.style.display = 'block'; - bottom.addEventListener('mousedown', isReverse ? onTopStart : onBottomStart); - bottom.addEventListener('touchstart', isReverse ? onTopStart : onBottomStart); + bottom.addEventListener( + 'mousedown', + isReverse ? onTopStart : onBottomStart + ); + bottom.addEventListener( + 'touchstart', + isReverse ? onTopStart : onBottomStart + ); bottom.addEventListener('mouseup', isReverse ? onEventEnd : onEventEnd); bottom.addEventListener('touchend', isReverse ? onEventEnd : onEventEnd); diff --git a/src/utils/delay.js b/src/utils/delay.js new file mode 100644 index 0000000..a583a50 --- /dev/null +++ b/src/utils/delay.js @@ -0,0 +1,9 @@ +export const delay = (func, ms = 150) => { + let timer; + return function (...args) { + clearTimeout(timer); + timer = setTimeout(() => { + func.apply(this, args); + }, ms); + }; +}; diff --git a/src/utils/hotspots/attach-events/hide-popup.js b/src/utils/hotspots/attach-events/hide-popup.js index 331eed2..6b7d118 100644 --- a/src/utils/hotspots/attach-events/hide-popup.js +++ b/src/utils/hotspots/attach-events/hide-popup.js @@ -3,4 +3,4 @@ export const hidePopup = (popup, isVisible) => { popup.removeAttribute('data-show'); popup.removeAttribute('data-cloudimage-360-show'); } -}; \ No newline at end of file +}; diff --git a/src/utils/hotspots/attach-events/show-popup.js b/src/utils/hotspots/attach-events/show-popup.js index 8022907..4c0bcf0 100644 --- a/src/utils/hotspots/attach-events/show-popup.js +++ b/src/utils/hotspots/attach-events/show-popup.js @@ -3,4 +3,4 @@ export const showPopup = (popup, popperInstance) => { popup.setAttribute('data-cloudimage-360-show', ''); popperInstance.update(); -} \ No newline at end of file +}; diff --git a/src/utils/hotspots/configs-error-handler.js b/src/utils/hotspots/configs-error-handler.js index dcb888e..568a5ed 100644 --- a/src/utils/hotspots/configs-error-handler.js +++ b/src/utils/hotspots/configs-error-handler.js @@ -1,20 +1,24 @@ export const configsErrorHandler = (hotspotProps) => { const { variant = {} } = hotspotProps; - const { - url, - title, - anchorId, - images, - description, - moreDetailsUrl - } = variant; + const { url, title, anchorId, images, description, moreDetailsUrl } = variant; if (url && !title) { - console.error('Cloudimage-360: Hotspot config with variant link must have title for the link'); + console.error( + 'Cloudimage-360: Hotspot config with variant link must have title for the link' + ); } - if (!title && !url && !anchorId && !images && !description && !moreDetailsUrl) { - console.error('Cloudimage-360: Hotspot config with custom variant must provide anchorId'); + if ( + !title && + !url && + !anchorId && + !images && + !description && + !moreDetailsUrl + ) { + console.error( + 'Cloudimage-360: Hotspot config with custom variant must provide anchorId' + ); } }; diff --git a/src/utils/hotspots/create-popper-instace.js b/src/utils/hotspots/create-popper-instace.js index 0fa01f8..1b24168 100644 --- a/src/utils/hotspots/create-popper-instace.js +++ b/src/utils/hotspots/create-popper-instace.js @@ -25,6 +25,5 @@ export const createPopperInstance = (popper, popupProps, container) => { ], }); - return popperInstance; }; diff --git a/src/utils/hotspots/elements/create-hotspot-icon.js b/src/utils/hotspots/elements/create-hotspot-icon.js index 31d1853..14ae84a 100644 --- a/src/utils/hotspots/elements/create-hotspot-icon.js +++ b/src/utils/hotspots/elements/create-hotspot-icon.js @@ -2,10 +2,17 @@ import { hidePopup } from '../attach-events/hide-popup'; import { showPopup } from '../attach-events/show-popup'; import { hideHotspotIcon } from '../hide-hotspot-icon'; -export const createHotspotIcon = (container, hotspotConfig, popup, popperInstance) => { +export const createHotspotIcon = ( + container, + hotspotConfig, + popup, + popperInstance +) => { const { indicatorSelector, variant } = hotspotConfig; const { url, anchorId } = variant; - const { popupProps: { open = false } } = hotspotConfig; + const { + popupProps: { open = false }, + } = hotspotConfig; let isVisible; const hotspotIcon = document.createElement('div'); @@ -25,7 +32,7 @@ export const createHotspotIcon = (container, hotspotConfig, popup, popperInstanc isVisible = false; !open && hidePopup(popup, isVisible); - } + }; hotspotIcon.onclick = (e) => e.stopPropagation(); @@ -38,9 +45,8 @@ export const createHotspotIcon = (container, hotspotConfig, popup, popperInstanc if (!open) { hideEvents.forEach((event) => { - hotspotIcon.addEventListener( - event, - () => setTimeout(() => hidePopup(popup, isVisible), 160) + hotspotIcon.addEventListener(event, () => + setTimeout(() => hidePopup(popup, isVisible), 160) ); }); } diff --git a/src/utils/hotspots/elements/create-hotspot-popup-link.js b/src/utils/hotspots/elements/create-hotspot-popup-link.js index 0983831..dbdabb2 100644 --- a/src/utils/hotspots/elements/create-hotspot-popup-link.js +++ b/src/utils/hotspots/elements/create-hotspot-popup-link.js @@ -1,7 +1,5 @@ export const createHotspotPopupLink = (variant) => { - const { - url, title, newTab, - } = variant; + const { url, title, newTab } = variant; const hyperLink = document.createElement('a'); diff --git a/src/utils/hotspots/elements/create-hotspots.js b/src/utils/hotspots/elements/create-hotspots.js index 7e8a755..38ca594 100644 --- a/src/utils/hotspots/elements/create-hotspots.js +++ b/src/utils/hotspots/elements/create-hotspots.js @@ -8,7 +8,12 @@ export const createHotspots = (container, hotspotsProps) => { const popup = createPopup(container, hotspotProps, popupProps); const popperInstance = createPopperInstance(popup, popupProps, container); - const hotspotIcon = createHotspotIcon(container, hotspotProps, popup, popperInstance); + const hotspotIcon = createHotspotIcon( + container, + hotspotProps, + popup, + popperInstance + ); popperInstance.state.elements.reference = hotspotIcon; popperInstance.update(); diff --git a/src/utils/hotspots/elements/create-images-carousel.js b/src/utils/hotspots/elements/create-images-carousel.js index c39cd77..7ae468c 100644 --- a/src/utils/hotspots/elements/create-images-carousel.js +++ b/src/utils/hotspots/elements/create-images-carousel.js @@ -18,6 +18,5 @@ export const createImagesCarousel = (images, popup, container) => { imagesCarousel.appendChild(carouselImage); }); - return [imagesCarousel, carouselDots]; }; diff --git a/src/utils/hotspots/elements/create-model-elements.js b/src/utils/hotspots/elements/create-model-elements.js index 4b5f56c..6054e3a 100644 --- a/src/utils/hotspots/elements/create-model-elements.js +++ b/src/utils/hotspots/elements/create-model-elements.js @@ -9,7 +9,7 @@ export const createModalElements = (variant, container, popup) => { title, description, moreDetailsUrl, - moreDetailsTitle = 'Read more' + moreDetailsTitle = 'Read more', } = variant; const modalWrapper = document.createElement('div'); @@ -17,7 +17,11 @@ export const createModalElements = (variant, container, popup) => { if (images) { const imagesCarouselWrapper = document.createElement('div'); - const [imagesCarousel, carouselDots] = createImagesCarousel(images, popup, container); + const [imagesCarousel, carouselDots] = createImagesCarousel( + images, + popup, + container + ); imagesCarouselWrapper.appendChild(imagesCarousel); diff --git a/src/utils/hotspots/elements/create-popup.js b/src/utils/hotspots/elements/create-popup.js index 46b4905..02178d0 100644 --- a/src/utils/hotspots/elements/create-popup.js +++ b/src/utils/hotspots/elements/create-popup.js @@ -20,13 +20,18 @@ export const createPopup = (container, hotspotConfig, popupProps) => { popup.onclick = (e) => e.stopPropagation(); - if (typeof variant === 'object' && images || description || moreDetailsUrl || (title && !url)) { + if ( + (typeof variant === 'object' && images) || + description || + moreDetailsUrl || + (title && !url) + ) { createModalElements(variant, container, popup); } else if (url) { const hotspotPopupLink = createHotspotPopupLink(variant); popup.appendChild(hotspotPopupLink); - } else if (typeof variant === 'string'){ + } else if (typeof variant === 'string') { try { const popupNode = getPopupNode(variant); const userPopup = popupNode.cloneNode(true); @@ -34,7 +39,9 @@ export const createPopup = (container, hotspotConfig, popupProps) => { popup.appendChild(userPopup); popupNode.parentNode.removeChild(popupNode); } catch { - console.error(`Cloudimage-360: Element with anchorId '${anchorId}' not exist in the DOM`); + console.error( + `Cloudimage-360: Element with anchorId '${anchorId}' not exist in the DOM` + ); } } diff --git a/src/utils/hotspots/elements/create-read-more-btn.js b/src/utils/hotspots/elements/create-read-more-btn.js index 23e31a9..3046ce6 100644 --- a/src/utils/hotspots/elements/create-read-more-btn.js +++ b/src/utils/hotspots/elements/create-read-more-btn.js @@ -8,4 +8,4 @@ export const createReadMoreBtn = (moreDetailsUrl, moreDetailsTitle) => { readMoreBtn.target = '_blank'; return readMoreBtn; -} \ No newline at end of file +}; diff --git a/src/utils/hotspots/generate-hotspots-configs.js b/src/utils/hotspots/generate-hotspots-configs.js index 73ad709..71d7181 100644 --- a/src/utils/hotspots/generate-hotspots-configs.js +++ b/src/utils/hotspots/generate-hotspots-configs.js @@ -19,11 +19,11 @@ export const generateHotspotsConfigs = (hotspotsProps) => { if (!anchorId) { const uniqueID = Math.floor(Math.random() * 10000); - anchorId = `cloudimage-360-${uniqueID}` + anchorId = `cloudimage-360-${uniqueID}`; } const hotspotConfig = { - variant : { ...variant, anchorId }, + variant: { ...variant, anchorId }, popupProps: popupConfig, positions, indicatorSelector, diff --git a/src/utils/hotspots/generate-popup-config.js b/src/utils/hotspots/generate-popup-config.js index 416bf2d..f6ee4c3 100644 --- a/src/utils/hotspots/generate-popup-config.js +++ b/src/utils/hotspots/generate-popup-config.js @@ -7,7 +7,6 @@ export const generatePopupConfig = (popupProps) => { open = false, } = popupProps; - const popupConfig = { popupSelector, arrow, diff --git a/src/utils/hotspots/prepare-hotspots-positions.js b/src/utils/hotspots/prepare-hotspots-positions.js index e8a6e9d..196f4c0 100644 --- a/src/utils/hotspots/prepare-hotspots-positions.js +++ b/src/utils/hotspots/prepare-hotspots-positions.js @@ -1,18 +1,27 @@ import { fillEmptyCoordWithPrevious } from './fill-empty-coord-with-previous'; -export const prepareHotspotsPositions = (positions) => positions.reduce((accumulate, current, currentIndex) => { - const isIncludesXcoord = !!current?.xCoord; - const isIncludesYcoord = !!current?.yCoord; +export const prepareHotspotsPositions = (positions) => + positions.reduce((accumulate, current, currentIndex) => { + const isIncludesXcoord = !!current?.xCoord; + const isIncludesYcoord = !!current?.yCoord; - if (!isIncludesXcoord) { - current.xCoord = fillEmptyCoordWithPrevious(positions, currentIndex, 'xCoord'); - } + if (!isIncludesXcoord) { + current.xCoord = fillEmptyCoordWithPrevious( + positions, + currentIndex, + 'xCoord' + ); + } - if (!isIncludesYcoord) { - current.yCoord = fillEmptyCoordWithPrevious(positions, currentIndex, 'yCoord'); - } + if (!isIncludesYcoord) { + current.yCoord = fillEmptyCoordWithPrevious( + positions, + currentIndex, + 'yCoord' + ); + } - accumulate.push(current); + accumulate.push(current); - return accumulate; -}, []); + return accumulate; + }, []); diff --git a/src/utils/hotspots/toggle-popup-events.js b/src/utils/hotspots/toggle-popup-events.js index 262e587..e972e3a 100644 --- a/src/utils/hotspots/toggle-popup-events.js +++ b/src/utils/hotspots/toggle-popup-events.js @@ -1,8 +1,9 @@ -import { isTrue } from "../../ci360.utils"; -import { getHotspotIcon } from "./get-hotspot-icon"; +import { isTrue } from '../../ci360.utils'; +import { getHotspotIcon } from './get-hotspot-icon'; export const togglePopupEvents = (hotspotsProps, event, isMouseDown) => { - const iClickOnHotspotIcon = event && isTrue(event.target, 'data-cloudimage-360-hotspot'); + const iClickOnHotspotIcon = + event && isTrue(event.target, 'data-cloudimage-360-hotspot'); if (iClickOnHotspotIcon) return; @@ -14,4 +15,4 @@ export const togglePopupEvents = (hotspotsProps, event, isMouseDown) => { hotspotIcon.style.pointerEvents = isMouseDown ? 'none' : 'all'; }); -} +}; diff --git a/src/utils/hotspots/update-hotspot-icon-position.js b/src/utils/hotspots/update-hotspot-icon-position.js index 3b7b9f5..f7b8c97 100644 --- a/src/utils/hotspots/update-hotspot-icon-position.js +++ b/src/utils/hotspots/update-hotspot-icon-position.js @@ -1,4 +1,10 @@ -export const updateHotspotIconPosition = (container, initialDimensions, icon, xCoord, yCoord) => { +export const updateHotspotIconPosition = ( + container, + initialDimensions, + icon, + xCoord, + yCoord +) => { icon.style.visibility = 'visible'; icon.style.opacity = 1; icon.style.zIndex = 100; @@ -9,8 +15,8 @@ export const updateHotspotIconPosition = (container, initialDimensions, icon, xC const positionXRatio = container.offsetWidth / initialDimensions[0]; const positionYRatio = container.offsetHeight / initialDimensions[1]; - const translateX = `${(positionXRatio * xCoord)}px`; - const translateY = `${(positionYRatio * yCoord)}px`; + const translateX = `${positionXRatio * xCoord}px`; + const translateY = `${positionYRatio * yCoord}px`; icon.style.transform = `translate3d(${translateX}, ${translateY}, 0)`; }; diff --git a/src/utils/hotspots/update-hotspots.js b/src/utils/hotspots/update-hotspots.js index 4af5d84..ef072c0 100644 --- a/src/utils/hotspots/update-hotspots.js +++ b/src/utils/hotspots/update-hotspots.js @@ -4,10 +4,15 @@ import { updateHotspotIconPosition } from './update-hotspot-icon-position'; import { hideHotspotIcon } from './hide-hotspot-icon'; import { getHotspotOriantaion } from './get-hotspot-orientation'; -export const updateHotspots = (container, hotspotsProps, activeImageX = 0, activeImageY = 0, movingDirection = 'x-axis') => { +export const updateHotspots = ( + container, + hotspotsProps, + activeImageX = 0, + activeImageY = 0, + movingDirection = 'x-axis' +) => { hotspotsProps.forEach((hotspotProps) => { - const { positions, initialDimensions, orientation, variant - } = hotspotProps; + const { positions, initialDimensions, orientation, variant } = hotspotProps; const { anchorId } = variant; const hotspotOriantaion = getHotspotOriantaion(movingDirection); @@ -15,15 +20,22 @@ export const updateHotspots = (container, hotspotsProps, activeImageX = 0, activ const hotspotsPositions = prepareHotspotsPositions(positions); - const currentPosition = hotspotsPositions - .find((hotspotPosition) => hotspotPosition.imageIndex === currentImageIndex); + const currentPosition = hotspotsPositions.find( + (hotspotPosition) => hotspotPosition.imageIndex === currentImageIndex + ); const hotspotIcon = getHotspotIcon(anchorId); if (currentPosition && hotspotOriantaion === orientation) { const { xCoord = 0, yCoord = 0 } = currentPosition; - updateHotspotIconPosition(container, initialDimensions, hotspotIcon, xCoord, yCoord); + updateHotspotIconPosition( + container, + initialDimensions, + hotspotIcon, + xCoord, + yCoord + ); } else { hideHotspotIcon(hotspotIcon); } diff --git a/src/utils/image-src/generate-cdn-path.js b/src/utils/image-src/generate-cdn-path.js new file mode 100644 index 0000000..40998d1 --- /dev/null +++ b/src/utils/image-src/generate-cdn-path.js @@ -0,0 +1,37 @@ +import { FALSY_VALUES } from '../constants'; +import getSizeAccordingToPixelRatio from '../responsive/get-size-according-to-pixel-ratio'; + +const buildCdnUrl = (src, ciToken, finalApiVersion) => { + const isCloudImageUrl = new URL(src).origin.includes('cloudimg'); + return isCloudImageUrl ? src : `https://${ciToken}.cloudimg.io/${finalApiVersion}${src}`; +}; + +const buildTransformationParams = ({ ciTransformation, loadOriginalImages, responsiveWidth, ciFilters }) => { + const sizeParam = loadOriginalImages ? '' : `width=${responsiveWidth}`; + const transformation = ciTransformation || sizeParam; + const filters = ciFilters ? `&f=${ciFilters}` : ''; + return `${transformation}${filters}`; +}; + +export const generateCdnPath = (srcConfig, loadOriginalImages) => { + const { container, folder, apiVersion, filename = '', ciParams } = srcConfig; + const { ciToken, ciFilters, ciTransformation } = ciParams || {}; + + const src = `${folder}${filename}`; + + if (!ciToken) return src; + + const version = !FALSY_VALUES.includes(apiVersion) ? apiVersion : null; + const finalApiVersion = version ? `${version}/` : ''; + const responsiveWidth = getSizeAccordingToPixelRatio(container.offsetWidth); + + const cdn = buildCdnUrl(src, ciToken, finalApiVersion); + const transformationParams = buildTransformationParams({ + ciTransformation, + loadOriginalImages, + responsiveWidth, + ciFilters, + }); + + return `${cdn}${transformationParams ? '?' : ''}${transformationParams}`; +}; diff --git a/src/utils/image-src/generate-high-preview-cdn-url.js b/src/utils/image-src/generate-high-preview-cdn-url.js new file mode 100644 index 0000000..5c2ece1 --- /dev/null +++ b/src/utils/image-src/generate-high-preview-cdn-url.js @@ -0,0 +1,3 @@ +import removeParamByRegex from './removeParamByRegex'; + +export const generateHighPreviewCdnUrl = (url) => removeParamByRegex(url, 'width'); diff --git a/src/utils/image-src/generate-images-path.js b/src/utils/image-src/generate-images-path.js deleted file mode 100644 index f4d0ce4..0000000 --- a/src/utils/image-src/generate-images-path.js +++ /dev/null @@ -1,30 +0,0 @@ -import { FALSY_VALUES } from '../../constants/falsy-values'; -import { getResponsiveWidthOfContainer } from '../responsive/get-responsive-width-of-container'; -import { getSizeAccordingToPixelRatio } from '../responsive/get-size-according-to-pixel-ratio'; - -export const generateImagesPath = (srcConfig, loadOriginalImages) => { - const { - container, folder, apiVersion, filename = '', ciParams, - } = srcConfig; - - const { ciToken, ciFilters, ciTransformation } = ciParams || {}; - - let src = `${folder}${filename}`; - - if (ciToken) { - const imageOffsetWidth = container.offsetWidth; - - const version = !FALSY_VALUES.includes(apiVersion) ? apiVersion : null; - - const finalApiVersion = version ? `${version}/` : ''; - const ciSizeNext = getSizeAccordingToPixelRatio(getResponsiveWidthOfContainer(imageOffsetWidth)); - - const isCloudImageUrl = new URL(src).origin.includes('cloudimg'); - const cdn = isCloudImageUrl ? src - : `https://${ciToken}.cloudimg.io/${finalApiVersion}${src}`; - - src = `${cdn}?${ciTransformation || `${!loadOriginalImages ? `width=${ciSizeNext}`: ''} `}${ciFilters ? `&f=${ciFilters}` : ''}`; - } - - return src; -}; diff --git a/src/utils/image-src/generate-low-preview-cdn-url.js b/src/utils/image-src/generate-low-preview-cdn-url.js new file mode 100644 index 0000000..d7e6903 --- /dev/null +++ b/src/utils/image-src/generate-low-preview-cdn-url.js @@ -0,0 +1,10 @@ +import removeParamByRegex from './removeParamByRegex'; + +const generateLowPreviewCdnUrl = (cdnUrl) => { + const cleanedCdnUrl = removeParamByRegex(cdnUrl, 'width'); + const separator = cleanedCdnUrl.includes('?') ? '&' : '?'; + + return `${cleanedCdnUrl}${separator}width=${150 * devicePixelRatio}`; +}; + +export default generateLowPreviewCdnUrl; diff --git a/src/utils/image-src/is-props-change-require-reload.js b/src/utils/image-src/is-props-change-require-reload.js index a7587f2..2e0a76b 100644 --- a/src/utils/image-src/is-props-change-require-reload.js +++ b/src/utils/image-src/is-props-change-require-reload.js @@ -1,15 +1,13 @@ -import { PROPS_REQUIRE_RELOAD } from "../../constants/props-require-reload"; +import { PROPS_REQUIRE_RELOAD } from '../constants'; -export const isPropsChangeRequireReload = (currentProps, changedProps) => ( - Object.keys(changedProps) - .reduce((acc, current) => { - const isPropChanged = currentProps[current] !== changedProps[current]; - const isSrcProp = PROPS_REQUIRE_RELOAD.includes(current); +export const isPropsChangeRequireReload = (currentProps, changedProps) => + Object.keys(changedProps).reduce((acc, current) => { + const isPropChanged = currentProps[current] !== changedProps[current]; + const isSrcProp = PROPS_REQUIRE_RELOAD.includes(current); - if (isSrcProp && isPropChanged) { - acc = true; - } + if (isSrcProp && isPropChanged) { + acc = true; + } - return acc; - }, false) -); \ No newline at end of file + return acc; + }, false); diff --git a/src/utils/image-src/removeParamByRegex.js b/src/utils/image-src/removeParamByRegex.js new file mode 100644 index 0000000..3babf11 --- /dev/null +++ b/src/utils/image-src/removeParamByRegex.js @@ -0,0 +1,14 @@ +const removeParamByRegex = (url, paramToRemove) => { + const [base, query] = url.split('?'); + if (!query) return url; + + const regex = new RegExp(`^${paramToRemove}=|&${paramToRemove}=`); + const cleanedQuery = query + .split('&') + .filter((param) => !regex.test(param)) + .join('&'); + + return cleanedQuery ? `${base}?${cleanedQuery}` : base; +}; + +export default removeParamByRegex; diff --git a/src/utils/index.js b/src/utils/index.js index 7d1f605..e855554 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,14 +1,15 @@ export { isPropsChangeRequireReload } from './image-src/is-props-change-require-reload'; +export { generateCdnPath } from './image-src/generate-cdn-path'; +export { generateHighPreviewCdnUrl } from './image-src/generate-high-preview-cdn-url'; -export { generateImagesPath } from './image-src/generate-images-path'; export { preloadImages } from './load-images/preload-images'; export { preloadOriginalImages } from './load-images/preload-original-images'; -export { initLazyload } from './load-images/lazyload/init-lazyload' +export { initLazyload } from './load-images/lazyload/init-lazyload'; +export { loadImage } from './load-images/load-image'; export { contain } from './responsive/contain'; export { getImageAspectRatio } from './responsive/get-image-aspect-ratio'; -export { getCurrentOriginalImage } from './magnify/get-current-original-image'; export { magnify } from './magnify/magnify'; export { generateZoomInSteps } from './zoom/generate-zoom-in-steps'; @@ -21,6 +22,8 @@ export { isCompletedOneCycle } from './auto-play/is-completed-one-cycle'; export { addClass } from './class-names/add-class'; export { removeClass } from './class-names/remove-class'; +export { shouldSwitchSpinDirection } from './spin/should-switch-spin-direction'; +export { switchSpinDirection } from './spin/switch-spin-direction'; export { getMovingDirection } from './spin-y/get-moving-direction'; export { getItemSkipped } from './controls/get-item-skipped'; @@ -33,3 +36,5 @@ export { createHotspots } from './hotspots/elements/create-hotspots'; export { generateHotspotsConfigs } from './hotspots/generate-hotspots-configs'; export { isMouseOnHotspot } from './hotspots/is-mouse-on-hotspot'; export { hideHotspotsIcons } from './hotspots/hide-hotspots-icons'; + +export { delay } from './delay'; diff --git a/src/utils/load-images/generate-images-cdn-links.js b/src/utils/load-images/generate-images-cdn-links.js new file mode 100644 index 0000000..e826dfd --- /dev/null +++ b/src/utils/load-images/generate-images-cdn-links.js @@ -0,0 +1,4 @@ +import { pad } from './pad'; + +export const generateImagesCdnLinks = (cdnPath, { amount = 0, indexZeroBase = 0 } = {}) => + Array.from({ length: amount }, (_, index) => cdnPath.replace('{index}', pad(index + 1, indexZeroBase))); diff --git a/src/utils/load-images/images-from-folder/prepare-images-from-folder.js b/src/utils/load-images/images-from-folder/prepare-images-from-folder.js deleted file mode 100644 index 2131a13..0000000 --- a/src/utils/load-images/images-from-folder/prepare-images-from-folder.js +++ /dev/null @@ -1,21 +0,0 @@ -import { AND_SYMBOL_REGEX, ORGINAL_SIZE_REGEX } from '../../../constants/regex'; -import { pad } from '../pad'; - -export const prepareImagesFromFolder = (imagesSrc, srcConfig, loadOriginalImages) => { - const { amount, indexZeroBase } = srcConfig || {}; - - return [...new Array(amount)].map((_item, index) => { - const nextZeroFilledIndex = pad(index + 1, indexZeroBase); - const imageSrc = imagesSrc.replace('{index}', nextZeroFilledIndex); - - if (loadOriginalImages) { - const imageOriginalSrc = imageSrc - .replace(ORGINAL_SIZE_REGEX, '') - .replace(AND_SYMBOL_REGEX, '?'); - - return imageOriginalSrc; - } - - return imageSrc; - }); -}; diff --git a/src/utils/load-images/images-from-list/prepare-images-from-list.js b/src/utils/load-images/images-from-list/prepare-images-from-list.js index d953126..ce0a044 100644 --- a/src/utils/load-images/images-from-list/prepare-images-from-list.js +++ b/src/utils/load-images/images-from-list/prepare-images-from-list.js @@ -1,6 +1,6 @@ -import { generateImagesPath } from '../../image-src/generate-images-path'; +import { generateCdnPath } from '../../image-src/generate-cdn-path'; -export const prepareImagesFromList = (images, srcConfig, loadOriginalImages ) => { +export const prepareImagesFromList = (images, srcConfig, loadOriginalImages) => { const { folder } = srcConfig; return images.map((src) => { @@ -8,6 +8,6 @@ export const prepareImagesFromList = (images, srcConfig, loadOriginalImages ) => nextSrcConfig.folder = /(http(s?)):\/\//gi.test(src) ? '' : folder; nextSrcConfig.filename = src; - return generateImagesPath(nextSrcConfig, loadOriginalImages); + return generateCdnPath(nextSrcConfig, loadOriginalImages); }); }; diff --git a/src/utils/load-images/lazyload/init-lazyload.js b/src/utils/load-images/lazyload/init-lazyload.js index ad9a0c5..25a0286 100644 --- a/src/utils/load-images/lazyload/init-lazyload.js +++ b/src/utils/load-images/lazyload/init-lazyload.js @@ -1,38 +1,57 @@ -import { prepareFirstImageFromFolder } from "./prepare-first-image/prepare-first-image-from-folder"; -import { prepareFirstImageFromList } from "./prepare-first-image/prepare-first-image-from-list"; +import generateLowPreviewCdnUrl from '../../image-src/generate-low-preview-cdn-url'; +import getFirstCdnImage from './prepare-first-image/get-first-cdn-image'; +import getFirstCdnImageFromList from './prepare-first-image/get-first-cdn-mage-from-list'; -export const initLazyload = (imagesSrc, srcConfig, cb) => { - const { imageList, lazySelector, innerBox } = srcConfig || {}; - let firstImageSrc; +const getFirstImageSrc = (imagesSrc, srcConfig) => { + const { imageList, indexZeroBase } = srcConfig; if (imageList) { try { const images = JSON.parse(imageList); - - firstImageSrc = prepareFirstImageFromList(images, srcConfig); + return getFirstCdnImageFromList(images, srcConfig); } catch (error) { console.error(`Wrong format in image-list attribute: ${error.message}`); } - } else { - firstImageSrc = prepareFirstImageFromFolder(imagesSrc, srcConfig); } - const image = new Image(); - - image.setAttribute('data-src', firstImageSrc); - image.style.position = 'absolute'; - image.style.top = 0; - image.style.left = 0; - image.style.width = '100%'; - image.style.maxWidth = '100%'; - image.style.maxHeight = '100%'; - + return getFirstCdnImage(imagesSrc, indexZeroBase); +}; - if (lazySelector) image.className = lazySelector; +const createImage = (src, lazySelector) => { + const image = new Image(); + image.setAttribute('data-src', src); + image.style.cssText = ` + width: 100%; + height: 100%; + object-fit: contain; + object-position: center; + filter: blur(10px); + `; + + if (lazySelector) image.className = `${lazySelector} cloudimage-lazy`; + + return image; +}; + +export const initLazyload = (cdnPath, srcConfig, onLoad) => { + const { lazySelector, innerBox } = srcConfig || {}; + + const firstImageSrc = getFirstImageSrc(cdnPath, srcConfig); + const lowPreviewSrc = generateLowPreviewCdnUrl(firstImageSrc); + const image = createImage(lowPreviewSrc, lazySelector); + + image.onload = (event) => { + if (onLoad) { + onLoad({ + event: event, + width: image.width, + height: image.height, + naturalWidth: image.naturalWidth, + naturalHeight: image.naturalHeight, + src: lowPreviewSrc, + }); + } + }; innerBox.appendChild(image); - - if (cb) { - image.onload = () => cb(image); - } -} \ No newline at end of file +}; diff --git a/src/utils/load-images/lazyload/prepare-first-image/get-first-cdn-image.js b/src/utils/load-images/lazyload/prepare-first-image/get-first-cdn-image.js new file mode 100644 index 0000000..3bfec78 --- /dev/null +++ b/src/utils/load-images/lazyload/prepare-first-image/get-first-cdn-image.js @@ -0,0 +1,9 @@ +import { pad } from '../../pad'; + +const getFirstCdnImage = (cdnPath, indexZeroBase) => { + const nextZeroFilledIndex = pad(1, indexZeroBase); + + return cdnPath.replace('{index}', nextZeroFilledIndex); +}; + +export default getFirstCdnImage; diff --git a/src/utils/load-images/lazyload/prepare-first-image/get-first-cdn-mage-from-list.js b/src/utils/load-images/lazyload/prepare-first-image/get-first-cdn-mage-from-list.js new file mode 100644 index 0000000..f917b20 --- /dev/null +++ b/src/utils/load-images/lazyload/prepare-first-image/get-first-cdn-mage-from-list.js @@ -0,0 +1,14 @@ +import { generateCdnPath } from '../../../image-src/generate-cdn-path'; + +const getFirstCdnImageFromList = (images, srcConfig) => { + const [firstImageSrc] = images; + const isAbsoluteUrl = /(https?):\/\//i.test(firstImageSrc); + + return generateCdnPath({ + ...srcConfig, + folder: isAbsoluteUrl ? '' : srcConfig.folder, + filename: firstImageSrc, + }); +}; + +export default getFirstCdnImageFromList; diff --git a/src/utils/load-images/lazyload/prepare-first-image/prepare-first-image-from-folder.js b/src/utils/load-images/lazyload/prepare-first-image/prepare-first-image-from-folder.js deleted file mode 100644 index b3456d8..0000000 --- a/src/utils/load-images/lazyload/prepare-first-image/prepare-first-image-from-folder.js +++ /dev/null @@ -1,8 +0,0 @@ -import { pad } from "../../pad"; - -export const prepareFirstImageFromFolder = (imagesSrcs, srcConfig) => { - const {indexZeroBase } = srcConfig || {}; - const nextZeroFilledIndex = pad(1, indexZeroBase); - - return imagesSrcs.replace('{index}', nextZeroFilledIndex); -} \ No newline at end of file diff --git a/src/utils/load-images/lazyload/prepare-first-image/prepare-first-image-from-list.js b/src/utils/load-images/lazyload/prepare-first-image/prepare-first-image-from-list.js deleted file mode 100644 index 7755215..0000000 --- a/src/utils/load-images/lazyload/prepare-first-image/prepare-first-image-from-list.js +++ /dev/null @@ -1,13 +0,0 @@ -import { generateImagesPath } from "../../../image-src/generate-images-path"; - -export const prepareFirstImageFromList = (images, srcConfig) => { - const { folder } = srcConfig; - - const firstImageSrc = images[0]; - - const nextSrcConfig = { ...srcConfig }; - nextSrcConfig.folder = /(http(s?)):\/\//gi.test(firstImageSrc) ? '' : folder; - nextSrcConfig.filename = firstImageSrc; - - return generateImagesPath(nextSrcConfig); -} \ No newline at end of file diff --git a/src/utils/load-images/load-image-as-promise.js b/src/utils/load-images/load-image-as-promise.js index b9f7bed..95faea9 100644 --- a/src/utils/load-images/load-image-as-promise.js +++ b/src/utils/load-images/load-image-as-promise.js @@ -4,6 +4,6 @@ export const loadImageAsPromise = (src, cb) => { const onImageLoad = () => cb(image); - image.onload = onImageLoad - image.onerror = onImageLoad + image.onload = onImageLoad; + image.onerror = onImageLoad; }; diff --git a/src/utils/load-images/load-image.js b/src/utils/load-images/load-image.js new file mode 100644 index 0000000..5d87054 --- /dev/null +++ b/src/utils/load-images/load-image.js @@ -0,0 +1,19 @@ +export const loadImage = (url, callback) => { + const image = new Image(); + + image.src = url; + image.onload = (event) => { + if (callback) { + callback({ + event: event, + width: image.width, + height: image.height, + naturalWidth: image.naturalWidth, + naturalHeight: image.naturalHeight, + src: url, + }); + } + }; + + image.onerror = function () {}; +}; diff --git a/src/utils/load-images/load-images-relative-to-container-size.js b/src/utils/load-images/load-images-relative-to-container-size.js deleted file mode 100644 index ddfcadd..0000000 --- a/src/utils/load-images/load-images-relative-to-container-size.js +++ /dev/null @@ -1,18 +0,0 @@ -import { loadImageAsPromise } from './load-image-as-promise'; - - -export const loadImagesRelativeToContainerSize = (imagesSrcs, cb, index = 0) => { - const imageSrc = imagesSrcs[index]; - - if (index > (imagesSrcs.length - 1)) return; - - const imageLoadCallback = (image) => { - const _index = index + 1; - - - cb(image, index); - loadImagesRelativeToContainerSize(imagesSrcs, cb, _index); - } - - loadImageAsPromise(imageSrc, imageLoadCallback); -}; diff --git a/src/utils/load-images/load-images.js b/src/utils/load-images/load-images.js new file mode 100644 index 0000000..3d22e3c --- /dev/null +++ b/src/utils/load-images/load-images.js @@ -0,0 +1,59 @@ +export const loadImages = ({ + imagesUrls, + onFirstImageLoad, + onImageLoad, + onAllImagesLoad, + autoplayReverse, +}) => { + let loadedCount = 0; + const totalImages = imagesUrls.length; + const loadedImages = []; + + const loadImage = (url, index) => { + const img = new Image(); + img.src = url; + + img.onload = () => { + loadedCount++; + loadedImages[index] = img; + onImageLoad?.(img, index); + + if (loadedCount === totalImages) { + onAllImagesLoad?.(loadedImages); + } + }; + + img.onerror = () => { + console.error(`Failed to load image: ${url}`); + loadedCount++; + + if (loadedCount === totalImages) { + onAllImagesLoad?.(loadedImages); + } + }; + }; + + const firstImg = new Image(); + firstImg.src = imagesUrls[autoplayReverse ? imagesUrls.length - 1 : 0]; + firstImg.onload = () => { + loadedImages[0] = firstImg; + loadedCount++; + onFirstImageLoad?.(firstImg); + onImageLoad?.(firstImg, 0); + + for (let i = 1; i < imagesUrls.length; i++) { + loadImage(imagesUrls[i], i); + } + }; + + firstImg.onerror = () => { + console.error(`Failed to load first image: ${imagesUrls[0]}`); + loadedCount++; + + for (let i = 1; i < imagesUrls.length; i++) { + loadImage(imagesUrls[i], i); + } + }; +}; + +export default loadImages; diff --git a/src/utils/load-images/load-original-images.js b/src/utils/load-images/load-original-images.js index f728008..c00abd0 100644 --- a/src/utils/load-images/load-original-images.js +++ b/src/utils/load-images/load-original-images.js @@ -1,7 +1,9 @@ import { loadImageAsPromise } from './load-image-as-promise'; export const loadOriginalImages = async (imagesSrcs, cb) => { - await Promise.all(imagesSrcs.map(async (src, index) => { - await loadImageAsPromise(src, index, cb); - })); + await Promise.all( + imagesSrcs.map(async (src, index) => { + await loadImageAsPromise(src, index, cb); + }) + ); }; diff --git a/src/utils/load-images/preload-images.js b/src/utils/load-images/preload-images.js index 0aa78f9..671855d 100644 --- a/src/utils/load-images/preload-images.js +++ b/src/utils/load-images/preload-images.js @@ -1,23 +1,85 @@ -/* eslint-disable no-console */ -import { loadImagesRelativeToContainerSize } from './load-images-relative-to-container-size'; -import { prepareImagesFromFolder } from './images-from-folder/prepare-images-from-folder'; -import { prepareImagesFromList } from './images-from-list/prepare-images-from-list'; - -export const preloadImages = (srcConfig, imagesSrc, cb) => { - const { imageList } = srcConfig || {}; - let imagesSrcs = []; - - if (imageList) { - try { - const images = JSON.parse(imageList); - - imagesSrcs = prepareImagesFromList(images, srcConfig); - } catch (error) { - console.error(`Wrong format in image-list attribute: ${error.message}`); +import { ORIENTATIONS } from '../constants'; +import { generateImagesCdnLinks } from './generate-images-cdn-links'; +import { loadImages } from './load-images'; + +export const preloadImages = ({ + cdnPathX, + cdnPathY, + configX, + configY, + onFirstImageLoad, + onImageLoad, + onAllImagesLoad, +}) => { + let imagesXUrls = []; + let imagesYUrls = []; + let allImagesLoaded = { x: false, y: false }; + let loadedImagesX = []; + let loadedImagesY = []; + + const handleAllImagesLoaded = () => { + if (allImagesLoaded.x && allImagesLoaded.y) { + onAllImagesLoad([...loadedImagesX, ...loadedImagesY]); + } + }; + + const loadImagesForOrientation = (cdnPath, config, orientation) => { + let imagesUrls = []; + + if (config.imageList) { + try { + const images = JSON.parse(config.imageList); + // imagesUrls = prepareImagesFromList(images, config); + } catch (error) { + console.error(`Wrong format in image-list attribute for ${orientation}: ${error.message}`); + } + } else { + imagesUrls = generateImagesCdnLinks(cdnPath, config); } + + return imagesUrls; + }; + + // Load X Images + if (cdnPathX) { + imagesXUrls = loadImagesForOrientation(cdnPathX, configX, ORIENTATIONS.X); + + loadImages({ + imagesUrls: imagesXUrls, + onFirstImageLoad, + onImageLoad: (img, index) => { + onImageLoad?.(img, index, ORIENTATIONS.X); + loadedImagesX[index] = img; + }, + onAllImagesLoad: (loadedImages) => { + loadedImagesX = loadedImages; + allImagesLoaded.x = true; + handleAllImagesLoaded(); + }, + autoplayReverse: configX.autoplayReverse, + }); } else { - imagesSrcs = prepareImagesFromFolder(imagesSrc, srcConfig); + allImagesLoaded.x = true; } - loadImagesRelativeToContainerSize(imagesSrcs, cb); + // Load Y Images + if (cdnPathY) { + imagesYUrls = loadImagesForOrientation(cdnPathY, configY, ORIENTATIONS.Y); + + loadImages({ + imagesUrls: imagesYUrls, + onImageLoad: (img, index) => { + onImageLoad?.(img, index, ORIENTATIONS.Y); + loadedImagesY[index] = img; + }, + onAllImagesLoad: (loadedImages) => { + loadedImagesY = loadedImages; + allImagesLoaded.y = true; + handleAllImagesLoaded(); + }, + autoplayReverse: configY.autoplayReverse, + }); + } else { + allImagesLoaded.y = true; + } }; diff --git a/src/utils/load-images/preload-original-images.js b/src/utils/load-images/preload-original-images.js index f8c1d88..e8fcc6d 100644 --- a/src/utils/load-images/preload-original-images.js +++ b/src/utils/load-images/preload-original-images.js @@ -1,23 +1 @@ -/* eslint-disable no-console */ -import { prepareImagesFromFolder } from './images-from-folder/prepare-images-from-folder'; -import { prepareImagesFromList } from './images-from-list/prepare-images-from-list'; -import { loadOriginalImages } from './load-original-images'; - -export const preloadOriginalImages = (srcConfig, imagesSrc, cb) => { - const { imageList } = srcConfig || {}; - let imagesSrcs = []; - - if (imageList) { - try { - const images = JSON.parse(imageList); - - imagesSrcs = prepareImagesFromList(images, srcConfig, true); - } catch (error) { - console.error(`Wrong format in image-list attribute: ${error.message}`); - } - } else { - imagesSrcs = prepareImagesFromFolder(imagesSrc, srcConfig, true); - } - - loadOriginalImages(imagesSrcs, cb); -}; +export const preloadOriginalImages = (srcConfig, imagesSrc, cb) => {}; diff --git a/src/utils/magnify/get-current-original-image.js b/src/utils/magnify/get-current-original-image.js index 5476408..ae358f8 100644 --- a/src/utils/magnify/get-current-original-image.js +++ b/src/utils/magnify/get-current-original-image.js @@ -1,16 +1,22 @@ import { ORIENTATIONS } from '../../constants/orientations'; import { AND_SYMBOL_REGEX, ORGINAL_SIZE_REGEX } from '../../constants/regex'; -export const getCurrentOriginalImage = (movingDirection, imagesX, imagesY, activeImageX, activeImageY) => { +export const getCurrentOriginalImage = ( + movingDirection, + imagesX, + imagesY, + activeImageX, + activeImageY +) => { const currentImage = new Image(); - const originalImagesXSrcs = imagesX.map((image) => image.src - .replace(ORGINAL_SIZE_REGEX, '') - .replace(AND_SYMBOL_REGEX, '?')); + const originalImagesXSrcs = imagesX.map((image) => + image.src.replace(ORGINAL_SIZE_REGEX, '').replace(AND_SYMBOL_REGEX, '?') + ); - const originalImagesYSrcs = imagesY.map((image) => image.src - .replace(ORGINAL_SIZE_REGEX, '') - .replace(AND_SYMBOL_REGEX, '?')); + const originalImagesYSrcs = imagesY.map((image) => + image.src.replace(ORGINAL_SIZE_REGEX, '').replace(AND_SYMBOL_REGEX, '?') + ); currentImage.src = originalImagesXSrcs[activeImageX - 1]; diff --git a/src/utils/magnify/get-cursor-position.js b/src/utils/magnify/get-cursor-position.js index ccec7e1..96e1e89 100644 --- a/src/utils/magnify/get-cursor-position.js +++ b/src/utils/magnify/get-cursor-position.js @@ -1,13 +1,10 @@ -export const getCursorPosition = (event = window.event, container) => { - let x = 0; - let y = 0; +export const getCursorPosition = (e, container) => { + const rect = container.getBoundingClientRect(); + const x = e.touches ? e.touches[0].clientX : e.clientX; + const y = e.touches ? e.touches[0].clientY : e.clientY; - const a = container.getBoundingClientRect(); - - x = event.pageX - a.left; - y = event.pageY - a.top; - x -= window.pageXOffset; - y -= window.pageYOffset; - - return { x, y }; + return { + x: x - rect.left, + y: y - rect.top, + }; }; diff --git a/src/utils/magnify/magnify.js b/src/utils/magnify/magnify.js index b28d430..b22d811 100644 --- a/src/utils/magnify/magnify.js +++ b/src/utils/magnify/magnify.js @@ -1,9 +1,9 @@ import { moveMagnifier } from './move-magnifier'; -export const magnify = (container, offset, currentImage, glass, zoom) => { +export const magnify = (clickEvent, container, offset, currentImage, glass, zoom) => { const { x: offsetX = 0, y: offsetY = 0 } = offset || {}; - const backgroundSizeX = (container.offsetWidth - (offsetX * 2)) * zoom; - const backgroundSizeY = (container.offsetHeight - (offsetY * 2)) * zoom; + const backgroundSizeX = (container.offsetWidth - offsetX * 2) * zoom; + const backgroundSizeY = (container.offsetHeight - offsetY * 2) * zoom; glass.setAttribute('class', 'cloudimage-360-img-magnifier-glass'); container.prepend(glass); @@ -16,20 +16,29 @@ export const magnify = (container, offset, currentImage, glass, zoom) => { const h = glass.offsetHeight / 2; const containerConfig = { - container, w, h, zoom, bw, offsetX, offsetY, + container, + w, + h, + zoom, + bw, + offsetX, + offsetY, }; + moveMagnifier(clickEvent, containerConfig, glass); + const MouseMoveHandler = (event) => { moveMagnifier(event, containerConfig, glass); }; const touchHandler = (event) => { + event.preventDefault(); + moveMagnifier(event, containerConfig, glass); }; glass.addEventListener('mousemove', MouseMoveHandler); container.addEventListener('mousemove', MouseMoveHandler); - glass.addEventListener('touchmove', touchHandler, { passive: true }); - container.addEventListener('touchmove', touchHandler, { passive: true }); + container.addEventListener('touchmove', touchHandler); }; diff --git a/src/utils/magnify/move-magnifier.js b/src/utils/magnify/move-magnifier.js index 672f00b..e177998 100644 --- a/src/utils/magnify/move-magnifier.js +++ b/src/utils/magnify/move-magnifier.js @@ -1,42 +1,24 @@ import { getCursorPosition } from './get-cursor-position'; export const moveMagnifier = (e, containerConfig, glass) => { - const { - container, w, h, zoom, bw, offsetX, offsetY, - } = containerConfig; - - let x; let y; + const { container, w, h, zoom, bw, offsetX, offsetY } = containerConfig; const pos = getCursorPosition(e, container); - x = pos.x; - y = pos.y; - - if (x > container.offsetWidth - (w / zoom)) { - x = container.offsetWidth - (w / zoom); - } - - if (x < w / zoom) { - x = w / zoom; - } + let x = pos.x; + let y = pos.y; - if (y > container.offsetHeight - (h / zoom)) { - y = container.offsetHeight - (h / zoom); - } - - if (y < h / zoom) { - y = h / zoom; - } + // Ensure the magnifier doesn't move outside container bounds + x = Math.max(w / zoom, Math.min(x, container.offsetWidth - w / zoom)); + y = Math.max(h / zoom, Math.min(y, container.offsetHeight - h / zoom)); + // Update the magnifier glass position glass.style.left = `${x - w}px`; glass.style.top = `${y - h}px`; - const backgroundPosX = ( - (x - offsetX) * zoom - ) - w + bw; - - const backgroundPosY = ( - (y - offsetY) * zoom - ) - h + bw; + // Calculate background position (where the magnified image should show) + const backgroundPosX = (x - offsetX) * zoom - w + bw; + const backgroundPosY = (y - offsetY) * zoom - h + bw; + // Apply background position to simulate zoom effect glass.style.backgroundPosition = `-${backgroundPosX}px -${backgroundPosY}px`; }; diff --git a/src/utils/responsive/fit.js b/src/utils/responsive/fit.js index 879b072..9028b4f 100644 --- a/src/utils/responsive/fit.js +++ b/src/utils/responsive/fit.js @@ -1,19 +1,29 @@ -export const fit = (contains) => (parentWidth, parentHeight, childWidth, childHeight, scale = 1, offsetX = 0.5, offsetY = 0.5) => { - const childRatio = childWidth / childHeight; - const parentRatio = parentWidth / parentHeight; - let width = parentWidth * scale; - let height = parentHeight * scale; +export const fit = + (contains) => + ( + parentWidth, + parentHeight, + childWidth, + childHeight, + scale = 1, + offsetX = 0.5, + offsetY = 0.5 + ) => { + const childRatio = childWidth / childHeight; + const parentRatio = parentWidth / parentHeight; + let width = parentWidth * scale; + let height = parentHeight * scale; - if (contains ? (childRatio > parentRatio) : (childRatio < parentRatio)) { - height = width / childRatio; - } else { - width = height * childRatio; - } + if (contains ? childRatio > parentRatio : childRatio < parentRatio) { + height = width / childRatio; + } else { + width = height * childRatio; + } - return { - width, - height, - offsetX: (parentWidth - width) * offsetX, - offsetY: (parentHeight - height) * offsetY, + return { + width, + height, + offsetX: (parentWidth - width) * offsetX, + offsetY: (parentHeight - height) * offsetY, + }; }; -}; diff --git a/src/utils/responsive/get-image-aspect-ratio.js b/src/utils/responsive/get-image-aspect-ratio.js index 1cc5ef1..416429f 100644 --- a/src/utils/responsive/get-image-aspect-ratio.js +++ b/src/utils/responsive/get-image-aspect-ratio.js @@ -7,12 +7,11 @@ export const getImageAspectRatio = (image, providedRatio) => { } if (providedRatio && typeof providedRatio === 'object') { - const mediaQueries = Object.keys(providedRatio) - .sort((a, b) => a - b); + const mediaQueries = Object.keys(providedRatio).sort((a, b) => a - b); - const activeMedia = mediaQueries.find((mediaQuery) => ( - window.innerWidth <= parseInt(mediaQuery, 10) - )); + const activeMedia = mediaQueries.find( + (mediaQuery) => window.innerWidth <= parseInt(mediaQuery, 10) + ); if (activeMedia) { imageAspectRatio = providedRatio[activeMedia]; @@ -23,4 +22,4 @@ export const getImageAspectRatio = (image, providedRatio) => { } catch { return 1; } -} \ No newline at end of file +}; diff --git a/src/utils/responsive/get-size-according-to-pixel-ratio.js b/src/utils/responsive/get-size-according-to-pixel-ratio.js index 0b7c6a4..1948a64 100644 --- a/src/utils/responsive/get-size-according-to-pixel-ratio.js +++ b/src/utils/responsive/get-size-according-to-pixel-ratio.js @@ -1,10 +1,6 @@ -export const getSizeAccordingToPixelRatio = (size) => { - const splittedSizes = size.toString().split('x'); - const result = []; - - [].forEach.call(splittedSizes, (splittedSize) => { - result.push(splittedSize * Math.round(window.devicePixelRatio || 1)); - }); - - return result.join('x'); +const getSizeAccordingToPixelRatio = (size = 1) => { + const pixelRatio = Math.round(window.devicePixelRatio || 1); + return parseInt(size) * pixelRatio; }; + +export default getSizeAccordingToPixelRatio; diff --git a/src/utils/spin-y/get-moving-direction.js b/src/utils/spin-y/get-moving-direction.js index ed175b2..88c7ddc 100644 --- a/src/utils/spin-y/get-moving-direction.js +++ b/src/utils/spin-y/get-moving-direction.js @@ -1,17 +1,13 @@ -import { ORIENTATIONS } from '../../constants/orientations'; - -export const getMovingDirection = (isStartSpin, allowSpinY, prevPosition, nextPositions, currentMovingDirection) => { - let movingDirection = ORIENTATIONS.CENTER; - - if (isStartSpin) return currentMovingDirection; - - const differenceInPositionX = Math.abs(prevPosition.x - nextPositions.x); - const differenceInPositionY = Math.abs(prevPosition.y - nextPositions.y); - const sensitivity = 10; - - if (differenceInPositionX > sensitivity) movingDirection = ORIENTATIONS.X; - - if (differenceInPositionY > sensitivity && allowSpinY) movingDirection = ORIENTATIONS.Y; - - return movingDirection; +export const getMovingDirection = (deltaX, deltaY, allowSpinY, threshold = 1) => { + // Check if the movement along the X-axis is greater than along the Y-axis + if (Math.abs(deltaX) - threshold > Math.abs(deltaY) || !allowSpinY) { + return deltaX > 0 ? 'right' : 'left'; + } + + // If Y-axis movement is allowed + if (Math.abs(deltaY) - threshold > Math.abs(deltaX) && allowSpinY) { + return deltaY > 0 ? 'down' : 'up'; + } + + return null; // Return null if no valid direction is determined }; diff --git a/src/utils/spin/ease-out.js b/src/utils/spin/ease-out.js new file mode 100644 index 0000000..3ad5204 --- /dev/null +++ b/src/utils/spin/ease-out.js @@ -0,0 +1,3 @@ +export const easeOutQuad = (t) => { + return t * (2 - t); +}; diff --git a/src/utils/spin/get-default-spin-direction.js b/src/utils/spin/get-default-spin-direction.js new file mode 100644 index 0000000..24be429 --- /dev/null +++ b/src/utils/spin/get-default-spin-direction.js @@ -0,0 +1,15 @@ +import { AUTOPLAY_BEHAVIOR } from '../constants'; + +export const getDefaultSpinDirection = (autoplayBehavior) => { + switch (autoplayBehavior) { + case AUTOPLAY_BEHAVIOR.SPIN_XY: + return 'x'; + case AUTOPLAY_BEHAVIOR.SPIN_YX: + return 'y'; + case AUTOPLAY_BEHAVIOR.SPIN_Y: + return 'y'; + case AUTOPLAY_BEHAVIOR.SPIN_X: + default: + return 'x'; + } +}; diff --git a/src/utils/spin/is-spin-keys-pressed.js b/src/utils/spin/is-spin-keys-pressed.js new file mode 100644 index 0000000..31d181c --- /dev/null +++ b/src/utils/spin/is-spin-keys-pressed.js @@ -0,0 +1,11 @@ +import { LEFT_RIGHT_KEYS, UP_DOWN_KEYS } from '../constants'; + +export const isSpinKeysPressed = (keyCode, allowSpinY) => { + const keys = [...LEFT_RIGHT_KEYS]; + + if (allowSpinY) { + return [...keys, ...UP_DOWN_KEYS].includes(keyCode); + } + + return keys.includes(keyCode); +}; diff --git a/src/utils/spin/should-switch-spin-direction.js b/src/utils/spin/should-switch-spin-direction.js new file mode 100644 index 0000000..b04af2a --- /dev/null +++ b/src/utils/spin/should-switch-spin-direction.js @@ -0,0 +1,23 @@ +import { AUTOPLAY_BEHAVIOR } from '../constants'; + +export const shouldSwitchSpinDirection = ({ + autoplayBehavior, + activeImageX, + activeImageY, + amountX, + amountY, + autoplayReverse, + spinDirection, +}) => { + const reachedEdgeX = activeImageX === (autoplayReverse ? 0 : amountX); + const reachedEdgeY = activeImageY === (autoplayReverse ? 0 : amountY); + + if ( + autoplayBehavior === AUTOPLAY_BEHAVIOR.SPIN_XY || + autoplayBehavior === AUTOPLAY_BEHAVIOR.SPIN_YX + ) { + return (spinDirection === 'x' && reachedEdgeX) || (spinDirection === 'y' && reachedEdgeY); + } + + return false; +}; diff --git a/src/utils/spin/switch-spin-direction.js b/src/utils/spin/switch-spin-direction.js new file mode 100644 index 0000000..45a1f18 --- /dev/null +++ b/src/utils/spin/switch-spin-direction.js @@ -0,0 +1,3 @@ +export const switchSpinDirection = (currentDirection) => { + return currentDirection === 'x' ? 'y' : 'x'; +}; diff --git a/src/utils/zoom/generate-zoom-in-steps.js b/src/utils/zoom/generate-zoom-in-steps.js index 41333b5..136a597 100644 --- a/src/utils/zoom/generate-zoom-in-steps.js +++ b/src/utils/zoom/generate-zoom-in-steps.js @@ -1,16 +1,15 @@ export const generateZoomInSteps = (zoomIntenisty) => { const transitionStepsFactor = 20; - return Array.from(Array(transitionStepsFactor)) - .reduce((acc, _, index) => { - const previousIndex = index - 1; - const previousValue = previousIndex < 0 ? 1 : acc[index - 1]; + return Array.from(Array(transitionStepsFactor)).reduce((acc, _, index) => { + const previousIndex = index - 1; + const previousValue = previousIndex < 0 ? 1 : acc[index - 1]; - const step = (previousValue) + ((zoomIntenisty - 1) / (transitionStepsFactor)); - const stepFixedValue = +step.toFixed(2); + const step = previousValue + (zoomIntenisty - 1) / transitionStepsFactor; + const stepFixedValue = +step.toFixed(2); - (acc || []).push(stepFixedValue); + (acc || []).push(stepFixedValue); - return acc; - }, []); + return acc; + }, []); }; diff --git a/src/utils/zoom/generate-zoom-out-steps.js b/src/utils/zoom/generate-zoom-out-steps.js index ca2d8f9..cfa3fbf 100644 --- a/src/utils/zoom/generate-zoom-out-steps.js +++ b/src/utils/zoom/generate-zoom-out-steps.js @@ -1,16 +1,15 @@ export const generateZoomOutSteps = (zoomIntenisty) => { const transitionStepsFactor = 20; - return Array.from(Array(transitionStepsFactor)) - .reduce((acc, _, index) => { - const previousIndex = index - 1; - const previousValue = previousIndex < 0 ? zoomIntenisty : acc[index - 1]; + return Array.from(Array(transitionStepsFactor)).reduce((acc, _, index) => { + const previousIndex = index - 1; + const previousValue = previousIndex < 0 ? zoomIntenisty : acc[index - 1]; - const step = previousValue - ((zoomIntenisty - 1) / transitionStepsFactor); - const stepFixedValue = +step.toFixed(2); + const step = previousValue - (zoomIntenisty - 1) / transitionStepsFactor; + const stepFixedValue = +step.toFixed(2); - (acc || []).push(stepFixedValue); + (acc || []).push(stepFixedValue); - return acc; - }, []); + return acc; + }, []); }; From 98e28f82763d4d51d72f495b15dccc546f6e363f Mon Sep 17 00:00:00 2001 From: Amr Wagdy Date: Fri, 18 Oct 2024 00:24:58 +0300 Subject: [PATCH 02/31] feat: Add refactored Pointer zoom, Magnifier and fullScreen --- examples/src/index.html | 53 ++-- src/ci360.service.js | 262 +++++++++++------- src/static/css/style.css | 62 +++-- .../create-loading-spinner.js | 7 + .../create-transition-overlay.js | 7 + src/utils/container-elements/index.js | 2 + src/utils/index.js | 7 + src/utils/load-images/preload-images.js | 2 +- .../zoom/calculate-offsets-from-events.js | 12 + src/utils/zoom/calculate-zoomed-dimensions.js | 5 + src/utils/zoom/calculate-zoomed-offset.js | 22 ++ 11 files changed, 296 insertions(+), 145 deletions(-) create mode 100644 src/utils/container-elements/create-loading-spinner.js create mode 100644 src/utils/container-elements/create-transition-overlay.js create mode 100644 src/utils/zoom/calculate-offsets-from-events.js create mode 100644 src/utils/zoom/calculate-zoomed-dimensions.js create mode 100644 src/utils/zoom/calculate-zoomed-offset.js diff --git a/examples/src/index.html b/examples/src/index.html index 60ebb6e..1b70b8c 100644 --- a/examples/src/index.html +++ b/examples/src/index.html @@ -26,7 +26,7 @@ JS Cloudimage 360 view @@ -35,18 +35,18 @@