├── .babelrc ├── .circleci └── config.yml ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples └── src │ ├── ExampleContainer.js │ ├── GlassExample.js │ ├── GlassExampleControls.js │ ├── Header.js │ ├── MagnifierExample.js │ ├── MagnifierExampleControls.js │ ├── PictureExample.js │ ├── PictureExampleControls.js │ ├── SideExample.js │ ├── SideExampleControls.js │ ├── favicon.ico │ ├── github-logo.png │ ├── index.html │ ├── index.js │ ├── npm-logo.png │ ├── sample-image.jpg │ └── style.css ├── package-lock.json ├── package.json ├── src ├── GlassMagnifier.js ├── GlassRenderer.js ├── Image.js ├── ImagePreviewOverlay.js ├── Magnifier.js ├── MagnifierContainer.js ├── MagnifierPreview.js ├── MagnifierPreviewRenderer.js ├── MagnifierRenderer.js ├── MagnifierZoom.js ├── PictureInPictureMagnifier.js ├── PictureInPictureRenderer.js ├── SideBySideMagnifier.js ├── SideBySideRenderer.js ├── __tests__ │ ├── styles.test.js │ └── utils.test.js ├── index.js ├── styles.js └── utils.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | branches: 9 | ignore: 10 | - gh-pages 11 | 12 | docker: 13 | - image: circleci/node:10.5.0 14 | 15 | working_directory: ~/repo 16 | 17 | steps: 18 | - checkout 19 | 20 | # Download and cache dependencies 21 | - restore_cache: 22 | keys: 23 | - v1-dependencies-{{ checksum "package.json" }} 24 | # fallback to using the latest cache if no exact match is found 25 | - v1-dependencies- 26 | 27 | - run: npm install 28 | 29 | - save_cache: 30 | paths: 31 | - node_modules 32 | key: v1-dependencies-{{ checksum "package.json" }} 33 | 34 | # run tests! 35 | - run: npm test 36 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples/dist 3 | dist 4 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | examples 3 | .babelrc 4 | .gitignore 5 | webpack.config.js 6 | .eslintrc.json 7 | coverage 8 | .circleci -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.4.0 (July 3rd, 2020) 4 | 5 | - Added props for changing activation method on SideBySideMagnifier and MagnifierPreview. 6 | 7 | ## 1.3.2 (May 9th, 2020) 8 | 9 | - Added support for images with transparency in Magnifier component by hiding preview image when zoomed. 10 | 11 | ## 1.3.1 (March 18th, 2020) 12 | 13 | - Fixed an issue preventing the custom magnifier components from functioning properly in an SSR project. 14 | - Improved SSR support by ensuring that the client side DOM matches the server side DOM when hydrating. 15 | 16 | ## 1.3.0 (March 16th, 2020) 17 | 18 | - Added additional layout/sizing options to the SideBySideMagnifier, including the ability to fill all available space on zoom. 19 | - Added additional customization options to preview overlays. 20 | - Added autoInPlace and inPlaceMinBreakpoint props to the MagnifierContainer component. These can be used to cause custom layout magnifiers to go into "in place" mode under certain conditions. 21 | 22 | ## 1.2.3 (February 6th, 2020) 23 | 24 | - Added onZoomStart and onZoomEnd props. 25 | 26 | ## 1.2.2 (December 25th, 2019) 27 | 28 | - Added a switchSides prop to the SideBySideMagnifier. When used, the zoomed image will be displayed to the left side of the preview (instead of the right side). 29 | 30 | ## 1.2.1 (October 1st, 2019) 31 | 32 | - Added image fallback feature. imageSrc and largeImageSrc props now accept an array of paths. 33 | 34 | ## 1.2.0 (September 29th, 2019) 35 | 36 | - Added MagnifierContainer, MagnifierPreview, and MagnifierZoom components for creating more advanced magnifier layouts. 37 | 38 | ## 1.1.2 (September 27th, 2019) 39 | 40 | - Updated react-input-position to improve touch interactions. 41 | 42 | ## 1.1.1 (September 14th, 2019) 43 | 44 | - Fixed typo in README. 45 | 46 | ## 1.1.0 (September 9th, 2019) 47 | 48 | - Added onImageLoad and onLargeImageLoad props to all magnifier components. 49 | 50 | ## 1.0.7 (September 1st, 2019) 51 | 52 | - Fixed remaining "window is not defined" error in SideBySideRenderer when used in SSR. All components now support SSR. 53 | 54 | ## 1.0.6 (September 1st, 2019) 55 | 56 | - Updated react-input-position@1.1.7. 57 | - Add styling to ensure large image width is set to auto. 58 | 59 | ## 1.0.5 (August 20th, 2019) 60 | 61 | - Fixed "window is not defined" error in SideBySideRenderer when used in SSR. 62 | - Font size is no longer zero when using PictureInPicture's renderOverlay render prop. 63 | - Added tests for styles and utils helpers. 64 | 65 | ## 1.0.4 (June 1st, 2019) 66 | 67 | - Improved the handling of images that are too small for the magnifying functionality. Some components used to behave erratically in this situation. Now, if an image is less than or equal to the size of the container, the magnifier component will simply size the image to the container and hide/disable the controls. 68 | - Updated README. 69 | 70 | ## 1.0.3 (May 16th, 2019) 71 | 72 | - Updated README. 73 | 74 | ## 1.0.2 (May 15th, 2019) 75 | 76 | - Updated github links. 77 | 78 | ## 1.0.1 (March 14th, 2019) 79 | 80 | - Updated react-input-position@1.1.3. 81 | - Added CHANGELOG. 82 | 83 | ## 1.0.0 (January 14th, 2019) 84 | 85 | - Initial public release. 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright Adam Risberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-image-magnifiers 2 | 3 | A collection of responsive, image magnifying React components for mouse and touch. Useful for product images in ecommerce sites, image galleries, stock photos, etc. 4 | 5 | [![npm version](https://img.shields.io/npm/v/react-image-magnifiers.svg?style=flat)](https://npmjs.org/package/react-image-magnifiers "View this project on npm") 6 | [![npm downloads](https://img.shields.io/npm/dm/react-image-magnifiers.svg?style=flat-square)](https://www.npmjs.com/package/react-image-magnifiers) 7 | [![MIT license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT) 8 | 9 | ## Magnifier components: 10 | 11 | - Magnifier: Can be zoomed in/out by click, double click, tap, double tap, or long touch. Click/touch and drag to move around the image while zoomed in. Alternate mode moves around the image via hover or touch/slide. 12 | - GlassMagnifier: Simulates a magnifying glass. Includes offset options for use with touch (so the user's finger doesn't block the magnifying glass). 13 | - SideBySideMagnifier: Displays a zoomed in view of the image on hover/touch alongside the small version. Automatically zooms in place when there's not enough room to display alongside. 14 | - PictureInPictureMagnifier: Displays a small preview image with a zoom area preview box in the corner of the component. User can move the preview box around to change the portion of the enlarged image to display. 15 | 16 | ## Custom layout components 17 | 18 | The following components can be used together to create more advanced magnifier layouts. These components can be styled to be any size/shape. The MagnifierPreview and MagnifierZoom must be within the same MagnifierContainer, but do not need to be siblings in the hierarchy of children. For example... 19 | 20 | ```JSX 21 | 22 |
23 | 24 |
25 | 26 |
27 | ``` 28 | 29 | _Note: The MagnifierZoom component will initially have zero height. Its only content uses absolute positioning. Therefore, height must be set with styling (className and/or style props, flex parent, etc)._ 30 | 31 | - MagnifierContainer: Links the MagnifierPreview and MagnifierZoom components together. 32 | - MagnifierPreview: Displays the interactive preview image. 33 | - MagnifierZoom: Displays the zoomed view of the image. 34 | 35 | ## Demo 36 | 37 | Visit the [react-image-magnifiers demo site](https://adamrisberg.github.io/react-image-magnifiers). 38 | 39 | ## Installation 40 | 41 | ```sh 42 | npm install --save react-image-magnifiers 43 | ``` 44 | 45 | ## Basic Usage 46 | 47 | ```JSX 48 | import { 49 | Magnifier, 50 | GlassMagnifier, 51 | SideBySideMagnifier, 52 | PictureInPictureMagnifier, 53 | MOUSE_ACTIVATION, 54 | TOUCH_ACTIVATION 55 | } from "react-image-magnifiers"; 56 | ... 57 | 58 | 63 | 64 | 71 | ``` 72 | 73 | Note: The zoom level of all components depends on the rendered size difference between the small and large versions of the image. On all components, the zoom functionality will be disabled if the large image size is <= to the small image's current rendered size. 74 | 75 | ## Common Props 76 | 77 | _Excluding MagnifierContainer, MagnifierZoom, & MagnifierPreview._ 78 | 79 | _imageSrc is the only required prop._ 80 | 81 | **cursorStyle:** Accepts any valid CSS cursor. Default: Magnifier - "zoom-in", GlassMagnifier - "none", SideBySideMagnifier - "crosshair", PictureInPicture - "crosshair". Type: string. 82 | 83 | **imageSrc:** Passed to the src of the small image (not zoomed). Also used for the large image (zoomed) if no largeImageSrc is set. Also accepts an array of image paths in case fallbacks are required. Each image path in the array will be tried in order until either one loads, or the end of the array is reached. Type: string or array of strings, Default: "". 84 | 85 | **largeImageSrc:** Passed to the src of the large image (zoomed). Also accepts an array of image paths in case fallbacks are required. Each image path in the array will be tried in order until either one loads, or the end of the array is reached. Type: string or array of strings, Default: "". 86 | 87 | **imageAlt:** Passed to the alt of both images. 88 | 89 | **style:** Passed to the style of the parent div. 90 | 91 | **className:** Passed to the className of the parent div. 92 | 93 | **renderOverlay:** Render prop for custom overlays. The render prop function will get called with a single boolean representing the active state. Be sure to use absolute position on your content to avoid changing the size/layout of the magnifier component, which would interfere with the functionality. Default: null, Type: function. 94 | 95 | **onImageLoad:** Passed to the onload of the small image (not zoomed). 96 | 97 | **onLargeImageLoad:** Passed to the onload of the large image (zoomed). 98 | 99 | **onZoomStart:** Callback to be executed on zoom start. Type: function. 100 | 101 | **onZoomEnd:** Callback to be executed on zoom end. Type: function. 102 | 103 | ## Magnifier Props 104 | 105 | **mouseActivation:** Sets the mouse method for zooming in/out. Accepts: "click" or "doubleClick". Can also import the MOUSE_ACTIVATION constants to assist. Type: string, Default: "click". 106 | 107 | **touchActivation:** Sets the touch method for zooming in/out. Accepts: "tap", "doubleTap", or "longTouch". Can also import the TOUCH_ACTIVATION constants to assist. Type: string, Default: "tap". 108 | 109 | **cursorStyleActive:** Cursor style while the component is in an active (zoomed in) state. Accepts any valid CSS cursor. Type: string, Default: "move" while dragToMove is active, otherwise "zoom-out". 110 | 111 | **dragToMove:** Movement of the image, while zoomed in, requires a click/touch drag gesture. Type: boolean, Default: true. 112 | 113 | **interactionSettings:** Settings to fine tune the mouse and/or touch settings. Accepts an object with any of the following properties: 114 | 115 | - **tapDurationInMs:** Sets the maximum length of touch events in order to be considered tap gestures. Type: number, Default: 180. 116 | - **doubleTapDurationInMs:** Sets the minimum length of time in which two tap gestures must be performed in order to be considered a double tap gesture. Type: number, Default: 400. 117 | - **longTouchDurationInMs:** Sets the minimum length of touch events in order to be consider a long touch gesture. Type: number, Default: 500. 118 | - **longTouchMoveLimit:** Sets the maximum movement allowed during a long touch gesture. Type: number, Default: 5. 119 | - **clickMoveLimit:** Sets the maximum movement allowed during a mouse click. Helps to differentiate between a drag and click. Type: number, Default: 5. 120 | 121 | ## GlassMagnifier Props 122 | 123 | **allowOverflow:** Allows the magnifying glass to spill out over the edges of the component. Type: boolean, Default: true. 124 | 125 | **magnifierBorderColor:** Color of the magnifying glass border. Accepts any valid CSS color. Type: string, Default: "rgba(255,255,255,.5)". 126 | 127 | **magnifierBorderSize:** Size of the magnifying glass border in px. Type: number, Default: 3. 128 | 129 | **magnifierBackgroundColor:** Background color of the magnifying glass. Can only be seen when overflow is allowed and the magnifying glass is at the edge of the image. Accepts any valid CSS color. Type: string, Default: "rgba(225,225,225,.5)". 130 | 131 | **magnifierOffsetX:** Horizontal offset of the magnifying glass from the mouse/touch position. Type: number, Default: 0. 132 | 133 | **magnifierOffsetY:** Vertical offset of the magnifying glass from the mouse/touch position. Type: number, Default: 0. 134 | 135 | **magnifierSize:** Size of the magnifying glass in px or %. Type: string (must include "px" or "%") or number, Default: "25%". 136 | 137 | **square:** Square magnifying glass. Type: boolean, Default: false. 138 | 139 | ## PictureInPictureMagnifier Props 140 | 141 | **cursorStyleActive:** Cursor style while click dragging to move preview box. Accepts any valid CSS cursor. Type: string, Default: If not provided, the cursorStyle is used. 142 | 143 | **previewHorizonalPos:** Horizontal alignment of the small preview image. Accepts "left" or "right". Cannot change during runtime unless the component is reloaded, see the example project on github for a workaround. Type: string, Default: "left". 144 | 145 | **previewVerticalPos:** Vertical alignment of the small preview image. Accepts "top" or "bottom". Cannot change during runtime unless the component is reloaded, see the example project on github for a workaround. Type: string, Default: "bottom". 146 | 147 | **previewOpacity:** Sets the opacity of the small preview image. Accepts a number between 0 and 1. Type: number, Default: 0.8. 148 | 149 | **previewOverlayBoxOpacity:** Sets the opacity of the white box (representing the zoom area) within the small preview image. Accepts a number between 0 and 1. Type: number, default: 0.8. 150 | 151 | **previewOverlayBackgroundColor:** Sets the color of the dark overlay (representing the area not shown during zoom). Accepts any valid CSS color. Type: string, Default: #000. 152 | 153 | **previewOverlayBoxColor:** Sets the color of the white box (representing the zoom area) within the preview image. Accepts any valid CSS color. Type: string, default: #fff. 154 | 155 | **previewOverlayBoxImage:** Used to add a background image to the white box (representing the zoom area) within the preview image. Accepts an image src path. Type: string, default: "". 156 | 157 | **previewOverlayBoxImageSize:** When using overlayBoxImage, this can be used to change the size of the background image. Accepts any valid CSS background-size value. Type: string, default: "". 158 | 159 | **previewOverlayOpacity:** Sets the opacity of the dark overlay (representing the area not shown during zoom). Accepts a number between 0 and 1. Type: number, Default: 0.4. 160 | 161 | **previewSizePercentage:** Sets the size (percentage) of the small preview image. Type: number, Default: 35. 162 | 163 | **shadow:** Activates a small box shadow around the small preview image. Type: boolean, Default: false. 164 | 165 | **shadowColor:** Sets the color of the box shadow around the small preview image. Accepts any valid CSS color. Type: string, Default: "rgba(0,0,0,.4)". 166 | 167 | _Note: onZoomStart and onZoomEnd behaves differently with PictureInPictureMagnifier. Because this component is always displaying the zoom image, these callbacks will be executed when the user starts or ends interacting with the small navigation image._ 168 | 169 | ## SideBySideMagnifier Props 170 | 171 | **mouseActivation:** Sets the mouse method for zooming in/out. Accepts: "click" or "doubleClick". Can also import the MOUSE_ACTIVATION constants to assist. Type: string, Default: "click". 172 | 173 | **touchActivation:** Sets the touch method for zooming in/out. Accepts: "tap", "doubleTap", or "longTouch". Can also import the TOUCH_ACTIVATION constants to assist. Type: string, Default: "tap". 174 | 175 | **alwaysInPlace:** Activate in place mode, which displays the zoomed image in the same place instead of to the side. By default, the component goes into this mode automatically whenever there isn't enough room to display the zoomed image alongside. Type: boolean, Default: false. 176 | 177 | **switchSides:** Displays the zoomed image on the left side of the preview, instead of the right side. Regardless of the intended side, the zoomed image will be displayed in place if there isn't enough room available. 178 | 179 | **fillAvailableSpace:** Instead of displaying the zoomed image in a container the same size as the preview image, the zoomed image will fill all available space, horizontally and vertically, to the side of the preview. It will not grow past the size of the image. Type: boolean, default: false. 180 | 181 | **fillAlignTop:** Used in conjunction with fillAvailableSpace to prevent the zoomed image container from filling all available space upwards. Instead it will align, vertically, to the top of the preview image and fill all space to the right and down. 182 | 183 | **fillGapLeft, fillGapRight, fillGapTop, and fillGapBottom:** Used in conjuction with fillAvailableSpace to add a gap between the zoom image container and the edge of the available space. 184 | 185 | **overlayBoxOpacity:** Sets the opacity of the white box (representing the zoom area) within the small preview image. Accepts a number between 0 and 1. Type: number, default: 0.8. 186 | 187 | **overlayOpacity:** Sets the opacity of the dark overlay (representing the area not shown during zoom). Accepts a number between 0 and 1. Type: number, Default: 0.5. 188 | 189 | **overlayBackgroundColor:** Sets the color of the dark overlay (representing the area not shown during zoom). Accepts any valid CSS color. Type: string, Default: #000. 190 | 191 | **overlayBoxColor:** Sets the color of the white box (representing the zoom area) within the preview image. Accepts any valid CSS color. Type: string, default: #fff. 192 | 193 | **overlayBoxImage:** Used to add a background image to the white box (representing the zoom area) within the preview image. Accepts an image src path. Type: string, default: "". 194 | 195 | **overlayBoxImageSize:** When using overlayBoxImage, this can be used to change the size of the background image. Accepts any valid CSS background-size value. Type: string, default: "". 196 | 197 | **zoomContainerBorder:** Used for styling the zoom container. Accepts any valid CSS border value. Type: string, default: "none". 198 | 199 | **zoomContainerBoxShadow:** Used for styling the zoom container. Accepts any valid CSS box-shadow value. Type: string, default: "none". 200 | 201 | **transitionSpeed:** Speed, in seconds, of the fade transition while zooming in/out. Type: number, Default: 0.4. 202 | 203 | **transitionSpeedInPlace:** Speed, in seconds, of the fade transition while zooming in/out while in place mode is active. Type: number, Default: 0.4. 204 | 205 | **inPlaceMinBreakpoint:** Cuases the magnifier to automatically switch to in place mode (displaying the zoomed image in the same place instead of to the side) based on a min width breakpoint. Accepts a number representing the target screen size (in pixels) when in place mode will become active. Type: number, default: 0. 206 | 207 | ## MagnifierContainer Props 208 | 209 | **style:** Passed to the style of the parent div. 210 | 211 | **className:** Passed to the className of the parent div. 212 | 213 | **autoInPlace:** Causes the magnifier to automatically switch to in place mode (zoomed view is displayed in the same place as the preview) when the MagnifierZoom component doesn't fit on the screen. Requires largeImageSrc to be set on the MagnifierPreview component. Type: boolean, default: false. 214 | 215 | **inPlaceMinBreakpoint:** Causes the magnifier to automatically switch to in place mode (zoomed view is displayed in the same place as the preview) based on a min width breakpoint. Accepts a number representing the target screen size (in pixels) when in place mode will become active. Requires largeImageSrc to be set on the MagnifierPreview component. Type: number, default: 0. 216 | 217 | ## MagnifierPreview Props 218 | 219 | **mouseActivation:** Sets the mouse method for zooming in/out. Accepts: "click" or "doubleClick". Can also import the MOUSE_ACTIVATION constants to assist. Type: string, Default: "click". 220 | 221 | **touchActivation:** Sets the touch method for zooming in/out. Accepts: "tap", "doubleTap", or "longTouch". Can also import the TOUCH_ACTIVATION constants to assist. Type: string, Default: "tap". 222 | 223 | **imageSrc:** Passed to the src of the image. Also accepts an array of image paths in case fallbacks are required. Each image path in the array will be tried in order until either one loads, or the end of the array is reached. Type: string or array of strings, Default: "". 224 | 225 | **imageAlt:** Passed to the alt of the image. 226 | 227 | **largeImageSrc:** Only available when using autoInPlace or inPlaceMinBreakpoint on the MagnifierContainer. Passed to the src of the large image (zoomed while in place mode is active). Also accepts an array of image paths in case fallbacks are required. Each image path in the array will be tried in order until either one loads, or the end of the array is reached. Type: string or array of strings, Default: "". 228 | 229 | **style:** Passed to the style of the parent div. 230 | 231 | **className:** Passed to the className of the parent div. 232 | 233 | **onImageLoad:** Passed to the onload of the image. 234 | 235 | **onLargeImageLoad:** Only available when using autoInPlace or inPlaceMinBreakpoint on the MagnifierContainer. Passed to the onload of the large image (zoomed while in place mode is active). 236 | 237 | **cursorStyle:** Accepts any valid CSS cursor. Type: string, Default: "crosshair". 238 | 239 | **transitionSpeed:** Speed, in seconds, of the fade transition while zooming in/out. Type: number, Default: 0.4. 240 | 241 | **overlayBoxOpacity:** Sets the opacity of the white box (representing the zoom area) within the small preview image. Accepts a number between 0 and 1. Type: number, default: 0.8. 242 | 243 | **overlayOpacity:** Sets the opacity of the dark overlay (representing the area not shown during zoom). Accepts a number between 0 and 1. Type: number, Default: 0.5. 244 | 245 | **overlayBackgroundColor:** Sets the color of the dark overlay (representing the area not shown during zoom). Accepts any valid CSS color. Type: string, Default: #000. 246 | 247 | **overlayBoxColor:** Sets the color of the white box (representing the zoom area) within the preview image. Accepts any valid CSS color. Type: string, default: #fff. 248 | 249 | **overlayBoxImage:** Used to add a background image to the white box (representing the zoom area) within the preview image. Accepts an image src path. Type: string, default: "". 250 | 251 | **overlayBoxImageSize:** When using overlayBoxImage, this can be used to change the size of the background image. Accepts any valid CSS background-size value. Type: string, default: "". 252 | 253 | **renderOverlay:** Render prop for custom overlays. The render prop function will get called with a single boolean representing the active state. Be sure to use absolute position on your content to avoid changing the size/layout of the magnifier component, which would interfere with the functionality. Default: null, Type: function. 254 | 255 | **onZoomStart:** Callback to be executed on zoom start. Type: function. 256 | 257 | **onZoomEnd:** Callback to be executed on zoom end. Type: function. 258 | 259 | ## MagnifierZoom Props 260 | 261 | **imageSrc:** Passed to the src of the image. Also accepts an array of image paths in case fallbacks are required. Each image path in the array will be tried in order until either one loads, or the end of the array is reached. Type: string or array of strings, Default: "". 262 | 263 | **imageAlt:** Passed to the alt of the image. 264 | 265 | **style:** Passed to the style of the parent div. 266 | 267 | **className:** Passed to the className of the parent div. 268 | 269 | **onImageLoad:** Passed to the onload of the small image (not zoomed). 270 | 271 | **transitionSpeed:** Speed, in seconds, of the fade transition while zooming in/out. Type: number, Default: 0.4. 272 | 273 | ## Example Project 274 | 275 | ```sh 276 | git clone https://github.com/adamrisberg/react-image-magnifiers.git 277 | cd react-image-magnifiers 278 | npm install 279 | npm start 280 | ``` 281 | -------------------------------------------------------------------------------- /examples/src/ExampleContainer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ExampleContainer = props => { 4 | const { title, children } = props; 5 | 6 | return ( 7 |
8 |

{title}

9 | {children} 10 |
11 | ); 12 | }; 13 | 14 | export default ExampleContainer; 15 | -------------------------------------------------------------------------------- /examples/src/GlassExample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import ExampleContainer from "./ExampleContainer"; 3 | import { GlassMagnifier } from "../../src"; 4 | import GlassExampleControls from "./GlassExampleControls"; 5 | 6 | class GlassExample extends Component { 7 | state = { 8 | allowOverflow: true, 9 | magnifierBorderSize: 5, 10 | magnifierBorderColor: "rgba(255, 255, 255, .5)", 11 | magnifierSize: "30%", 12 | square: false 13 | }; 14 | 15 | handleSize = e => { 16 | const value = e.target.value + "%"; 17 | this.setState(() => ({ magnifierSize: value })); 18 | }; 19 | 20 | handleBorderSize = e => { 21 | const value = Number(e.target.value); 22 | this.setState(() => ({ magnifierBorderSize: value })); 23 | }; 24 | 25 | handleBoolChange = key => e => { 26 | const value = Boolean(e.target.value); 27 | this.setState(() => ({ [key]: value })); 28 | }; 29 | 30 | handleTextChange = key => e => { 31 | const value = e.target.value; 32 | this.setState(() => ({ [key]: value })); 33 | }; 34 | 35 | render() { 36 | const { 37 | allowOverflow, 38 | magnifierSize, 39 | magnifierBorderSize, 40 | magnifierBorderColor, 41 | square 42 | } = this.state; 43 | 44 | const { image, largeImage } = this.props; 45 | 46 | return ( 47 | 48 | 49 |
50 | 60 | 67 |
68 |
69 |
70 | ); 71 | } 72 | } 73 | 74 | export default GlassExample; 75 | -------------------------------------------------------------------------------- /examples/src/GlassExampleControls.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const GlassExampleControls = props => { 4 | const { 5 | handleSize, 6 | handleBorderSize, 7 | handleBoolChange, 8 | handleTextChange, 9 | borderColor 10 | } = props; 11 | 12 | return ( 13 |
14 |
15 | 22 | 29 |
30 |
31 | 44 | 59 |
60 | 64 |
65 | ); 66 | }; 67 | 68 | export default GlassExampleControls; 69 | -------------------------------------------------------------------------------- /examples/src/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Header = () => { 4 | return ( 5 |
6 |
7 |
8 |

react-image-magnifiers

9 |
10 |
11 | 12 | Github Logo 13 | 14 | 15 | NPM Logo 16 | 17 |
18 |
19 |
20 | ); 21 | }; 22 | 23 | export default Header; 24 | -------------------------------------------------------------------------------- /examples/src/MagnifierExample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import ExampleContainer from "./ExampleContainer"; 3 | import { Magnifier, MOUSE_ACTIVATION, TOUCH_ACTIVATION } from "../../src"; 4 | import MagnifierExampleControls from "./MagnifierExampleControls"; 5 | 6 | class MagnifierExample extends Component { 7 | state = { 8 | mouseActivation: MOUSE_ACTIVATION.CLICK, 9 | touchActivation: TOUCH_ACTIVATION.TAP, 10 | dragToMove: true 11 | }; 12 | 13 | handleBoolChange = key => e => { 14 | const value = Boolean(e.target.value); 15 | this.setState(() => ({ [key]: value })); 16 | }; 17 | 18 | handleActivationChange = key => e => { 19 | const value = e.target.value; 20 | this.setState(() => ({ [key]: value })); 21 | }; 22 | 23 | render() { 24 | const { mouseActivation, touchActivation, dragToMove } = this.state; 25 | 26 | const { image, largeImage } = this.props; 27 | 28 | return ( 29 | 30 | 31 |
32 | 40 | 44 |
45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | export default MagnifierExample; 52 | -------------------------------------------------------------------------------- /examples/src/MagnifierExampleControls.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | TOUCH_ACTIVATION, 4 | MOUSE_ACTIVATION 5 | } from "react-input-position"; 6 | 7 | const MagnifierExampleControls = props => { 8 | const { 9 | handleBoolChange, 10 | handleActivationChange 11 | } = props; 12 | 13 | return ( 14 |
15 | 22 | 30 | 37 |
38 | ); 39 | }; 40 | 41 | export default MagnifierExampleControls; -------------------------------------------------------------------------------- /examples/src/PictureExample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import ExampleContainer from "./ExampleContainer"; 3 | import { PictureInPictureMagnifier } from "../../src"; 4 | import PictureExampleControls from "./PictureExampleControls"; 5 | 6 | class PictureExample extends Component { 7 | state = { 8 | previewHorizontalPos: "left", 9 | previewVerticalPos: "bottom", 10 | previewSizePercentage: 35, 11 | previewOpacity: 1, 12 | shadow: false, 13 | show: true 14 | }; 15 | 16 | handlePosChange = key => e => { 17 | const value = e.target.value; 18 | this.setState(() => ({ [key]: value, show: false }), this.show); 19 | }; 20 | 21 | handleShadowChange = e => { 22 | const value = Boolean(e.target.value); 23 | this.setState(() => ({ shadow: value })); 24 | }; 25 | 26 | handleSizeChange = e => { 27 | const value = Number(e.target.value); 28 | this.setState( 29 | () => ({ previewSizePercentage: value, show: false }), 30 | this.show 31 | ); 32 | }; 33 | 34 | handleOpacityChange = e => { 35 | const value = Number(e.target.value); 36 | this.setState(() => ({ previewOpacity: value })); 37 | }; 38 | 39 | show = () => { 40 | this.setState(() => ({ show: true })); 41 | }; 42 | 43 | render() { 44 | const { 45 | previewHorizontalPos, 46 | previewVerticalPos, 47 | previewSizePercentage, 48 | previewOpacity, 49 | shadow 50 | } = this.state; 51 | 52 | const { image, largeImage } = this.props; 53 | 54 | return ( 55 | 56 | 57 |
58 | {this.state.show ? ( 59 | 69 | ) : null} 70 | 76 |
77 |
78 |
79 | ); 80 | } 81 | } 82 | 83 | export default PictureExample; 84 | -------------------------------------------------------------------------------- /examples/src/PictureExampleControls.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PictureExampleControls = props => { 4 | const { 5 | handlePosChange, 6 | handleShadowChange, 7 | handleSizeChange, 8 | handleOpacityChange 9 | } = props; 10 | 11 | return ( 12 |
13 |
14 | 21 | 28 |
29 |
30 | 41 | 48 |
49 | 65 |
66 | ); 67 | }; 68 | 69 | export default PictureExampleControls; -------------------------------------------------------------------------------- /examples/src/SideExample.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import ExampleContainer from "./ExampleContainer"; 3 | import { SideBySideMagnifier } from "../../src"; 4 | import SideExampleControls from "./SideExampleControls"; 5 | 6 | class SideExample extends Component { 7 | state = { 8 | alwaysInPlace: false, 9 | overlayOpacity: 0.6, 10 | switchSides: false, 11 | fillAvailableSpace: false, 12 | fillAlignTop: false, 13 | fillGapLeft: 0, 14 | fillGapRight: 10, 15 | fillGapTop: 10, 16 | fillGapBottom: 10 17 | }; 18 | 19 | handleBoolChange = key => e => { 20 | const value = Boolean(e.target.value); 21 | this.setState(() => ({ [key]: value })); 22 | }; 23 | 24 | handleNumberChange = key => e => { 25 | const value = Number(e.target.value); 26 | this.setState(() => ({ [key]: value })); 27 | }; 28 | 29 | render() { 30 | const { 31 | alwaysInPlace, 32 | overlayOpacity, 33 | switchSides, 34 | fillAvailableSpace, 35 | fillAlignTop, 36 | fillGapLeft, 37 | fillGapRight, 38 | fillGapTop, 39 | fillGapBottom 40 | } = this.state; 41 | 42 | const { image, largeImage } = this.props; 43 | 44 | return ( 45 | 46 | 47 |
48 | 67 | 72 |
73 |
74 |
75 | ); 76 | } 77 | } 78 | 79 | export default SideExample; 80 | -------------------------------------------------------------------------------- /examples/src/SideExampleControls.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SideExampleControls = props => { 4 | const { handleBoolChange, handleNumberChange, enableFillControls } = props; 5 | const labelStyle = { color: enableFillControls ? "#000" : "#999" }; 6 | 7 | return ( 8 |
9 | 20 |
21 | 28 | 38 |
39 |
40 | 56 |
57 |
58 | 74 |
75 |
76 | ); 77 | }; 78 | 79 | function Select({ style, className, label, disabled, onChange, defaultValue }) { 80 | return ( 81 | 95 | ); 96 | } 97 | 98 | export default SideExampleControls; 99 | -------------------------------------------------------------------------------- /examples/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamRisberg/react-image-magnifiers/6a5207c23c0727faf0c3c7a96813b9a13e158882/examples/src/favicon.ico -------------------------------------------------------------------------------- /examples/src/github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamRisberg/react-image-magnifiers/6a5207c23c0727faf0c3c7a96813b9a13e158882/examples/src/github-logo.png -------------------------------------------------------------------------------- /examples/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | react-image-magnifiers Demo 9 | 10 | 11 | 12 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /examples/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import GlassExample from "./GlassExample"; 5 | import MagnifierExample from "./MagnifierExample"; 6 | import PictureExample from "./PictureExample"; 7 | import SideExample from "./SideExample"; 8 | import Header from "./Header"; 9 | 10 | import "./style.css"; 11 | 12 | const image = require("./sample-image.jpg"); 13 | 14 | const DemoApp = () => { 15 | return ( 16 | 17 |
18 |
19 | 20 | 21 | 22 | 23 |
24 | 25 | ); 26 | }; 27 | 28 | ReactDOM.render(, document.getElementById("root")); 29 | -------------------------------------------------------------------------------- /examples/src/npm-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamRisberg/react-image-magnifiers/6a5207c23c0727faf0c3c7a96813b9a13e158882/examples/src/npm-logo.png -------------------------------------------------------------------------------- /examples/src/sample-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamRisberg/react-image-magnifiers/6a5207c23c0727faf0c3c7a96813b9a13e158882/examples/src/sample-image.jpg -------------------------------------------------------------------------------- /examples/src/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: inherit; 5 | } 6 | 7 | html { 8 | font-size: 16px; 9 | } 10 | 11 | body { 12 | box-sizing: border-box; 13 | font-family: "Roboto"; 14 | } 15 | 16 | p { 17 | margin-bottom: 20px; 18 | } 19 | 20 | #root { 21 | overflow: hidden; 22 | } 23 | 24 | .flex { 25 | display: flex; 26 | align-items: flex-start; 27 | flex-wrap: wrap; 28 | } 29 | 30 | .app { 31 | max-width: 1400px; 32 | margin: 0 auto; 33 | padding: 0 10px 10px 10px; 34 | } 35 | 36 | .padding-20 { 37 | padding: 20px; 38 | } 39 | 40 | .margin-top-10 { 41 | margin-top: 10px; 42 | } 43 | 44 | .input-position { 45 | width: 50%; 46 | position: relative; 47 | } 48 | 49 | /* HEADER */ 50 | 51 | .header { 52 | margin-bottom: 40px; 53 | background-color: #111; 54 | color: white; 55 | } 56 | 57 | .header-wrapper { 58 | max-width: 1400px; 59 | padding: 10px; 60 | margin: 0 auto; 61 | display: flex; 62 | justify-content: space-between; 63 | align-items: center; 64 | } 65 | 66 | .header img { 67 | height: 32px; 68 | } 69 | 70 | .header img:last-of-type { 71 | margin-left: 20px; 72 | } 73 | 74 | .brand { 75 | display: inline-block; 76 | } 77 | 78 | .brand h1 { 79 | font-weight: 500; 80 | font-size: 1.8rem; 81 | } 82 | 83 | /* IMAGE EXAMPLE */ 84 | 85 | .image-example { 86 | display: flex; 87 | flex-wrap: wrap; 88 | border-bottom: 2px solid #333; 89 | } 90 | 91 | .image-tip { 92 | position: absolute; 93 | bottom: 0; 94 | left: 0; 95 | background-color: rgba(0, 0, 0, 0.7); 96 | padding: 10px 15px; 97 | color: #fff; 98 | width: 100%; 99 | text-align: center; 100 | } 101 | 102 | /* IMAGE SAMPLE CODE */ 103 | 104 | .sample-code { 105 | background-color: #f4f4f4; 106 | padding: 20px 25px; 107 | border: 1px solid #ccc; 108 | overflow-x: scroll; 109 | } 110 | 111 | /* CONTROLS */ 112 | 113 | .controls { 114 | width: 50%; 115 | padding: 20px 25px; 116 | font-weight: 500; 117 | } 118 | 119 | select, 120 | input { 121 | display: block; 122 | width: 100%; 123 | font-family: inherit; 124 | font-size: inherit; 125 | padding: 2px 4px; 126 | font-weight: 400; 127 | margin-top: 2px; 128 | } 129 | 130 | input { 131 | padding: 4px 8px; 132 | } 133 | 134 | .note { 135 | font-size: 0.95rem; 136 | font-weight: 400; 137 | margin-top: 5px; 138 | font-style: italic; 139 | } 140 | 141 | .label { 142 | margin-bottom: 15px; 143 | display: block; 144 | } 145 | 146 | .label:last-of-type { 147 | margin-bottom: 0; 148 | } 149 | 150 | .label-flex { 151 | display: flex; 152 | margin-bottom: 15px; 153 | } 154 | 155 | .label-left { 156 | width: calc(50% - 10px); 157 | margin-right: 10px; 158 | } 159 | 160 | .label-right { 161 | width: 50%; 162 | } 163 | 164 | /* INPUT INFO */ 165 | 166 | .input-info { 167 | padding: 20px; 168 | text-align: center; 169 | height: 100%; 170 | margin: auto; 171 | display: flex; 172 | flex-direction: column; 173 | justify-content: space-between; 174 | border-right: 1px solid #ccc; 175 | background-color: #f4f4f4; 176 | } 177 | 178 | .input-info div { 179 | margin-bottom: 10px; 180 | } 181 | 182 | .input-info div:last-of-type { 183 | margin-bottom: 0; 184 | } 185 | 186 | .color-active { 187 | background-color: #f2fff0; 188 | } 189 | 190 | /* EXAMPLE CONTAINER */ 191 | 192 | .example-container { 193 | margin-bottom: 40px; 194 | border: 2px solid #333; 195 | } 196 | 197 | .example-container h2 { 198 | font-size: 1.5rem; 199 | background-color: #333; 200 | color: #fff; 201 | padding: 10px 20px; 202 | text-align: center; 203 | font-weight: 500; 204 | } 205 | 206 | /* IMAGE EXAMPLE RENDERER */ 207 | 208 | .small-image { 209 | width: 100%; 210 | display: block; 211 | visibility: hidden; 212 | } 213 | 214 | .show { 215 | visibility: visible; 216 | } 217 | 218 | .large-image { 219 | position: absolute; 220 | top: 0; 221 | left: 0; 222 | z-index: -1; 223 | } 224 | 225 | @media only screen and (max-width: 750px) { 226 | .controls { 227 | padding: 10px 15px; 228 | } 229 | } 230 | 231 | @media only screen and (max-width: 640px) { 232 | .app { 233 | padding: 0 0 10px 0; 234 | } 235 | 236 | .input-position { 237 | width: 100%; 238 | order: 2; 239 | margin-top: 10px; 240 | } 241 | 242 | .controls { 243 | width: 100%; 244 | padding: 20px 15px; 245 | } 246 | 247 | .example-container { 248 | margin-bottom: 40px; 249 | border-left: none; 250 | border-right: none; 251 | } 252 | } 253 | 254 | @media only screen and (max-width: 440px) { 255 | .header-wrapper { 256 | display: block; 257 | } 258 | 259 | .logos { 260 | text-align: center; 261 | } 262 | 263 | .header img { 264 | height: 24px; 265 | } 266 | 267 | .brand { 268 | width: 100%; 269 | margin-bottom: 10px; 270 | } 271 | 272 | .brand h1 { 273 | font-weight: 500; 274 | font-size: 1.5rem; 275 | text-align: center; 276 | } 277 | 278 | .example-container h2 { 279 | font-size: 1.3rem; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-image-magnifiers", 3 | "version": "1.4.0", 4 | "description": "A collection of responsive, image magnifying React components for mouse and touch.", 5 | "keywords": [ 6 | "react", 7 | "image", 8 | "photo", 9 | "picture", 10 | "magnify", 11 | "magnifying glass", 12 | "magnifier", 13 | "zoom", 14 | "enlarge", 15 | "responsive", 16 | "touch", 17 | "ecommerce", 18 | "product" 19 | ], 20 | "license": "MIT", 21 | "author": "Adam Risberg ", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/adamrisberg/react-image-magnifiers.git" 25 | }, 26 | "homepage": "https://adamrisberg.github.io/react-image-magnifiers", 27 | "main": "dist/index.js", 28 | "scripts": { 29 | "start": "webpack-dev-server --mode development", 30 | "test": "jest", 31 | "test:watch": "jest --watch", 32 | "test:coverage": "jest --coverage", 33 | "transpile": "rimraf dist && babel src -d dist --ignore src/__tests__/*", 34 | "prepublishOnly": "npm run transpile", 35 | "build": "rimraf examples/dist && webpack --mode production", 36 | "deploy": "gh-pages -d examples/dist", 37 | "publish-demo": "npm run build && npm run deploy" 38 | }, 39 | "peerDependencies": { 40 | "react": "^16.8.0", 41 | "react-dom": "^16.8.0" 42 | }, 43 | "devDependencies": { 44 | "@babel/cli": "^7.10.4", 45 | "@babel/core": "^7.10.4", 46 | "@babel/plugin-proposal-class-properties": "^7.10.4", 47 | "@babel/preset-env": "^7.10.4", 48 | "@babel/preset-react": "^7.10.4", 49 | "@types/jest": "^24.9.1", 50 | "babel-eslint": "^9.0.0", 51 | "babel-jest": "^24.9.0", 52 | "babel-loader": "^8.1.0", 53 | "css-loader": "^2.1.1", 54 | "enzyme": "^3.11.0", 55 | "enzyme-adapter-react-16": "^1.15.2", 56 | "eslint": "^5.16.0", 57 | "eslint-config-react-app": "^3.0.8", 58 | "eslint-loader": "^2.2.1", 59 | "eslint-plugin-flowtype": "^2.50.3", 60 | "eslint-plugin-import": "^2.22.0", 61 | "eslint-plugin-jsx-a11y": "^6.3.1", 62 | "eslint-plugin-react": "^7.20.3", 63 | "file-loader": "^3.0.1", 64 | "gh-pages": "^2.2.0", 65 | "html-webpack-plugin": "^3.2.0", 66 | "jest": "^24.9.0", 67 | "react": "^16.13.1", 68 | "react-dom": "^16.13.1", 69 | "rimraf": "^2.7.1", 70 | "style-loader": "^0.23.1", 71 | "webpack": "^4.43.0", 72 | "webpack-cli": "^3.3.12", 73 | "webpack-dev-server": "^3.11.0", 74 | "websocket-extensions": "^0.1.4" 75 | }, 76 | "dependencies": { 77 | "prop-types": "^15.7.2", 78 | "react-input-position": "^1.3.1" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/GlassMagnifier.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import utils from "./utils"; 4 | import ReactInputPosition, { 5 | TOUCH_ACTIVATION, 6 | MOUSE_ACTIVATION 7 | } from "react-input-position"; 8 | import GlassRenderer from "./GlassRenderer"; 9 | 10 | const GlassMagnifier = props => { 11 | const { 12 | imageSrc, 13 | largeImageSrc, 14 | imageAlt, 15 | magnifierBorderSize, 16 | magnifierBorderColor, 17 | magnifierBackgroundColor, 18 | magnifierSize, 19 | magnifierOffsetX, 20 | magnifierOffsetY, 21 | square, 22 | cursorStyle, 23 | renderOverlay, 24 | allowOverflow, 25 | style, 26 | className, 27 | onImageLoad, 28 | onLargeImageLoad, 29 | onZoomStart, 30 | onZoomEnd 31 | } = props; 32 | 33 | return ( 34 | 49 | 65 | 66 | ); 67 | }; 68 | 69 | GlassMagnifier.propTypes = { 70 | imageSrc: PropTypes.oneOfType([ 71 | PropTypes.string, 72 | PropTypes.arrayOf(PropTypes.string) 73 | ]), 74 | largeImageSrc: PropTypes.oneOfType([ 75 | PropTypes.string, 76 | PropTypes.arrayOf(PropTypes.string) 77 | ]), 78 | imageAlt: PropTypes.string, 79 | allowOverflow: PropTypes.bool, 80 | magnifierBorderSize: PropTypes.number, 81 | magnifierBorderColor: PropTypes.string, 82 | magnifierBackgroundColor: PropTypes.string, 83 | magnifierSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 84 | magnifierOffsetX: PropTypes.number, 85 | magnifierOffsetY: PropTypes.number, 86 | square: PropTypes.bool, 87 | cursorStyle: PropTypes.string, 88 | renderOverlay: PropTypes.func, 89 | className: PropTypes.string, 90 | style: PropTypes.object, 91 | onImageLoad: PropTypes.func, 92 | onLargeImageLoad: PropTypes.func 93 | }; 94 | 95 | GlassMagnifier.defaultProps = { 96 | imageSrc: "", 97 | largeImageSrc: "", 98 | imageAlt: "", 99 | allowOverflow: false, 100 | magnifierBorderSize: 3, 101 | magnifierBorderColor: "rgba(255,255,255,.5)", 102 | magnifierBackgroundColor: "rgba(225,225,225,.5)", 103 | magnifierSize: "25%", 104 | magnifierOffsetX: 0, 105 | magnifierOffsetY: 0, 106 | square: false, 107 | cursorStyle: "none", 108 | onImageLoad: utils.noop, 109 | onLargeImageLoad: utils.noop 110 | }; 111 | 112 | export default GlassMagnifier; 113 | -------------------------------------------------------------------------------- /src/GlassRenderer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import utils from "./utils"; 3 | import Image from "./Image"; 4 | import styles from "./styles"; 5 | 6 | const GlassRenderer = props => { 7 | const { 8 | itemRef, 9 | itemPosition, 10 | activePosition, 11 | elementDimensions, 12 | itemDimensions, 13 | active, 14 | imageSrc, 15 | largeImageSrc, 16 | imageAlt, 17 | magnifierBorderSize, 18 | magnifierBorderColor, 19 | magnifierBackgroundColor, 20 | square, 21 | magnifierSize, 22 | magnifierOffsetX, 23 | magnifierOffsetY, 24 | renderOverlay, 25 | cursorStyle, 26 | onImageLoad, 27 | onLargeImageLoad, 28 | onLoadRefresh 29 | } = props; 30 | 31 | const legalSize = itemDimensions.width > elementDimensions.width; 32 | const isActive = legalSize && active; 33 | 34 | const magnifierSizeNum = utils.convertWidthToPx( 35 | magnifierSize, 36 | elementDimensions.width 37 | ); 38 | 39 | const positionOffset = magnifierSizeNum / 2; 40 | 41 | const position = { 42 | x: itemPosition.x - activePosition.x + positionOffset - magnifierBorderSize, 43 | y: itemPosition.y - activePosition.y + positionOffset - magnifierBorderSize 44 | }; 45 | 46 | const divPosition = { 47 | x: activePosition.x - positionOffset + magnifierOffsetX, 48 | y: activePosition.y - positionOffset + magnifierOffsetY 49 | }; 50 | 51 | const borderRadius = square ? "0" : "50%"; 52 | 53 | return ( 54 | 55 | {imageAlt} 67 |
83 | {imageAlt} 91 |
92 | {renderOverlay ? renderOverlay(active) : null} 93 |
94 | ); 95 | }; 96 | 97 | export default GlassRenderer; 98 | -------------------------------------------------------------------------------- /src/Image.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import utils from "./utils"; 3 | 4 | const Image = React.forwardRef(function(props, ref) { 5 | const { onImageLoad, onLoadRefresh, src, alt, ...otherProps } = props; 6 | 7 | const [imageIdx, setImageIdx] = React.useState(0); 8 | const imageErrorRef = React.useRef(false); 9 | const imageArr = src.constructor === Array ? src : [src]; 10 | 11 | return ( 12 | {alt} { 17 | onImageLoad(e); 18 | 19 | if (imageErrorRef.current) { 20 | onLoadRefresh(); 21 | } 22 | }} 23 | onError={e => { 24 | if (imageIdx < imageArr.length) { 25 | imageErrorRef.current = true; 26 | setImageIdx(idx => idx + 1); 27 | } 28 | }} 29 | {...otherProps} 30 | /> 31 | ); 32 | }); 33 | 34 | Image.defaultProps = { 35 | onImageLoad: utils.noop, 36 | onLoadRefresh: utils.noop 37 | }; 38 | 39 | export default Image; 40 | -------------------------------------------------------------------------------- /src/ImagePreviewOverlay.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./styles"; 3 | 4 | const ImagePreviewOverlay = props => { 5 | const { 6 | previewWidth, 7 | previewHeight, 8 | previewPosLeft, 9 | previewPosRight, 10 | previewPosTop, 11 | previewPosBottom, 12 | imageWidth, 13 | imageHeight, 14 | overlayOpacity, 15 | overlayBoxOpacity, 16 | active, 17 | transitionSpeed, 18 | overlayBackgroundColor, 19 | overlayBoxColor, 20 | overlayBoxImage, 21 | overlayBoxImageSize 22 | } = props; 23 | 24 | const opacity = active ? overlayOpacity : 0; 25 | const boxOpacity = active ? overlayBoxOpacity : 0; 26 | 27 | return ( 28 | 29 |
42 |
51 |
61 |
71 |
81 |
82 | ); 83 | }; 84 | 85 | ImagePreviewOverlay.defaultProps = { 86 | overlayOpacity: 0.5, 87 | overlayBoxOpacity: 0.8, 88 | transitionSpeed: 0.4, 89 | overlayBackgroundColor: "#000", 90 | overlayBoxColor: "#fff", 91 | overlayBoxImage: "", 92 | overlayBoxImageSize: "" 93 | }; 94 | 95 | export default ImagePreviewOverlay; 96 | -------------------------------------------------------------------------------- /src/Magnifier.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import utils from "./utils"; 4 | import ReactInputPosition, { 5 | TOUCH_ACTIVATION, 6 | MOUSE_ACTIVATION 7 | } from "react-input-position"; 8 | import MagnifierRenderer from "./MagnifierRenderer"; 9 | 10 | const Magnifier = props => { 11 | const { 12 | imageSrc, 13 | largeImageSrc, 14 | imageAlt, 15 | cursorStyle, 16 | cursorStyleActive, 17 | renderOverlay, 18 | dragToMove, 19 | className, 20 | style, 21 | mouseActivation, 22 | touchActivation, 23 | interactionSettings, 24 | onImageLoad, 25 | onLargeImageLoad, 26 | onImageError, 27 | onLargeImageError, 28 | onZoomStart, 29 | onZoomEnd 30 | } = props; 31 | 32 | const finalActiveCursorStyle = 33 | cursorStyleActive || dragToMove ? "move" : "zoom-out"; 34 | 35 | return ( 36 | 49 | 61 | 62 | ); 63 | }; 64 | 65 | Magnifier.propTypes = { 66 | imageSrc: PropTypes.oneOfType([ 67 | PropTypes.string, 68 | PropTypes.arrayOf(PropTypes.string) 69 | ]), 70 | largeImageSrc: PropTypes.oneOfType([ 71 | PropTypes.string, 72 | PropTypes.arrayOf(PropTypes.string) 73 | ]), 74 | imageAlt: PropTypes.string, 75 | cursorStyle: PropTypes.string, 76 | cursorStyleActive: PropTypes.string, 77 | renderOverlay: PropTypes.func, 78 | dragToMove: PropTypes.bool, 79 | className: PropTypes.string, 80 | style: PropTypes.object, 81 | mouseActivation: PropTypes.string, 82 | touchActivation: PropTypes.string, 83 | interactionSettings: PropTypes.shape({ 84 | tapDurationInMs: PropTypes.number, 85 | doubleTapDurationInMs: PropTypes.number, 86 | longTouchDurationInMs: PropTypes.number, 87 | longTouchMoveLimit: PropTypes.number, 88 | clickMoveLimit: PropTypes.number 89 | }), 90 | onImageLoad: PropTypes.func, 91 | onLargeImageLoad: PropTypes.func 92 | }; 93 | 94 | Magnifier.defaultProps = { 95 | imageSrc: "", 96 | largeImageSrc: "", 97 | imageAlt: "", 98 | cursorStyle: "zoom-in", 99 | cursorStyleActive: "", 100 | dragToMove: true, 101 | mouseActivation: MOUSE_ACTIVATION.CLICK, 102 | touchActivation: TOUCH_ACTIVATION.TAP, 103 | interactionSettings: {}, 104 | onImageLoad: utils.noop, 105 | onLargeImageLoad: utils.noop 106 | }; 107 | 108 | export default Magnifier; 109 | -------------------------------------------------------------------------------- /src/MagnifierContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { defaultState } from "react-input-position"; 4 | import utils from "./utils"; 5 | 6 | export const MagnifierContext = React.createContext(); 7 | 8 | class MagnifierContainer extends Component { 9 | state = { 10 | inputPositionState: defaultState 11 | }; 12 | zoomContainerRef = React.createRef(); 13 | zoomImageRef = React.createRef(); 14 | zoomImageDimensions = { width: 0, height: 0 }; 15 | 16 | static propTypes = { 17 | className: PropTypes.string, 18 | style: PropTypes.object, 19 | autoInPlace: PropTypes.bool, 20 | inPlaceMinBreakpoint: PropTypes.number 21 | }; 22 | 23 | static defaultProps = { 24 | inPlaceMinBreakpoint: 0 25 | }; 26 | 27 | getZoomContainerDimensions = () => { 28 | if (!this.zoomContainerRef.current) { 29 | return { width: 0, height: 0, left: 0, right: 0, top: 0, bottom: 0 }; 30 | } 31 | 32 | const { 33 | width, 34 | height, 35 | left, 36 | right, 37 | top, 38 | bottom 39 | } = this.zoomContainerRef.current.getBoundingClientRect(); 40 | 41 | return { width, height, left, right, top, bottom }; 42 | }; 43 | 44 | getZoomImageDimensions() { 45 | if (!this.zoomImageDimensions.width && this.zoomImageRef.current) { 46 | const rect = this.zoomImageRef.current.getBoundingClientRect(); 47 | this.zoomImageDimensions = { 48 | width: rect.width, 49 | height: rect.height 50 | }; 51 | } 52 | return this.zoomImageDimensions; 53 | } 54 | 55 | onUpdate = changes => { 56 | this.setState({ inputPositionState: changes }); 57 | }; 58 | 59 | onZoomImageLoad = e => { 60 | const rect = e.target.getBoundingClientRect(); 61 | this.zoomImageDimensions = { 62 | width: rect.width, 63 | height: rect.height 64 | }; 65 | }; 66 | 67 | getContextValue() { 68 | return { 69 | stateOverride: this.state.inputPositionState, 70 | isActive: this.state.inputPositionState.active, 71 | onUpdate: this.onUpdate, 72 | zoomImageDimensions: this.zoomImageDimensions, 73 | zoomRef: this.zoomContainerRef, 74 | zoomImageRef: this.zoomImageRef, 75 | onZoomImageLoad: this.onZoomImageLoad, 76 | ...this.calculatePositions() 77 | }; 78 | } 79 | 80 | calculatePositions() { 81 | const { elementDimensions, itemPosition } = this.state.inputPositionState; 82 | const zoomContainerDimensions = this.getZoomContainerDimensions(); 83 | const zoomImageDimensions = this.getZoomImageDimensions(); 84 | 85 | let inPlace = false; 86 | const { autoInPlace, inPlaceMinBreakpoint } = this.props; 87 | 88 | if (autoInPlace || inPlaceMinBreakpoint) { 89 | try { 90 | const { left, right } = zoomContainerDimensions; 91 | const windowWidth = window.innerWidth; 92 | 93 | if ( 94 | windowWidth < inPlaceMinBreakpoint || 95 | left < 0 || 96 | right > windowWidth 97 | ) { 98 | inPlace = true; 99 | } 100 | } catch (e) {} 101 | } 102 | 103 | const smallImageSize = { 104 | width: elementDimensions.width, 105 | height: elementDimensions.height 106 | }; 107 | 108 | const previewSize = { 109 | width: Math.floor( 110 | smallImageSize.width * 111 | (zoomContainerDimensions.width / zoomImageDimensions.width) 112 | ), 113 | height: Math.floor( 114 | smallImageSize.height * 115 | (zoomContainerDimensions.height / zoomImageDimensions.height) 116 | ) 117 | }; 118 | 119 | let position = { x: 0, y: 0 }; 120 | const itemPositionAdj = { ...itemPosition }; 121 | 122 | const previewOffset = { 123 | x: inPlace ? 0 : previewSize.width / 2, 124 | y: inPlace ? 0 : previewSize.height / 2 125 | }; 126 | 127 | itemPositionAdj.x = Math.max(previewOffset.x, itemPositionAdj.x); 128 | itemPositionAdj.x = Math.min( 129 | smallImageSize.width - previewOffset.x, 130 | itemPositionAdj.x 131 | ); 132 | itemPositionAdj.y = Math.max(previewOffset.y, itemPositionAdj.y); 133 | itemPositionAdj.y = Math.min( 134 | smallImageSize.height - previewOffset.y, 135 | itemPositionAdj.y 136 | ); 137 | 138 | position = { ...itemPositionAdj }; 139 | 140 | const zoomContainerSize = inPlace 141 | ? smallImageSize 142 | : zoomContainerDimensions; 143 | 144 | position.x = utils.convertRange( 145 | previewOffset.x, 146 | smallImageSize.width - previewOffset.x, 147 | zoomImageDimensions.width * -1 + zoomContainerSize.width, 148 | 0, 149 | position.x 150 | ); 151 | position.y = utils.convertRange( 152 | previewOffset.y, 153 | smallImageSize.height - previewOffset.y, 154 | zoomImageDimensions.height * -1 + zoomContainerSize.height, 155 | 0, 156 | position.y 157 | ); 158 | 159 | position.x = utils.invertNumber( 160 | zoomImageDimensions.width * -1 + zoomContainerSize.width, 161 | 0, 162 | position.x 163 | ); 164 | position.y = utils.invertNumber( 165 | zoomImageDimensions.height * -1 + zoomContainerSize.height, 166 | 0, 167 | position.y 168 | ); 169 | 170 | previewSize.left = Math.floor(itemPositionAdj.x - previewOffset.x) || 0; 171 | previewSize.right = Math.floor(itemPositionAdj.x + previewOffset.x) || 0; 172 | previewSize.top = Math.floor(itemPositionAdj.y - previewOffset.y) || 0; 173 | previewSize.bottom = Math.floor(itemPositionAdj.y + previewOffset.y) || 0; 174 | 175 | return { 176 | position, 177 | smallImageSize, 178 | previewSize, 179 | zoomContainerDimensions, 180 | inPlace 181 | }; 182 | } 183 | 184 | render() { 185 | const { style, className } = this.props; 186 | 187 | return ( 188 |
189 | 190 | {this.props.children} 191 | 192 |
193 | ); 194 | } 195 | } 196 | 197 | export default MagnifierContainer; 198 | -------------------------------------------------------------------------------- /src/MagnifierPreview.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import utils from "./utils"; 4 | import ReactInputPosition, { 5 | MOUSE_ACTIVATION, 6 | TOUCH_ACTIVATION 7 | } from "react-input-position"; 8 | import MagnifierPreviewRenderer from "./MagnifierPreviewRenderer"; 9 | import { MagnifierContext } from "./MagnifierContainer"; 10 | 11 | function MagnifierPreview(props) { 12 | const { 13 | imageSrc, 14 | imageAlt, 15 | largeImageSrc, 16 | className, 17 | style, 18 | cursorStyle, 19 | onImageLoad, 20 | onLargeImageLoad, 21 | renderOverlay, 22 | overlayOpacity, 23 | overlayBoxOpacity, 24 | overlayBackgroundColor, 25 | overlayBoxColor, 26 | overlayBoxImage, 27 | overlayBoxImageSize, 28 | transitionSpeed, 29 | onZoomStart, 30 | onZoomEnd, 31 | mouseActivation, 32 | touchActivation 33 | } = props; 34 | 35 | const { 36 | stateOverride, 37 | onUpdate, 38 | zoomImageDimensions, 39 | previewSize, 40 | smallImageSize, 41 | position, 42 | inPlace 43 | } = React.useContext(MagnifierContext); 44 | 45 | return ( 46 | 59 | 79 | 80 | ); 81 | } 82 | 83 | MagnifierPreview.propTypes = { 84 | className: PropTypes.string, 85 | style: PropTypes.object, 86 | cursorStyle: PropTypes.string, 87 | imageSrc: PropTypes.oneOfType([ 88 | PropTypes.string, 89 | PropTypes.arrayOf(PropTypes.string) 90 | ]), 91 | largeImageSrc: PropTypes.oneOfType([ 92 | PropTypes.string, 93 | PropTypes.arrayOf(PropTypes.string) 94 | ]), 95 | imageAlt: PropTypes.string, 96 | onImageLoad: PropTypes.func, 97 | onLargeImageLoad: PropTypes.func, 98 | renderOverlay: PropTypes.func, 99 | overlayBoxOpacity: PropTypes.number, 100 | overlayOpacity: PropTypes.number, 101 | overlayBackgroundColor: PropTypes.string, 102 | overlayBoxColor: PropTypes.string, 103 | overlayBoxImage: PropTypes.string, 104 | overlayBoxImageSize: PropTypes.string, 105 | transitionSpeed: PropTypes.number, 106 | mouseActivation: PropTypes.string, 107 | touchActivation: PropTypes.string 108 | }; 109 | 110 | MagnifierPreview.defaultProps = { 111 | cursorStyle: "crosshair", 112 | imageSrc: "", 113 | imageAlt: "", 114 | overlayOpacity: 0.5, 115 | overlayBoxOpacity: 0.8, 116 | overlayBackgroundColor: "#000", 117 | overlayBoxColor: "#fff", 118 | overlayBoxImage: "", 119 | overlayBoxImageSize: "", 120 | transitionSpeed: 0.4, 121 | onImageLoad: utils.noop, 122 | onLargeImageLoad: utils.noop, 123 | mouseActivation: MOUSE_ACTIVATION.HOVER, 124 | touchActivation: TOUCH_ACTIVATION.TOUCH 125 | }; 126 | 127 | export default MagnifierPreview; 128 | -------------------------------------------------------------------------------- /src/MagnifierPreviewRenderer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "./Image"; 3 | import ImagePreviewOverlay from "./ImagePreviewOverlay"; 4 | import styles from "./styles"; 5 | 6 | function MagnifierPreviewRenderer(props) { 7 | const { 8 | image, 9 | largeImage, 10 | alt, 11 | previewSize, 12 | smallImageSize, 13 | overlayOpacity, 14 | overlayBoxOpacity, 15 | overlayBackgroundColor, 16 | overlayBoxColor, 17 | overlayBoxImage, 18 | overlayBoxImageSize, 19 | active, 20 | onImageLoad, 21 | onLargeImageLoad, 22 | renderOverlay, 23 | transitionSpeed, 24 | inPlace: shouldBeInPlace, 25 | position 26 | } = props; 27 | 28 | // Ensures that client and server dom match when using SSR. 29 | const [inPlace, setInPlace] = React.useState(false); 30 | React.useEffect(() => { 31 | setInPlace(shouldBeInPlace); 32 | }, [shouldBeInPlace]); 33 | 34 | return ( 35 |
36 | {alt} 42 | 60 | {inPlace ? ( 61 |
73 | {alt} 81 |
82 | ) : null} 83 | {renderOverlay ? renderOverlay(active) : null} 84 |
85 | ); 86 | } 87 | 88 | export default MagnifierPreviewRenderer; 89 | -------------------------------------------------------------------------------- /src/MagnifierRenderer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "./Image"; 3 | import styles from "./styles"; 4 | 5 | const MagnifierRenderer = props => { 6 | const { 7 | itemPosition, 8 | active, 9 | elementDimensions, 10 | itemDimensions, 11 | imageSrc, 12 | largeImageSrc, 13 | imageAlt, 14 | itemRef, 15 | renderOverlay, 16 | cursorStyle, 17 | cursorStyleActive, 18 | onImageLoad, 19 | onLargeImageLoad, 20 | onLoadRefresh 21 | } = props; 22 | 23 | const legalSize = itemDimensions.width > elementDimensions.width; 24 | const isActive = legalSize && active; 25 | const finalCursorStyle = !legalSize 26 | ? "default" 27 | : active 28 | ? cursorStyleActive 29 | : cursorStyle; 30 | 31 | return ( 32 |
33 | {imageAlt} 44 |
51 | {imageAlt} 63 |
64 | {renderOverlay ? renderOverlay(active) : null} 65 |
66 | ); 67 | }; 68 | 69 | export default MagnifierRenderer; 70 | -------------------------------------------------------------------------------- /src/MagnifierZoom.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import utils from "./utils"; 4 | import styles from "./styles"; 5 | import { MagnifierContext } from "./MagnifierContainer"; 6 | import Image from "./Image"; 7 | 8 | function MagnifierZoom(props) { 9 | const { 10 | imageSrc, 11 | imageAlt, 12 | className, 13 | style, 14 | onImageLoad, 15 | transitionSpeed 16 | } = props; 17 | 18 | const { 19 | zoomImageDimensions, 20 | zoomContainerDimensions, 21 | position, 22 | onZoomImageLoad, 23 | zoomRef, 24 | zoomImageRef, 25 | isActive, 26 | inPlace 27 | } = React.useContext(MagnifierContext); 28 | 29 | let invalidVertical = 30 | zoomImageDimensions.height <= zoomContainerDimensions.height; 31 | let invalidHorizontal = 32 | zoomImageDimensions.width <= zoomContainerDimensions.width; 33 | 34 | return ( 35 |
45 | {imageAlt} { 57 | onZoomImageLoad(e); 58 | onImageLoad(e); 59 | }} 60 | /> 61 |
62 | ); 63 | } 64 | 65 | MagnifierZoom.propTypes = { 66 | className: PropTypes.string, 67 | style: PropTypes.object, 68 | imageSrc: PropTypes.oneOfType([ 69 | PropTypes.string, 70 | PropTypes.arrayOf(PropTypes.string) 71 | ]), 72 | imageAlt: PropTypes.string, 73 | onImageLoad: PropTypes.func, 74 | transitionSpeed: PropTypes.number 75 | }; 76 | 77 | MagnifierZoom.defaultProps = { 78 | style: {}, 79 | imageSrc: "", 80 | imageAlt: "", 81 | onImageLoad: utils.noop, 82 | transitionSpeed: 0.4 83 | }; 84 | 85 | export default MagnifierZoom; 86 | -------------------------------------------------------------------------------- /src/PictureInPictureMagnifier.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import utils from "./utils"; 4 | import ReactInputPosition, { 5 | TOUCH_ACTIVATION, 6 | MOUSE_ACTIVATION 7 | } from "react-input-position"; 8 | import PictureInPictureRenderer from "./PictureInPictureRenderer"; 9 | 10 | class PictureInPictureMagnifier extends Component { 11 | containerRef = React.createRef(); 12 | 13 | static propTypes = { 14 | imageSrc: PropTypes.oneOfType([ 15 | PropTypes.string, 16 | PropTypes.arrayOf(PropTypes.string) 17 | ]), 18 | largeImageSrc: PropTypes.oneOfType([ 19 | PropTypes.string, 20 | PropTypes.arrayOf(PropTypes.string) 21 | ]), 22 | imageAlt: PropTypes.string, 23 | previewSizePercentage: PropTypes.number, 24 | previewHorizontalPos: PropTypes.oneOf(["left", "right"]), 25 | previewVerticalPos: PropTypes.oneOf(["top", "bottom"]), 26 | previewOpacity: PropTypes.number, 27 | previewOverlayOpacity: PropTypes.number, 28 | previewOverlayBoxOpacity: PropTypes.number, 29 | previewOverlayBackgroundColor: PropTypes.string, 30 | previewOverlayBoxColor: PropTypes.string, 31 | previewOverlayBoxImage: PropTypes.string, 32 | previewOverlayBoxImageSize: PropTypes.string, 33 | cursorStyle: PropTypes.string, 34 | cursorStyleActive: PropTypes.string, 35 | shadow: PropTypes.bool, 36 | shadowColor: PropTypes.string, 37 | renderOverlay: PropTypes.func, 38 | className: PropTypes.string, 39 | style: PropTypes.object, 40 | onImageLoad: PropTypes.func, 41 | onLargeImageLoad: PropTypes.func, 42 | onZoomStart: PropTypes.func, 43 | onZoomEnd: PropTypes.func 44 | }; 45 | 46 | static defaultProps = { 47 | imageSrc: "", 48 | largeImageSrc: "", 49 | imageAlt: "", 50 | previewSizePercentage: 35, 51 | previewHorizontalPos: "left", 52 | previewVerticalPos: "bottom", 53 | previewOpacity: 0.8, 54 | previewOverlayOpacity: 0.4, 55 | previewOverlayBoxOpacity: 0.8, 56 | previewOverlayBackgroundColor: "#000", 57 | previewOverlayBoxColor: "#fff", 58 | previewOverlayBoxImage: "", 59 | previewOverlayBoxImageSize: "", 60 | cursorStyle: "crosshair", 61 | cursorStyleActive: "", 62 | shadowColor: "rgba(0,0,0,.4)", 63 | onImageLoad: utils.noop, 64 | onLargeImageLoad: utils.noop 65 | }; 66 | 67 | render() { 68 | const { 69 | imageSrc, 70 | largeImageSrc, 71 | imageAlt, 72 | previewSizePercentage, 73 | previewHorizontalPos, 74 | previewVerticalPos, 75 | previewOpacity, 76 | previewOverlayOpacity, 77 | previewOverlayBoxOpacity, 78 | previewOverlayBackgroundColor, 79 | previewOverlayBoxColor, 80 | previewOverlayBoxImage, 81 | previewOverlayBoxImageSize, 82 | cursorStyle, 83 | cursorStyleActive, 84 | shadow, 85 | shadowColor, 86 | renderOverlay, 87 | className, 88 | style, 89 | onImageLoad, 90 | onLargeImageLoad, 91 | onZoomStart, 92 | onZoomEnd 93 | } = this.props; 94 | 95 | return ( 96 |
106 | 124 | 144 | 145 |
146 | ); 147 | } 148 | } 149 | 150 | export default PictureInPictureMagnifier; 151 | -------------------------------------------------------------------------------- /src/PictureInPictureRenderer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import utils from "./utils"; 3 | import styles from "./styles"; 4 | import Image from "./Image"; 5 | import ImagePreviewOverlay from "./ImagePreviewOverlay"; 6 | 7 | const PictureInPictureRenderer = props => { 8 | const { 9 | active, 10 | itemPosition, 11 | elementDimensions, 12 | itemDimensions, 13 | imageSrc, 14 | largeImageSrc, 15 | imageAlt, 16 | itemRef, 17 | previewSizePercentage, 18 | containerRef, 19 | previewVerticalPos, 20 | previewOpacity, 21 | previewOverlayOpacity, 22 | previewOverlayBoxOpacity, 23 | previewOverlayBackgroundColor, 24 | previewOverlayBoxColor, 25 | previewOverlayBoxImage, 26 | previewOverlayBoxImageSize, 27 | renderOverlay, 28 | cursorStyle, 29 | cursorStyleActive, 30 | onLoadRefresh, 31 | onImageLoad, 32 | onLargeImageLoad 33 | } = props; 34 | 35 | const sizeMult = 100 / previewSizePercentage; 36 | 37 | let containerTop = 0; 38 | let containerLeft = 0; 39 | let containerWidth = 0; 40 | 41 | if (containerRef.current) { 42 | containerWidth = containerRef.current.getBoundingClientRect().width; 43 | 44 | if (previewVerticalPos === "bottom") { 45 | containerTop = elementDimensions.height * (sizeMult - 1); 46 | containerRef.current.style.paddingTop = `${containerTop}px`; 47 | } else { 48 | containerRef.current.style.paddingBottom = `${elementDimensions.height * 49 | (sizeMult - 1)}px`; 50 | } 51 | 52 | if (containerRef.current.style.textAlign === "right") { 53 | containerLeft = elementDimensions.width * (sizeMult - 1); 54 | } 55 | } 56 | 57 | const smallImageSize = { 58 | width: elementDimensions.width, 59 | height: elementDimensions.height 60 | }; 61 | 62 | const previewSize = { 63 | width: Math.floor( 64 | smallImageSize.width * 65 | (smallImageSize.width / itemDimensions.width) * 66 | sizeMult 67 | ), 68 | height: Math.floor( 69 | smallImageSize.height * 70 | (smallImageSize.height / itemDimensions.height) * 71 | sizeMult 72 | ) 73 | }; 74 | 75 | if (isNaN(previewSize.width)) { 76 | previewSize.width = 0; 77 | previewSize.height = 0; 78 | } 79 | 80 | let position = { x: 0, y: 0 }; 81 | const itemPositionAdj = { ...itemPosition }; 82 | 83 | const previewOffset = { 84 | x: previewSize.width / 2, 85 | y: previewSize.height / 2 86 | }; 87 | 88 | itemPositionAdj.x = Math.max(previewOffset.x, itemPositionAdj.x); 89 | itemPositionAdj.x = Math.min( 90 | smallImageSize.width - previewOffset.x, 91 | itemPositionAdj.x 92 | ); 93 | itemPositionAdj.y = Math.max(previewOffset.y, itemPositionAdj.y); 94 | itemPositionAdj.y = Math.min( 95 | smallImageSize.height - previewOffset.y, 96 | itemPositionAdj.y 97 | ); 98 | 99 | position = { ...itemPositionAdj }; 100 | 101 | position.x = utils.convertRange( 102 | previewOffset.x, 103 | smallImageSize.width - previewOffset.x, 104 | itemDimensions.width * -1 + containerWidth, 105 | 0, 106 | position.x 107 | ); 108 | position.y = utils.convertRange( 109 | previewOffset.y, 110 | smallImageSize.height - previewOffset.y, 111 | itemDimensions.height * -1 + smallImageSize.height * sizeMult, 112 | 0, 113 | position.y 114 | ); 115 | 116 | position.x = utils.invertNumber( 117 | itemDimensions.width * -1 + containerWidth, 118 | 0, 119 | position.x 120 | ); 121 | position.y = utils.invertNumber( 122 | itemDimensions.height * -1 + smallImageSize.height * sizeMult, 123 | 0, 124 | position.y 125 | ); 126 | 127 | previewSize.left = Math.floor(itemPositionAdj.x - previewOffset.x) || 0; 128 | previewSize.right = Math.floor(itemPositionAdj.x + previewOffset.x) || 0; 129 | previewSize.top = Math.floor(itemPositionAdj.y - previewOffset.y) || 0; 130 | previewSize.bottom = Math.floor(itemPositionAdj.y + previewOffset.y) || 0; 131 | 132 | const legalSize = previewSize.width < smallImageSize.width; 133 | const finalCursorStyle = active ? cursorStyleActive : cursorStyle; 134 | 135 | return ( 136 |
142 | {imageAlt} { 152 | onLoadRefresh(); 153 | onImageLoad(e); 154 | }} 155 | onLoadRefresh={onLoadRefresh} 156 | /> 157 |
172 | {imageAlt} 184 | {imageAlt} 192 | {renderOverlay ? renderOverlay(active) : null} 193 |
194 | 211 |
212 | ); 213 | }; 214 | 215 | export default PictureInPictureRenderer; 216 | -------------------------------------------------------------------------------- /src/SideBySideMagnifier.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import utils from "./utils"; 4 | import ReactInputPosition, { 5 | TOUCH_ACTIVATION, 6 | MOUSE_ACTIVATION 7 | } from "react-input-position"; 8 | import SideBySideRenderer from "./SideBySideRenderer"; 9 | 10 | const SideBySideMagnifier = props => { 11 | const { 12 | imageSrc, 13 | largeImageSrc, 14 | imageAlt, 15 | overlayOpacity, 16 | overlayBoxOpacity, 17 | overlayBackgroundColor, 18 | overlayBoxColor, 19 | overlayBoxImage, 20 | overlayBoxImageSize, 21 | cursorStyle, 22 | alwaysInPlace, 23 | transitionSpeed, 24 | transitionSpeedInPlace, 25 | renderOverlay, 26 | className, 27 | style, 28 | onImageLoad, 29 | onLargeImageLoad, 30 | switchSides, 31 | onZoomStart, 32 | onZoomEnd, 33 | fillAvailableSpace, 34 | fillAlignTop, 35 | fillGapLeft, 36 | fillGapRight, 37 | fillGapTop, 38 | fillGapBottom, 39 | inPlaceMinBreakpoint, 40 | zoomContainerBorder, 41 | zoomContainerBoxShadow, 42 | mouseActivation, 43 | touchActivation 44 | } = props; 45 | 46 | return ( 47 | 57 | 85 | 86 | ); 87 | }; 88 | 89 | SideBySideMagnifier.propTypes = { 90 | imageSrc: PropTypes.oneOfType([ 91 | PropTypes.string, 92 | PropTypes.arrayOf(PropTypes.string) 93 | ]), 94 | largeImageSrc: PropTypes.oneOfType([ 95 | PropTypes.string, 96 | PropTypes.arrayOf(PropTypes.string) 97 | ]), 98 | imageAlt: PropTypes.string, 99 | overlayOpacity: PropTypes.number, 100 | overlayBoxOpacity: PropTypes.number, 101 | overlayBackgroundColor: PropTypes.string, 102 | overlayBoxColor: PropTypes.string, 103 | overlayBoxImage: PropTypes.string, 104 | overlayBoxImageSize: PropTypes.string, 105 | cursorStyle: PropTypes.string, 106 | alwaysInPlace: PropTypes.bool, 107 | transitionSpeed: PropTypes.number, 108 | transitionSpeedInPlace: PropTypes.number, 109 | renderOverlay: PropTypes.func, 110 | className: PropTypes.string, 111 | style: PropTypes.object, 112 | onImageLoad: PropTypes.func, 113 | onLargeImageLoad: PropTypes.func, 114 | fillAvailableSpace: PropTypes.bool, 115 | fillAlignTop: PropTypes.bool, 116 | fillGapLeft: PropTypes.number, 117 | fillGapRight: PropTypes.number, 118 | fillGapTop: PropTypes.number, 119 | fillGapBottom: PropTypes.number, 120 | inPlaceMinBreakpoint: PropTypes.number, 121 | zoomContainerBorder: PropTypes.string, 122 | zoomContainerBoxShadow: PropTypes.string, 123 | mouseActivation: PropTypes.string, 124 | touchActivation: PropTypes.string 125 | }; 126 | 127 | SideBySideMagnifier.defaultProps = { 128 | imageSrc: "", 129 | largeImageSrc: "", 130 | imageAlt: "", 131 | overlayOpacity: 0.5, 132 | overlayBoxOpacity: 0.8, 133 | overlayBackgroundColor: "#000", 134 | overlayBoxColor: "#fff", 135 | overlayBoxImage: "", 136 | overlayBoxImageSize: "", 137 | cursorStyle: "crosshair", 138 | transitionSpeed: 0.4, 139 | transitionSpeedInPlace: 0.4, 140 | onImageLoad: utils.noop, 141 | onLargeImageLoad: utils.noop, 142 | fillAvailableSpace: true, 143 | fillAlignTop: false, 144 | fillGapLeft: 0, 145 | fillGapRight: 0, 146 | fillGapTop: 0, 147 | fillGapBottom: 0, 148 | inPlaceMinBreakpoint: 0, 149 | zoomContainerBorder: "none", 150 | zoomContainerBoxShadow: "none", 151 | mouseActivation: MOUSE_ACTIVATION.HOVER, 152 | touchActivation: TOUCH_ACTIVATION.TOUCH 153 | }; 154 | 155 | export default SideBySideMagnifier; 156 | -------------------------------------------------------------------------------- /src/SideBySideRenderer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import utils from "./utils"; 3 | import styles from "./styles"; 4 | import Image from "./Image"; 5 | import ImagePreviewOverlay from "./ImagePreviewOverlay"; 6 | 7 | const SideBySideRenderer = props => { 8 | const { 9 | itemPosition, 10 | active, 11 | elementDimensions, 12 | elementOffset, 13 | itemDimensions, 14 | imageSrc, 15 | largeImageSrc, 16 | imageAlt, 17 | itemRef, 18 | overlayOpacity, 19 | overlayBoxOpacity, 20 | overlayBackgroundColor, 21 | overlayBoxColor, 22 | overlayBoxImage, 23 | overlayBoxImageSize, 24 | alwaysInPlace, 25 | transitionSpeed, 26 | transitionSpeedInPlace, 27 | renderOverlay, 28 | cursorStyle, 29 | onImageLoad, 30 | onLargeImageLoad, 31 | onLoadRefresh, 32 | switchSides, 33 | fillAvailableSpace, 34 | fillAlignTop, 35 | fillGapLeft, 36 | fillGapRight, 37 | fillGapTop, 38 | fillGapBottom, 39 | inPlaceMinBreakpoint, 40 | zoomContainerBorder, 41 | zoomContainerBoxShadow 42 | } = props; 43 | 44 | const zoomContainerDimensions = { 45 | width: elementDimensions.width, 46 | height: elementDimensions.height 47 | }; 48 | 49 | const zoomContainerStyle = {}; 50 | 51 | let availableWidth = 0; 52 | let availableHeight = 0; 53 | let windowWidth = 0; 54 | 55 | const zoomGapVertical = fillGapTop + fillGapBottom; 56 | const zoomGapHorizontal = fillGapLeft + fillGapRight; 57 | 58 | try { 59 | const { clientWidth, clientHeight } = document.documentElement; 60 | const { innerWidth } = window; 61 | availableWidth = clientWidth; 62 | availableHeight = clientHeight; 63 | windowWidth = innerWidth; 64 | } catch (e) {} 65 | 66 | let inPlace = alwaysInPlace || windowWidth < inPlaceMinBreakpoint; 67 | 68 | if (fillAvailableSpace && !inPlace) { 69 | const left = elementDimensions.width + elementOffset.left; 70 | 71 | if (fillAlignTop) { 72 | zoomContainerDimensions.height = Math.min( 73 | itemDimensions.height, 74 | availableHeight - elementOffset.top - zoomGapVertical + fillGapTop 75 | ); 76 | zoomContainerDimensions.top = fillGapTop; 77 | } else { 78 | zoomContainerDimensions.height = Math.min( 79 | itemDimensions.height, 80 | availableHeight - zoomGapVertical 81 | ); 82 | 83 | const offsetTop = -elementOffset.top + fillGapTop; 84 | 85 | const maxOffsetTop = 86 | availableHeight - 87 | elementOffset.top - 88 | (zoomContainerDimensions.height + fillGapBottom); 89 | 90 | let limitedTop = Math.max(offsetTop, maxOffsetTop); 91 | 92 | zoomContainerDimensions.top = limitedTop; 93 | } 94 | 95 | zoomContainerDimensions.top = Math.min(zoomContainerDimensions.top, 0); 96 | zoomContainerStyle.top = `${zoomContainerDimensions.top}px`; 97 | 98 | if (switchSides) { 99 | zoomContainerDimensions.width = Math.min( 100 | itemDimensions.width, 101 | elementOffset.left - zoomGapHorizontal 102 | ); 103 | zoomContainerDimensions.right = elementDimensions.width + fillGapRight; 104 | zoomContainerStyle.right = `${zoomContainerDimensions.right}px`; 105 | } else { 106 | zoomContainerDimensions.width = Math.min( 107 | itemDimensions.width, 108 | availableWidth - left - zoomGapHorizontal 109 | ); 110 | zoomContainerDimensions.left = elementDimensions.width + fillGapLeft; 111 | zoomContainerStyle.left = `${zoomContainerDimensions.left}px`; 112 | } 113 | } else { 114 | if (switchSides) { 115 | inPlace = inPlace || elementOffset.left < elementDimensions.width; 116 | } else { 117 | inPlace = 118 | inPlace || 119 | elementDimensions.width * 2 + elementOffset.left > availableWidth; 120 | } 121 | } 122 | 123 | const legalSize = itemDimensions.width > elementDimensions.width; 124 | const isActive = legalSize && active; 125 | const transSpeed = inPlace ? transitionSpeedInPlace : transitionSpeed; 126 | 127 | const smallImageSize = { 128 | width: elementDimensions.width, 129 | height: elementDimensions.height 130 | }; 131 | 132 | const previewSize = { 133 | width: Math.floor( 134 | smallImageSize.width * 135 | (zoomContainerDimensions.width / itemDimensions.width) 136 | ), 137 | height: Math.floor( 138 | smallImageSize.height * 139 | (zoomContainerDimensions.height / itemDimensions.height) 140 | ) 141 | }; 142 | 143 | let position = { x: 0, y: 0 }; 144 | const itemPositionAdj = { ...itemPosition }; 145 | 146 | const previewOffset = { 147 | x: inPlace ? 0 : previewSize.width / 2, 148 | y: inPlace ? 0 : previewSize.height / 2 149 | }; 150 | 151 | itemPositionAdj.x = Math.max(previewOffset.x, itemPositionAdj.x); 152 | itemPositionAdj.x = Math.min( 153 | smallImageSize.width - previewOffset.x, 154 | itemPositionAdj.x 155 | ); 156 | itemPositionAdj.y = Math.max(previewOffset.y, itemPositionAdj.y); 157 | itemPositionAdj.y = Math.min( 158 | smallImageSize.height - previewOffset.y, 159 | itemPositionAdj.y 160 | ); 161 | 162 | position = { ...itemPositionAdj }; 163 | 164 | const zoomContainerSize = inPlace ? smallImageSize : zoomContainerDimensions; 165 | 166 | position.x = utils.convertRange( 167 | previewOffset.x, 168 | smallImageSize.width - previewOffset.x, 169 | itemDimensions.width * -1 + zoomContainerSize.width, 170 | 0, 171 | position.x 172 | ); 173 | position.y = utils.convertRange( 174 | previewOffset.y, 175 | smallImageSize.height - previewOffset.y, 176 | itemDimensions.height * -1 + zoomContainerSize.height, 177 | 0, 178 | position.y 179 | ); 180 | 181 | position.x = utils.invertNumber( 182 | itemDimensions.width * -1 + zoomContainerSize.width, 183 | 0, 184 | position.x 185 | ); 186 | position.y = utils.invertNumber( 187 | itemDimensions.height * -1 + zoomContainerSize.height, 188 | 0, 189 | position.y 190 | ); 191 | 192 | previewSize.left = Math.floor(itemPositionAdj.x - previewOffset.x) || 0; 193 | previewSize.right = Math.floor(itemPositionAdj.x + previewOffset.x) || 0; 194 | previewSize.top = Math.floor(itemPositionAdj.y - previewOffset.y) || 0; 195 | previewSize.bottom = Math.floor(itemPositionAdj.y + previewOffset.y) || 0; 196 | 197 | return ( 198 |
199 | {imageAlt} 210 |
226 | {imageAlt} 234 |
235 | 253 | {renderOverlay ? renderOverlay(active) : null} 254 |
255 | ); 256 | }; 257 | 258 | export default SideBySideRenderer; 259 | -------------------------------------------------------------------------------- /src/__tests__/styles.test.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles"; 2 | 3 | describe("getLargeImageStyle", () => { 4 | it("returns correct transform property", () => { 5 | const style = styles.getLargeImageStyle(10, 15); 6 | expect(style.transform).toBe("translate(10px, 15px)"); 7 | }); 8 | 9 | it("returns correct visibility property when active is true", () => { 10 | const style = styles.getLargeImageStyle(10, 15, true); 11 | expect(style.visibility).toBe("visible"); 12 | }); 13 | 14 | it("returns correct visibility property when active is false", () => { 15 | const style = styles.getLargeImageStyle(10, 15, false); 16 | expect(style.visibility).toBe("hidden"); 17 | }); 18 | }); 19 | 20 | describe("getZoomContainerStyle", () => { 21 | it("returns correct width & height properties", () => { 22 | const style = styles.getZoomContainerStyle(400, 500); 23 | expect(style.width).toBe("400px"); 24 | expect(style.height).toBe("500px"); 25 | }); 26 | 27 | it("returns correct left property when inPlace is true", () => { 28 | const style = styles.getZoomContainerStyle(400, 500, true); 29 | expect(style.left).toBe("0px"); 30 | }); 31 | 32 | it("returns correct left property when inPlace is true", () => { 33 | const style = styles.getZoomContainerStyle(400, 500, false); 34 | expect(style.left).toBe("400px"); 35 | }); 36 | }); 37 | 38 | describe("getOverlayCenterStyle", () => { 39 | const style = styles.getOverlayCenterStyle(400, 500, 20, 30, 0.8, 0.3); 40 | 41 | it("returns correct width & height properties", () => { 42 | expect(style.width).toBe("400px"); 43 | expect(style.height).toBe("500px"); 44 | }); 45 | 46 | it("returns correct transform property", () => { 47 | expect(style.transform).toBe("translate(20px, 30px)"); 48 | }); 49 | 50 | it("returns correct opacity property", () => { 51 | expect(style.opacity).toBe(0.8); 52 | }); 53 | 54 | it("returns correct transition property", () => { 55 | expect(style.transition).toBe("opacity 0.3s ease"); 56 | }); 57 | }); 58 | 59 | describe("getOverlayTopStyle", () => { 60 | const style = styles.getOverlayTopStyle(400, 500, 0.8, 0.3); 61 | 62 | it("returns correct width & height properties", () => { 63 | expect(style.width).toBe("400px"); 64 | expect(style.height).toBe("500px"); 65 | }); 66 | 67 | it("returns correct opacity property", () => { 68 | expect(style.opacity).toBe(0.8); 69 | }); 70 | 71 | it("returns correct transition property", () => { 72 | expect(style.transition).toBe("opacity 0.3s ease"); 73 | }); 74 | }); 75 | 76 | describe("getOverlayLeftStyle", () => { 77 | const style = styles.getOverlayLeftStyle(400, 500, 10, 0.8, 0.3); 78 | runOverlayTests(style); 79 | }); 80 | 81 | describe("getOverlayRightStyle", () => { 82 | const style = styles.getOverlayRightStyle(400, 500, 10, 0.8, 0.3); 83 | runOverlayTests(style); 84 | }); 85 | 86 | describe("getOverlayBottomStyle", () => { 87 | const style = styles.getOverlayBottomStyle(400, 500, 10, 0.8, 0.3); 88 | runOverlayTests(style); 89 | }); 90 | 91 | function runOverlayTests(style) { 92 | it("returns correct width & height properties", () => { 93 | expect(style.width).toBe("400px"); 94 | expect(style.height).toBe("500px"); 95 | }); 96 | 97 | it("returns correct top property", () => { 98 | expect(style.top).toBe("10px"); 99 | }); 100 | 101 | it("returns correct opacity property", () => { 102 | expect(style.opacity).toBe(0.8); 103 | }); 104 | 105 | it("returns correct transition property", () => { 106 | expect(style.transition).toBe("opacity 0.3s ease"); 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.js: -------------------------------------------------------------------------------- 1 | import utils from "../utils"; 2 | 3 | describe("invertNumber", () => { 4 | it("inverts a number within a range", () => { 5 | expect(utils.invertNumber(0, 10, 7)).toBe(3); 6 | }); 7 | 8 | it("inverts a negative number within a range", () => { 9 | expect(utils.invertNumber(-100, 0, -75)).toBe(-25); 10 | }); 11 | }); 12 | 13 | describe("convertRange", () => { 14 | it("converts number to equivalent in new range", () => { 15 | expect(utils.convertRange(0, 10, 100, 200, 5)).toBe(150); 16 | expect(utils.convertRange(0, 50, 50, 100, 5)).toBe(55); 17 | }); 18 | }); 19 | 20 | describe("convertWidthToPx", () => { 21 | it("converts percentage string to number of pixels", () => { 22 | expect(utils.convertWidthToPx("75%", 500)).toBe(375); 23 | }); 24 | 25 | it("converts string px to number of pixels", () => { 26 | expect(utils.convertWidthToPx("75px")).toBe(75); 27 | }); 28 | 29 | it("converts string number to number", () => { 30 | expect(utils.convertWidthToPx("75")).toBe(75); 31 | }); 32 | 33 | it("returns number if called with a number", () => { 34 | expect(utils.convertWidthToPx(75)).toBe(75); 35 | }); 36 | 37 | it("throws an error if width is not a number or string", () => { 38 | expect(utils.convertWidthToPx.bind(null, {})).toThrow(); 39 | }); 40 | }); 41 | 42 | describe("convertWidthToString", () => { 43 | it("converts number to string with 'px' suffix", () => { 44 | expect(utils.convertWidthToString(75)).toBe("75px"); 45 | }); 46 | 47 | it("returns original string if given a string", () => { 48 | expect(utils.convertWidthToString("75px")).toBe("75px"); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | MOUSE_ACTIVATION as MOUSE, 3 | TOUCH_ACTIVATION as TOUCH 4 | } from "react-input-position"; 5 | 6 | export const MOUSE_ACTIVATION = { 7 | CLICK: MOUSE.CLICK, 8 | DOUBLE_CLICK: MOUSE.DOUBLE_CLICK 9 | }; 10 | export const TOUCH_ACTIVATION = { 11 | TAP: TOUCH.TAP, 12 | DOUBLE_TAP: TOUCH.DOUBLE_TAP, 13 | LONG_TOUCH: TOUCH.LONG_TOUCH 14 | }; 15 | export { default as SideBySideMagnifier } from "./SideBySideMagnifier"; 16 | export { default as Magnifier } from "./Magnifier"; 17 | export { default as GlassMagnifier } from "./GlassMagnifier"; 18 | export { 19 | default as PictureInPictureMagnifier 20 | } from "./PictureInPictureMagnifier"; 21 | export { default as MagnifierContainer } from "./MagnifierContainer"; 22 | export { default as MagnifierPreview } from "./MagnifierPreview"; 23 | export { default as MagnifierZoom } from "./MagnifierZoom"; 24 | -------------------------------------------------------------------------------- /src/styles.js: -------------------------------------------------------------------------------- 1 | function getLargeImageStyle(positionX, positionY, active) { 2 | return { 3 | position: "absolute", 4 | boxSizing: "border-box", 5 | display: "block", 6 | top: 0, 7 | left: 0, 8 | transform: `translate(${positionX}px, ${positionY}px)`, 9 | zIndex: "1", 10 | visibility: !active ? "hidden" : "visible", 11 | width: "auto" 12 | }; 13 | } 14 | 15 | function getZoomContainerStyle(width, height, inPlace, switchSides) { 16 | const style = { 17 | position: "absolute", 18 | boxSizing: "border-box", 19 | pointerEvents: "none", 20 | width: `${width}px`, 21 | height: `${height}px`, 22 | top: "0", 23 | overflow: "hidden" 24 | }; 25 | 26 | if (inPlace) { 27 | style.left = "0px"; 28 | } else if (switchSides) { 29 | style.right = `${width}px`; 30 | } else { 31 | style.left = `${width}px`; 32 | } 33 | 34 | return style; 35 | } 36 | 37 | function getOverlayCenterStyle( 38 | width, 39 | height, 40 | left, 41 | top, 42 | opacity, 43 | transitionSpeed, 44 | color, 45 | backgroundImage, 46 | backgroundImageSize 47 | ) { 48 | const backgroundStyle = {}; 49 | 50 | if (backgroundImage) { 51 | backgroundStyle.backgroundImage = `url("${backgroundImage}")`; 52 | } 53 | 54 | if (backgroundImageSize) { 55 | backgroundStyle.backgroundSize = backgroundImageSize; 56 | } 57 | 58 | return { 59 | position: "absolute", 60 | width: `${width}px`, 61 | height: `${height}px`, 62 | left: 0, 63 | top: 0, 64 | boxSizing: "border-box", 65 | transform: `translate(${left}px, ${top}px)`, 66 | border: `1px solid ${color}`, 67 | opacity: opacity, 68 | transition: `opacity ${transitionSpeed}s ease`, 69 | zIndex: "15", 70 | pointerEvents: "none", 71 | ...backgroundStyle 72 | }; 73 | } 74 | 75 | function getOverlayTopStyle( 76 | width, 77 | height, 78 | opacity, 79 | transitionSpeed, 80 | backgroundColor 81 | ) { 82 | return { 83 | backgroundColor: backgroundColor, 84 | position: "absolute", 85 | boxSizing: "border-box", 86 | top: 0, 87 | left: 0, 88 | width: `${width}px`, 89 | height: `${height}px`, 90 | zIndex: "10", 91 | transition: `opacity ${transitionSpeed}s ease`, 92 | opacity: opacity, 93 | transform: "scale3d(1,1,1)", 94 | pointerEvents: "none" 95 | }; 96 | } 97 | 98 | function getOverlayLeftStyle( 99 | width, 100 | height, 101 | top, 102 | opacity, 103 | transitionSpeed, 104 | backgroundColor 105 | ) { 106 | return { 107 | backgroundColor: backgroundColor, 108 | position: "absolute", 109 | boxSizing: "border-box", 110 | width: `${width}px`, 111 | top: `${top}px`, 112 | left: 0, 113 | height: `${height}px`, 114 | zIndex: "10", 115 | transition: `opacity ${transitionSpeed}s ease`, 116 | opacity: opacity, 117 | transform: "scale3d(1,1,1)", 118 | pointerEvents: "none" 119 | }; 120 | } 121 | 122 | function getOverlayRightStyle( 123 | width, 124 | height, 125 | top, 126 | opacity, 127 | transitionSpeed, 128 | backgroundColor 129 | ) { 130 | return { 131 | backgroundColor: backgroundColor, 132 | position: "absolute", 133 | boxSizing: "border-box", 134 | top: `${top}px`, 135 | right: 0, 136 | width: `${width}px`, 137 | height: `${height}px`, 138 | zIndex: "10", 139 | transition: `opacity ${transitionSpeed}s ease`, 140 | opacity: opacity, 141 | transform: "scale3d(1,1,1)", 142 | pointerEvents: "none" 143 | }; 144 | } 145 | 146 | function getOverlayBottomStyle( 147 | width, 148 | height, 149 | top, 150 | opacity, 151 | transitionSpeed, 152 | backgroundColor 153 | ) { 154 | return { 155 | backgroundColor: backgroundColor, 156 | position: "absolute", 157 | boxSizing: "border-box", 158 | top: `${top}px`, 159 | width: `${width}px`, 160 | height: `${height}px`, 161 | zIndex: "10", 162 | transition: `opacity ${transitionSpeed}s ease`, 163 | opacity: opacity, 164 | transform: "scale3d(1,1,1)", 165 | pointerEvents: "none" 166 | }; 167 | } 168 | 169 | function getMagnifierZoomStyle(active, transitionSpeed) { 170 | return { 171 | position: "relative", 172 | opacity: active ? 1 : 0, 173 | transition: `opacity ${transitionSpeed}s ease` 174 | }; 175 | } 176 | 177 | export default { 178 | getLargeImageStyle, 179 | getZoomContainerStyle, 180 | getOverlayCenterStyle, 181 | getOverlayTopStyle, 182 | getOverlayLeftStyle, 183 | getOverlayRightStyle, 184 | getOverlayBottomStyle, 185 | getMagnifierZoomStyle 186 | }; 187 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | function invertNumber(min, max, num) { 2 | return max + min - num; 3 | } 4 | 5 | function convertRange(oldMin, oldMax, newMin, newMax, oldValue) { 6 | const percent = (oldValue - oldMin) / (oldMax - oldMin); 7 | const result = percent * (newMax - newMin) + newMin; 8 | return result || 0; 9 | } 10 | 11 | function convertWidthToPx(width, containerWidth) { 12 | if (typeof width === "number") { 13 | return width; 14 | } 15 | if (typeof width !== "string") { 16 | throw new Error(`Received: ${width} - Size must be a number or string`); 17 | } 18 | if (width.substr(-1) === "%") { 19 | const percent = 100 / Number(width.slice(0, -1)); 20 | return containerWidth / percent; 21 | } 22 | if (width.substr(-2) === "px") { 23 | return Number(width.slice(0, -2)); 24 | } 25 | return Number(width); 26 | } 27 | 28 | function convertWidthToString(width) { 29 | if (typeof width === "number") { 30 | return width + "px"; 31 | } 32 | return width; 33 | } 34 | 35 | function noop() {} 36 | 37 | export default { 38 | invertNumber, 39 | convertRange, 40 | convertWidthToPx, 41 | convertWidthToString, 42 | noop 43 | }; 44 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const htmlWebpackPlugin = new HtmlWebpackPlugin({ 4 | template: path.join(__dirname, "examples/src/index.html"), 5 | filename: "./index.html", 6 | favicon: "./examples/src/favicon.ico" 7 | }); 8 | 9 | module.exports = { 10 | entry: path.join(__dirname, "examples/src/index.js"), 11 | output: { 12 | path: path.join(__dirname, "examples/dist"), 13 | filename: "bundle.js" 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(js|jsx)$/, 19 | use: ["babel-loader", "eslint-loader"], 20 | exclude: /node_modules/ 21 | }, 22 | { 23 | test: /\.(jpg|jpeg|png)$/, 24 | use: "file-loader" 25 | }, 26 | { 27 | test: /\.css$/, 28 | use: ["style-loader", "css-loader"] 29 | } 30 | ] 31 | }, 32 | plugins: [htmlWebpackPlugin], 33 | resolve: { 34 | extensions: [".js", ".jsx"] 35 | }, 36 | devServer: { 37 | port: 3001 38 | } 39 | }; --------------------------------------------------------------------------------