hexo-plugin-waterfall 0.1.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/LICENSE +21 -0
- package/README.md +86 -0
- package/index.js +19 -0
- package/lib/config.js +15 -0
- package/lib/consts.js +6 -0
- package/lib/filter.js +24 -0
- package/lib/tag.js +68 -0
- package/lib/templates.js +462 -0
- package/package.json +15 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 wuhunyu
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# hexo-plugin-waterfall
|
|
2
|
+
|
|
3
|
+
Hexo 瀑布流图片画廊插件,支持 Lightbox 放大预览和图片懒加载。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install hexo-plugin-waterfall --save
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
或者将插件目录复制到 Hexo 项目的 `node_modules` 目录下。
|
|
12
|
+
|
|
13
|
+
## 配置
|
|
14
|
+
|
|
15
|
+
在 Hexo 的 `_config.yml` 中添加以下配置:
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
waterfall:
|
|
19
|
+
enable: true # 是否启用插件
|
|
20
|
+
columns: 3 # 默认列数
|
|
21
|
+
gap: 10px # 图片间距
|
|
22
|
+
borderRadius: 5px # 图片圆角
|
|
23
|
+
lazyload: true # 启用懒加载
|
|
24
|
+
lightbox: true # 启用点击放大
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 使用
|
|
28
|
+
|
|
29
|
+
在 Markdown 文件中使用 `waterfall` 标签:
|
|
30
|
+
|
|
31
|
+
```markdown
|
|
32
|
+
{% waterfall 3 %}
|
|
33
|
+
<img alt="图片描述1" src="/images/photo1.jpg"/>
|
|
34
|
+
<img alt="图片描述2" src="/images/photo2.jpg"/>
|
|
35
|
+
<img alt="图片描述3" src="/images/photo3.jpg"/>
|
|
36
|
+
<img alt="图片描述4" src="/images/photo4.jpg"/>
|
|
37
|
+
{% endwaterfall %}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 参数说明
|
|
41
|
+
|
|
42
|
+
- `3`:可选参数,指定列数。如果不指定,则使用配置文件中的 `columns` 值。
|
|
43
|
+
|
|
44
|
+
### 图片属性
|
|
45
|
+
|
|
46
|
+
- `src`:图片路径(必填)
|
|
47
|
+
- `alt`:图片描述(可选),在 Lightbox 中会显示为图片标题
|
|
48
|
+
|
|
49
|
+
## 功能特性
|
|
50
|
+
|
|
51
|
+
### 瀑布流布局
|
|
52
|
+
|
|
53
|
+
使用 CSS `column-count` 实现瀑布流布局,自动响应式适配:
|
|
54
|
+
- 桌面端:使用指定列数
|
|
55
|
+
- 平板端(<=768px):2 列
|
|
56
|
+
- 手机端(<=480px):1 列
|
|
57
|
+
|
|
58
|
+
### Lightbox 放大预览
|
|
59
|
+
|
|
60
|
+
- 点击图片弹出全屏预览
|
|
61
|
+
- 左右箭头切换图片
|
|
62
|
+
- 键盘支持:← → 切换,ESC 关闭
|
|
63
|
+
- 显示图片描述(alt 属性)
|
|
64
|
+
- 显示图片计数器
|
|
65
|
+
|
|
66
|
+
### 图片懒加载
|
|
67
|
+
|
|
68
|
+
- 使用 IntersectionObserver API
|
|
69
|
+
- 图片进入可视区域时才加载
|
|
70
|
+
- 加载前显示占位动画
|
|
71
|
+
- 加载完成淡入效果
|
|
72
|
+
|
|
73
|
+
## 示例效果
|
|
74
|
+
|
|
75
|
+
```markdown
|
|
76
|
+
{% waterfall 4 %}
|
|
77
|
+
<img alt="春日樱花" src="/gallery/sakura.jpg"/>
|
|
78
|
+
<img alt="夏日海滩" src="/gallery/beach.jpg"/>
|
|
79
|
+
<img alt="秋日红叶" src="/gallery/autumn.jpg"/>
|
|
80
|
+
<img alt="冬日雪景" src="/gallery/winter.jpg"/>
|
|
81
|
+
{% endwaterfall %}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const loadConfig = require('./lib/config');
|
|
4
|
+
const registerTag = require('./lib/tag');
|
|
5
|
+
const registerFilter = require('./lib/filter');
|
|
6
|
+
|
|
7
|
+
// Load configuration
|
|
8
|
+
const config = loadConfig(hexo);
|
|
9
|
+
|
|
10
|
+
// Skip if disabled
|
|
11
|
+
if (!config.enable) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Register tag: {% waterfall n %} ... {% endwaterfall %}
|
|
16
|
+
hexo.extend.tag.register('waterfall', registerTag(hexo, config), { ends: true });
|
|
17
|
+
|
|
18
|
+
// Register filter: inject CSS and JS after HTML render
|
|
19
|
+
hexo.extend.filter.register('after_render:html', registerFilter(hexo, config));
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = function (hexo) {
|
|
4
|
+
const defaultConfig = {
|
|
5
|
+
enable: true,
|
|
6
|
+
columns: 3,
|
|
7
|
+
gap: '10px',
|
|
8
|
+
borderRadius: '5px',
|
|
9
|
+
lazyload: true,
|
|
10
|
+
lightbox: true
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const config = hexo.config.waterfall || {};
|
|
14
|
+
return Object.assign({}, defaultConfig, config);
|
|
15
|
+
};
|
package/lib/consts.js
ADDED
package/lib/filter.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { WATERFALL_CONTAINER } = require('./consts');
|
|
4
|
+
const templates = require('./templates');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register filter to inject CSS and JS
|
|
8
|
+
* @param {object} hexo - Hexo instance
|
|
9
|
+
* @param {object} config - Plugin configuration
|
|
10
|
+
* @returns {function} Filter handler function
|
|
11
|
+
*/
|
|
12
|
+
module.exports = function (hexo, config) {
|
|
13
|
+
return function (str) {
|
|
14
|
+
// Only inject if page contains waterfall container
|
|
15
|
+
if (str.indexOf(WATERFALL_CONTAINER) === -1) {
|
|
16
|
+
return str;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const assets = templates.renderInjectedAssets(config);
|
|
20
|
+
|
|
21
|
+
// Inject before </body>
|
|
22
|
+
return str.replace('</body>', assets + '</body>');
|
|
23
|
+
};
|
|
24
|
+
};
|
package/lib/tag.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const templates = require('./templates');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extract attribute value from img tag
|
|
7
|
+
* @param {string} imgTag - The full img tag string
|
|
8
|
+
* @param {string} attrName - Attribute name to extract
|
|
9
|
+
* @returns {string} Attribute value or empty string
|
|
10
|
+
*/
|
|
11
|
+
function extractAttribute(imgTag, attrName) {
|
|
12
|
+
// Match both single and double quotes
|
|
13
|
+
const regex = new RegExp(attrName + '=["\']([^"\']*)["\']', 'i');
|
|
14
|
+
const match = imgTag.match(regex);
|
|
15
|
+
return match ? match[1] : '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse img tags from content
|
|
20
|
+
* @param {string} content - HTML content containing img tags
|
|
21
|
+
* @returns {Array} Array of image objects {src, alt}
|
|
22
|
+
*/
|
|
23
|
+
function parseImages(content) {
|
|
24
|
+
const images = [];
|
|
25
|
+
// Match all <img> tags
|
|
26
|
+
const imgTagRegex = /<img\s+[^>]*\/?>/gi;
|
|
27
|
+
let match;
|
|
28
|
+
|
|
29
|
+
while ((match = imgTagRegex.exec(content)) !== null) {
|
|
30
|
+
const imgTag = match[0];
|
|
31
|
+
const src = extractAttribute(imgTag, 'src');
|
|
32
|
+
|
|
33
|
+
// Only add if src exists
|
|
34
|
+
if (src) {
|
|
35
|
+
images.push({
|
|
36
|
+
src: src,
|
|
37
|
+
alt: extractAttribute(imgTag, 'alt')
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return images;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register waterfall tag handler
|
|
47
|
+
* @param {object} hexo - Hexo instance
|
|
48
|
+
* @param {object} config - Plugin configuration
|
|
49
|
+
* @returns {function} Tag handler function
|
|
50
|
+
*/
|
|
51
|
+
module.exports = function (hexo, config) {
|
|
52
|
+
return function (args, content) {
|
|
53
|
+
// Parse columns from args, fallback to config default
|
|
54
|
+
// Validate range: 1-12
|
|
55
|
+
let columns = parseInt(args[0], 10) || config.columns;
|
|
56
|
+
columns = Math.min(Math.max(columns, 1), 12);
|
|
57
|
+
|
|
58
|
+
// Parse images from content
|
|
59
|
+
const images = parseImages(content);
|
|
60
|
+
|
|
61
|
+
if (images.length === 0) {
|
|
62
|
+
return '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Generate HTML
|
|
66
|
+
return templates.renderTagHtml(images, columns, config);
|
|
67
|
+
};
|
|
68
|
+
};
|
package/lib/templates.js
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { WATERFALL_CONTAINER, WATERFALL_ITEM } = require('./consts');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Escape HTML special characters to prevent XSS
|
|
7
|
+
* @param {string} str - Input string
|
|
8
|
+
* @returns {string} Escaped string
|
|
9
|
+
*/
|
|
10
|
+
function escapeHtml(str) {
|
|
11
|
+
if (!str) return '';
|
|
12
|
+
return String(str)
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''')
|
|
16
|
+
.replace(/</g, '<')
|
|
17
|
+
.replace(/>/g, '>');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sanitize CSS value to prevent CSS injection
|
|
22
|
+
* @param {string} value - CSS value
|
|
23
|
+
* @param {string} defaultValue - Default fallback value
|
|
24
|
+
* @returns {string} Sanitized value
|
|
25
|
+
*/
|
|
26
|
+
function sanitizeCssValue(value, defaultValue) {
|
|
27
|
+
if (!value) return defaultValue;
|
|
28
|
+
// Only allow valid CSS length values
|
|
29
|
+
if (/^[\d.]+(px|em|rem|%|vw|vh)?$/.test(String(value))) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
return defaultValue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate HTML for the waterfall tag
|
|
37
|
+
* @param {Array} images - Array of image objects {src, alt}
|
|
38
|
+
* @param {number} columns - Number of columns
|
|
39
|
+
* @param {object} config - Plugin configuration
|
|
40
|
+
* @returns {string} HTML string
|
|
41
|
+
*/
|
|
42
|
+
exports.renderTagHtml = function (images, columns, config) {
|
|
43
|
+
const imagesHtml = images.map((img, index) => {
|
|
44
|
+
const safeSrc = escapeHtml(img.src);
|
|
45
|
+
const safeAlt = escapeHtml(img.alt || '');
|
|
46
|
+
const src = config.lazyload ? '' : safeSrc;
|
|
47
|
+
const dataSrc = config.lazyload ? `data-src="${safeSrc}"` : '';
|
|
48
|
+
const lazyClass = config.lazyload ? 'waterfall-lazy' : '';
|
|
49
|
+
const clickHandler = config.lightbox ? `onclick="event.stopPropagation(); event.preventDefault(); waterfallLightbox.open(this, ${index})"` : '';
|
|
50
|
+
|
|
51
|
+
return `
|
|
52
|
+
<div class="${WATERFALL_ITEM}">
|
|
53
|
+
<img src="${src}" ${dataSrc} alt="${safeAlt}" class="${lazyClass}" ${clickHandler}/>
|
|
54
|
+
</div>
|
|
55
|
+
`;
|
|
56
|
+
}).join('');
|
|
57
|
+
|
|
58
|
+
return `
|
|
59
|
+
<div class="${WATERFALL_CONTAINER}" style="--wf-columns: ${columns};" data-columns="${columns}">
|
|
60
|
+
${imagesHtml}
|
|
61
|
+
</div>
|
|
62
|
+
`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate CSS and JS to inject into the page
|
|
67
|
+
* @param {object} config - Plugin configuration
|
|
68
|
+
* @returns {string} CSS and JS string
|
|
69
|
+
*/
|
|
70
|
+
exports.renderInjectedAssets = function (config) {
|
|
71
|
+
// Sanitize CSS values
|
|
72
|
+
const safeGap = sanitizeCssValue(config.gap, '10px');
|
|
73
|
+
const safeBorderRadius = sanitizeCssValue(config.borderRadius, '5px');
|
|
74
|
+
const safeColumns = Math.min(Math.max(parseInt(config.columns, 10) || 3, 1), 12);
|
|
75
|
+
|
|
76
|
+
const css = `
|
|
77
|
+
<style>
|
|
78
|
+
/* Waterfall Container */
|
|
79
|
+
.${WATERFALL_CONTAINER} {
|
|
80
|
+
column-count: var(--wf-columns, ${safeColumns});
|
|
81
|
+
column-gap: ${safeGap};
|
|
82
|
+
width: 100%;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Waterfall Item */
|
|
86
|
+
.${WATERFALL_ITEM} {
|
|
87
|
+
break-inside: avoid;
|
|
88
|
+
margin-bottom: ${safeGap};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.${WATERFALL_ITEM} img {
|
|
92
|
+
width: 100%;
|
|
93
|
+
display: block;
|
|
94
|
+
border-radius: ${safeBorderRadius};
|
|
95
|
+
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
96
|
+
cursor: ${config.lightbox ? 'pointer' : 'default'};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.${WATERFALL_ITEM} img:hover {
|
|
100
|
+
transform: scale(1.02);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Lazy Loading Styles */
|
|
104
|
+
.waterfall-lazy {
|
|
105
|
+
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
106
|
+
background-size: 200% 100%;
|
|
107
|
+
animation: waterfall-shimmer 1.5s infinite;
|
|
108
|
+
min-height: 150px;
|
|
109
|
+
opacity: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.waterfall-lazy.loaded {
|
|
113
|
+
background: none;
|
|
114
|
+
animation: none;
|
|
115
|
+
opacity: 1;
|
|
116
|
+
min-height: auto;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.waterfall-lazy.error {
|
|
120
|
+
background: #f5f5f5;
|
|
121
|
+
animation: none;
|
|
122
|
+
opacity: 1;
|
|
123
|
+
min-height: 100px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@keyframes waterfall-shimmer {
|
|
127
|
+
0% { background-position: 200% 0; }
|
|
128
|
+
100% { background-position: -200% 0; }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Lightbox Styles */
|
|
132
|
+
.waterfall-lightbox-overlay {
|
|
133
|
+
position: fixed;
|
|
134
|
+
top: 0;
|
|
135
|
+
left: 0;
|
|
136
|
+
width: 100%;
|
|
137
|
+
height: 100%;
|
|
138
|
+
background: rgba(0, 0, 0, 0.9);
|
|
139
|
+
z-index: 9999;
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
opacity: 0;
|
|
145
|
+
pointer-events: none;
|
|
146
|
+
transition: opacity 0.3s ease;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.waterfall-lightbox-overlay.active {
|
|
150
|
+
opacity: 1;
|
|
151
|
+
pointer-events: auto;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.waterfall-lightbox-content {
|
|
155
|
+
position: relative;
|
|
156
|
+
max-width: 90vw;
|
|
157
|
+
max-height: 80vh;
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
align-items: center;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.waterfall-lightbox-content img {
|
|
164
|
+
max-width: 100%;
|
|
165
|
+
max-height: 75vh;
|
|
166
|
+
object-fit: contain;
|
|
167
|
+
border-radius: 4px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.waterfall-lightbox-caption {
|
|
171
|
+
color: #fff;
|
|
172
|
+
text-align: center;
|
|
173
|
+
padding: 15px 20px;
|
|
174
|
+
font-size: 14px;
|
|
175
|
+
max-width: 80vw;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.waterfall-lightbox-close {
|
|
179
|
+
position: absolute;
|
|
180
|
+
top: 20px;
|
|
181
|
+
right: 20px;
|
|
182
|
+
color: #fff;
|
|
183
|
+
font-size: 30px;
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
z-index: 10001;
|
|
186
|
+
width: 40px;
|
|
187
|
+
height: 40px;
|
|
188
|
+
display: flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
justify-content: center;
|
|
191
|
+
transition: transform 0.2s ease;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.waterfall-lightbox-close:hover {
|
|
195
|
+
transform: scale(1.1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.waterfall-lightbox-nav {
|
|
199
|
+
position: absolute;
|
|
200
|
+
top: 50%;
|
|
201
|
+
transform: translateY(-50%);
|
|
202
|
+
color: #fff;
|
|
203
|
+
font-size: 40px;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
padding: 20px;
|
|
206
|
+
user-select: none;
|
|
207
|
+
transition: opacity 0.2s ease;
|
|
208
|
+
z-index: 10001;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.waterfall-lightbox-nav:hover {
|
|
212
|
+
opacity: 0.7;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.waterfall-lightbox-prev {
|
|
216
|
+
left: 20px;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.waterfall-lightbox-next {
|
|
220
|
+
right: 20px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.waterfall-lightbox-counter {
|
|
224
|
+
position: absolute;
|
|
225
|
+
bottom: 20px;
|
|
226
|
+
left: 50%;
|
|
227
|
+
transform: translateX(-50%);
|
|
228
|
+
color: rgba(255, 255, 255, 0.7);
|
|
229
|
+
font-size: 14px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* Responsive Breakpoints */
|
|
233
|
+
@media (max-width: 768px) {
|
|
234
|
+
.${WATERFALL_CONTAINER} {
|
|
235
|
+
column-count: 2 !important;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.waterfall-lightbox-nav {
|
|
239
|
+
font-size: 30px;
|
|
240
|
+
padding: 10px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.waterfall-lightbox-prev {
|
|
244
|
+
left: 10px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.waterfall-lightbox-next {
|
|
248
|
+
right: 10px;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
@media (max-width: 480px) {
|
|
253
|
+
.${WATERFALL_CONTAINER} {
|
|
254
|
+
column-count: 1 !important;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
</style>`;
|
|
258
|
+
|
|
259
|
+
let script = '';
|
|
260
|
+
|
|
261
|
+
// Lazy loading script
|
|
262
|
+
if (config.lazyload) {
|
|
263
|
+
script += `
|
|
264
|
+
<script>
|
|
265
|
+
(function() {
|
|
266
|
+
var lazyImages = document.querySelectorAll('.waterfall-lazy');
|
|
267
|
+
|
|
268
|
+
if ('IntersectionObserver' in window) {
|
|
269
|
+
var observer = new IntersectionObserver(function(entries) {
|
|
270
|
+
entries.forEach(function(entry) {
|
|
271
|
+
if (entry.isIntersecting) {
|
|
272
|
+
var img = entry.target;
|
|
273
|
+
var src = img.getAttribute('data-src');
|
|
274
|
+
if (src) {
|
|
275
|
+
img.src = src;
|
|
276
|
+
img.onload = function() {
|
|
277
|
+
img.classList.add('loaded');
|
|
278
|
+
};
|
|
279
|
+
img.onerror = function() {
|
|
280
|
+
img.classList.add('loaded');
|
|
281
|
+
img.classList.add('error');
|
|
282
|
+
};
|
|
283
|
+
img.removeAttribute('data-src');
|
|
284
|
+
}
|
|
285
|
+
observer.unobserve(img);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}, { rootMargin: '50px' });
|
|
289
|
+
|
|
290
|
+
lazyImages.forEach(function(img) {
|
|
291
|
+
observer.observe(img);
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
// Fallback for browsers without IntersectionObserver
|
|
295
|
+
lazyImages.forEach(function(img) {
|
|
296
|
+
var src = img.getAttribute('data-src');
|
|
297
|
+
if (src) {
|
|
298
|
+
img.src = src;
|
|
299
|
+
img.onload = function() {
|
|
300
|
+
img.classList.add('loaded');
|
|
301
|
+
};
|
|
302
|
+
img.onerror = function() {
|
|
303
|
+
img.classList.add('loaded');
|
|
304
|
+
img.classList.add('error');
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
})();
|
|
310
|
+
</script>`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Lightbox script
|
|
314
|
+
if (config.lightbox) {
|
|
315
|
+
script += `
|
|
316
|
+
<script>
|
|
317
|
+
var waterfallLightbox = (function() {
|
|
318
|
+
var overlay = null;
|
|
319
|
+
var images = [];
|
|
320
|
+
var currentIndex = 0;
|
|
321
|
+
|
|
322
|
+
function createOverlay() {
|
|
323
|
+
if (overlay) return;
|
|
324
|
+
|
|
325
|
+
overlay = document.createElement('div');
|
|
326
|
+
overlay.className = 'waterfall-lightbox-overlay';
|
|
327
|
+
overlay.innerHTML =
|
|
328
|
+
'<span class="waterfall-lightbox-close">×</span>' +
|
|
329
|
+
'<span class="waterfall-lightbox-nav waterfall-lightbox-prev">❮</span>' +
|
|
330
|
+
'<span class="waterfall-lightbox-nav waterfall-lightbox-next">❯</span>' +
|
|
331
|
+
'<div class="waterfall-lightbox-content">' +
|
|
332
|
+
'<img src="" alt=""/>' +
|
|
333
|
+
'<div class="waterfall-lightbox-caption"></div>' +
|
|
334
|
+
'</div>' +
|
|
335
|
+
'<div class="waterfall-lightbox-counter"></div>';
|
|
336
|
+
|
|
337
|
+
document.body.appendChild(overlay);
|
|
338
|
+
|
|
339
|
+
// Close button
|
|
340
|
+
overlay.querySelector('.waterfall-lightbox-close').addEventListener('click', function(e) {
|
|
341
|
+
e.stopPropagation();
|
|
342
|
+
e.preventDefault();
|
|
343
|
+
close();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Nav buttons
|
|
347
|
+
overlay.querySelector('.waterfall-lightbox-prev').addEventListener('click', function(e) {
|
|
348
|
+
e.stopPropagation();
|
|
349
|
+
prev();
|
|
350
|
+
});
|
|
351
|
+
overlay.querySelector('.waterfall-lightbox-next').addEventListener('click', function(e) {
|
|
352
|
+
e.stopPropagation();
|
|
353
|
+
next();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Close on overlay click
|
|
357
|
+
overlay.addEventListener('click', function(e) {
|
|
358
|
+
if (e.target === overlay) {
|
|
359
|
+
close();
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Keyboard navigation
|
|
364
|
+
document.addEventListener('keydown', handleKeydown);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function handleKeydown(e) {
|
|
368
|
+
if (!overlay || !overlay.classList.contains('active')) return;
|
|
369
|
+
|
|
370
|
+
switch(e.key) {
|
|
371
|
+
case 'Escape':
|
|
372
|
+
close();
|
|
373
|
+
break;
|
|
374
|
+
case 'ArrowLeft':
|
|
375
|
+
prev();
|
|
376
|
+
break;
|
|
377
|
+
case 'ArrowRight':
|
|
378
|
+
next();
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function collectImages(clickedImg) {
|
|
384
|
+
var container = clickedImg.closest('.${WATERFALL_CONTAINER}');
|
|
385
|
+
var imgs = container.querySelectorAll('.${WATERFALL_ITEM} img');
|
|
386
|
+
images = Array.prototype.map.call(imgs, function(img) {
|
|
387
|
+
return {
|
|
388
|
+
src: img.getAttribute('data-src') || img.src,
|
|
389
|
+
alt: img.alt || ''
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function showImage(index) {
|
|
395
|
+
if (index < 0 || index >= images.length) return;
|
|
396
|
+
|
|
397
|
+
currentIndex = index;
|
|
398
|
+
var img = overlay.querySelector('.waterfall-lightbox-content img');
|
|
399
|
+
var caption = overlay.querySelector('.waterfall-lightbox-caption');
|
|
400
|
+
var counter = overlay.querySelector('.waterfall-lightbox-counter');
|
|
401
|
+
|
|
402
|
+
img.src = images[index].src;
|
|
403
|
+
img.alt = images[index].alt;
|
|
404
|
+
caption.textContent = images[index].alt;
|
|
405
|
+
caption.style.display = images[index].alt ? 'block' : 'none';
|
|
406
|
+
counter.textContent = (index + 1) + ' / ' + images.length;
|
|
407
|
+
|
|
408
|
+
// Show/hide nav buttons
|
|
409
|
+
overlay.querySelector('.waterfall-lightbox-prev').style.display = images.length > 1 ? 'block' : 'none';
|
|
410
|
+
overlay.querySelector('.waterfall-lightbox-next').style.display = images.length > 1 ? 'block' : 'none';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function open(clickedImg, index) {
|
|
414
|
+
createOverlay();
|
|
415
|
+
collectImages(clickedImg);
|
|
416
|
+
showImage(index);
|
|
417
|
+
|
|
418
|
+
// Prevent body scroll
|
|
419
|
+
document.body.style.overflow = 'hidden';
|
|
420
|
+
|
|
421
|
+
// Show overlay with animation
|
|
422
|
+
requestAnimationFrame(function() {
|
|
423
|
+
overlay.classList.add('active');
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function close() {
|
|
428
|
+
if (!overlay) return;
|
|
429
|
+
|
|
430
|
+
overlay.classList.remove('active');
|
|
431
|
+
document.body.style.overflow = '';
|
|
432
|
+
|
|
433
|
+
setTimeout(function() {
|
|
434
|
+
images = [];
|
|
435
|
+
currentIndex = 0;
|
|
436
|
+
}, 300);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function prev() {
|
|
440
|
+
var newIndex = currentIndex - 1;
|
|
441
|
+
if (newIndex < 0) newIndex = images.length - 1;
|
|
442
|
+
showImage(newIndex);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function next() {
|
|
446
|
+
var newIndex = currentIndex + 1;
|
|
447
|
+
if (newIndex >= images.length) newIndex = 0;
|
|
448
|
+
showImage(newIndex);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
open: open,
|
|
453
|
+
close: close,
|
|
454
|
+
prev: prev,
|
|
455
|
+
next: next
|
|
456
|
+
};
|
|
457
|
+
})();
|
|
458
|
+
</script>`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return css + script;
|
|
462
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hexo-plugin-waterfall",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"hexo",
|
|
7
|
+
"plugin",
|
|
8
|
+
"waterfall",
|
|
9
|
+
"gallery",
|
|
10
|
+
"lightbox"
|
|
11
|
+
],
|
|
12
|
+
"author": "wuhunyu",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"description": "Hexo waterfall image gallery plugin with lightbox support."
|
|
15
|
+
}
|