npm package: @superkoders/modal
Description: Fancybox like modal
Author: Petr Grochal
Latest version: 2.2.0

Modal window/popup which main purpose is to open as image galery. But it can handle also other type of content like Youtube, Vimeo or HTML5 videos, iframes, custom HTML content or HTML content from some existing element on the page. It can recognize type of media from the source url. If it is not possible to recognize media type, default type is iframe. You can force media type by `medium` parameter (see examples below).

## Installing
Using npm
```
$ npm install @superkoders/modal
```
Using yarn
```
$ yarn add @superkoders/modal
```

## How to use
There are two methods exported: `initModal` and `createModal`. First one adds click listeners to each element in given context which matches `modalSelector` option (by default `'[data-modal]'`). It returns array of modal instances created. Second one is for creating custom modal instance.

### initModal: `(options: SKModalOptions) => SKModal[]`
```
import { initModal } from '@superkoders/modal';

// use default options
initModal();

// or set some custom options
initModal({
	customWrapperClass: 'my-custom-modal',
	closeOnBgClick: true,
});
```

#### How to create modal triggers in your HTML code
The easiest way is to do it with an `a` tag using its `href` attribute as an url for modal.
```
<a href="https://www.youtube.com/watch?v=RhMYBfF7-hE" data-modal>Open Youtube video in the modal</a>
```
If you want to use some other element like `button`, you need to specify url in the `data-modal` attribute.
```
<button data-modal='{"url": "https://www.youtube.com/watch?v=RhMYBfF7-hE"}'>Open Youtube video in the modal</button>
```

### createModal: `(items: SKModalItem | SKModalItem[], options?: Partial<SKModalOptions>) => SKModal`
In this case, there are no triggers which open modal. Once you create an instance, you need to open it by yourself. Also you need to pass modal items, where only mandatory property is `url`. In our example below the modal will have two items, first is some image, second is some video.
```
import { createModal } from '@superkoders/modal';

const items = [
	{
		url: 'url/to/some/image.jpg'
	},
	{
		url: 'url/to/some/video.mp4'
	}
]

// create instance
const customModal = createModal(items, {
	customWrapperClass: 'my-custom-modal',
	closeOnBgClick: true,
});

// open modal
customModal.methods.open();

// go to next slide
customModal.methods.next();

// close modal
customModal.methods.close();
```

## Documentation

### Modal options `SKModalOptions`

These options you can adjust during initialization.

#### context: `Element | Document`

Context element, in which to search for modal items (triggers)\
Default value: `document`

#### parentElement: `Element`

Element, which is used as a parent for modal element in DOM\
Default value: `document.body`

#### modalSelector: `string`

Element selector, which is used for modal triggers\
Default value: `'[data-modal]'`

#### closeSelector: `string`

Element selector, which is used to close the modal\
Default value: `'.js-modal-close'`

#### customWrapperClass: `string`

Custom CSS class, which is added to the modal element\
Default value: `undefined`

#### infinite: `boolean`

It determines if modal items are shown in a loop\
Default value: `false`

#### closeOnBgClick: `boolean`

It determines if modal will close after background click\
Default value: `undefined`

#### plugins: `SKModalPlugin[]`

Plugins array to initialize\
Default value: `undefined`

#### hideNav: `boolean`

It determines if the navigation (arrows, bullets) is hidden or not\
Default value: `undefined`

#### customData: `any`

Any data which you can use for example in plugins\
Default value: undefined

#### closeIcon: `string`

String which represents an icon for close button, it can be also HTML code\
Default value: `'&#x000D7;'`

#### prevIcon: `string`

String which represents an icon for previous button, it can be also HTML code\
Default value: `'&#x02190;'`

#### nextIcon: `string`

String which represents an icon for next button, it can be also HTML code\
Default value: `'&#x02192;'`

#### loadIcon: `string`

String which represents an icon for loader, it can be also HTML code\
Default value: `'<span class="b-modal__loader-icon"><span>&#x021BA;</span></span>'`

#### removeOnClose: `boolean`

It determines if modal element will be removed from DOM after close\
Default value: `false`

#### enableFocusTrap: `boolean`
Enables or disables focus trap.
Default value: `true`

#### focusTrapOptions: `FocusTrapOptions`

Focus trap custom options. See [Focus trap package page](https://www.npmjs.com/package/focus-trap) for more info
Default value:
```
{
	allowOutsideClick: true,
}
```

#### useSwipe: `boolean`
Enables or disables swipe slide change on touch devices.
Default value: `true`

#### ariaLabel: `string`
Modal title for assistive technologies.
Default value: `undefined`

#### buildStructure: `(modal: Element, fragments: SKModalFragments) => void`

Method which builds the modal HTML structure, not really necessary to change if you use grid for CSS\
Default value:
```
(modal, {
	ariaLabel,
	header,
	titleElem,
	descElem,
	content,
	prevElem,
	nextElem,
	navElem,
	loader,
	bg,
}) => {
	const wrapper = document.createElement('div');
	wrapper.classList.add('b-modal__wrapper');

	if (ariaLabel) {
		modal.setAttribute('aria-labelledby', ariaLabel.id);
		modal.appendChild(ariaLabel);
	}

	wrapper.appendChild(header);
	wrapper.appendChild(titleElem);
	wrapper.appendChild(descElem);
	wrapper.appendChild(content);
	wrapper.appendChild(prevElem);
	wrapper.appendChild(nextElem);
	wrapper.appendChild(navElem);
	modal.appendChild(wrapper);
	modal.appendChild(loader);
	modal.appendChild(bg);
}
```

#### headerTpl: `(options?: SKModalOptions) => string`

Method for header template\
Default value:
```
(options) => `
	<button type="button" class="b-modal__close ${ModalClasses.CLOSE}">
		<span class="b-modal__close-text u-vhide">Close</span>
		${options.closeIcon}
	</button>
`
```

#### titleTpl: `(text: string) => string`

Method for title template\
Default value: 
```
(text: string) => `<h3>${text}</h3>`
```

#### descTpl: `(text: string) => string`

Method for description template\
Default value:
```
(text: string) => `<p>${text}<p>`
```

#### prevTpl: `(options?: SKModalOptions) => string`

Method for previous button template\
Default value:
```
(options) => `<button type="button" class="b-modal__prev-btn ${ModalClasses.PREV_TRIGGER}">${options.prevIcon}</button>`
```

#### nextTpl: `(options?: SKModalOptions) => string`

Method for next button template\
Default value:
```
(options) => `<button type="button" class="b-modal__next-btn ${ModalClasses.NEXT_TRIGGER}">${options.nextIcon}</button>`
```

#### navTpl: `(items: SKModalItem[]) => string`

Method for navigation (bullets) template\
Default value:
```
(items) =>
	items
		.map((_item, index) => `<span class="b-modal__nav-item ${ModalClasses.NAV_TRIGGER}" data-modal-index="${index}"></span>`)
		.join('')
```

#### loaderTpl: `(options?: SKModalOptions) => string`

Method for loader template\
Default value:
```
(options) => `<span class="b-modal__loader-icon"><span>${options.loadIcon}</span></span>`
```

#### imageLoader: `(src: string) => Promise<Node>`

Method which is used for loading images\
Default value:
```
(src) => {
	return new Promise((resolve, reject) => {
		let img = new Image();
		img.addEventListener('load', () => resolve(img));
		img.addEventListener('error', () => {
			reject(new Error(`Failed to load image with src: ${src}`));
		});
		img.draggable = false;
		img.src = src;
	});
}
```

#### fetchLoader: `(url: string, element?: Element) => Promise<string>`

Method which is used for fetching content from a server\
Default value:
```
async (url) => await fetch(url).then((response) => response.text())
```

#### onModalOpen: `(event?: Event, data?: { id: number }) => void`

Handler, which runs after modal is opened\
Default value: `undefined`

#### onModalClose: `(event?: Event, data?: { id: number }) => void`

Handler, which runs after modal is closed\
Default value: `undefined`

#### onModalContentLoaded: `(event?: Event, data?: { modal: SKModal }) => void`

Handler, which runs after modal content is loaded\
Default value: `undefined`

#### onModalSlideChanged: `(event?: Event, data?: { page: number }) => void`

Handler, which runs after modal slide has changed\
Default value: `undefined`

### Modal items (SKModalItem)

All these parameters is possible to add to `data-modal` attribute. In case of `createModal`, these are properties of the modal item object.

#### url: `string | string[]`

Media url. In case of images, it can be an array. Then more images are shown on one slide.

#### medium: `ModalMedium`

Media type. Posible values in `ModalMedium` enum exported from `@superkoders/modal`

#### title: `string`

Text, which will appear in modal title

#### desc: `string`

Text, which will appear in modal description

#### gallery: `string`

Gallery key, which is used for grouping items into one modal. If it is undefined, each modal uses its own popup.

#### modalClass: `string`

CSS class which will be aplied for modal wrapper element, when modal is opened.

#### [key: string]: `any`

It is possible to add more custom parameters, which can be used in plugins for example.

### Plugins (SKModalPlugin)

Plugins enable another modal customization. It has these three parameters:

#### init: (modal: SKModal) => void

Init method

#### destroy: (modal: SKModal) => void

Destroy method, it runs after modal redraw (if `reInitOnRedraw` is set to `true`) or after modal destroy

#### reInitOnRedraw: boolean

It determines, if plugin should be reinitialized after modal redraw.

#### Plugin example
my-plugin.js
```
export const myPlugin = {
	init: (modal) => {
		// do some stuff like add event listeners or add some elements to DOM
	},
	destroy: (modal) => {
		// use when you need to remove some listeners or remove some elements from DOM
	},
	reInitOnRedraw: true,
}
```
modal.js
```
import { initModal } from '@superkoders/modal';
import { myPlugin } from './my-plugin.js';

initModal({
	plugins: [myPlugin]
});
```

## Version 2.0 major changes

1. The wrapper element is now a `dialog` element. It has better accessibility. You can check browser support [here](https://caniuse.com/dialog). If you need to support browser wich don't support `dialog` element, you can use older version (1.7.0).
2. `scroll-lock` npm package has been removed. We prefer to use CSS pure solution. It can be done by adding `scrollbar-gutter: stable` on `html` element and `overflow: clip` on body element. For this purpose, class `is-modal-open` is added to `body` when dialog is opened. If you prefer to use `scroll-lock`, you can use older version (1.7.0)
3. The `dialog` element comes with its own focus trap functionality. This however did not meet our requirements, so we are still using `focus-trap` npm package for this. If you prefer to use native `dialog` focus trap functionality, you can disable focus trap with `enableFocusTrap` option (set it to `false`).

## Troubleshooting

`@superkoders/modal` uses some functionality from `@superkoders/sk-tools`. If you are already using `@superkoders/sk-tools` on your project, check if the version is at least `1.4.0`. If not, you need to update the package.