1 | <p align="center">
|
2 | <a href="https://medium-zoom.francoischalifour.com"><img src="logo.svg" alt="Demo" width="64"></a>
|
3 | <h3 align="center">medium-zoom</h3>
|
4 | <p align="center">A JavaScript library for zooming images like Medium</p>
|
5 | </p>
|
6 |
|
7 | <p align="center">
|
8 | <a href="https://www.npmjs.com/package/medium-zoom">
|
9 | <img src="https://img.shields.io/npm/v/medium-zoom.svg?style=flat-square" alt="version">
|
10 | </a>
|
11 | <a href="https://github.com/francoischalifour/medium-zoom/blob/master/LICENSE">
|
12 | <img src="https://img.shields.io/npm/l/medium-zoom.svg?style=flat-square" alt="MIT license">
|
13 | </a>
|
14 | <a href="http://npmcharts.com/compare/medium-zoom">
|
15 | <img src="https://img.shields.io/npm/dm/medium-zoom.svg?style=flat-square" alt="downloads">
|
16 | </a>
|
17 | <br>
|
18 | <a href="https://unpkg.com/medium-zoom/dist/">
|
19 | <img src="http://img.badgesize.io/https://unpkg.com/medium-zoom/dist/medium-zoom.min.js?compression=gzip&label=gzip%20size&style=flat-square" alt="gzip size">
|
20 | </a>
|
21 | <a href="https://github.com/francoischalifour/medium-zoom/blob/master/package.json">
|
22 | <img src="https://img.shields.io/badge/dependencies-none-lightgrey.svg?style=flat-square" alt="no dependencies">
|
23 | </a>
|
24 | <a href="https://travis-ci.org/francoischalifour/medium-zoom">
|
25 | <img src="https://img.shields.io/travis/francoischalifour/medium-zoom.svg?style=flat-square" alt="travis">
|
26 | </a>
|
27 | </p>
|
28 |
|
29 | <p align="center">
|
30 | <a href="https://medium-zoom.francoischalifour.com">
|
31 | <img src="https://user-images.githubusercontent.com/6137112/43369906-7623239a-9376-11e8-978b-6e089be499fb.gif" alt="Medium Zoom Demo">
|
32 | </a>
|
33 | <br>
|
34 | <br>
|
35 | <strong>
|
36 | <a href="https://codesandbox.io/s/github/francoischalifour/medium-zoom/tree/master/website">🔬 Playground</a> ・
|
37 | <a href="https://medium-zoom.francoischalifour.com">🔎 Demo</a> ・
|
38 | <a href="https://medium-zoom.francoischalifour.com/storybook">📚 Storybook</a>
|
39 | </strong>
|
40 | </p>
|
41 |
|
42 | <details>
|
43 | <summary><strong>Contents</strong></summary>
|
44 |
|
45 |
|
46 | Generate the table of contents using:
|
47 |
|
48 | ```
|
49 | npx doctoc README.md --maxlevel 3
|
50 | ```
|
51 | -->
|
52 |
|
53 |
|
54 |
|
55 |
|
56 | - [Features](#features)
|
57 | - [Installation](#installation)
|
58 | - [Usage](#usage)
|
59 | - [API](#api)
|
60 | - [Selectors](#selectors)
|
61 | - [Options](#options)
|
62 | - [Methods](#methods)
|
63 | - [Attributes](#attributes)
|
64 | - [Events](#events)
|
65 | - [Examples](#examples)
|
66 | - [Browser support](#browser-support)
|
67 | - [Contributing](#contributing)
|
68 | - [License](#license)
|
69 |
|
70 |
|
71 |
|
72 | </details>
|
73 |
|
74 | ## Features
|
75 |
|
76 | - 📱 **Responsive** — _scale on mobile and desktop_
|
77 | - 🚀 **Performant and lightweight** — _should be able to reach 60 [fps](https://en.wikipedia.org/wiki/Frame_rate)_
|
78 | - ⚡️ **High definition support** — _load the HD version of your image on zoom_
|
79 | - 🔎 **Flexibility** — _apply the zoom to a selection of images_
|
80 | - 🖱 **Mouse, keyboard and gesture friendly** — _click anywhere, press a key or scroll away to close the zoom_
|
81 | - 🎂 **Event handling** — _trigger events when the zoom enters a new state_
|
82 | - 📦 **Customization** — _set your own margin, background and scroll offset_
|
83 | - 🔧 **Pluggable** — _add your own features to the zoom_
|
84 | - 💎 **Custom templates** — _extend the default look to match the UI of your app_
|
85 |
|
86 | ## Installation
|
87 |
|
88 | The module is available on the [npm](https://www.npmjs.com) registry.
|
89 |
|
90 | ```sh
|
91 | npm install medium-zoom
|
92 | # or
|
93 | yarn add medium-zoom
|
94 | ```
|
95 |
|
96 | ###### Download
|
97 |
|
98 | - [Normal](https://cdn.jsdelivr.net/npm/medium-zoom/dist/medium-zoom.js)
|
99 | - [Minified](https://cdn.jsdelivr.net/npm/medium-zoom/dist/medium-zoom.min.js)
|
100 |
|
101 | ###### CDN
|
102 |
|
103 | - [jsDelivr](https://www.jsdelivr.com/package/npm/medium-zoom)
|
104 | - [unpkg](https://unpkg.com/medium-zoom/)
|
105 |
|
106 | ## Usage
|
107 |
|
108 | > [Try it out in the browser](https://codesandbox.io/s/github/francoischalifour/medium-zoom/tree/master/website)
|
109 |
|
110 | Import the library as a module:
|
111 |
|
112 | ```js
|
113 | import mediumZoom from 'medium-zoom'
|
114 | ```
|
115 |
|
116 | Or import the library with a script tag:
|
117 |
|
118 | ```html
|
119 | <script src="node_modules/medium-zoom/dist/medium-zoom.min.js"></script>
|
120 | ```
|
121 |
|
122 | That's it! You don't need to import any CSS styles.
|
123 |
|
124 | Assuming you add the `data-zoomable` attribute to your images:
|
125 |
|
126 | ```js
|
127 | mediumZoom('[data-zoomable]')
|
128 | ```
|
129 |
|
130 | ## API
|
131 |
|
132 | ```ts
|
133 | mediumZoom(selector?: string | HTMLElement | HTMLElement[] | NodeList, options?: object): Zoom
|
134 | ```
|
135 |
|
136 | ### Selectors
|
137 |
|
138 | The selector allows attaching images to the zoom. It can be of the following types:
|
139 |
|
140 | - [CSS selectors](https://developer.mozilla.org/docs/Web/CSS/CSS_Selectors)
|
141 | - [`HTMLElement`](https://developer.mozilla.org/docs/Web/API/HTMLElement)
|
142 | - [`NodeList`](https://developer.mozilla.org/docs/Web/API/NodeList)
|
143 | - [`Array`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array)
|
144 |
|
145 | ```js
|
146 | // CSS selector
|
147 | mediumZoom('[data-zoomable]')
|
148 |
|
149 | // HTMLElement
|
150 | mediumZoom(document.querySelector('#cover'))
|
151 |
|
152 | // NodeList
|
153 | mediumZoom(document.querySelectorAll('[data-zoomable]'))
|
154 |
|
155 | // Array
|
156 | const images = [
|
157 | document.querySelector('#cover'),
|
158 | ...document.querySelectorAll('[data-zoomable]'),
|
159 | ]
|
160 |
|
161 | mediumZoom(images)
|
162 | ```
|
163 |
|
164 | ### Options
|
165 |
|
166 | The options enable the customization of the zoom. They are defined as an object with the following properties:
|
167 |
|
168 | | Property | Type | Default | Description |
|
169 | | -------------- | ------------------------------------- | -------- | --------------------------------------------------------------------------- |
|
170 | | `margin` | `number` | `0` | The space outside the zoomed image |
|
171 | | `background` | `string` | `"#fff"` | The background of the overlay |
|
172 | | `scrollOffset` | `number` | `40` | The number of pixels to scroll to close the zoom |
|
173 | | `container` | `string` \| `HTMLElement` \| `object` | `null` | The viewport to render the zoom in<br> [Read more →](docs/container.md) |
|
174 | | `template` | `string` \| `HTMLTemplateElement` | `null` | The template element to display on zoom<br> [Read more →](docs/template.md) |
|
175 |
|
176 | ```js
|
177 | mediumZoom('[data-zoomable]', {
|
178 | margin: 24,
|
179 | background: '#BADA55',
|
180 | scrollOffset: 0,
|
181 | container: '#zoom-container',
|
182 | template: '#zoom-template',
|
183 | })
|
184 | ```
|
185 |
|
186 | ### Methods
|
187 |
|
188 | #### `open({ target?: HTMLElement }): Promise<Zoom>`
|
189 |
|
190 | Opens the zoom and returns a promise resolving with the zoom.
|
191 |
|
192 | ```js
|
193 | const zoom = mediumZoom('[data-zoomable]')
|
194 |
|
195 | zoom.open()
|
196 | ```
|
197 |
|
198 | _Emits an event [`open`](#events) on animation start and [`opened`](#events) when completed._
|
199 |
|
200 | #### `close(): Promise<Zoom>`
|
201 |
|
202 | Closes the zoom and returns a promise resolving with the zoom.
|
203 |
|
204 | ```js
|
205 | const zoom = mediumZoom('[data-zoomable]')
|
206 |
|
207 | zoom.close()
|
208 | ```
|
209 |
|
210 | _Emits an event [`close`](#events) on animation start and [`closed`](#events) when completed._
|
211 |
|
212 | #### `toggle({ target?: HTMLElement }): Promise<Zoom>`
|
213 |
|
214 | Opens the zoom when closed / dismisses the zoom when opened, and returns a promise resolving with the zoom.
|
215 |
|
216 | ```js
|
217 | const zoom = mediumZoom('[data-zoomable]')
|
218 |
|
219 | zoom.toggle()
|
220 | ```
|
221 |
|
222 | #### `attach(...selectors: string[] | HTMLElement[] | NodeList[] | Array[]): Zoom`
|
223 |
|
224 | Attaches the images to the zoom and returns the zoom.
|
225 |
|
226 | ```js
|
227 | const zoom = mediumZoom()
|
228 |
|
229 | zoom.attach('#image-1', '#image-2')
|
230 | zoom.attach(
|
231 | document.querySelector('#image-3'),
|
232 | document.querySelectorAll('[data-zoomable]')
|
233 | )
|
234 | ```
|
235 |
|
236 | #### `detach(...selectors: string[] | HTMLElement[] | NodeList[] | Array[]): Zoom`
|
237 |
|
238 | Releases the images from the zoom and returns the zoom.
|
239 |
|
240 | ```js
|
241 | const zoom = mediumZoom('[data-zoomable]')
|
242 |
|
243 | zoom.detach('#image-1', document.querySelector('#image-2')) // detach two images
|
244 | zoom.detach() // detach all images
|
245 | ```
|
246 |
|
247 | _Emits an event [`detach`](#events) on the image._
|
248 |
|
249 | #### `update(options: object): Zoom`
|
250 |
|
251 | Updates the options and returns the zoom.
|
252 |
|
253 | ```js
|
254 | const zoom = mediumZoom('[data-zoomable]')
|
255 |
|
256 | zoom.update({ background: '#BADA55' })
|
257 | ```
|
258 |
|
259 | _Emits an event [`update`](#events) on each image of the zoom._
|
260 |
|
261 | #### `clone(options?: object): Zoom`
|
262 |
|
263 | Clones the zoom with provided options merged with the current ones and returns the zoom.
|
264 |
|
265 | ```js
|
266 | const zoom = mediumZoom('[data-zoomable]', { background: '#BADA55' })
|
267 |
|
268 | const clonedZoom = zoom.clone({ margin: 48 })
|
269 |
|
270 | clonedZoom.getOptions() // => { background: '#BADA55', margin: 48, ... }
|
271 | ```
|
272 |
|
273 | #### `on(type: string, listener: () => void, options?: boolean | AddEventListenerOptions): Zoom`
|
274 |
|
275 | Registers the listener on each target of the zoom.
|
276 |
|
277 | The same `options` as [`addEventListener`](https://developer.mozilla.org/docs/Web/API/EventTarget/addEventListener#Parameters) are used.
|
278 |
|
279 | ```js
|
280 | const zoom = mediumZoom('[data-zoomable]')
|
281 |
|
282 | zoom.on('closed', event => {
|
283 | // the image has been closed
|
284 | })
|
285 |
|
286 | zoom.on(
|
287 | 'open',
|
288 | event => {
|
289 | // the image has been opened (tracked only once)
|
290 | },
|
291 | { once: true }
|
292 | )
|
293 | ```
|
294 |
|
295 | The zoom object is accessible in `event.detail.zoom`.
|
296 |
|
297 | #### `off(type: string, listener: () => void, options?: boolean | AddEventListenerOptions): Zoom`
|
298 |
|
299 | Removes the previously registered listener on each target of the zoom.
|
300 |
|
301 | The same `options` as [`removeEventListener`](https://developer.mozilla.org/docs/Web/API/EventTarget/removeEventListener#Parameters) are used.
|
302 |
|
303 | ```js
|
304 | const zoom = mediumZoom('[data-zoomable]')
|
305 |
|
306 | function listener(event) {
|
307 | // ...
|
308 | }
|
309 |
|
310 | zoom.on('open', listener)
|
311 | // ...
|
312 | zoom.off('open', listener)
|
313 | ```
|
314 |
|
315 | The zoom object is accessible in `event.detail.zoom`.
|
316 |
|
317 | #### `getOptions(): object`
|
318 |
|
319 | Returns the zoom options as an object.
|
320 |
|
321 | ```js
|
322 | const zoom = mediumZoom({ background: '#BADA55' })
|
323 |
|
324 | zoom.getOptions() // => { background: '#BADA55', ... }
|
325 | ```
|
326 |
|
327 | #### `getImages(): HTMLElement[]`
|
328 |
|
329 | Returns the images attached to the zoom as an array of [`HTMLElement`s](https://developer.mozilla.org/docs/Web/API/HTMLElement).
|
330 |
|
331 | ```js
|
332 | const zoom = mediumZoom('[data-zoomable]')
|
333 |
|
334 | zoom.getImages() // => [HTMLElement, HTMLElement]
|
335 | ```
|
336 |
|
337 | #### `getZoomedImage(): HTMLElement`
|
338 |
|
339 | Returns the current zoomed image as an [`HTMLElement`](https://developer.mozilla.org/docs/Web/API/HTMLElement) or `null` if none.
|
340 |
|
341 | ```js
|
342 | const zoom = mediumZoom('[data-zoomable]')
|
343 |
|
344 | zoom.getZoomedImage() // => null
|
345 | zoom.open().then(() => {
|
346 | zoom.getZoomedImage() // => HTMLElement
|
347 | })
|
348 | ```
|
349 |
|
350 | ### Attributes
|
351 |
|
352 | #### `data-zoom-src`
|
353 |
|
354 | Specifies the high definition image to open on zoom. This image loads when the user clicks on the source image.
|
355 |
|
356 | ```html
|
357 | <img src="image-thumbnail.jpg" data-zoom-src="image-hd.jpg" alt="My image" />
|
358 | ```
|
359 |
|
360 | ### Events
|
361 |
|
362 | | Event | Description |
|
363 | | ------ | --------------------------------------------------- |
|
364 | | open | Fired immediately when the `open` method is called |
|
365 | | opened | Fired when the zoom has finished being animated |
|
366 | | close | Fired immediately when the `close` method is called |
|
367 | | closed | Fired when the zoom out has finished being animated |
|
368 | | detach | Fired when the `detach` method is called |
|
369 | | update | Fired when the `update` method is called |
|
370 |
|
371 | ```js
|
372 | const zoom = mediumZoom('[data-zoomable]')
|
373 |
|
374 | zoom.on('open', event => {
|
375 | // track when the image is zoomed
|
376 | })
|
377 | ```
|
378 |
|
379 | The zoom object is accessible in `event.detail.zoom`.
|
380 |
|
381 | ## Examples
|
382 |
|
383 | <details>
|
384 | <summary>Trigger a zoom from another element</summary>
|
385 |
|
386 | ```js
|
387 | const button = document.querySelector('[data-action="zoom"]')
|
388 | const zoom = mediumZoom('#image')
|
389 |
|
390 | button.addEventListener('click', () => zoom.open())
|
391 | ```
|
392 |
|
393 | </details>
|
394 |
|
395 | <details>
|
396 | <summary>Track an event (for analytics)</summary>
|
397 |
|
398 | You can use the `open` event to keep track of how many times a user interacts with your image. This can be useful if you want to gather some analytics on user engagement.
|
399 |
|
400 | ```js
|
401 | let counter = 0
|
402 | const zoom = mediumZoom('#image-tracked')
|
403 |
|
404 | zoom.on('open', event => {
|
405 | console.log(`"${event.target.alt}" has been zoomed ${++counter} times`)
|
406 | })
|
407 | ```
|
408 |
|
409 | </details>
|
410 |
|
411 | <details>
|
412 | <summary>Detach a zoom once closed</summary>
|
413 |
|
414 | ```js
|
415 | const zoom = mediumZoom('[data-zoomable]')
|
416 |
|
417 | zoom.on('closed', () => zoom.detach(), { once: true })
|
418 | ```
|
419 |
|
420 | </details>
|
421 |
|
422 | <details>
|
423 | <summary>Attach jQuery elements</summary>
|
424 |
|
425 | jQuery elements are compatible with `medium-zoom` once converted to an array.
|
426 |
|
427 | ```js
|
428 | mediumZoom($('[data-zoomable]').toArray())
|
429 | ```
|
430 |
|
431 | </details>
|
432 |
|
433 | <details>
|
434 | <summary>Create a zoomable React component</summary>
|
435 |
|
436 | **Using React hooks**
|
437 |
|
438 | ```js
|
439 | import React from 'react'
|
440 | import mediumZoom from 'medium-zoom'
|
441 |
|
442 | function ImageZoom({ zoom, src, alt, background }) {
|
443 | const zoomRef = React.useRef(zoom.clone({ background }))
|
444 |
|
445 | function attachZoom(image) {
|
446 | zoomRef.current.attach(image)
|
447 | }
|
448 |
|
449 | return <img src={src} alt={alt} ref={attachZoom} />
|
450 | }
|
451 |
|
452 | function App() {
|
453 | const zoom = React.useRef(mediumZoom({ background: '#000', margin: 48 }))
|
454 |
|
455 | render() {
|
456 | return (
|
457 | <ImageZoom src="image.jpg" alt="Image" zoom={zoom.current} color="#BADA55" />
|
458 | )
|
459 | }
|
460 | }
|
461 | ```
|
462 |
|
463 | **Using React classes**
|
464 |
|
465 | ```js
|
466 | import React, { Component } from 'react'
|
467 | import mediumZoom from 'medium-zoom'
|
468 |
|
469 | class ImageZoom extends Component {
|
470 | zoom = this.props.zoom.clone({
|
471 | background: this.props.color,
|
472 | })
|
473 |
|
474 | attachZoom = image => {
|
475 | this.zoom.attach(image)
|
476 | }
|
477 |
|
478 | render() {
|
479 | return (
|
480 | <img src={this.props.src} alt={this.props.alt} ref={this.attachZoom} />
|
481 | )
|
482 | }
|
483 | }
|
484 |
|
485 | class App extends Component {
|
486 | zoom = mediumZoom({ background: '#000', margin: 48 })
|
487 |
|
488 | render() {
|
489 | return (
|
490 | <ImageZoom src="image.jpg" alt="Image" zoom={this.zoom} color="#BADA55" />
|
491 | )
|
492 | }
|
493 | }
|
494 | ```
|
495 |
|
496 | </details>
|
497 | <br>
|
498 |
|
499 | You can see [more examples](examples/) including [React](examples/react) and [Vue](examples/vue), or check out the [storybook](https://medium-zoom.francoischalifour.com/storybook).
|
500 |
|
501 | ## Browser support
|
502 |
|
503 | | IE | Edge | Chrome | Firefox | Safari |
|
504 | | --------------- | --------------- | ------ | ------- | ------ |
|
505 | | 10<sup>\*</sup> | 12<sup>\*</sup> | 36 | 34 | 9 |
|
506 |
|
507 | <sup>\*</sup> _These browsers require a [`template` polyfill](https://github.com/webcomponents/template) when using [custom templates](docs/template.md)_.
|
508 |
|
509 | <blockquote>
|
510 | <p align="center">
|
511 | Cross-browser testing is sponsored by
|
512 | </p>
|
513 | <p align="center">
|
514 | <a href="https://www.browserstack.com">
|
515 | <img src="https://user-images.githubusercontent.com/6137112/44587083-35987000-a7b2-11e8-8e0d-8ba15de83802.png" alt="BrowserStack" height="35">
|
516 | </a>
|
517 | </p>
|
518 | </blockquote>
|
519 |
|
520 | ## Contributing
|
521 |
|
522 | - Run `yarn` to install Node dev dependencies
|
523 | - Run `yarn start` to build the library in watch mode
|
524 | - Run `yarn run storybook` to see your changes at http://localhost:9001
|
525 |
|
526 | Please read the [contributing guidelines](CONTRIBUTING.md) for more detailed explanations.
|
527 |
|
528 | _You can also use [npm](https://www.npmjs.com)._
|
529 |
|
530 | ## License
|
531 |
|
532 | MIT © [François Chalifour](https://francoischalifour.com)
|