@thisiscrowd/crowdbox 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +348 -0
- package/dist/crowdbox.css +1 -0
- package/dist/crowdbox.css.map +1 -0
- package/dist/crowdbox.esm.js +1557 -0
- package/dist/crowdbox.esm.js.map +1 -0
- package/dist/crowdbox.umd.js +8 -0
- package/dist/crowdbox.umd.js.map +1 -0
- package/package.json +59 -0
- package/src/adapters/iframe.js +30 -0
- package/src/adapters/image.js +91 -0
- package/src/adapters/video.js +44 -0
- package/src/adapters/vimeo.js +52 -0
- package/src/adapters/youtube.js +54 -0
- package/src/browser.js +7 -0
- package/src/core/Crowdbox.js +739 -0
- package/src/core/EventEmitter.js +43 -0
- package/src/core/LightboxGallery.js +2 -0
- package/src/core/Registry.js +26 -0
- package/src/core/utils.js +115 -0
- package/src/index.js +61 -0
- package/src/plugins/download/download.js +50 -0
- package/src/plugins/fullscreen/fullscreen.js +65 -0
- package/src/plugins/share/share.js +63 -0
- package/src/plugins/thumbs/thumbs.js +108 -0
- package/src/plugins/zoom/zoom.js +51 -0
- package/src/react/GalleryGrid.jsx +75 -0
- package/src/react/Lightbox.jsx +36 -0
- package/src/react/useLightbox.js +34 -0
- package/src/styles/_animations.scss +62 -0
- package/src/styles/_mosaic.scss +46 -0
- package/src/styles/_responsive.scss +34 -0
- package/src/styles/_thumbnails.scss +66 -0
- package/src/styles/_toolbar.scss +150 -0
- package/src/styles/_variables.scss +23 -0
- package/src/styles/main.scss +164 -0
- package/types/index.d.ts +239 -0
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thisiscrowd/crowdbox",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A modern, lightweight, extensible Crowdbox gallery plugin supporting images and video",
|
|
6
|
+
"main": "dist/crowdbox.umd.js",
|
|
7
|
+
"module": "dist/crowdbox.esm.js",
|
|
8
|
+
"types": "types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/crowdbox.esm.js",
|
|
12
|
+
"require": "./dist/crowdbox.umd.js"
|
|
13
|
+
},
|
|
14
|
+
"./styles": "./dist/crowdbox.css"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"types",
|
|
19
|
+
"src"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "rollup -c rollup.config.js -w",
|
|
23
|
+
"build": "rollup -c rollup.config.js",
|
|
24
|
+
"build:css": "sass src/styles/main.scss dist/crowdbox.css --style=compressed",
|
|
25
|
+
"build:all": "npm run build && npm run build:css",
|
|
26
|
+
"lint": "eslint src --ext .js",
|
|
27
|
+
"prepublishOnly": "npm run build:all"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"lightbox",
|
|
31
|
+
"gallery",
|
|
32
|
+
"image",
|
|
33
|
+
"video",
|
|
34
|
+
"modal",
|
|
35
|
+
"fancybox",
|
|
36
|
+
"slider",
|
|
37
|
+
"zoom",
|
|
38
|
+
"wordpress",
|
|
39
|
+
"nextjs",
|
|
40
|
+
"react"
|
|
41
|
+
],
|
|
42
|
+
"author": "",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
46
|
+
"@rollup/plugin-terser": "^0.4.0",
|
|
47
|
+
"rollup": "^3.0.0",
|
|
48
|
+
"rollup-plugin-scss": "^4.0.0",
|
|
49
|
+
"sass": "^1.60.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"react": ">=17.0.0",
|
|
53
|
+
"react-dom": ">=17.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"react": { "optional": true },
|
|
57
|
+
"react-dom": { "optional": true }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const IframeAdapter = {
|
|
2
|
+
type: 'iframe',
|
|
3
|
+
|
|
4
|
+
canHandle(item) {
|
|
5
|
+
return item.type === 'iframe';
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
render(item) {
|
|
9
|
+
const wrapper = document.createElement('div');
|
|
10
|
+
wrapper.className = 'lgx-content lgx-content--iframe';
|
|
11
|
+
|
|
12
|
+
const iframe = document.createElement('iframe');
|
|
13
|
+
iframe.className = 'lgx-iframe';
|
|
14
|
+
iframe.src = item.src;
|
|
15
|
+
iframe.allowFullscreen = true;
|
|
16
|
+
iframe.setAttribute('loading', 'lazy');
|
|
17
|
+
iframe.title = item.caption ?? 'Embedded content';
|
|
18
|
+
if (item.iframeWidth) iframe.style.width = item.iframeWidth;
|
|
19
|
+
if (item.iframeHeight) iframe.style.height = item.iframeHeight;
|
|
20
|
+
|
|
21
|
+
iframe.addEventListener('load', () => wrapper.classList.add('lgx-content--loaded'));
|
|
22
|
+
|
|
23
|
+
wrapper.appendChild(iframe);
|
|
24
|
+
return wrapper;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
getThumbnail(item) {
|
|
28
|
+
return item.thumb ?? null;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const COLS = 8;
|
|
2
|
+
const ROWS = 7;
|
|
3
|
+
const TILE_DURATION = 420; // ms — must match CSS animation duration
|
|
4
|
+
const MAX_DELAY = 380; // ms — last tile starts at this offset
|
|
5
|
+
|
|
6
|
+
// Tile colours — dark palette with subtle variation
|
|
7
|
+
const TILE_COLORS = [
|
|
8
|
+
'#111116', '#13131a', '#0f0f14', '#161620',
|
|
9
|
+
'#121218', '#101015', '#141419', '#0e0e13',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function buildMosaic() {
|
|
13
|
+
const mosaic = document.createElement('div');
|
|
14
|
+
mosaic.className = 'lgx-mosaic';
|
|
15
|
+
mosaic.style.gridTemplateColumns = `repeat(${COLS}, 1fr)`;
|
|
16
|
+
mosaic.style.gridTemplateRows = `repeat(${ROWS}, 1fr)`;
|
|
17
|
+
|
|
18
|
+
for (let r = 0; r < ROWS; r++) {
|
|
19
|
+
for (let c = 0; c < COLS; c++) {
|
|
20
|
+
const tile = document.createElement('div');
|
|
21
|
+
tile.className = 'lgx-mosaic__tile';
|
|
22
|
+
|
|
23
|
+
// Diagonal wave: top-left tiles dissolve first
|
|
24
|
+
const diagRatio = (r + c) / (ROWS + COLS - 2); // 0 → 1
|
|
25
|
+
const delay = Math.round(diagRatio * MAX_DELAY);
|
|
26
|
+
tile.style.setProperty('--lgx-tile-delay', `${delay}ms`);
|
|
27
|
+
|
|
28
|
+
// Pick a colour from the palette so adjacent tiles vary slightly
|
|
29
|
+
tile.style.background = TILE_COLORS[(r * COLS + c) % TILE_COLORS.length];
|
|
30
|
+
|
|
31
|
+
mosaic.appendChild(tile);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return mosaic;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const ImageAdapter = {
|
|
39
|
+
type: 'image',
|
|
40
|
+
|
|
41
|
+
canHandle(item) {
|
|
42
|
+
return item.type === 'image' || /\.(jpg|jpeg|png|gif|webp|avif|svg)(\?.*)?$/i.test(item.src ?? '');
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
render(item) {
|
|
46
|
+
const wrapper = document.createElement('div');
|
|
47
|
+
wrapper.className = 'lgx-content lgx-content--image';
|
|
48
|
+
// position:relative and overflow:hidden are set in CSS on .lgx-content--image
|
|
49
|
+
// so the mosaic tiles (position:absolute inside) are clipped correctly.
|
|
50
|
+
|
|
51
|
+
const img = document.createElement('img');
|
|
52
|
+
img.className = 'lgx-image';
|
|
53
|
+
img.alt = item.alt ?? item.caption ?? '';
|
|
54
|
+
img.draggable = false;
|
|
55
|
+
|
|
56
|
+
// Low-res thumb while full image loads
|
|
57
|
+
if (item.thumb) {
|
|
58
|
+
img.src = item.thumb;
|
|
59
|
+
img.style.filter = 'blur(8px)';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const loadStart = Date.now();
|
|
63
|
+
const hi = new Image();
|
|
64
|
+
hi.onload = () => {
|
|
65
|
+
img.src = hi.src;
|
|
66
|
+
img.style.filter = '';
|
|
67
|
+
img.style.transition = 'filter 0.3s';
|
|
68
|
+
wrapper.classList.add('lgx-content--loaded');
|
|
69
|
+
|
|
70
|
+
// Skip mosaic when the image was served from cache (load time < 40 ms) —
|
|
71
|
+
// it would collide with the slide-in transition and cause flickering.
|
|
72
|
+
const fromNetwork = Date.now() - loadStart > 40;
|
|
73
|
+
if (fromNetwork) {
|
|
74
|
+
const mosaic = buildMosaic();
|
|
75
|
+
wrapper.appendChild(mosaic);
|
|
76
|
+
const totalMs = MAX_DELAY + TILE_DURATION + 60;
|
|
77
|
+
setTimeout(() => mosaic.remove(), totalMs);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
hi.onerror = () => wrapper.classList.add('lgx-content--error');
|
|
82
|
+
hi.src = item.src;
|
|
83
|
+
|
|
84
|
+
wrapper.appendChild(img);
|
|
85
|
+
return wrapper;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
getThumbnail(item) {
|
|
89
|
+
return item.thumb ?? item.src;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const VideoAdapter = {
|
|
2
|
+
type: 'video',
|
|
3
|
+
|
|
4
|
+
canHandle(item) {
|
|
5
|
+
return item.type === 'video' || /\.(mp4|webm|ogg|mov)(\?.*)?$/i.test(item.src ?? '');
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
render(item) {
|
|
9
|
+
const wrapper = document.createElement('div');
|
|
10
|
+
wrapper.className = 'lgx-content lgx-content--video';
|
|
11
|
+
|
|
12
|
+
const video = document.createElement('video');
|
|
13
|
+
video.className = 'lgx-video';
|
|
14
|
+
video.controls = true;
|
|
15
|
+
video.playsInline = true;
|
|
16
|
+
video.preload = 'metadata';
|
|
17
|
+
if (item.autoplay) video.autoplay = true;
|
|
18
|
+
if (item.loop) video.loop = true;
|
|
19
|
+
if (item.muted) video.muted = true;
|
|
20
|
+
if (item.poster) video.poster = item.poster;
|
|
21
|
+
video.addEventListener('play', () => {
|
|
22
|
+
wrapper.dispatchEvent(new CustomEvent('lgx:media-play', { bubbles: true }));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const sources = Array.isArray(item.src) ? item.src : [item.src];
|
|
26
|
+
sources.forEach((s) => {
|
|
27
|
+
const src = typeof s === 'string' ? { src: s } : s;
|
|
28
|
+
const source = document.createElement('source');
|
|
29
|
+
source.src = src.src;
|
|
30
|
+
if (src.type) source.type = src.type;
|
|
31
|
+
video.appendChild(source);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
wrapper.appendChild(video);
|
|
35
|
+
wrapper.classList.add('lgx-content--loaded');
|
|
36
|
+
|
|
37
|
+
wrapper._pause = () => video.pause();
|
|
38
|
+
return wrapper;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
getThumbnail(item) {
|
|
42
|
+
return item.thumb ?? item.poster ?? null;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getVimeoId } from '../core/utils.js';
|
|
2
|
+
|
|
3
|
+
export const VimeoAdapter = {
|
|
4
|
+
type: 'vimeo',
|
|
5
|
+
|
|
6
|
+
canHandle(item) {
|
|
7
|
+
return item.type === 'vimeo' || /vimeo\.com/.test(item.src ?? '');
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
render(item) {
|
|
11
|
+
const wrapper = document.createElement('div');
|
|
12
|
+
wrapper.className = 'lgx-content lgx-content--vimeo lgx-content--iframe';
|
|
13
|
+
|
|
14
|
+
const id = getVimeoId(item.src);
|
|
15
|
+
if (!id) {
|
|
16
|
+
wrapper.textContent = 'Invalid Vimeo URL';
|
|
17
|
+
return wrapper;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const params = new URLSearchParams({
|
|
21
|
+
autoplay: item.autoplay ? '1' : '0',
|
|
22
|
+
title: '0',
|
|
23
|
+
byline: '0',
|
|
24
|
+
portrait: '0',
|
|
25
|
+
api: '1',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const iframe = document.createElement('iframe');
|
|
29
|
+
iframe.className = 'lgx-iframe';
|
|
30
|
+
iframe.src = `https://player.vimeo.com/video/${id}?${params}`;
|
|
31
|
+
iframe.allow = 'autoplay; fullscreen; picture-in-picture';
|
|
32
|
+
iframe.allowFullscreen = true;
|
|
33
|
+
iframe.setAttribute('loading', 'lazy');
|
|
34
|
+
iframe.title = item.caption ?? 'Vimeo video';
|
|
35
|
+
|
|
36
|
+
iframe.addEventListener('load', () => wrapper.classList.add('lgx-content--loaded'));
|
|
37
|
+
|
|
38
|
+
wrapper.appendChild(iframe);
|
|
39
|
+
|
|
40
|
+
wrapper._pause = () => {
|
|
41
|
+
try {
|
|
42
|
+
iframe.contentWindow?.postMessage('{"method":"pause"}', '*');
|
|
43
|
+
} catch (_) {}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return wrapper;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
getThumbnail(item) {
|
|
50
|
+
return item.thumb ?? null;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getYouTubeId } from '../core/utils.js';
|
|
2
|
+
|
|
3
|
+
export const YouTubeAdapter = {
|
|
4
|
+
type: 'youtube',
|
|
5
|
+
|
|
6
|
+
canHandle(item) {
|
|
7
|
+
return item.type === 'youtube' || /youtube\.com|youtu\.be/.test(item.src ?? '');
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
render(item) {
|
|
11
|
+
const wrapper = document.createElement('div');
|
|
12
|
+
wrapper.className = 'lgx-content lgx-content--youtube lgx-content--iframe';
|
|
13
|
+
|
|
14
|
+
const id = getYouTubeId(item.src);
|
|
15
|
+
if (!id) {
|
|
16
|
+
wrapper.textContent = 'Invalid YouTube URL';
|
|
17
|
+
return wrapper;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const params = new URLSearchParams({
|
|
21
|
+
autoplay: item.autoplay ? '1' : '0',
|
|
22
|
+
rel: '0',
|
|
23
|
+
modestbranding: '1',
|
|
24
|
+
enablejsapi: '1',
|
|
25
|
+
...(item.start ? { start: item.start } : {}),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const iframe = document.createElement('iframe');
|
|
29
|
+
iframe.className = 'lgx-iframe';
|
|
30
|
+
iframe.src = `https://www.youtube.com/embed/${id}?${params}`;
|
|
31
|
+
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
|
|
32
|
+
iframe.allowFullscreen = true;
|
|
33
|
+
iframe.setAttribute('loading', 'lazy');
|
|
34
|
+
iframe.title = item.caption ?? 'YouTube video';
|
|
35
|
+
|
|
36
|
+
iframe.addEventListener('load', () => wrapper.classList.add('lgx-content--loaded'));
|
|
37
|
+
|
|
38
|
+
wrapper.appendChild(iframe);
|
|
39
|
+
|
|
40
|
+
wrapper._pause = () => {
|
|
41
|
+
try {
|
|
42
|
+
iframe.contentWindow?.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
|
|
43
|
+
} catch (_) {}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return wrapper;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
getThumbnail(item) {
|
|
50
|
+
if (item.thumb) return item.thumb;
|
|
51
|
+
const id = getYouTubeId(item.src);
|
|
52
|
+
return id ? `https://img.youtube.com/vi/${id}/mqdefault.jpg` : null;
|
|
53
|
+
},
|
|
54
|
+
};
|
package/src/browser.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Browser / UMD entry point.
|
|
2
|
+
// Runs all adapter + plugin registrations from index.js,
|
|
3
|
+
// then exports Crowdbox as the single default so that
|
|
4
|
+
// window.Crowdbox is the constructor in script-tag usage.
|
|
5
|
+
import './index.js';
|
|
6
|
+
|
|
7
|
+
export { Crowdbox as default } from './index.js';
|