├── .gitignore
├── assets
└── screenshot.png
├── .npmignore
├── postcss.config.js
├── .eslintrc
├── src
├── svg
│ ├── toolbox.svg
│ ├── slider.svg
│ └── fit.svg
├── uploader.js
├── tunes.js
├── index.pcss
├── index.js
└── ui.js
├── vite.config.js
├── LICENSE
├── package.json
├── README.md
├── dist
├── gallery.umd.js
└── gallery.mjs
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | npm-debug.log
3 | .idea/
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/VolgaIgor/editorjs-gallery/HEAD/assets/screenshot.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | dev/
3 | src/
4 | .babelrc
5 | .eslintrc
6 | vite.config.js
7 | postcss.config.js
8 | yarn.lock
9 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('postcss-nested-ancestors'),
4 | require('postcss-nested'),
5 | ],
6 | };
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "codex"
4 | ],
5 | "globals": {
6 | "fetch": true,
7 | "ImageConfig": true,
8 | "ImageToolData": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/svg/toolbox.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
3 | import * as pkg from "./package.json";
4 |
5 | const NODE_ENV = process.argv.mode || "development";
6 | const VERSION = pkg.version;
7 |
8 | export default {
9 | build: {
10 | copyPublicDir: false,
11 | lib: {
12 | entry: path.resolve(__dirname, "src", "index.js"),
13 | name: "ImageGallery",
14 | fileName: "gallery",
15 | },
16 | },
17 | define: {
18 | NODE_ENV: JSON.stringify(NODE_ENV),
19 | VERSION: JSON.stringify(VERSION),
20 | },
21 |
22 | plugins: [cssInjectedByJsPlugin()],
23 | };
24 |
--------------------------------------------------------------------------------
/src/svg/slider.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 СodeX
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.
22 |
--------------------------------------------------------------------------------
/src/svg/fit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@kiberpro/editorjs-gallery",
3 | "version": "1.3.0",
4 | "keywords": [
5 | "codex editor",
6 | "tool",
7 | "image",
8 | "gallery",
9 | "editor.js",
10 | "editorjs"
11 | ],
12 | "description": "Image Gallery Tool with source field for Editor.js",
13 | "license": "MIT",
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/VolgaIgor/editorjs-gallery.git"
17 | },
18 | "files": [
19 | "dist"
20 | ],
21 | "main": "./dist/gallery.umd.js",
22 | "module": "./dist/gallery.mjs",
23 | "exports": {
24 | ".": {
25 | "import": "./dist/gallery.mjs",
26 | "require": "./dist/gallery.umd.js"
27 | }
28 | },
29 | "scripts": {
30 | "dev": "vite",
31 | "build": "vite build",
32 | "lint": "eslint src/ --ext .js",
33 | "lint:errors": "eslint src/ --ext .js --quiet",
34 | "lint:fix": "eslint src/ --ext .js --fix"
35 | },
36 | "author": "Igor Shuvalov «VolgaIgor» & CodeX",
37 | "devDependencies": {
38 | "@codexteam/ajax": "^4.2.0",
39 | "eslint": "^6.8.0",
40 | "eslint-config-codex": "^1.3.3",
41 | "eslint-loader": "^4.0.0",
42 | "formidable": "^1.2.1",
43 | "postcss-nested": "^4.1.0",
44 | "postcss-nested-ancestors": "^2.0.0",
45 | "request": "^2.88.0",
46 | "vite": "^4.5.0",
47 | "vite-plugin-css-injected-by-js": "^3.3.0"
48 | },
49 | "dependencies": {
50 | "@codexteam/icons": "^0.0.6"
51 | },
52 | "bugs": {
53 | "url": "https://github.com/VolgaIgor/editorjs-gallery/issues"
54 | },
55 | "homepage": "https://github.com/VolgaIgor/editorjs-gallery#readme"
56 | }
57 |
--------------------------------------------------------------------------------
/src/uploader.js:
--------------------------------------------------------------------------------
1 | import ajax from '@codexteam/ajax';
2 |
3 | /**
4 | * Module for file uploading. Handle 3 scenarios:
5 | * 1. Select file from device and upload
6 | * 2. Upload by pasting URL
7 | * 3. Upload by pasting file from Clipboard or by Drag'n'Drop
8 | */
9 | export default class Uploader {
10 | /**
11 | * @param {object} params - uploader module params
12 | * @param {ImageConfig} params.config - image tool config
13 | */
14 | constructor({ config }) {
15 | this.config = config;
16 | }
17 |
18 | /**
19 | * Handle clicks on the upload file button
20 | */
21 | uploadSelectedFiles(maxElementCount, { onPreview, onUpload, onError }) {
22 | ajax.selectFiles({
23 | accept: this.config.types,
24 | multiple: true
25 | }).then((files) => {
26 | let loadedFiles = 0;
27 | for (var i = 0; i < files.length; i++) {
28 | if (maxElementCount !== null && loadedFiles == maxElementCount) {
29 | break;
30 | } else {
31 | loadedFiles++;
32 | }
33 |
34 | let file = files[i];
35 | let previewElem = onPreview(file);
36 |
37 | let uploader;
38 |
39 | if (this.config.uploader && typeof this.config.uploader.uploadByFile === 'function') {
40 | const customUpload = this.config.uploader.uploadByFile(file);
41 |
42 | if (!isPromise(customUpload)) {
43 | console.warn('Custom uploader method uploadByFile should return a Promise');
44 | }
45 |
46 | uploader = customUpload;
47 | } else {
48 | uploader = this.uploadByFile(file);
49 | }
50 |
51 | uploader.then((response) => {
52 | onUpload(response, previewElem);
53 | }).catch((error) => {
54 | onError(error, previewElem);
55 | });
56 | }
57 | });
58 | }
59 |
60 | /**
61 | * Default file uploader
62 | * Fires ajax.post()
63 | *
64 | * @param {File} file - file pasted by drag-n-drop
65 | */
66 | uploadByFile(file) {
67 | const formData = new FormData();
68 |
69 | formData.append(this.config.field, file);
70 |
71 | if (this.config.additionalRequestData && Object.keys(this.config.additionalRequestData).length) {
72 | Object.entries(this.config.additionalRequestData).forEach(([name, value]) => {
73 | formData.append(name, value);
74 | });
75 | }
76 |
77 | return ajax.post({
78 | url: this.config.endpoints.byFile,
79 | data: formData,
80 | type: ajax.contentType.JSON,
81 | headers: this.config.additionalRequestHeaders,
82 | }).then(response => response.body);
83 | }
84 | }
85 |
86 | /**
87 | * Check if passed object is a Promise
88 | *
89 | * @param {*} object - object to check
90 | * @returns {boolean}
91 | */
92 | function isPromise(object) {
93 | return object && typeof object.then === "function";
94 | }
95 |
--------------------------------------------------------------------------------
/src/tunes.js:
--------------------------------------------------------------------------------
1 | import { make } from './ui';
2 | import sliderIcon from './svg/slider.svg?raw';
3 | import fitIcon from './svg/fit.svg?raw';
4 |
5 | /**
6 | * Working with Block Tunes
7 | */
8 | export default class Tunes {
9 | /**
10 | * @param {object} tune - image tool Tunes managers
11 | * @param {object} tune.api - Editor API
12 | * @param {object} tune.actions - list of user defined tunes
13 | * @param {Function} tune.onChange - tune toggling callback
14 | */
15 | constructor({ api, actions, onChange }) {
16 | this.api = api;
17 | this.actions = actions;
18 | this.onChange = onChange;
19 | this.buttons = [];
20 | }
21 |
22 | /**
23 | * Available Image tunes
24 | *
25 | * @returns {{name: string, icon: string, title: string}[]}
26 | */
27 | static get tunes() {
28 | return [
29 | {
30 | name: 'slider',
31 | icon: sliderIcon,
32 | title: 'Slider',
33 | },
34 | {
35 | name: 'fit',
36 | icon: fitIcon,
37 | title: 'Fit',
38 | },
39 | ];
40 | }
41 |
42 | /**
43 | * Styles
44 | *
45 | * @returns {{wrapper: string, buttonBase: *, button: string, buttonActive: *}}
46 | */
47 | get CSS() {
48 | return {
49 | wrapper: 'image-gallery__tune-wrapper',
50 | buttonBase: this.api.styles.button,
51 | button: 'image-gallery__tune',
52 | buttonActive: 'active',
53 | };
54 | }
55 |
56 | /**
57 | * Makes buttons with tunes
58 | *
59 | * @param {ImageGalleryData} toolData - generate Elements of tunes
60 | * @returns {Element}
61 | */
62 | render(toolData) {
63 | const wrapper = make('div', this.CSS.wrapper);
64 |
65 | const tunes = this.actions ?? Tunes.tunes;
66 |
67 | this.buttons = [];
68 |
69 | tunes.forEach(tune => {
70 | const title = this.api.i18n.t(tune.title);
71 | const el = make('div', [this.CSS.buttonBase, this.CSS.button], {
72 | innerHTML: tune.icon,
73 | title,
74 | });
75 |
76 | el.addEventListener('click', () => {
77 | this.tuneClicked(tune.name, tune.action);
78 | });
79 |
80 | el.dataset.tune = tune.name;
81 | el.classList.toggle(this.CSS.buttonActive, toolData.style === tune.name);
82 |
83 | this.buttons.push(el);
84 |
85 | this.api.tooltip.onHover(el, title, {
86 | placement: 'top',
87 | });
88 |
89 | wrapper.appendChild(el);
90 | });
91 |
92 | return wrapper;
93 | }
94 |
95 | /**
96 | * Clicks to one of the tunes
97 | *
98 | * @param {string} tuneName - clicked tune name
99 | * @param {Function} customFunction - function to execute on click
100 | */
101 | tuneClicked(tuneName, customFunction) {
102 | if (typeof customFunction === 'function') {
103 | if (!customFunction(tuneName)) {
104 | return false;
105 | }
106 | }
107 |
108 | this.buttons.forEach(button => {
109 | button.classList.toggle(this.CSS.buttonActive, button.dataset.tune === tuneName);
110 | });
111 |
112 | this.onChange(tuneName);
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Gallery block for Editor.js
4 |
5 | Loader based on [editor-js/image](https://github.com/editor-js/image).
6 |
7 | ### Preview
8 | 
9 |
10 | ### Features
11 | * Multiple downloads
12 | * Sorting uploaded images (providing by [SortableJS](https://github.com/SortableJS/Sortable))
13 | * Limit the number of images
14 | * Two view selector (slider and fit)
15 |
16 | ## Installation
17 | ### Install via NPM
18 | Get the package
19 |
20 | ```shell
21 | $ npm i @kiberpro/editorjs-gallery
22 | ```
23 |
24 | Include module at your application
25 |
26 | ```javascript
27 | import ImageGallery from '@kiberpro/editorjs-gallery';
28 | ```
29 |
30 | ### Load from CDN
31 |
32 | You can load a specific version of the package from jsDelivr CDN.
33 |
34 | Require this script on a page with Editor.js.
35 |
36 | ```html
37 |
38 | ```
39 |
40 | ### Download to your project's source dir
41 |
42 | 1. Upload folder `dist` from repository
43 | 2. Add `dist/gallery.umd.js` file to your page.
44 |
45 | ### Enable sorting
46 | To enable sorting, include the SortableJS library and send it to the configuration:
47 | ```shell
48 | $ npm i sortablejs
49 | ```
50 | ```javascript
51 | import Sortable from 'sortablejs';
52 | ```
53 |
54 | ## Usage
55 | ```javascript
56 | var editor = EditorJS({
57 | // ...
58 | tools: {
59 | // ...
60 | gallery: {
61 | class: ImageGallery,
62 | config: {
63 | sortableJs: Sortable,
64 | endpoints: {
65 | byFile: 'http://localhost:8008/uploadFile',
66 | }
67 | },
68 | },
69 | }
70 | // ...
71 | });
72 | ```
73 |
74 | ## Config Params
75 |
76 | Gallery block supports these configuration parameters:
77 |
78 | | Field | Type | Description |
79 | | ----- | -------- | ------------------ |
80 | | sortableJs | `object` | SortableJS library |
81 | | maxElementCount | `int` | (default: `undefined`) Maximum allowed number of images |
82 | | buttonContent | `string` | (default: `Select an Image`) Label for upload button |
83 | | uploader | `{{uploadByFile: function}}` | Optional custom uploading method. [See details](https://github.com/editor-js/image#providing-custom-uploading-methods). |
84 | | actions | `[{name: string, icon: string, title: string}]` | Array with custom switches |
85 | | [And others from the original ](https://github.com/editor-js/image#config-params) |
86 |
87 | Also you can add a localized string:
88 | ```javascript
89 | new Editorjs({
90 | // ...
91 | tools: {
92 | gallery: ImageGallery
93 | },
94 | i18n: {
95 | tools: {
96 | gallery: {
97 | 'Select an Image': 'Выберите изображение',
98 | 'Delete': 'Удалить',
99 | 'Gallery caption': 'Подпись'
100 | }
101 | }
102 | },
103 | })
104 | ```
105 |
106 | ## Output data
107 |
108 | This Tool returns `data` with following format
109 |
110 | | Field | Type | Description |
111 | | -------------- | --------- | -------------------------------- |
112 | | files | `object[]` | Uploaded file datas. Any data got from backend uploader. Always contain the `url` property |
113 | | source | `string` | image's source |
114 | | style | `string` | (`fit` of `slider`) gallery view |
115 |
--------------------------------------------------------------------------------
/src/index.pcss:
--------------------------------------------------------------------------------
1 | .image-gallery {
2 | --bg-color: #cdd1e0;
3 | --front-color: #388ae5;
4 | --border-color: #e8e8eb;
5 |
6 | &__container {
7 | background: black;
8 | margin-bottom: 10px;
9 | padding: 5px;
10 | }
11 |
12 | &__controls {
13 | display: flex;
14 | gap: 10px;
15 | padding: 8px 2px 3px;
16 | }
17 |
18 | &__items {
19 | display: grid;
20 | gap: 10px;
21 | grid-template-columns: 1fr 1fr 1fr;
22 | padding: 10px;
23 | background-color: #222222;
24 | }
25 |
26 | &__items:empty {
27 | display: none;
28 | }
29 |
30 | &__preloaders {
31 | display: flex;
32 | flex-grow: 1;
33 | flex-wrap: nowrap;
34 | padding: 5px;
35 | gap: 8px;
36 | overflow: hidden;
37 | }
38 |
39 | &__preloader {
40 | min-width: 30px;
41 | height: 30px;
42 | border-radius: 50%;
43 | background-size: cover;
44 | position: relative;
45 | background-color: var(--bg-color);
46 | background-position: center center;
47 |
48 | &::after {
49 | content: "";
50 | position: absolute;
51 | z-index: 3;
52 | width: 30px;
53 | height: 30px;
54 | border-radius: 50%;
55 | border: 2px solid var(--bg-color);
56 | border-top-color: var(--front-color);
57 | left: 50%;
58 | top: 50%;
59 | margin-top: -15px;
60 | margin-left: -15px;
61 | animation: image-preloader-spin 2s infinite linear;
62 | box-sizing: border-box;
63 | }
64 | }
65 |
66 | .sortable &__image {
67 | cursor: move;
68 | }
69 |
70 | &__image {
71 | position: relative;
72 | overflow: hidden;
73 | aspect-ratio: 16 / 9;
74 | user-select: none;
75 | background-color: black;
76 | border-radius: 3px;
77 | padding: 5px;
78 |
79 | &.sortable-ghost {
80 | opacity: .75;
81 | }
82 |
83 | &--empty,
84 | &--loading {
85 | display: none;
86 | }
87 |
88 | &-picture {
89 | border-radius: 3px;
90 | max-width: 100%;
91 | height: 100%;
92 | display: block;
93 | margin: auto;
94 | object-fit: cover;
95 | pointer-events: none;
96 | }
97 |
98 | &-trash {
99 | position: absolute;
100 | top: 3px;
101 | right: 3px;
102 | cursor: pointer;
103 | color: #fff;
104 | font-size: 18px;
105 | background-color: rgba(0, 0, 0, .25);
106 | line-height: 1;
107 | padding: 6px 8px;
108 | border-radius: 3px;
109 | transition: background-color .1s;
110 |
111 | &:hover {
112 | background-color: rgba(0, 0, 0, .5);
113 | }
114 | }
115 | }
116 |
117 | &__counter {
118 | display: flex;
119 | align-items: center;
120 | color: gray;
121 | font-size: 14px;
122 | margin-right: 6px;
123 | }
124 |
125 | &__caption {
126 | &[contentEditable="true"][data-placeholder]::before {
127 | position: absolute !important;
128 | content: attr(data-placeholder);
129 | color: #707684;
130 | font-weight: normal;
131 | display: none;
132 | }
133 |
134 | &[contentEditable="true"][data-placeholder]:empty {
135 | &::before {
136 | display: block;
137 | }
138 |
139 | &:focus::before {
140 | display: none;
141 | }
142 | }
143 | }
144 |
145 | &__caption {
146 | margin-bottom: 10px;
147 | }
148 |
149 | .cdx-button {
150 | height: 40px;
151 | display: flex;
152 | align-items: center;
153 | justify-content: center;
154 | padding: 12px;
155 | gap: 5px;
156 | white-space: nowrap;
157 | }
158 |
159 | /**
160 | * Tunes
161 | * ----------------
162 | */
163 |
164 | &__tune-wrapper {
165 | display: flex;
166 | gap: 6px;
167 | margin: 6px 0;
168 |
169 | &:first-child {
170 | margin-top: 0;
171 | }
172 |
173 | &:last-child {
174 | margin-bottom: 0;
175 | }
176 | }
177 |
178 | &__tune {
179 | flex-grow: 1;
180 | padding: 6px;
181 | color: var(--color-text-primary);
182 | display: flex;
183 | align-items: center;
184 | justify-content: center;
185 |
186 | &.active {
187 | background: var(--color-background-icon-active);
188 | color: var(--color-text-icon-active);
189 | border-color: var(--color-text-icon-active);
190 | }
191 |
192 | & svg {
193 | width: 24px;
194 | height: 24px;
195 | }
196 | }
197 | }
198 |
199 | @keyframes image-preloader-spin {
200 | 0% {
201 | transform: rotate(0deg);
202 | }
203 |
204 | 100% {
205 | transform: rotate(360deg);
206 | }
207 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Image Gallery Tool for the Editor.js
3 | *
4 | * @author Igor Shuvalov «VolgaIgor»
5 | * @license MIT
6 | * @see {@link https://github.com/VolgaIgor/editorjs-gallery}
7 | *
8 | * To developers.
9 | * To simplify Tool structure, we split it to 4 parts:
10 | * 1) index.js — main Tool's interface, public API and methods for working with data
11 | * 2) uploader.js — module that has methods for sending files via AJAX: from device, by URL or File pasting
12 | * 3) ui.js — module for UI manipulations: render, showing preloader, etc
13 | * 4) tunes.js — working with Block Tunes: render buttons, handle clicks
14 | *
15 | * For debug purposes there is a testing server
16 | * that can save uploaded files and return a Response {@link UploadResponseFormat}
17 | *
18 | * $ node dev/server.js
19 | *
20 | * It will expose 8008 port, so you can pass http://localhost:8008 with the Tools config:
21 | *
22 | * gallery: {
23 | * class: ImageGallery,
24 | * config: {
25 | * endpoints: {
26 | * byFile: 'http://localhost:8008/uploadFile',
27 | * }
28 | * },
29 | * },
30 | */
31 |
32 | /**
33 | * @typedef {object} ImageGalleryDataFile
34 | * @description Image Gallery Tool's files data format
35 | * @property {string} url — image URL
36 | */
37 |
38 | /**
39 | * @typedef {object} ImageGalleryData
40 | * @description Image Tool's input and output data format
41 | * @property {boolean} style - slider or fit
42 | * @property {string} caption — gallery caption
43 | * @property {ImageGalleryDataFile[]} files — Image file data returned from backend
44 | */
45 |
46 | // eslint-disable-next-line
47 | import css from './index.pcss';
48 | import Ui from './ui';
49 | import Tunes from './tunes';
50 | import ToolboxIcon from './svg/toolbox.svg?raw';
51 | import Uploader from './uploader';
52 |
53 | /**
54 | * @typedef {object} ImageConfig
55 | * @description Config supported by Tool
56 | * @property {object} endpoints - upload endpoints
57 | * @property {string} endpoints.byFile - upload by file
58 | * @property {string} field - field name for uploaded image
59 | * @property {string} types - available mime-types
60 | * @property {object} additionalRequestData - any data to send with requests
61 | * @property {object} additionalRequestHeaders - allows to pass custom headers with Request
62 | * @property {string} buttonContent - overrides for Select File button
63 | * @property {object} [uploader] - optional custom uploader
64 | * @property {function(File): Promise.} [uploader.uploadByFile] - method that upload image by File
65 | */
66 |
67 | /**
68 | * @typedef {object} UploadResponseFormat
69 | * @description This format expected from backend on file uploading
70 | * @property {number} success - 1 for successful uploading, 0 for failure
71 | * @property {object} file - Object with file data.
72 | * 'url' is required,
73 | * also can contain any additional data that will be saved and passed back
74 | * @property {string} file.url - [Required] image source URL
75 | */
76 | export default class ImageGallery {
77 | /**
78 | * Notify core that read-only mode is supported
79 | *
80 | * @returns {boolean}
81 | */
82 | static get isReadOnlySupported() {
83 | return true;
84 | }
85 |
86 | /**
87 | * Get Tool toolbox settings
88 | * icon - Tool icon's SVG
89 | * title - title to show in toolbox
90 | *
91 | * @returns {{icon: string, title: string}}
92 | */
93 | static get toolbox() {
94 | return {
95 | icon: ToolboxIcon,
96 | title: 'Gallery',
97 | };
98 | }
99 |
100 | /**
101 | * @param {object} tool - tool properties got from editor.js
102 | * @param {ImageGalleryData} tool.data - previously saved data
103 | * @param {ImageConfig} tool.config - user config for Tool
104 | * @param {object} tool.api - Editor.js API
105 | * @param {boolean} tool.readOnly - read-only mode flag
106 | */
107 | constructor({ data, config, api, readOnly }) {
108 | this.api = api;
109 | this.readOnly = readOnly;
110 |
111 | /**
112 | * Tool's initial config
113 | */
114 | this.config = {
115 | endpoints: config.endpoints || '',
116 | additionalRequestData: config.additionalRequestData || {},
117 | additionalRequestHeaders: config.additionalRequestHeaders || {},
118 | field: config.field || 'image',
119 | types: config.types || 'image/*',
120 | buttonContent: config.buttonContent || '',
121 | uploader: config.uploader || undefined,
122 | actions: config.actions || undefined,
123 | maxElementCount: config.maxElementCount || undefined,
124 | sortableJs: config.sortableJs,
125 | };
126 |
127 | /**
128 | * Module for file uploading
129 | */
130 | this.uploader = new Uploader({
131 | config: this.config,
132 | });
133 |
134 | /**
135 | * Module for working with UI
136 | */
137 | this.ui = new Ui({
138 | api,
139 | config: this.config,
140 | onSelectFile: () => {
141 | let maxElementCount = (this.config.maxElementCount) ? this.config.maxElementCount - this._data.files.length : null;
142 | this.uploader.uploadSelectedFiles(maxElementCount, {
143 | onPreview: (file) => {
144 | return this.ui.getPreloader(file);
145 | },
146 | onUpload: (response, previewElem) => {
147 | this.onUpload(response, previewElem);
148 | },
149 | onError: (error, previewElem) => {
150 | this.uploadingFailed(error, previewElem);
151 | },
152 | });
153 | },
154 | onDeleteFile: (id) => {
155 | this.deleteImage(id);
156 | },
157 | onMoveFile: (oldId, newId) => {
158 | this.moveImage(oldId, newId);
159 | },
160 | readOnly,
161 | });
162 |
163 | /**
164 | * Module for working with tunes
165 | */
166 | this.tunes = new Tunes({
167 | api,
168 | actions: this.config.actions,
169 | onChange: (styleName) => this.styleToggled(styleName),
170 | });
171 |
172 | /**
173 | * Set saved state
174 | */
175 | this._data = {};
176 | this.data = data;
177 | }
178 |
179 | /**
180 | * Renders Block content
181 | *
182 | * @public
183 | *
184 | * @returns {HTMLDivElement}
185 | */
186 | render() {
187 | return this.ui.render(this.data);
188 | }
189 |
190 | rendered() {
191 | this.checkMaxElemCount();
192 |
193 | return this.ui.onRendered();
194 | }
195 |
196 | /**
197 | * Validate data: check if Image exists
198 | *
199 | * @param {ImageGalleryData} savedData — data received after saving
200 | * @returns {boolean} false if saved data is not correct, otherwise true
201 | * @public
202 | */
203 | validate(savedData) {
204 | if (!savedData.files || !savedData.files.length) {
205 | return false;
206 | }
207 |
208 | return true;
209 | }
210 |
211 | /**
212 | * Return Block data
213 | *
214 | * @public
215 | *
216 | * @returns {ImageGalleryData}
217 | */
218 | save() {
219 | const caption = this.ui.nodes.caption;
220 |
221 | this._data.caption = caption.innerHTML;
222 |
223 | return this.data;
224 | }
225 |
226 | /**
227 | * Makes buttons with tunes
228 | *
229 | * @public
230 | *
231 | * @returns {Element}
232 | */
233 | renderSettings() {
234 | return this.tunes.render(this.data);
235 | }
236 |
237 | /**
238 | * Set new image file
239 | *
240 | * @private
241 | *
242 | * @param {ImageGalleryDataFile} file - uploaded file data
243 | */
244 | appendImage(file) {
245 | if (file && file.url) {
246 | if (this.config.maxElementCount && this._data.files.length >= this.config.maxElementCount) {
247 | return;
248 | }
249 |
250 | this._data.files.push(file);
251 | this.ui.appendImage(file);
252 |
253 | this.checkMaxElemCount();
254 | }
255 | }
256 |
257 | /**
258 | * Move image file
259 | *
260 | * @private
261 | *
262 | * @param {integer} from - target image old index
263 | * @param {integer} to - target image new index
264 | */
265 | moveImage(from, to) {
266 | if (to >= this._data.files.length) {
267 | to = this._data.files.length - 1;
268 | }
269 | this._data.files.splice(to, 0, this._data.files.splice(from, 1)[0]);
270 | }
271 |
272 | /**
273 | * Delete image file
274 | *
275 | * @private
276 | *
277 | * @param {integer} id - image index
278 | */
279 | deleteImage(id) {
280 | if (this._data.files[id] !== undefined) {
281 | this._data.files.splice(id, 1);
282 |
283 | this.checkMaxElemCount();
284 | }
285 | }
286 |
287 | /**
288 | * Private methods
289 | * ̿̿ ̿̿ ̿̿ ̿'̿'\̵͇̿̿\з= ( ▀ ͜͞ʖ▀) =ε/̵͇̿̿/’̿’̿ ̿ ̿̿ ̿̿ ̿̿
290 | */
291 |
292 | /**
293 | * Stores all Tool's data
294 | *
295 | * @private
296 | *
297 | * @param {ImageGalleryData} data - data in Image Tool format
298 | */
299 | set data(data) {
300 | this._data.files = [];
301 | if (data.files) {
302 | data.files.forEach(file => {
303 | this.appendImage(file);
304 | });
305 | }
306 |
307 | this._data.caption = data.caption || '';
308 | this.ui.fillCaption(this._data.caption);
309 |
310 | let style = data.style || '';
311 | this.styleToggled(style);
312 | }
313 |
314 | /**
315 | * Return Tool data
316 | *
317 | * @private
318 | *
319 | * @returns {ImageGalleryData}
320 | */
321 | get data() {
322 | return this._data;
323 | }
324 |
325 | /**
326 | * File uploading callback
327 | *
328 | * @private
329 | *
330 | * @param {UploadResponseFormat} response - uploading server response
331 | * @returns {void}
332 | */
333 | onUpload(response, previewElem) {
334 | this.ui.removePreloader(previewElem);
335 | if (response.success && response.file) {
336 | this.appendImage(response.file);
337 | } else {
338 | this.uploadingFailed('incorrect response: ' + JSON.stringify(response));
339 | }
340 | }
341 |
342 | /**
343 | * Handle uploader errors
344 | *
345 | * @private
346 | * @param {string} errorText - uploading error text
347 | * @returns {void}
348 | */
349 | uploadingFailed(errorText, previewElem) {
350 | this.ui.removePreloader(previewElem);
351 |
352 | console.log('Image Tool: uploading failed because of', errorText);
353 |
354 | this.api.notifier.show({
355 | message: this.api.i18n.t('Couldn’t upload image. Please try another.'),
356 | style: 'error',
357 | });
358 | }
359 |
360 | /**
361 | * Callback fired when Block Tune is activated
362 | *
363 | * @private
364 | *
365 | * @param {string} tuneName - tune that has been clicked
366 | * @returns {void}
367 | */
368 | styleToggled(tuneName) {
369 | if (tuneName === 'fit') {
370 | this._data.style = 'fit';
371 | } else {
372 | this._data.style = 'slider';
373 | }
374 | }
375 |
376 | checkMaxElemCount() {
377 | this.ui.updateLimitCounter(this._data.files.length, this.config.maxElementCount);
378 |
379 | if (this.config.maxElementCount && this._data.files.length >= this.config.maxElementCount) {
380 | this.ui.hideFileButton();
381 | } else {
382 | this.ui.showFileButton();
383 | }
384 | }
385 | }
386 |
--------------------------------------------------------------------------------
/src/ui.js:
--------------------------------------------------------------------------------
1 | import { IconPicture, IconTrash } from '@codexteam/icons'
2 |
3 | /**
4 | * Class for working with UI:
5 | * - rendering base structure
6 | * - show/hide preview
7 | * - apply tune view
8 | */
9 | export default class Ui {
10 | /**
11 | * @param {object} ui - image tool Ui module
12 | * @param {object} ui.api - Editor.js API
13 | * @param {ImageConfig} ui.config - user config
14 | * @param {Function} ui.onSelectFile - callback for clicks on Select file button
15 | * @param {boolean} ui.readOnly - read-only mode flag
16 | */
17 | constructor({ api, config, onSelectFile, onDeleteFile, onMoveFile, readOnly }) {
18 | this.api = api;
19 | this.config = config;
20 | this.onSelectFile = onSelectFile;
21 | this.onDeleteFile = onDeleteFile;
22 | this.onMoveFile = onMoveFile;
23 | this.readOnly = readOnly;
24 | this.nodes = {
25 | wrapper: make('div', [this.CSS.baseClass, this.CSS.wrapper]),
26 | fileButton: this.createFileButton(),
27 | container: make('div', this.CSS.container),
28 | itemsContainer: make('div', this.CSS.itemsContainer),
29 | controls: make('div', this.CSS.controls),
30 | preloaderContainer: make('div', this.CSS.preloaderContainer),
31 | caption: make('div', [this.CSS.input, this.CSS.caption], {
32 | contentEditable: !this.readOnly,
33 | }),
34 | };
35 |
36 | /**
37 | * Create base structure
38 | *
39 | *
40 | *
41 | *
42 | *
43 | *
44 | *
45 | *
46 | *
47 | *
48 | *
49 | *
50 | *
51 | */
52 | this.nodes.caption.dataset.placeholder = this.api.i18n.t('Gallery caption');
53 |
54 | if (!this.readOnly) {
55 | this.nodes.controls.appendChild(this.nodes.preloaderContainer);
56 | if (this.config.maxElementCount) {
57 | this.nodes.limitCounter = make('div', this.CSS.limitCounter);
58 | this.nodes.controls.appendChild(this.nodes.limitCounter);
59 | }
60 | this.nodes.controls.appendChild(this.nodes.fileButton);
61 | }
62 |
63 | this.nodes.container.appendChild(this.nodes.itemsContainer);
64 | if (!this.readOnly) {
65 | this.nodes.container.appendChild(this.nodes.controls);
66 | }
67 |
68 | this.nodes.wrapper.appendChild(this.nodes.container);
69 |
70 | if (!this.readOnly) {
71 | this.nodes.wrapper.appendChild(this.nodes.caption);
72 | }
73 |
74 | ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
75 | this.nodes.itemsContainer.addEventListener(eventName, function (e) {
76 | e.preventDefault();
77 | e.stopPropagation();
78 | }, false);
79 | });
80 | }
81 |
82 | /**
83 | * CSS classes
84 | *
85 | * @returns {object}
86 | */
87 | get CSS() {
88 | return {
89 | baseClass: this.api.styles.block,
90 | loading: this.api.styles.loader,
91 | input: this.api.styles.input,
92 | button: this.api.styles.button,
93 |
94 | /**
95 | * Tool's classes
96 | */
97 | wrapper: 'image-gallery',
98 | container: 'image-gallery__container',
99 | controls: 'image-gallery__controls',
100 | limitCounter: 'image-gallery__counter',
101 | itemsContainer: 'image-gallery__items',
102 | imageContainer: 'image-gallery__image',
103 | preloaderContainer: 'image-gallery__preloaders',
104 | imagePreloader: 'image-gallery__preloader',
105 | imageEl: 'image-gallery__image-picture',
106 | trashButton: 'image-gallery__image-trash',
107 | caption: 'image-gallery__caption',
108 | };
109 | };
110 |
111 | /**
112 | * Ui statuses:
113 | * - empty
114 | * - uploading
115 | * - filled
116 | *
117 | * @returns {{EMPTY: string, UPLOADING: string, FILLED: string}}
118 | */
119 | static get status() {
120 | return {
121 | EMPTY: 'empty',
122 | UPLOADING: 'loading',
123 | FILLED: 'filled',
124 | };
125 | }
126 |
127 | /**
128 | * Renders tool UI
129 | *
130 | * @param {ImageGalleryData} toolData - saved tool data
131 | * @returns {Element}
132 | */
133 | render(toolData) {
134 | return this.nodes.wrapper;
135 | }
136 |
137 | onRendered() {
138 | if (!this.readOnly && !this.sortable) {
139 | this.sortable = new this.config.sortableJs(this.nodes.itemsContainer, {
140 | handle: `.${this.CSS.imageContainer}`,
141 | filter: `.${this.CSS.trashButton}`,
142 | onStart: () => {
143 | this.nodes.itemsContainer.classList.add(`${this.CSS.itemsContainer}--drag`);
144 | },
145 | onEnd: (evt) => {
146 | this.nodes.itemsContainer.classList.remove(`${this.CSS.itemsContainer}--drag`);
147 |
148 | if (evt.oldIndex !== evt.newIndex) {
149 | this.onMoveFile(evt.oldIndex, evt.newIndex);
150 | }
151 | }
152 | });
153 |
154 | this.nodes.itemsContainer.classList.add('sortable')
155 | }
156 | }
157 |
158 | /**
159 | * Creates upload-file button
160 | *
161 | * @returns {Element}
162 | */
163 | createFileButton() {
164 | const button = make('div', [this.CSS.button]);
165 |
166 | button.innerHTML = this.config.buttonContent || `${IconPicture} ${this.api.i18n.t('Select an Image')}`;
167 |
168 | button.addEventListener('click', () => {
169 | this.onSelectFile();
170 | });
171 |
172 | return button;
173 | }
174 |
175 | /**
176 | * Shows uploading button
177 | *
178 | * @returns {void}
179 | */
180 | showFileButton() {
181 | this.nodes.fileButton.style.display = '';
182 | }
183 |
184 | /**
185 | * Hide uploading button
186 | *
187 | * @returns {void}
188 | */
189 | hideFileButton() {
190 | this.nodes.fileButton.style.display = 'none';
191 | }
192 |
193 | getPreloader(file) {
194 | /**
195 | * @type {HTMLElement}
196 | */
197 | let preloader = make('div', this.CSS.imagePreloader);
198 |
199 | this.nodes.preloaderContainer.append(preloader);
200 |
201 | const reader = new FileReader();
202 | reader.readAsDataURL(file);
203 | reader.onload = (e) => {
204 | preloader.style.backgroundImage = `url(${e.target.result})`;
205 | };
206 |
207 | return preloader;
208 | }
209 |
210 | removePreloader(preloader) {
211 | preloader.remove();
212 | }
213 |
214 | /**
215 | * Shows an image
216 | *
217 | * @param {ImageGalleryDataFile} file - image file object
218 | * @returns {void}
219 | */
220 | appendImage(file) {
221 | let url = file.url;
222 |
223 | /**
224 | * Check for a source extension to compose element correctly: video tag for mp4, img — for others
225 | */
226 | const tag = /\.mp4$/.test(url) ? 'VIDEO' : 'IMG';
227 |
228 | const attributes = {
229 | src: url
230 | };
231 |
232 | /**
233 | * We use eventName variable because IMG and VIDEO tags have different event to be called on source load
234 | * - IMG: load
235 | * - VIDEO: loadeddata
236 | *
237 | * @type {string}
238 | */
239 | let eventName = 'load';
240 |
241 | /**
242 | * Update attributes and eventName if source is a mp4 video
243 | */
244 | if (tag === 'VIDEO') {
245 | /**
246 | * Add attributes for playing muted mp4 as a gif
247 | *
248 | * @type {boolean}
249 | */
250 | attributes.autoplay = false;
251 | attributes.muted = true;
252 | attributes.playsinline = true;
253 |
254 | /**
255 | * Change event to be listened
256 | *
257 | * @type {string}
258 | */
259 | eventName = 'loadeddata';
260 | }
261 |
262 | /**
263 | * @type {Element}
264 | */
265 | let imageContainer = make('div', [this.CSS.imageContainer]);
266 |
267 | /**
268 | * Compose tag with defined attributes
269 | *
270 | * @type {Element}
271 | */
272 | let imageEl = make(tag, this.CSS.imageEl, attributes);
273 |
274 | /**
275 | * Add load event listener
276 | */
277 | imageEl.addEventListener(eventName, () => {
278 | this.toggleStatus(imageContainer, Ui.status.FILLED);
279 | });
280 |
281 | imageContainer.appendChild(imageEl);
282 |
283 | const title = this.api.i18n.t('Delete');
284 |
285 | if (!this.readOnly) {
286 | /**
287 | * @type {Element}
288 | */
289 | let imageTrash = make('div', [this.CSS.trashButton], {
290 | innerHTML: IconTrash,
291 | title,
292 | });
293 |
294 | this.api.tooltip.onHover(imageTrash, title, {
295 | placement: 'top',
296 | });
297 |
298 | imageTrash.addEventListener('click', () => {
299 | this.api.tooltip.hide();
300 |
301 | let arrayChild = Array.prototype.slice.call(this.nodes.itemsContainer.children);
302 | let elIndex = arrayChild.indexOf(imageContainer);
303 |
304 | if (elIndex !== -1) {
305 | this.nodes.itemsContainer.removeChild(imageContainer);
306 |
307 | this.onDeleteFile(elIndex);
308 | }
309 | });
310 |
311 | imageContainer.appendChild(imageTrash);
312 | }
313 |
314 | this.nodes.itemsContainer.append(imageContainer);
315 | }
316 |
317 | /**
318 | * Shows caption input
319 | *
320 | * @param {string} text - caption text
321 | * @returns {void}
322 | */
323 | fillCaption(text) {
324 | if (this.nodes.caption) {
325 | this.nodes.caption.innerHTML = text;
326 | }
327 | }
328 |
329 | /**
330 | * Changes UI status
331 | *
332 | * @param {Element} elem
333 | * @param {string} status - see {@link Ui.status} constants
334 | * @returns {void}
335 | */
336 | toggleStatus(elem, status) {
337 | for (const statusType in Ui.status) {
338 | if (Object.prototype.hasOwnProperty.call(Ui.status, statusType)) {
339 | elem.classList.toggle(`${this.CSS.imageContainer}--${Ui.status[statusType]}`, status === Ui.status[statusType]);
340 | }
341 | }
342 | }
343 |
344 | /**
345 | * @param {int} imageCount
346 | * @param {int|null} limitCounter
347 | * @returns {void}
348 | */
349 | updateLimitCounter(imageCount, limitCounter) {
350 | if (limitCounter && this.nodes.limitCounter) {
351 | if (imageCount === 0) {
352 | this.nodes.limitCounter.style.display = 'none';
353 | } else {
354 | this.nodes.limitCounter.style.display = null;
355 | this.nodes.limitCounter.innerText = `${imageCount} / ${limitCounter}`;
356 | }
357 | }
358 | }
359 | }
360 |
361 | /**
362 | * Helper for making Elements with attributes
363 | *
364 | * @param {string} tagName - new Element tag name
365 | * @param {Array|string} classNames - list or name of CSS class
366 | * @param {object} attributes - any attributes
367 | * @returns {Element}
368 | */
369 | export const make = function make(tagName, classNames = null, attributes = {}) {
370 | const el = document.createElement(tagName);
371 |
372 | if (Array.isArray(classNames)) {
373 | el.classList.add(...classNames);
374 | } else if (classNames) {
375 | el.classList.add(classNames);
376 | }
377 |
378 | for (const attrName in attributes) {
379 | el[attrName] = attributes[attrName];
380 | }
381 |
382 | return el;
383 | };
384 |
--------------------------------------------------------------------------------
/dist/gallery.umd.js:
--------------------------------------------------------------------------------
1 | (function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode('.image-gallery{--bg-color: #cdd1e0;--front-color: #388ae5;--border-color: #e8e8eb}.image-gallery__container{background:black;margin-bottom:10px;padding:5px}.image-gallery__controls{display:flex;gap:10px;padding:8px 2px 3px}.image-gallery__items{display:grid;gap:10px;grid-template-columns:1fr 1fr 1fr;padding:10px;background-color:#222}.image-gallery__items:empty{display:none}.image-gallery__preloaders{display:flex;flex-grow:1;flex-wrap:nowrap;padding:5px;gap:8px;overflow:hidden}.image-gallery__preloader{min-width:30px;height:30px;border-radius:50%;background-size:cover;position:relative;background-color:var(--bg-color);background-position:center center}.image-gallery__preloader:after{content:"";position:absolute;z-index:3;width:30px;height:30px;border-radius:50%;border:2px solid var(--bg-color);border-top-color:var(--front-color);left:50%;top:50%;margin-top:-15px;margin-left:-15px;animation:image-preloader-spin 2s infinite linear;box-sizing:border-box}.sortable .image-gallery__image{cursor:move}.image-gallery__image{position:relative;overflow:hidden;aspect-ratio:16 / 9;-webkit-user-select:none;user-select:none;background-color:#000;border-radius:3px;padding:5px}.image-gallery__image.sortable-ghost{opacity:.75}.image-gallery__image--empty,.image-gallery__image--loading{display:none}.image-gallery__image-picture{border-radius:3px;max-width:100%;height:100%;display:block;margin:auto;object-fit:cover;pointer-events:none}.image-gallery__image-trash{position:absolute;top:3px;right:3px;cursor:pointer;color:#fff;font-size:18px;background-color:#00000040;line-height:1;padding:6px 8px;border-radius:3px;transition:background-color .1s}.image-gallery__image-trash:hover{background-color:#00000080}.image-gallery__counter{display:flex;align-items:center;color:gray;font-size:14px;margin-right:6px}.image-gallery__caption[contentEditable=true][data-placeholder]:before{position:absolute!important;content:attr(data-placeholder);color:#707684;font-weight:400;display:none}.image-gallery__caption[contentEditable=true][data-placeholder]:empty:before{display:block}.image-gallery__caption[contentEditable=true][data-placeholder]:empty:focus:before{display:none}.image-gallery__caption{margin-bottom:10px}.image-gallery .cdx-button{height:40px;display:flex;align-items:center;justify-content:center;padding:12px;gap:5px;white-space:nowrap}.image-gallery__tune-wrapper{display:flex;gap:6px;margin:6px 0}.image-gallery__tune-wrapper:first-child{margin-top:0}.image-gallery__tune-wrapper:last-child{margin-bottom:0}.image-gallery__tune{flex-grow:1;padding:6px;color:var(--color-text-primary);display:flex;align-items:center;justify-content:center}.image-gallery__tune.active{background:var(--color-background-icon-active);color:var(--color-text-icon-active);border-color:var(--color-text-icon-active)}.image-gallery__tune svg{width:24px;height:24px}@keyframes image-preloader-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}')),document.head.appendChild(e)}}catch(a){console.error("vite-plugin-css-injected-by-js",a)}})();
2 | (function(M,F){typeof exports=="object"&&typeof module<"u"?module.exports=F():typeof define=="function"&&define.amd?define(F):(M=typeof globalThis<"u"?globalThis:M||self,M.ImageGallery=F())})(this,function(){"use strict";const M=`.image-gallery{--bg-color: #cdd1e0;--front-color: #388ae5;--border-color: #e8e8eb}.image-gallery__container{background:black;margin-bottom:10px;padding:5px}.image-gallery__controls{display:flex;gap:10px;padding:8px 2px 3px}.image-gallery__items{display:grid;gap:10px;grid-template-columns:1fr 1fr 1fr;padding:10px;background-color:#222}.image-gallery__items:empty{display:none}.image-gallery__preloaders{display:flex;flex-grow:1;flex-wrap:nowrap;padding:5px;gap:8px;overflow:hidden}.image-gallery__preloader{min-width:30px;height:30px;border-radius:50%;background-size:cover;position:relative;background-color:var(--bg-color);background-position:center center}.image-gallery__preloader:after{content:"";position:absolute;z-index:3;width:30px;height:30px;border-radius:50%;border:2px solid var(--bg-color);border-top-color:var(--front-color);left:50%;top:50%;margin-top:-15px;margin-left:-15px;animation:image-preloader-spin 2s infinite linear;box-sizing:border-box}.sortable .image-gallery__image{cursor:move}.image-gallery__image{position:relative;overflow:hidden;aspect-ratio:16 / 9;-webkit-user-select:none;user-select:none;background-color:#000;border-radius:3px;padding:5px}.image-gallery__image.sortable-ghost{opacity:.75}.image-gallery__image--empty,.image-gallery__image--loading{display:none}.image-gallery__image-picture{border-radius:3px;max-width:100%;height:100%;display:block;margin:auto;object-fit:cover;pointer-events:none}.image-gallery__image-trash{position:absolute;top:3px;right:3px;cursor:pointer;color:#fff;font-size:18px;background-color:#00000040;line-height:1;padding:6px 8px;border-radius:3px;transition:background-color .1s}.image-gallery__image-trash:hover{background-color:#00000080}.image-gallery__counter{display:flex;align-items:center;color:gray;font-size:14px;margin-right:6px}.image-gallery__caption[contentEditable=true][data-placeholder]:before{position:absolute!important;content:attr(data-placeholder);color:#707684;font-weight:400;display:none}.image-gallery__caption[contentEditable=true][data-placeholder]:empty:before{display:block}.image-gallery__caption[contentEditable=true][data-placeholder]:empty:focus:before{display:none}.image-gallery__caption{margin-bottom:10px}.image-gallery .cdx-button{height:40px;display:flex;align-items:center;justify-content:center;padding:12px;gap:5px;white-space:nowrap}.image-gallery__tune-wrapper{display:flex;gap:6px;margin:6px 0}.image-gallery__tune-wrapper:first-child{margin-top:0}.image-gallery__tune-wrapper:last-child{margin-bottom:0}.image-gallery__tune{flex-grow:1;padding:6px;color:var(--color-text-primary);display:flex;align-items:center;justify-content:center}.image-gallery__tune.active{background:var(--color-background-icon-active);color:var(--color-text-icon-active);border-color:var(--color-text-icon-active)}.image-gallery__tune svg{width:24px;height:24px}@keyframes image-preloader-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}
3 | `,F='',q='';class k{constructor({api:r,config:s,onSelectFile:d,onDeleteFile:o,onMoveFile:i,readOnly:t}){this.api=r,this.config=s,this.onSelectFile=d,this.onDeleteFile=o,this.onMoveFile=i,this.readOnly=t,this.nodes={wrapper:C("div",[this.CSS.baseClass,this.CSS.wrapper]),fileButton:this.createFileButton(),container:C("div",this.CSS.container),itemsContainer:C("div",this.CSS.itemsContainer),controls:C("div",this.CSS.controls),preloaderContainer:C("div",this.CSS.preloaderContainer),caption:C("div",[this.CSS.input,this.CSS.caption],{contentEditable:!this.readOnly})},this.nodes.caption.dataset.placeholder=this.api.i18n.t("Gallery caption"),this.readOnly||(this.nodes.controls.appendChild(this.nodes.preloaderContainer),this.config.maxElementCount&&(this.nodes.limitCounter=C("div",this.CSS.limitCounter),this.nodes.controls.appendChild(this.nodes.limitCounter)),this.nodes.controls.appendChild(this.nodes.fileButton)),this.nodes.container.appendChild(this.nodes.itemsContainer),this.readOnly||this.nodes.container.appendChild(this.nodes.controls),this.nodes.wrapper.appendChild(this.nodes.container),this.readOnly||this.nodes.wrapper.appendChild(this.nodes.caption),["dragenter","dragover","dragleave","drop"].forEach(c=>{this.nodes.itemsContainer.addEventListener(c,function(g){g.preventDefault(),g.stopPropagation()},!1)})}get CSS(){return{baseClass:this.api.styles.block,loading:this.api.styles.loader,input:this.api.styles.input,button:this.api.styles.button,wrapper:"image-gallery",container:"image-gallery__container",controls:"image-gallery__controls",limitCounter:"image-gallery__counter",itemsContainer:"image-gallery__items",imageContainer:"image-gallery__image",preloaderContainer:"image-gallery__preloaders",imagePreloader:"image-gallery__preloader",imageEl:"image-gallery__image-picture",trashButton:"image-gallery__image-trash",caption:"image-gallery__caption"}}static get status(){return{EMPTY:"empty",UPLOADING:"loading",FILLED:"filled"}}render(r){return this.nodes.wrapper}onRendered(){!this.readOnly&&!this.sortable&&(this.sortable=new this.config.sortableJs(this.nodes.itemsContainer,{handle:`.${this.CSS.imageContainer}`,filter:`.${this.CSS.trashButton}`,onStart:()=>{this.nodes.itemsContainer.classList.add(`${this.CSS.itemsContainer}--drag`)},onEnd:r=>{this.nodes.itemsContainer.classList.remove(`${this.CSS.itemsContainer}--drag`),r.oldIndex!==r.newIndex&&this.onMoveFile(r.oldIndex,r.newIndex)}}),this.nodes.itemsContainer.classList.add("sortable"))}createFileButton(){const r=C("div",[this.CSS.button]);return r.innerHTML=this.config.buttonContent||`${F} ${this.api.i18n.t("Select an Image")}`,r.addEventListener("click",()=>{this.onSelectFile()}),r}showFileButton(){this.nodes.fileButton.style.display=""}hideFileButton(){this.nodes.fileButton.style.display="none"}getPreloader(r){let s=C("div",this.CSS.imagePreloader);this.nodes.preloaderContainer.append(s);const d=new FileReader;return d.readAsDataURL(r),d.onload=o=>{s.style.backgroundImage=`url(${o.target.result})`},s}removePreloader(r){r.remove()}appendImage(r){let s=r.url;const d=/\.mp4$/.test(s)?"VIDEO":"IMG",o={src:s};let i="load";d==="VIDEO"&&(o.autoplay=!1,o.muted=!0,o.playsinline=!0,i="loadeddata");let t=C("div",[this.CSS.imageContainer]),c=C(d,this.CSS.imageEl,o);c.addEventListener(i,()=>{this.toggleStatus(t,k.status.FILLED)}),t.appendChild(c);const g=this.api.i18n.t("Delete");if(!this.readOnly){let a=C("div",[this.CSS.trashButton],{innerHTML:q,title:g});this.api.tooltip.onHover(a,g,{placement:"top"}),a.addEventListener("click",()=>{this.api.tooltip.hide();let p=Array.prototype.slice.call(this.nodes.itemsContainer.children).indexOf(t);p!==-1&&(this.nodes.itemsContainer.removeChild(t),this.onDeleteFile(p))}),t.appendChild(a)}this.nodes.itemsContainer.append(t)}fillCaption(r){this.nodes.caption&&(this.nodes.caption.innerHTML=r)}toggleStatus(r,s){for(const d in k.status)Object.prototype.hasOwnProperty.call(k.status,d)&&r.classList.toggle(`${this.CSS.imageContainer}--${k.status[d]}`,s===k.status[d])}updateLimitCounter(r,s){s&&this.nodes.limitCounter&&(r===0?this.nodes.limitCounter.style.display="none":(this.nodes.limitCounter.style.display=null,this.nodes.limitCounter.innerText=`${r} / ${s}`))}}const C=function(r,s=null,d={}){const o=document.createElement(r);Array.isArray(s)?o.classList.add(...s):s&&o.classList.add(s);for(const i in d)o[i]=d[i];return o},P='',D='';class O{constructor({api:r,actions:s,onChange:d}){this.api=r,this.actions=s,this.onChange=d,this.buttons=[]}static get tunes(){return[{name:"slider",icon:P,title:"Slider"},{name:"fit",icon:D,title:"Fit"}]}get CSS(){return{wrapper:"image-gallery__tune-wrapper",buttonBase:this.api.styles.button,button:"image-gallery__tune",buttonActive:"active"}}render(r){const s=C("div",this.CSS.wrapper),d=this.actions??O.tunes;return this.buttons=[],d.forEach(o=>{const i=this.api.i18n.t(o.title),t=C("div",[this.CSS.buttonBase,this.CSS.button],{innerHTML:o.icon,title:i});t.addEventListener("click",()=>{this.tuneClicked(o.name,o.action)}),t.dataset.tune=o.name,t.classList.toggle(this.CSS.buttonActive,r.style===o.name),this.buttons.push(t),this.api.tooltip.onHover(t,i,{placement:"top"}),s.appendChild(t)}),s}tuneClicked(r,s){if(typeof s=="function"&&!s(r))return!1;this.buttons.forEach(d=>{d.classList.toggle(this.CSS.buttonActive,d.dataset.tune===r)}),this.onChange(r)}}const H='';function R(E){return E&&E.__esModule&&Object.prototype.hasOwnProperty.call(E,"default")?E.default:E}var I={exports:{}};(function(E,r){(function(s,d){E.exports=d()})(window,function(){return function(s){var d={};function o(i){if(d[i])return d[i].exports;var t=d[i]={i,l:!1,exports:{}};return s[i].call(t.exports,t,t.exports,o),t.l=!0,t.exports}return o.m=s,o.c=d,o.d=function(i,t,c){o.o(i,t)||Object.defineProperty(i,t,{enumerable:!0,get:c})},o.r=function(i){typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(i,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(i,"__esModule",{value:!0})},o.t=function(i,t){if(1&t&&(i=o(i)),8&t||4&t&&typeof i=="object"&&i&&i.__esModule)return i;var c=Object.create(null);if(o.r(c),Object.defineProperty(c,"default",{enumerable:!0,value:i}),2&t&&typeof i!="string")for(var g in i)o.d(c,g,(function(a){return i[a]}).bind(null,g));return c},o.n=function(i){var t=i&&i.__esModule?function(){return i.default}:function(){return i};return o.d(t,"a",t),t},o.o=function(i,t){return Object.prototype.hasOwnProperty.call(i,t)},o.p="",o(o.s=3)}([function(s,d){var o;o=function(){return this}();try{o=o||new Function("return this")()}catch{typeof window=="object"&&(o=window)}s.exports=o},function(s,d,o){(function(i){var t=o(2),c=setTimeout;function g(){}function a(n){if(!(this instanceof a))throw new TypeError("Promises must be constructed via new");if(typeof n!="function")throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=void 0,this._deferreds=[],e(n,this)}function h(n,u){for(;n._state===3;)n=n._value;n._state!==0?(n._handled=!0,a._immediateFn(function(){var l=n._state===1?u.onFulfilled:u.onRejected;if(l!==null){var y;try{y=l(n._value)}catch(m){return void v(u.promise,m)}p(u.promise,y)}else(n._state===1?p:v)(u.promise,n._value)})):n._deferreds.push(u)}function p(n,u){try{if(u===n)throw new TypeError("A promise cannot be resolved with itself.");if(u&&(typeof u=="object"||typeof u=="function")){var l=u.then;if(u instanceof a)return n._state=3,n._value=u,void b(n);if(typeof l=="function")return void e((y=l,m=u,function(){y.apply(m,arguments)}),n)}n._state=1,n._value=u,b(n)}catch(f){v(n,f)}var y,m}function v(n,u){n._state=2,n._value=u,b(n)}function b(n){n._state===2&&n._deferreds.length===0&&a._immediateFn(function(){n._handled||a._unhandledRejectionFn(n._value)});for(var u=0,l=n._deferreds.length;u0&&arguments[0]!==void 0?arguments[0]:{};if(e.url&&typeof e.url!="string")throw new Error("Url must be a string");if(e.url=e.url||"",e.method&&typeof e.method!="string")throw new Error("`method` must be a string or null");if(e.method=e.method?e.method.toUpperCase():"GET",e.headers&&i(e.headers)!=="object")throw new Error("`headers` must be an object or null");if(e.headers=e.headers||{},e.type&&(typeof e.type!="string"||!Object.values(t).includes(e.type)))throw new Error("`type` must be taken from module's «contentType» library");if(e.progress&&typeof e.progress!="function")throw new Error("`progress` must be a function or null");if(e.progress=e.progress||function(n){},e.beforeSend=e.beforeSend||function(n){},e.ratio&&typeof e.ratio!="number")throw new Error("`ratio` must be a number");if(e.ratio<0||e.ratio>100)throw new Error("`ratio` must be in a 0-100 interval");if(e.ratio=e.ratio||90,e.accept&&typeof e.accept!="string")throw new Error("`accept` must be a string with a list of allowed mime-types");if(e.accept=e.accept||"*/*",e.multiple&&typeof e.multiple!="boolean")throw new Error("`multiple` must be a true or false");if(e.multiple=e.multiple||!1,e.fieldName&&typeof e.fieldName!="string")throw new Error("`fieldName` must be a string");return e.fieldName=e.fieldName||"files",e},h=function(e){switch(e.method){case"GET":var n=p(e.data,t.URLENCODED);delete e.data,e.url=/\?/.test(e.url)?e.url+"&"+n:e.url+"?"+n;break;case"POST":case"PUT":case"DELETE":case"UPDATE":var u=function(){return(arguments.length>0&&arguments[0]!==void 0?arguments[0]:{}).type||t.JSON}(e);(b.isFormData(e.data)||b.isFormElement(e.data))&&(u=t.FORM),e.data=p(e.data,u),u!==w.contentType.FORM&&(e.headers["content-type"]=u)}return e},p=function(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};switch(arguments.length>1?arguments[1]:void 0){case t.URLENCODED:return b.urlEncode(e);case t.JSON:return b.jsonEncode(e);case t.FORM:return b.formEncode(e);default:return e}},v=function(e){return e>=200&&e<300},{contentType:t={URLENCODED:"application/x-www-form-urlencoded; charset=utf-8",FORM:"multipart/form-data",JSON:"application/json; charset=utf-8"},request:c,get:function(e){return e.method="GET",c(e)},post:g,transport:function(e){return e=a(e),b.selectFiles(e).then(function(n){for(var u=new FormData,l=0;l=0&&(a._idleTimeoutId=setTimeout(function(){a._onTimeout&&a._onTimeout()},h))},o(6),d.setImmediate=typeof self<"u"&&self.setImmediate||i!==void 0&&i.setImmediate||this&&this.setImmediate,d.clearImmediate=typeof self<"u"&&self.clearImmediate||i!==void 0&&i.clearImmediate||this&&this.clearImmediate}).call(this,o(0))},function(s,d,o){(function(i,t){(function(c,g){if(!c.setImmediate){var a,h,p,v,b,w=1,e={},n=!1,u=c.document,l=Object.getPrototypeOf&&Object.getPrototypeOf(c);l=l&&l.setTimeout?l:c,{}.toString.call(c.process)==="[object process]"?a=function(f){t.nextTick(function(){m(f)})}:function(){if(c.postMessage&&!c.importScripts){var f=!0,_=c.onmessage;return c.onmessage=function(){f=!1},c.postMessage("","*"),c.onmessage=_,f}}()?(v="setImmediate$"+Math.random()+"$",b=function(f){f.source===c&&typeof f.data=="string"&&f.data.indexOf(v)===0&&m(+f.data.slice(v.length))},c.addEventListener?c.addEventListener("message",b,!1):c.attachEvent("onmessage",b),a=function(f){c.postMessage(v+f,"*")}):c.MessageChannel?((p=new MessageChannel).port1.onmessage=function(f){m(f.data)},a=function(f){p.port2.postMessage(f)}):u&&"onreadystatechange"in u.createElement("script")?(h=u.documentElement,a=function(f){var _=u.createElement("script");_.onreadystatechange=function(){m(f),_.onreadystatechange=null,h.removeChild(_),_=null},h.appendChild(_)}):a=function(f){setTimeout(m,0,f)},l.setImmediate=function(f){typeof f!="function"&&(f=new Function(""+f));for(var _=new Array(arguments.length-1),S=0;S<_.length;S++)_[S]=arguments[S+1];var x={callback:f,args:_};return e[w]=x,a(w),w++},l.clearImmediate=y}function y(f){delete e[f]}function m(f){if(n)setTimeout(m,0,f);else{var _=e[f];if(_){n=!0;try{(function(S){var x=S.callback,T=S.args;switch(T.length){case 0:x();break;case 1:x(T[0]);break;case 2:x(T[0],T[1]);break;case 3:x(T[0],T[1],T[2]);break;default:x.apply(g,T)}})(_)}finally{y(f),n=!1}}}}})(typeof self>"u"?i===void 0?this:i:self)}).call(this,o(0),o(7))},function(s,d){var o,i,t=s.exports={};function c(){throw new Error("setTimeout has not been defined")}function g(){throw new Error("clearTimeout has not been defined")}function a(l){if(o===setTimeout)return setTimeout(l,0);if((o===c||!o)&&setTimeout)return o=setTimeout,setTimeout(l,0);try{return o(l,0)}catch{try{return o.call(null,l,0)}catch{return o.call(this,l,0)}}}(function(){try{o=typeof setTimeout=="function"?setTimeout:c}catch{o=c}try{i=typeof clearTimeout=="function"?clearTimeout:g}catch{i=g}})();var h,p=[],v=!1,b=-1;function w(){v&&h&&(v=!1,h.length?p=h.concat(p):b=-1,p.length&&e())}function e(){if(!v){var l=a(w);v=!0;for(var y=p.length;y;){for(h=p,p=[];++b1)for(var m=1;m HTMLElement")}},{key:"isObject",value:function(p){return Object.prototype.toString.call(p)==="[object Object]"}},{key:"isFormData",value:function(p){return p instanceof FormData}},{key:"isFormElement",value:function(p){return p instanceof HTMLFormElement}},{key:"selectFiles",value:function(){var p=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};return new Promise(function(v,b){var w=document.createElement("INPUT");w.type="file",p.multiple&&w.setAttribute("multiple","multiple"),p.accept&&w.setAttribute("accept",p.accept),w.style.display="none",document.body.appendChild(w),w.addEventListener("change",function(e){var n=e.target.files;v(n),document.body.removeChild(w)},!1),w.click()})}},{key:"parseHeaders",value:function(p){var v=p.trim().split(/[\r\n]+/),b={};return v.forEach(function(w){var e=w.split(": "),n=e.shift(),u=e.join(": ");n&&(b[n]=u)}),b}}],(a=null)&&i(g.prototype,a),h&&i(g,h),c}()},function(s,d){var o=function(t){return encodeURIComponent(t).replace(/[!'()*]/g,escape).replace(/%20/g,"+")},i=function(t,c,g,a){return c=c||null,g=g||"&",a=a||null,t?function(h){for(var p=new Array,v=0;v{let t=0;for(var c=0;c{d(p,a)}).catch(p=>{o(p,a)})}})}uploadByFile(r){const s=new FormData;return s.append(this.config.field,r),this.config.additionalRequestData&&Object.keys(this.config.additionalRequestData).length&&Object.entries(this.config.additionalRequestData).forEach(([d,o])=>{s.append(d,o)}),j.post({url:this.config.endpoints.byFile,data:s,type:j.contentType.JSON,headers:this.config.additionalRequestHeaders}).then(d=>d.body)}}function Z(E){return E&&typeof E.then=="function"}/**
4 | * Image Gallery Tool for the Editor.js
5 | *
6 | * @author Igor Shuvalov «VolgaIgor»
7 | * @license MIT
8 | * @see {@link https://github.com/VolgaIgor/editorjs-gallery}
9 | *
10 | * To developers.
11 | * To simplify Tool structure, we split it to 4 parts:
12 | * 1) index.js — main Tool's interface, public API and methods for working with data
13 | * 2) uploader.js — module that has methods for sending files via AJAX: from device, by URL or File pasting
14 | * 3) ui.js — module for UI manipulations: render, showing preloader, etc
15 | * 4) tunes.js — working with Block Tunes: render buttons, handle clicks
16 | *
17 | * For debug purposes there is a testing server
18 | * that can save uploaded files and return a Response {@link UploadResponseFormat}
19 | *
20 | * $ node dev/server.js
21 | *
22 | * It will expose 8008 port, so you can pass http://localhost:8008 with the Tools config:
23 | *
24 | * gallery: {
25 | * class: ImageGallery,
26 | * config: {
27 | * endpoints: {
28 | * byFile: 'http://localhost:8008/uploadFile',
29 | * }
30 | * },
31 | * },
32 | */class N{static get isReadOnlySupported(){return!0}static get toolbox(){return{icon:H,title:"Gallery"}}constructor({data:r,config:s,api:d,readOnly:o}){this.api=d,this.readOnly=o,this.config={endpoints:s.endpoints||"",additionalRequestData:s.additionalRequestData||{},additionalRequestHeaders:s.additionalRequestHeaders||{},field:s.field||"image",types:s.types||"image/*",buttonContent:s.buttonContent||"",uploader:s.uploader||void 0,actions:s.actions||void 0,maxElementCount:s.maxElementCount||void 0,sortableJs:s.sortableJs},this.uploader=new A({config:this.config}),this.ui=new k({api:d,config:this.config,onSelectFile:()=>{let i=this.config.maxElementCount?this.config.maxElementCount-this._data.files.length:null;this.uploader.uploadSelectedFiles(i,{onPreview:t=>this.ui.getPreloader(t),onUpload:(t,c)=>{this.onUpload(t,c)},onError:(t,c)=>{this.uploadingFailed(t,c)}})},onDeleteFile:i=>{this.deleteImage(i)},onMoveFile:(i,t)=>{this.moveImage(i,t)},readOnly:o}),this.tunes=new O({api:d,actions:this.config.actions,onChange:i=>this.styleToggled(i)}),this._data={},this.data=r}render(){return this.ui.render(this.data)}rendered(){return this.checkMaxElemCount(),this.ui.onRendered()}validate(r){return!(!r.files||!r.files.length)}save(){const r=this.ui.nodes.caption;return this._data.caption=r.innerHTML,this.data}renderSettings(){return this.tunes.render(this.data)}appendImage(r){if(r&&r.url){if(this.config.maxElementCount&&this._data.files.length>=this.config.maxElementCount)return;this._data.files.push(r),this.ui.appendImage(r),this.checkMaxElemCount()}}moveImage(r,s){s>=this._data.files.length&&(s=this._data.files.length-1),this._data.files.splice(s,0,this._data.files.splice(r,1)[0])}deleteImage(r){this._data.files[r]!==void 0&&(this._data.files.splice(r,1),this.checkMaxElemCount())}set data(r){this._data.files=[],r.files&&r.files.forEach(d=>{this.appendImage(d)}),this._data.caption=r.caption||"",this.ui.fillCaption(this._data.caption);let s=r.style||"";this.styleToggled(s)}get data(){return this._data}onUpload(r,s){this.ui.removePreloader(s),r.success&&r.file?this.appendImage(r.file):this.uploadingFailed("incorrect response: "+JSON.stringify(r))}uploadingFailed(r,s){this.ui.removePreloader(s),console.log("Image Tool: uploading failed because of",r),this.api.notifier.show({message:this.api.i18n.t("Couldn’t upload image. Please try another."),style:"error"})}styleToggled(r){r==="fit"?this._data.style="fit":this._data.style="slider"}checkMaxElemCount(){this.ui.updateLimitCounter(this._data.files.length,this.config.maxElementCount),this.config.maxElementCount&&this._data.files.length>=this.config.maxElementCount?this.ui.hideFileButton():this.ui.showFileButton()}}return N});
33 |
--------------------------------------------------------------------------------
/dist/gallery.mjs:
--------------------------------------------------------------------------------
1 | (function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode('.image-gallery{--bg-color: #cdd1e0;--front-color: #388ae5;--border-color: #e8e8eb}.image-gallery__container{background:black;margin-bottom:10px;padding:5px}.image-gallery__controls{display:flex;gap:10px;padding:8px 2px 3px}.image-gallery__items{display:grid;gap:10px;grid-template-columns:1fr 1fr 1fr;padding:10px;background-color:#222}.image-gallery__items:empty{display:none}.image-gallery__preloaders{display:flex;flex-grow:1;flex-wrap:nowrap;padding:5px;gap:8px;overflow:hidden}.image-gallery__preloader{min-width:30px;height:30px;border-radius:50%;background-size:cover;position:relative;background-color:var(--bg-color);background-position:center center}.image-gallery__preloader:after{content:"";position:absolute;z-index:3;width:30px;height:30px;border-radius:50%;border:2px solid var(--bg-color);border-top-color:var(--front-color);left:50%;top:50%;margin-top:-15px;margin-left:-15px;animation:image-preloader-spin 2s infinite linear;box-sizing:border-box}.sortable .image-gallery__image{cursor:move}.image-gallery__image{position:relative;overflow:hidden;aspect-ratio:16 / 9;-webkit-user-select:none;user-select:none;background-color:#000;border-radius:3px;padding:5px}.image-gallery__image.sortable-ghost{opacity:.75}.image-gallery__image--empty,.image-gallery__image--loading{display:none}.image-gallery__image-picture{border-radius:3px;max-width:100%;height:100%;display:block;margin:auto;object-fit:cover;pointer-events:none}.image-gallery__image-trash{position:absolute;top:3px;right:3px;cursor:pointer;color:#fff;font-size:18px;background-color:#00000040;line-height:1;padding:6px 8px;border-radius:3px;transition:background-color .1s}.image-gallery__image-trash:hover{background-color:#00000080}.image-gallery__counter{display:flex;align-items:center;color:gray;font-size:14px;margin-right:6px}.image-gallery__caption[contentEditable=true][data-placeholder]:before{position:absolute!important;content:attr(data-placeholder);color:#707684;font-weight:400;display:none}.image-gallery__caption[contentEditable=true][data-placeholder]:empty:before{display:block}.image-gallery__caption[contentEditable=true][data-placeholder]:empty:focus:before{display:none}.image-gallery__caption{margin-bottom:10px}.image-gallery .cdx-button{height:40px;display:flex;align-items:center;justify-content:center;padding:12px;gap:5px;white-space:nowrap}.image-gallery__tune-wrapper{display:flex;gap:6px;margin:6px 0}.image-gallery__tune-wrapper:first-child{margin-top:0}.image-gallery__tune-wrapper:last-child{margin-bottom:0}.image-gallery__tune{flex-grow:1;padding:6px;color:var(--color-text-primary);display:flex;align-items:center;justify-content:center}.image-gallery__tune.active{background:var(--color-background-icon-active);color:var(--color-text-icon-active);border-color:var(--color-text-icon-active)}.image-gallery__tune svg{width:24px;height:24px}@keyframes image-preloader-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}')),document.head.appendChild(e)}}catch(a){console.error("vite-plugin-css-injected-by-js",a)}})();
2 | const L = '', I = '';
3 | class T {
4 | /**
5 | * @param {object} ui - image tool Ui module
6 | * @param {object} ui.api - Editor.js API
7 | * @param {ImageConfig} ui.config - user config
8 | * @param {Function} ui.onSelectFile - callback for clicks on Select file button
9 | * @param {boolean} ui.readOnly - read-only mode flag
10 | */
11 | constructor({ api: o, config: s, onSelectFile: d, onDeleteFile: r, onMoveFile: i, readOnly: t }) {
12 | this.api = o, this.config = s, this.onSelectFile = d, this.onDeleteFile = r, this.onMoveFile = i, this.readOnly = t, this.nodes = {
13 | wrapper: x("div", [this.CSS.baseClass, this.CSS.wrapper]),
14 | fileButton: this.createFileButton(),
15 | container: x("div", this.CSS.container),
16 | itemsContainer: x("div", this.CSS.itemsContainer),
17 | controls: x("div", this.CSS.controls),
18 | preloaderContainer: x("div", this.CSS.preloaderContainer),
19 | caption: x("div", [this.CSS.input, this.CSS.caption], {
20 | contentEditable: !this.readOnly
21 | })
22 | }, this.nodes.caption.dataset.placeholder = this.api.i18n.t("Gallery caption"), this.readOnly || (this.nodes.controls.appendChild(this.nodes.preloaderContainer), this.config.maxElementCount && (this.nodes.limitCounter = x("div", this.CSS.limitCounter), this.nodes.controls.appendChild(this.nodes.limitCounter)), this.nodes.controls.appendChild(this.nodes.fileButton)), this.nodes.container.appendChild(this.nodes.itemsContainer), this.readOnly || this.nodes.container.appendChild(this.nodes.controls), this.nodes.wrapper.appendChild(this.nodes.container), this.readOnly || this.nodes.wrapper.appendChild(this.nodes.caption), ["dragenter", "dragover", "dragleave", "drop"].forEach((c) => {
23 | this.nodes.itemsContainer.addEventListener(c, function(g) {
24 | g.preventDefault(), g.stopPropagation();
25 | }, !1);
26 | });
27 | }
28 | /**
29 | * CSS classes
30 | *
31 | * @returns {object}
32 | */
33 | get CSS() {
34 | return {
35 | baseClass: this.api.styles.block,
36 | loading: this.api.styles.loader,
37 | input: this.api.styles.input,
38 | button: this.api.styles.button,
39 | /**
40 | * Tool's classes
41 | */
42 | wrapper: "image-gallery",
43 | container: "image-gallery__container",
44 | controls: "image-gallery__controls",
45 | limitCounter: "image-gallery__counter",
46 | itemsContainer: "image-gallery__items",
47 | imageContainer: "image-gallery__image",
48 | preloaderContainer: "image-gallery__preloaders",
49 | imagePreloader: "image-gallery__preloader",
50 | imageEl: "image-gallery__image-picture",
51 | trashButton: "image-gallery__image-trash",
52 | caption: "image-gallery__caption"
53 | };
54 | }
55 | /**
56 | * Ui statuses:
57 | * - empty
58 | * - uploading
59 | * - filled
60 | *
61 | * @returns {{EMPTY: string, UPLOADING: string, FILLED: string}}
62 | */
63 | static get status() {
64 | return {
65 | EMPTY: "empty",
66 | UPLOADING: "loading",
67 | FILLED: "filled"
68 | };
69 | }
70 | /**
71 | * Renders tool UI
72 | *
73 | * @param {ImageGalleryData} toolData - saved tool data
74 | * @returns {Element}
75 | */
76 | render(o) {
77 | return this.nodes.wrapper;
78 | }
79 | onRendered() {
80 | !this.readOnly && !this.sortable && (this.sortable = new this.config.sortableJs(this.nodes.itemsContainer, {
81 | handle: `.${this.CSS.imageContainer}`,
82 | filter: `.${this.CSS.trashButton}`,
83 | onStart: () => {
84 | this.nodes.itemsContainer.classList.add(`${this.CSS.itemsContainer}--drag`);
85 | },
86 | onEnd: (o) => {
87 | this.nodes.itemsContainer.classList.remove(`${this.CSS.itemsContainer}--drag`), o.oldIndex !== o.newIndex && this.onMoveFile(o.oldIndex, o.newIndex);
88 | }
89 | }), this.nodes.itemsContainer.classList.add("sortable"));
90 | }
91 | /**
92 | * Creates upload-file button
93 | *
94 | * @returns {Element}
95 | */
96 | createFileButton() {
97 | const o = x("div", [this.CSS.button]);
98 | return o.innerHTML = this.config.buttonContent || `${L} ${this.api.i18n.t("Select an Image")}`, o.addEventListener("click", () => {
99 | this.onSelectFile();
100 | }), o;
101 | }
102 | /**
103 | * Shows uploading button
104 | *
105 | * @returns {void}
106 | */
107 | showFileButton() {
108 | this.nodes.fileButton.style.display = "";
109 | }
110 | /**
111 | * Hide uploading button
112 | *
113 | * @returns {void}
114 | */
115 | hideFileButton() {
116 | this.nodes.fileButton.style.display = "none";
117 | }
118 | getPreloader(o) {
119 | let s = x("div", this.CSS.imagePreloader);
120 | this.nodes.preloaderContainer.append(s);
121 | const d = new FileReader();
122 | return d.readAsDataURL(o), d.onload = (r) => {
123 | s.style.backgroundImage = `url(${r.target.result})`;
124 | }, s;
125 | }
126 | removePreloader(o) {
127 | o.remove();
128 | }
129 | /**
130 | * Shows an image
131 | *
132 | * @param {ImageGalleryDataFile} file - image file object
133 | * @returns {void}
134 | */
135 | appendImage(o) {
136 | let s = o.url;
137 | const d = /\.mp4$/.test(s) ? "VIDEO" : "IMG", r = {
138 | src: s
139 | };
140 | let i = "load";
141 | d === "VIDEO" && (r.autoplay = !1, r.muted = !0, r.playsinline = !0, i = "loadeddata");
142 | let t = x("div", [this.CSS.imageContainer]), c = x(d, this.CSS.imageEl, r);
143 | c.addEventListener(i, () => {
144 | this.toggleStatus(t, T.status.FILLED);
145 | }), t.appendChild(c);
146 | const g = this.api.i18n.t("Delete");
147 | if (!this.readOnly) {
148 | let a = x("div", [this.CSS.trashButton], {
149 | innerHTML: I,
150 | title: g
151 | });
152 | this.api.tooltip.onHover(a, g, {
153 | placement: "top"
154 | }), a.addEventListener("click", () => {
155 | this.api.tooltip.hide();
156 | let p = Array.prototype.slice.call(this.nodes.itemsContainer.children).indexOf(t);
157 | p !== -1 && (this.nodes.itemsContainer.removeChild(t), this.onDeleteFile(p));
158 | }), t.appendChild(a);
159 | }
160 | this.nodes.itemsContainer.append(t);
161 | }
162 | /**
163 | * Shows caption input
164 | *
165 | * @param {string} text - caption text
166 | * @returns {void}
167 | */
168 | fillCaption(o) {
169 | this.nodes.caption && (this.nodes.caption.innerHTML = o);
170 | }
171 | /**
172 | * Changes UI status
173 | *
174 | * @param {Element} elem
175 | * @param {string} status - see {@link Ui.status} constants
176 | * @returns {void}
177 | */
178 | toggleStatus(o, s) {
179 | for (const d in T.status)
180 | Object.prototype.hasOwnProperty.call(T.status, d) && o.classList.toggle(`${this.CSS.imageContainer}--${T.status[d]}`, s === T.status[d]);
181 | }
182 | /**
183 | * @param {int} imageCount
184 | * @param {int|null} limitCounter
185 | * @returns {void}
186 | */
187 | updateLimitCounter(o, s) {
188 | s && this.nodes.limitCounter && (o === 0 ? this.nodes.limitCounter.style.display = "none" : (this.nodes.limitCounter.style.display = null, this.nodes.limitCounter.innerText = `${o} / ${s}`));
189 | }
190 | }
191 | const x = function(o, s = null, d = {}) {
192 | const r = document.createElement(o);
193 | Array.isArray(s) ? r.classList.add(...s) : s && r.classList.add(s);
194 | for (const i in d)
195 | r[i] = d[i];
196 | return r;
197 | }, q = '', P = '';
198 | class O {
199 | /**
200 | * @param {object} tune - image tool Tunes managers
201 | * @param {object} tune.api - Editor API
202 | * @param {object} tune.actions - list of user defined tunes
203 | * @param {Function} tune.onChange - tune toggling callback
204 | */
205 | constructor({ api: o, actions: s, onChange: d }) {
206 | this.api = o, this.actions = s, this.onChange = d, this.buttons = [];
207 | }
208 | /**
209 | * Available Image tunes
210 | *
211 | * @returns {{name: string, icon: string, title: string}[]}
212 | */
213 | static get tunes() {
214 | return [
215 | {
216 | name: "slider",
217 | icon: q,
218 | title: "Slider"
219 | },
220 | {
221 | name: "fit",
222 | icon: P,
223 | title: "Fit"
224 | }
225 | ];
226 | }
227 | /**
228 | * Styles
229 | *
230 | * @returns {{wrapper: string, buttonBase: *, button: string, buttonActive: *}}
231 | */
232 | get CSS() {
233 | return {
234 | wrapper: "image-gallery__tune-wrapper",
235 | buttonBase: this.api.styles.button,
236 | button: "image-gallery__tune",
237 | buttonActive: "active"
238 | };
239 | }
240 | /**
241 | * Makes buttons with tunes
242 | *
243 | * @param {ImageGalleryData} toolData - generate Elements of tunes
244 | * @returns {Element}
245 | */
246 | render(o) {
247 | const s = x("div", this.CSS.wrapper), d = this.actions ?? O.tunes;
248 | return this.buttons = [], d.forEach((r) => {
249 | const i = this.api.i18n.t(r.title), t = x("div", [this.CSS.buttonBase, this.CSS.button], {
250 | innerHTML: r.icon,
251 | title: i
252 | });
253 | t.addEventListener("click", () => {
254 | this.tuneClicked(r.name, r.action);
255 | }), t.dataset.tune = r.name, t.classList.toggle(this.CSS.buttonActive, o.style === r.name), this.buttons.push(t), this.api.tooltip.onHover(t, i, {
256 | placement: "top"
257 | }), s.appendChild(t);
258 | }), s;
259 | }
260 | /**
261 | * Clicks to one of the tunes
262 | *
263 | * @param {string} tuneName - clicked tune name
264 | * @param {Function} customFunction - function to execute on click
265 | */
266 | tuneClicked(o, s) {
267 | if (typeof s == "function" && !s(o))
268 | return !1;
269 | this.buttons.forEach((d) => {
270 | d.classList.toggle(this.CSS.buttonActive, d.dataset.tune === o);
271 | }), this.onChange(o);
272 | }
273 | }
274 | const D = '';
275 | function H(E) {
276 | return E && E.__esModule && Object.prototype.hasOwnProperty.call(E, "default") ? E.default : E;
277 | }
278 | var j = { exports: {} };
279 | (function(E, o) {
280 | (function(s, d) {
281 | E.exports = d();
282 | })(window, function() {
283 | return function(s) {
284 | var d = {};
285 | function r(i) {
286 | if (d[i])
287 | return d[i].exports;
288 | var t = d[i] = { i, l: !1, exports: {} };
289 | return s[i].call(t.exports, t, t.exports, r), t.l = !0, t.exports;
290 | }
291 | return r.m = s, r.c = d, r.d = function(i, t, c) {
292 | r.o(i, t) || Object.defineProperty(i, t, { enumerable: !0, get: c });
293 | }, r.r = function(i) {
294 | typeof Symbol < "u" && Symbol.toStringTag && Object.defineProperty(i, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(i, "__esModule", { value: !0 });
295 | }, r.t = function(i, t) {
296 | if (1 & t && (i = r(i)), 8 & t || 4 & t && typeof i == "object" && i && i.__esModule)
297 | return i;
298 | var c = /* @__PURE__ */ Object.create(null);
299 | if (r.r(c), Object.defineProperty(c, "default", { enumerable: !0, value: i }), 2 & t && typeof i != "string")
300 | for (var g in i)
301 | r.d(c, g, (function(a) {
302 | return i[a];
303 | }).bind(null, g));
304 | return c;
305 | }, r.n = function(i) {
306 | var t = i && i.__esModule ? function() {
307 | return i.default;
308 | } : function() {
309 | return i;
310 | };
311 | return r.d(t, "a", t), t;
312 | }, r.o = function(i, t) {
313 | return Object.prototype.hasOwnProperty.call(i, t);
314 | }, r.p = "", r(r.s = 3);
315 | }([function(s, d) {
316 | var r;
317 | r = function() {
318 | return this;
319 | }();
320 | try {
321 | r = r || new Function("return this")();
322 | } catch {
323 | typeof window == "object" && (r = window);
324 | }
325 | s.exports = r;
326 | }, function(s, d, r) {
327 | (function(i) {
328 | var t = r(2), c = setTimeout;
329 | function g() {
330 | }
331 | function a(n) {
332 | if (!(this instanceof a))
333 | throw new TypeError("Promises must be constructed via new");
334 | if (typeof n != "function")
335 | throw new TypeError("not a function");
336 | this._state = 0, this._handled = !1, this._value = void 0, this._deferreds = [], e(n, this);
337 | }
338 | function h(n, u) {
339 | for (; n._state === 3; )
340 | n = n._value;
341 | n._state !== 0 ? (n._handled = !0, a._immediateFn(function() {
342 | var l = n._state === 1 ? u.onFulfilled : u.onRejected;
343 | if (l !== null) {
344 | var y;
345 | try {
346 | y = l(n._value);
347 | } catch (m) {
348 | return void v(u.promise, m);
349 | }
350 | p(u.promise, y);
351 | } else
352 | (n._state === 1 ? p : v)(u.promise, n._value);
353 | })) : n._deferreds.push(u);
354 | }
355 | function p(n, u) {
356 | try {
357 | if (u === n)
358 | throw new TypeError("A promise cannot be resolved with itself.");
359 | if (u && (typeof u == "object" || typeof u == "function")) {
360 | var l = u.then;
361 | if (u instanceof a)
362 | return n._state = 3, n._value = u, void b(n);
363 | if (typeof l == "function")
364 | return void e((y = l, m = u, function() {
365 | y.apply(m, arguments);
366 | }), n);
367 | }
368 | n._state = 1, n._value = u, b(n);
369 | } catch (f) {
370 | v(n, f);
371 | }
372 | var y, m;
373 | }
374 | function v(n, u) {
375 | n._state = 2, n._value = u, b(n);
376 | }
377 | function b(n) {
378 | n._state === 2 && n._deferreds.length === 0 && a._immediateFn(function() {
379 | n._handled || a._unhandledRejectionFn(n._value);
380 | });
381 | for (var u = 0, l = n._deferreds.length; u < l; u++)
382 | h(n, n._deferreds[u]);
383 | n._deferreds = null;
384 | }
385 | function w(n, u, l) {
386 | this.onFulfilled = typeof n == "function" ? n : null, this.onRejected = typeof u == "function" ? u : null, this.promise = l;
387 | }
388 | function e(n, u) {
389 | var l = !1;
390 | try {
391 | n(function(y) {
392 | l || (l = !0, p(u, y));
393 | }, function(y) {
394 | l || (l = !0, v(u, y));
395 | });
396 | } catch (y) {
397 | if (l)
398 | return;
399 | l = !0, v(u, y);
400 | }
401 | }
402 | a.prototype.catch = function(n) {
403 | return this.then(null, n);
404 | }, a.prototype.then = function(n, u) {
405 | var l = new this.constructor(g);
406 | return h(this, new w(n, u, l)), l;
407 | }, a.prototype.finally = t.a, a.all = function(n) {
408 | return new a(function(u, l) {
409 | if (!n || n.length === void 0)
410 | throw new TypeError("Promise.all accepts an array");
411 | var y = Array.prototype.slice.call(n);
412 | if (y.length === 0)
413 | return u([]);
414 | var m = y.length;
415 | function f(S, C) {
416 | try {
417 | if (C && (typeof C == "object" || typeof C == "function")) {
418 | var k = C.then;
419 | if (typeof k == "function")
420 | return void k.call(C, function(F) {
421 | f(S, F);
422 | }, l);
423 | }
424 | y[S] = C, --m == 0 && u(y);
425 | } catch (F) {
426 | l(F);
427 | }
428 | }
429 | for (var _ = 0; _ < y.length; _++)
430 | f(_, y[_]);
431 | });
432 | }, a.resolve = function(n) {
433 | return n && typeof n == "object" && n.constructor === a ? n : new a(function(u) {
434 | u(n);
435 | });
436 | }, a.reject = function(n) {
437 | return new a(function(u, l) {
438 | l(n);
439 | });
440 | }, a.race = function(n) {
441 | return new a(function(u, l) {
442 | for (var y = 0, m = n.length; y < m; y++)
443 | n[y].then(u, l);
444 | });
445 | }, a._immediateFn = typeof i == "function" && function(n) {
446 | i(n);
447 | } || function(n) {
448 | c(n, 0);
449 | }, a._unhandledRejectionFn = function(n) {
450 | typeof console < "u" && console && console.warn("Possible Unhandled Promise Rejection:", n);
451 | }, d.a = a;
452 | }).call(this, r(5).setImmediate);
453 | }, function(s, d, r) {
454 | d.a = function(i) {
455 | var t = this.constructor;
456 | return this.then(function(c) {
457 | return t.resolve(i()).then(function() {
458 | return c;
459 | });
460 | }, function(c) {
461 | return t.resolve(i()).then(function() {
462 | return t.reject(c);
463 | });
464 | });
465 | };
466 | }, function(s, d, r) {
467 | function i(e) {
468 | return (i = typeof Symbol == "function" && typeof Symbol.iterator == "symbol" ? function(n) {
469 | return typeof n;
470 | } : function(n) {
471 | return n && typeof Symbol == "function" && n.constructor === Symbol && n !== Symbol.prototype ? "symbol" : typeof n;
472 | })(e);
473 | }
474 | r(4);
475 | var t, c, g, a, h, p, v, b = r(8), w = (c = function(e) {
476 | return new Promise(function(n, u) {
477 | e = a(e), (e = h(e)).beforeSend && e.beforeSend();
478 | var l = window.XMLHttpRequest ? new window.XMLHttpRequest() : new window.ActiveXObject("Microsoft.XMLHTTP");
479 | l.open(e.method, e.url), l.setRequestHeader("X-Requested-With", "XMLHttpRequest"), Object.keys(e.headers).forEach(function(m) {
480 | var f = e.headers[m];
481 | l.setRequestHeader(m, f);
482 | });
483 | var y = e.ratio;
484 | l.upload.addEventListener("progress", function(m) {
485 | var f = Math.round(m.loaded / m.total * 100), _ = Math.ceil(f * y / 100);
486 | e.progress(Math.min(_, 100));
487 | }, !1), l.addEventListener("progress", function(m) {
488 | var f = Math.round(m.loaded / m.total * 100), _ = Math.ceil(f * (100 - y) / 100) + y;
489 | e.progress(Math.min(_, 100));
490 | }, !1), l.onreadystatechange = function() {
491 | if (l.readyState === 4) {
492 | var m = l.response;
493 | try {
494 | m = JSON.parse(m);
495 | } catch {
496 | }
497 | var f = b.parseHeaders(l.getAllResponseHeaders()), _ = { body: m, code: l.status, headers: f };
498 | v(l.status) ? n(_) : u(_);
499 | }
500 | }, l.send(e.data);
501 | });
502 | }, g = function(e) {
503 | return e.method = "POST", c(e);
504 | }, a = function() {
505 | var e = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {};
506 | if (e.url && typeof e.url != "string")
507 | throw new Error("Url must be a string");
508 | if (e.url = e.url || "", e.method && typeof e.method != "string")
509 | throw new Error("`method` must be a string or null");
510 | if (e.method = e.method ? e.method.toUpperCase() : "GET", e.headers && i(e.headers) !== "object")
511 | throw new Error("`headers` must be an object or null");
512 | if (e.headers = e.headers || {}, e.type && (typeof e.type != "string" || !Object.values(t).includes(e.type)))
513 | throw new Error("`type` must be taken from module's «contentType» library");
514 | if (e.progress && typeof e.progress != "function")
515 | throw new Error("`progress` must be a function or null");
516 | if (e.progress = e.progress || function(n) {
517 | }, e.beforeSend = e.beforeSend || function(n) {
518 | }, e.ratio && typeof e.ratio != "number")
519 | throw new Error("`ratio` must be a number");
520 | if (e.ratio < 0 || e.ratio > 100)
521 | throw new Error("`ratio` must be in a 0-100 interval");
522 | if (e.ratio = e.ratio || 90, e.accept && typeof e.accept != "string")
523 | throw new Error("`accept` must be a string with a list of allowed mime-types");
524 | if (e.accept = e.accept || "*/*", e.multiple && typeof e.multiple != "boolean")
525 | throw new Error("`multiple` must be a true or false");
526 | if (e.multiple = e.multiple || !1, e.fieldName && typeof e.fieldName != "string")
527 | throw new Error("`fieldName` must be a string");
528 | return e.fieldName = e.fieldName || "files", e;
529 | }, h = function(e) {
530 | switch (e.method) {
531 | case "GET":
532 | var n = p(e.data, t.URLENCODED);
533 | delete e.data, e.url = /\?/.test(e.url) ? e.url + "&" + n : e.url + "?" + n;
534 | break;
535 | case "POST":
536 | case "PUT":
537 | case "DELETE":
538 | case "UPDATE":
539 | var u = function() {
540 | return (arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}).type || t.JSON;
541 | }(e);
542 | (b.isFormData(e.data) || b.isFormElement(e.data)) && (u = t.FORM), e.data = p(e.data, u), u !== w.contentType.FORM && (e.headers["content-type"] = u);
543 | }
544 | return e;
545 | }, p = function() {
546 | var e = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {};
547 | switch (arguments.length > 1 ? arguments[1] : void 0) {
548 | case t.URLENCODED:
549 | return b.urlEncode(e);
550 | case t.JSON:
551 | return b.jsonEncode(e);
552 | case t.FORM:
553 | return b.formEncode(e);
554 | default:
555 | return e;
556 | }
557 | }, v = function(e) {
558 | return e >= 200 && e < 300;
559 | }, { contentType: t = { URLENCODED: "application/x-www-form-urlencoded; charset=utf-8", FORM: "multipart/form-data", JSON: "application/json; charset=utf-8" }, request: c, get: function(e) {
560 | return e.method = "GET", c(e);
561 | }, post: g, transport: function(e) {
562 | return e = a(e), b.selectFiles(e).then(function(n) {
563 | for (var u = new FormData(), l = 0; l < n.length; l++)
564 | u.append(e.fieldName, n[l], n[l].name);
565 | b.isObject(e.data) && Object.keys(e.data).forEach(function(m) {
566 | var f = e.data[m];
567 | u.append(m, f);
568 | });
569 | var y = e.beforeSend;
570 | return e.beforeSend = function() {
571 | return y(n);
572 | }, e.data = u, g(e);
573 | });
574 | }, selectFiles: function(e) {
575 | return delete (e = a(e)).beforeSend, b.selectFiles(e);
576 | } });
577 | s.exports = w;
578 | }, function(s, d, r) {
579 | r.r(d);
580 | var i = r(1);
581 | window.Promise = window.Promise || i.a;
582 | }, function(s, d, r) {
583 | (function(i) {
584 | var t = i !== void 0 && i || typeof self < "u" && self || window, c = Function.prototype.apply;
585 | function g(a, h) {
586 | this._id = a, this._clearFn = h;
587 | }
588 | d.setTimeout = function() {
589 | return new g(c.call(setTimeout, t, arguments), clearTimeout);
590 | }, d.setInterval = function() {
591 | return new g(c.call(setInterval, t, arguments), clearInterval);
592 | }, d.clearTimeout = d.clearInterval = function(a) {
593 | a && a.close();
594 | }, g.prototype.unref = g.prototype.ref = function() {
595 | }, g.prototype.close = function() {
596 | this._clearFn.call(t, this._id);
597 | }, d.enroll = function(a, h) {
598 | clearTimeout(a._idleTimeoutId), a._idleTimeout = h;
599 | }, d.unenroll = function(a) {
600 | clearTimeout(a._idleTimeoutId), a._idleTimeout = -1;
601 | }, d._unrefActive = d.active = function(a) {
602 | clearTimeout(a._idleTimeoutId);
603 | var h = a._idleTimeout;
604 | h >= 0 && (a._idleTimeoutId = setTimeout(function() {
605 | a._onTimeout && a._onTimeout();
606 | }, h));
607 | }, r(6), d.setImmediate = typeof self < "u" && self.setImmediate || i !== void 0 && i.setImmediate || this && this.setImmediate, d.clearImmediate = typeof self < "u" && self.clearImmediate || i !== void 0 && i.clearImmediate || this && this.clearImmediate;
608 | }).call(this, r(0));
609 | }, function(s, d, r) {
610 | (function(i, t) {
611 | (function(c, g) {
612 | if (!c.setImmediate) {
613 | var a, h, p, v, b, w = 1, e = {}, n = !1, u = c.document, l = Object.getPrototypeOf && Object.getPrototypeOf(c);
614 | l = l && l.setTimeout ? l : c, {}.toString.call(c.process) === "[object process]" ? a = function(f) {
615 | t.nextTick(function() {
616 | m(f);
617 | });
618 | } : function() {
619 | if (c.postMessage && !c.importScripts) {
620 | var f = !0, _ = c.onmessage;
621 | return c.onmessage = function() {
622 | f = !1;
623 | }, c.postMessage("", "*"), c.onmessage = _, f;
624 | }
625 | }() ? (v = "setImmediate$" + Math.random() + "$", b = function(f) {
626 | f.source === c && typeof f.data == "string" && f.data.indexOf(v) === 0 && m(+f.data.slice(v.length));
627 | }, c.addEventListener ? c.addEventListener("message", b, !1) : c.attachEvent("onmessage", b), a = function(f) {
628 | c.postMessage(v + f, "*");
629 | }) : c.MessageChannel ? ((p = new MessageChannel()).port1.onmessage = function(f) {
630 | m(f.data);
631 | }, a = function(f) {
632 | p.port2.postMessage(f);
633 | }) : u && "onreadystatechange" in u.createElement("script") ? (h = u.documentElement, a = function(f) {
634 | var _ = u.createElement("script");
635 | _.onreadystatechange = function() {
636 | m(f), _.onreadystatechange = null, h.removeChild(_), _ = null;
637 | }, h.appendChild(_);
638 | }) : a = function(f) {
639 | setTimeout(m, 0, f);
640 | }, l.setImmediate = function(f) {
641 | typeof f != "function" && (f = new Function("" + f));
642 | for (var _ = new Array(arguments.length - 1), S = 0; S < _.length; S++)
643 | _[S] = arguments[S + 1];
644 | var C = { callback: f, args: _ };
645 | return e[w] = C, a(w), w++;
646 | }, l.clearImmediate = y;
647 | }
648 | function y(f) {
649 | delete e[f];
650 | }
651 | function m(f) {
652 | if (n)
653 | setTimeout(m, 0, f);
654 | else {
655 | var _ = e[f];
656 | if (_) {
657 | n = !0;
658 | try {
659 | (function(S) {
660 | var C = S.callback, k = S.args;
661 | switch (k.length) {
662 | case 0:
663 | C();
664 | break;
665 | case 1:
666 | C(k[0]);
667 | break;
668 | case 2:
669 | C(k[0], k[1]);
670 | break;
671 | case 3:
672 | C(k[0], k[1], k[2]);
673 | break;
674 | default:
675 | C.apply(g, k);
676 | }
677 | })(_);
678 | } finally {
679 | y(f), n = !1;
680 | }
681 | }
682 | }
683 | }
684 | })(typeof self > "u" ? i === void 0 ? this : i : self);
685 | }).call(this, r(0), r(7));
686 | }, function(s, d) {
687 | var r, i, t = s.exports = {};
688 | function c() {
689 | throw new Error("setTimeout has not been defined");
690 | }
691 | function g() {
692 | throw new Error("clearTimeout has not been defined");
693 | }
694 | function a(l) {
695 | if (r === setTimeout)
696 | return setTimeout(l, 0);
697 | if ((r === c || !r) && setTimeout)
698 | return r = setTimeout, setTimeout(l, 0);
699 | try {
700 | return r(l, 0);
701 | } catch {
702 | try {
703 | return r.call(null, l, 0);
704 | } catch {
705 | return r.call(this, l, 0);
706 | }
707 | }
708 | }
709 | (function() {
710 | try {
711 | r = typeof setTimeout == "function" ? setTimeout : c;
712 | } catch {
713 | r = c;
714 | }
715 | try {
716 | i = typeof clearTimeout == "function" ? clearTimeout : g;
717 | } catch {
718 | i = g;
719 | }
720 | })();
721 | var h, p = [], v = !1, b = -1;
722 | function w() {
723 | v && h && (v = !1, h.length ? p = h.concat(p) : b = -1, p.length && e());
724 | }
725 | function e() {
726 | if (!v) {
727 | var l = a(w);
728 | v = !0;
729 | for (var y = p.length; y; ) {
730 | for (h = p, p = []; ++b < y; )
731 | h && h[b].run();
732 | b = -1, y = p.length;
733 | }
734 | h = null, v = !1, function(m) {
735 | if (i === clearTimeout)
736 | return clearTimeout(m);
737 | if ((i === g || !i) && clearTimeout)
738 | return i = clearTimeout, clearTimeout(m);
739 | try {
740 | i(m);
741 | } catch {
742 | try {
743 | return i.call(null, m);
744 | } catch {
745 | return i.call(this, m);
746 | }
747 | }
748 | }(l);
749 | }
750 | }
751 | function n(l, y) {
752 | this.fun = l, this.array = y;
753 | }
754 | function u() {
755 | }
756 | t.nextTick = function(l) {
757 | var y = new Array(arguments.length - 1);
758 | if (arguments.length > 1)
759 | for (var m = 1; m < arguments.length; m++)
760 | y[m - 1] = arguments[m];
761 | p.push(new n(l, y)), p.length !== 1 || v || a(e);
762 | }, n.prototype.run = function() {
763 | this.fun.apply(null, this.array);
764 | }, t.title = "browser", t.browser = !0, t.env = {}, t.argv = [], t.version = "", t.versions = {}, t.on = u, t.addListener = u, t.once = u, t.off = u, t.removeListener = u, t.removeAllListeners = u, t.emit = u, t.prependListener = u, t.prependOnceListener = u, t.listeners = function(l) {
765 | return [];
766 | }, t.binding = function(l) {
767 | throw new Error("process.binding is not supported");
768 | }, t.cwd = function() {
769 | return "/";
770 | }, t.chdir = function(l) {
771 | throw new Error("process.chdir is not supported");
772 | }, t.umask = function() {
773 | return 0;
774 | };
775 | }, function(s, d, r) {
776 | function i(c, g) {
777 | for (var a = 0; a < g.length; a++) {
778 | var h = g[a];
779 | h.enumerable = h.enumerable || !1, h.configurable = !0, "value" in h && (h.writable = !0), Object.defineProperty(c, h.key, h);
780 | }
781 | }
782 | var t = r(9);
783 | s.exports = function() {
784 | function c() {
785 | (function(p, v) {
786 | if (!(p instanceof v))
787 | throw new TypeError("Cannot call a class as a function");
788 | })(this, c);
789 | }
790 | var g, a, h;
791 | return g = c, h = [{ key: "urlEncode", value: function(p) {
792 | return t(p);
793 | } }, { key: "jsonEncode", value: function(p) {
794 | return JSON.stringify(p);
795 | } }, { key: "formEncode", value: function(p) {
796 | if (this.isFormData(p))
797 | return p;
798 | if (this.isFormElement(p))
799 | return new FormData(p);
800 | if (this.isObject(p)) {
801 | var v = new FormData();
802 | return Object.keys(p).forEach(function(b) {
803 | var w = p[b];
804 | v.append(b, w);
805 | }), v;
806 | }
807 | throw new Error("`data` must be an instance of Object, FormData or