senangwebs-photobooth 1.0.1 → 2.0.2
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 +219 -235
- package/dist/swp.css +884 -344
- package/dist/swp.js +1 -1
- package/examples/data-attribute.html +69 -0
- package/examples/index.html +56 -51
- package/examples/studio.html +83 -0
- package/package.json +12 -5
- package/src/css/swp.css +884 -344
- package/src/js/core/Canvas.js +398 -0
- package/src/js/core/EventEmitter.js +188 -0
- package/src/js/core/History.js +250 -0
- package/src/js/core/Keyboard.js +323 -0
- package/src/js/filters/FilterManager.js +248 -0
- package/src/js/index.js +48 -0
- package/src/js/io/Clipboard.js +52 -0
- package/src/js/io/FileManager.js +150 -0
- package/src/js/layers/BlendModes.js +342 -0
- package/src/js/layers/Layer.js +415 -0
- package/src/js/layers/LayerManager.js +459 -0
- package/src/js/selection/Selection.js +167 -0
- package/src/js/swp.js +297 -709
- package/src/js/tools/BaseTool.js +264 -0
- package/src/js/tools/BrushTool.js +314 -0
- package/src/js/tools/CropTool.js +400 -0
- package/src/js/tools/EraserTool.js +155 -0
- package/src/js/tools/EyedropperTool.js +184 -0
- package/src/js/tools/FillTool.js +109 -0
- package/src/js/tools/GradientTool.js +141 -0
- package/src/js/tools/HandTool.js +51 -0
- package/src/js/tools/MarqueeTool.js +103 -0
- package/src/js/tools/MoveTool.js +465 -0
- package/src/js/tools/ShapeTool.js +285 -0
- package/src/js/tools/TextTool.js +253 -0
- package/src/js/tools/ToolManager.js +277 -0
- package/src/js/tools/ZoomTool.js +68 -0
- package/src/js/ui/ColorManager.js +71 -0
- package/src/js/ui/UI.js +1211 -0
- package/swp_preview1.png +0 -0
- package/swp_preview2.png +0 -0
- package/webpack.config.js +4 -11
- package/dist/styles.js +0 -1
- package/examples/customization.html +0 -360
- package/spec.md +0 -239
- package/swp_preview.png +0 -0
package/src/js/swp.js
CHANGED
|
@@ -1,726 +1,314 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SenangWebs Photobooth (SWP)
|
|
3
|
-
*
|
|
4
|
-
* @version
|
|
3
|
+
* Professional browser-based image editor
|
|
4
|
+
* @version 2.0.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import '../css/swp.css';
|
|
8
|
+
import { EventEmitter, Events } from './core/EventEmitter.js';
|
|
9
|
+
import { Canvas } from './core/Canvas.js';
|
|
10
|
+
import { History } from './core/History.js';
|
|
11
|
+
import { Keyboard } from './core/Keyboard.js';
|
|
12
|
+
import { LayerManager } from './layers/LayerManager.js';
|
|
13
|
+
import { ToolManager } from './tools/ToolManager.js';
|
|
14
|
+
import { Selection } from './selection/Selection.js';
|
|
15
|
+
import { FilterManager } from './filters/FilterManager.js';
|
|
16
|
+
import { ColorManager } from './ui/ColorManager.js';
|
|
17
|
+
import { UI } from './ui/UI.js';
|
|
18
|
+
import { FileManager } from './io/FileManager.js';
|
|
19
|
+
import { Clipboard } from './io/Clipboard.js';
|
|
8
20
|
|
|
9
21
|
class SWP {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
showLabels: options.showLabels !== undefined ? options.showLabels : true,
|
|
18
|
-
labels: {
|
|
19
|
-
upload: options.labels?.upload !== undefined ? options.labels.upload : 'Upload',
|
|
20
|
-
rotateLeft: options.labels?.rotateLeft !== undefined ? options.labels.rotateLeft : null,
|
|
21
|
-
rotateRight: options.labels?.rotateRight !== undefined ? options.labels.rotateRight : null,
|
|
22
|
-
flipH: options.labels?.flipH !== undefined ? options.labels.flipH : null,
|
|
23
|
-
flipV: options.labels?.flipV !== undefined ? options.labels.flipV : null,
|
|
24
|
-
resize: options.labels?.resize !== undefined ? options.labels.resize : 'Resize',
|
|
25
|
-
adjust: options.labels?.adjust !== undefined ? options.labels.adjust : 'Adjust',
|
|
26
|
-
filters: options.labels?.filters !== undefined ? options.labels.filters : 'Filters',
|
|
27
|
-
reset: options.labels?.reset !== undefined ? options.labels.reset : 'Reset',
|
|
28
|
-
save: options.labels?.save !== undefined ? options.labels.save : 'Save'
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
this.canvas = null;
|
|
33
|
-
this.ctx = null;
|
|
34
|
-
this.originalImage = null;
|
|
35
|
-
this.currentImage = null;
|
|
36
|
-
this.history = [];
|
|
37
|
-
this.currentState = {
|
|
38
|
-
brightness: 100,
|
|
39
|
-
contrast: 100,
|
|
40
|
-
saturation: 100,
|
|
41
|
-
rotation: 0,
|
|
42
|
-
flipH: false,
|
|
43
|
-
flipV: false,
|
|
44
|
-
filter: 'none'
|
|
45
|
-
};
|
|
46
|
-
this.eventListeners = {};
|
|
47
|
-
|
|
48
|
-
this.init();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
init() {
|
|
52
|
-
this.createUI();
|
|
53
|
-
if (this.options.imageUrl) {
|
|
54
|
-
this.loadImage(this.options.imageUrl);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
createUI() {
|
|
59
|
-
// Clear container
|
|
60
|
-
this.container.innerHTML = '';
|
|
61
|
-
this.container.classList.add('swp-container');
|
|
62
|
-
|
|
63
|
-
// Create main structure
|
|
64
|
-
const wrapper = document.createElement('div');
|
|
65
|
-
wrapper.className = 'swp-wrapper';
|
|
66
|
-
|
|
67
|
-
// Create toolbar
|
|
68
|
-
const toolbar = document.createElement('div');
|
|
69
|
-
toolbar.className = 'swp-toolbar';
|
|
70
|
-
toolbar.innerHTML = this.createToolbarHTML();
|
|
71
|
-
|
|
72
|
-
// Create layout container
|
|
73
|
-
const layoutContainer = document.createElement('div');
|
|
74
|
-
layoutContainer.className = 'swp-layout-container';
|
|
75
|
-
|
|
76
|
-
// Create control container
|
|
77
|
-
const controlContainer = document.createElement('div');
|
|
78
|
-
controlContainer.className = 'swp-control-container';
|
|
79
|
-
|
|
80
|
-
// Create canvas container
|
|
81
|
-
const canvasContainer = document.createElement('div');
|
|
82
|
-
canvasContainer.className = 'swp-canvas-container';
|
|
83
|
-
|
|
84
|
-
// Create canvas
|
|
85
|
-
this.canvas = document.createElement('canvas');
|
|
86
|
-
this.canvas.width = this.options.width;
|
|
87
|
-
this.canvas.height = this.options.height;
|
|
88
|
-
this.canvas.className = 'swp-canvas';
|
|
89
|
-
this.ctx = this.canvas.getContext('2d');
|
|
90
|
-
|
|
91
|
-
canvasContainer.appendChild(this.canvas);
|
|
92
|
-
|
|
93
|
-
// Create adjustments panel
|
|
94
|
-
const adjustmentsPanel = document.createElement('div');
|
|
95
|
-
adjustmentsPanel.className = 'swp-adjustments-panel';
|
|
96
|
-
adjustmentsPanel.innerHTML = this.createAdjustmentsPanelHTML();
|
|
97
|
-
|
|
98
|
-
// Create filters panel
|
|
99
|
-
const filtersPanel = document.createElement('div');
|
|
100
|
-
filtersPanel.className = 'swp-filters-panel';
|
|
101
|
-
filtersPanel.innerHTML = this.createFiltersPanelHTML();
|
|
102
|
-
|
|
103
|
-
// Create resize panel
|
|
104
|
-
const resizePanel = document.createElement('div');
|
|
105
|
-
resizePanel.className = 'swp-resize-panel';
|
|
106
|
-
resizePanel.innerHTML = this.createResizePanelHTML();
|
|
107
|
-
|
|
108
|
-
// Append elements
|
|
109
|
-
wrapper.appendChild(toolbar);
|
|
110
|
-
wrapper.appendChild(layoutContainer);
|
|
111
|
-
|
|
112
|
-
layoutContainer.appendChild(canvasContainer);
|
|
113
|
-
layoutContainer.appendChild(controlContainer);
|
|
114
|
-
|
|
115
|
-
controlContainer.appendChild(adjustmentsPanel);
|
|
116
|
-
controlContainer.appendChild(filtersPanel);
|
|
117
|
-
controlContainer.appendChild(resizePanel);
|
|
118
|
-
this.container.appendChild(wrapper);
|
|
119
|
-
|
|
120
|
-
// Bind events
|
|
121
|
-
this.bindEvents();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
createToolbarHTML() {
|
|
125
|
-
const { showIcons, showLabels, labels } = this.options;
|
|
126
|
-
|
|
127
|
-
const createButton = (action, icon, label, title) => {
|
|
128
|
-
const showIcon = showIcons ? `<span class="swp-icon"><i class="${icon}"></i></span>` : '';
|
|
129
|
-
const showLabel = showLabels && label !== null ? `<span>${label}</span>` : '';
|
|
130
|
-
return `<button class="swp-btn${!showLabel ? ' swp-btn-icon-only' : ''}" data-action="${action}" title="${title}">${showIcon}${showLabel}</button>`;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
return `
|
|
134
|
-
<div class="swp-toolbar-group">
|
|
135
|
-
${createButton('upload', 'fas fa-folder', labels.upload, 'Upload Image')}
|
|
136
|
-
<input type="file" id="swp-file-input" accept="image/*" style="display: none;">
|
|
137
|
-
</div>
|
|
138
|
-
|
|
139
|
-
<div class="swp-toolbar-group" style="margin: 0px auto;">
|
|
140
|
-
${createButton('rotate-left', 'fas fa-undo', labels.rotateLeft, 'Rotate Left')}
|
|
141
|
-
${createButton('rotate-right', 'fas fa-redo', labels.rotateRight, 'Rotate Right')}
|
|
142
|
-
${createButton('flip-h', 'fas fa-arrows-alt-h', labels.flipH, 'Flip Horizontal')}
|
|
143
|
-
${createButton('flip-v', 'fas fa-arrows-alt-v', labels.flipV, 'Flip Vertical')}
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
<div class="swp-toolbar-group">
|
|
147
|
-
${createButton('reset', 'fas fa-history', labels.reset, 'Reset')}
|
|
148
|
-
</div>
|
|
149
|
-
|
|
150
|
-
<div class="swp-toolbar-group">
|
|
151
|
-
${createButton('toggle-resize', 'fas fa-expand-arrows-alt', labels.resize, 'Resize')}
|
|
152
|
-
${createButton('toggle-adjustments', 'fas fa-sliders-h', labels.adjust, 'Adjustments')}
|
|
153
|
-
${createButton('toggle-filters', 'fas fa-magic', labels.filters, 'Filters')}
|
|
154
|
-
</div>
|
|
155
|
-
<div class="swp-toolbar-group">
|
|
156
|
-
<button class="swp-btn swp-btn-primary${!showLabels || labels.save === null ? ' swp-btn-icon-only' : ''}" data-action="download" title="Download">
|
|
157
|
-
${showIcons ? '<span class="swp-icon"><i class="fas fa-save"></i></span>' : ''}
|
|
158
|
-
${showLabels && labels.save !== null ? `<span>${labels.save}</span>` : ''}
|
|
159
|
-
</button>
|
|
160
|
-
</div>
|
|
161
|
-
`;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
createAdjustmentsPanelHTML() {
|
|
165
|
-
return `
|
|
166
|
-
<h3>Adjustments</h3>
|
|
167
|
-
<div class="swp-adjustment">
|
|
168
|
-
<label>Brightness</label>
|
|
169
|
-
<input type="range" id="swp-brightness" min="0" max="200" value="100">
|
|
170
|
-
<span class="swp-value">100%</span>
|
|
171
|
-
</div>
|
|
172
|
-
<div class="swp-adjustment">
|
|
173
|
-
<label>Contrast</label>
|
|
174
|
-
<input type="range" id="swp-contrast" min="0" max="200" value="100">
|
|
175
|
-
<span class="swp-value">100%</span>
|
|
176
|
-
</div>
|
|
177
|
-
<div class="swp-adjustment">
|
|
178
|
-
<label>Saturation</label>
|
|
179
|
-
<input type="range" id="swp-saturation" min="0" max="200" value="100">
|
|
180
|
-
<span class="swp-value">100%</span>
|
|
181
|
-
</div>
|
|
182
|
-
`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
createResizePanelHTML() {
|
|
186
|
-
return `
|
|
187
|
-
<h3>Resize Image</h3>
|
|
188
|
-
<div class="swp-resize-controls">
|
|
189
|
-
<div class="swp-adjustment">
|
|
190
|
-
<label>Width (px)</label>
|
|
191
|
-
<input type="number" id="swp-resize-width" min="1" max="5000" value="800">
|
|
192
|
-
</div>
|
|
193
|
-
<div class="swp-adjustment">
|
|
194
|
-
<label>Height (px)</label>
|
|
195
|
-
<input type="number" id="swp-resize-height" min="1" max="5000" value="600">
|
|
196
|
-
</div>
|
|
197
|
-
<div class="swp-adjustment">
|
|
198
|
-
<label>
|
|
199
|
-
<input type="checkbox" id="swp-maintain-ratio" checked>
|
|
200
|
-
Maintain aspect ratio
|
|
201
|
-
</label>
|
|
202
|
-
</div>
|
|
203
|
-
<button class="swp-btn swp-btn-primary" data-action="apply-resize">Apply Resize</button>
|
|
204
|
-
</div>
|
|
205
|
-
`;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
createFiltersPanelHTML() {
|
|
209
|
-
const filters = [
|
|
210
|
-
{ name: 'none', label: 'None' },
|
|
211
|
-
{ name: 'grayscale', label: 'Grayscale' },
|
|
212
|
-
{ name: 'sepia', label: 'Sepia' },
|
|
213
|
-
{ name: 'invert', label: 'Invert' },
|
|
214
|
-
{ name: 'blur', label: 'Blur' }
|
|
215
|
-
];
|
|
216
|
-
|
|
217
|
-
return `
|
|
218
|
-
<h3>Filters</h3>
|
|
219
|
-
<div class="swp-filters-grid">
|
|
220
|
-
${filters.map(filter => `
|
|
221
|
-
<button class="swp-filter-btn ${filter.name === 'none' ? 'active' : ''}"
|
|
222
|
-
data-filter="${filter.name}">
|
|
223
|
-
<span class="swp-filter-preview" data-filter="${filter.name}"></span>
|
|
224
|
-
<span>${filter.label}</span>
|
|
225
|
-
</button>
|
|
226
|
-
`).join('')}
|
|
227
|
-
</div>
|
|
228
|
-
`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
bindEvents() {
|
|
232
|
-
// Toolbar buttons
|
|
233
|
-
this.container.querySelectorAll('[data-action]').forEach(btn => {
|
|
234
|
-
btn.addEventListener('click', (e) => {
|
|
235
|
-
const action = e.currentTarget.getAttribute('data-action');
|
|
236
|
-
this.handleAction(action);
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// File input
|
|
241
|
-
const fileInput = this.container.querySelector('#swp-file-input');
|
|
242
|
-
if (fileInput) {
|
|
243
|
-
fileInput.addEventListener('change', (e) => {
|
|
244
|
-
const file = e.target.files[0];
|
|
245
|
-
if (file) {
|
|
246
|
-
const reader = new FileReader();
|
|
247
|
-
reader.onload = (event) => {
|
|
248
|
-
this.loadImage(event.target.result);
|
|
249
|
-
};
|
|
250
|
-
reader.readAsDataURL(file);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Adjustment sliders
|
|
256
|
-
['brightness', 'contrast', 'saturation'].forEach(adj => {
|
|
257
|
-
const slider = this.container.querySelector(`#swp-${adj}`);
|
|
258
|
-
if (slider) {
|
|
259
|
-
slider.addEventListener('input', (e) => {
|
|
260
|
-
const value = parseInt(e.target.value);
|
|
261
|
-
e.target.nextElementSibling.textContent = value + '%';
|
|
262
|
-
this.setAdjustment(adj, value);
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
// Filter buttons
|
|
268
|
-
this.container.querySelectorAll('[data-filter]').forEach(btn => {
|
|
269
|
-
if (btn.classList.contains('swp-filter-btn')) {
|
|
270
|
-
btn.addEventListener('click', (e) => {
|
|
271
|
-
const filter = e.currentTarget.getAttribute('data-filter');
|
|
272
|
-
this.applyFilter(filter);
|
|
273
|
-
|
|
274
|
-
// Update active state
|
|
275
|
-
this.container.querySelectorAll('.swp-filter-btn').forEach(b => {
|
|
276
|
-
b.classList.remove('active');
|
|
277
|
-
});
|
|
278
|
-
e.currentTarget.classList.add('active');
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
bindResizeInputs() {
|
|
286
|
-
const widthInput = this.container.querySelector('#swp-resize-width');
|
|
287
|
-
const heightInput = this.container.querySelector('#swp-resize-height');
|
|
288
|
-
const maintainRatio = this.container.querySelector('#swp-maintain-ratio');
|
|
289
|
-
|
|
290
|
-
if (!widthInput || !heightInput || !this.currentImage) return;
|
|
291
|
-
|
|
292
|
-
// Remove old event listeners by cloning and replacing
|
|
293
|
-
const newWidthInput = widthInput.cloneNode(true);
|
|
294
|
-
const newHeightInput = heightInput.cloneNode(true);
|
|
295
|
-
widthInput.parentNode.replaceChild(newWidthInput, widthInput);
|
|
296
|
-
heightInput.parentNode.replaceChild(newHeightInput, heightInput);
|
|
297
|
-
|
|
298
|
-
const aspectRatio = this.currentImage.width / this.currentImage.height;
|
|
299
|
-
|
|
300
|
-
// Real-time width adjustment with aspect ratio
|
|
301
|
-
newWidthInput.addEventListener('input', (e) => {
|
|
302
|
-
const newWidth = parseInt(e.target.value);
|
|
303
|
-
|
|
304
|
-
if (maintainRatio.checked) {
|
|
305
|
-
const newHeight = Math.round(newWidth / aspectRatio);
|
|
306
|
-
newHeightInput.value = newHeight;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Real-time preview
|
|
310
|
-
this.updateCanvasSize(parseInt(newWidthInput.value), parseInt(newHeightInput.value));
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Real-time height adjustment with aspect ratio
|
|
314
|
-
newHeightInput.addEventListener('input', (e) => {
|
|
315
|
-
const newHeight = parseInt(e.target.value);
|
|
316
|
-
|
|
317
|
-
if (maintainRatio.checked) {
|
|
318
|
-
const newWidth = Math.round(newHeight * aspectRatio);
|
|
319
|
-
newWidthInput.value = newWidth;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Real-time preview
|
|
323
|
-
this.updateCanvasSize(parseInt(newWidthInput.value), parseInt(newHeightInput.value));
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
updateCanvasSize(width, height) {
|
|
328
|
-
if (!this.currentImage || !width || !height || width < 1 || height < 1) return;
|
|
329
|
-
|
|
330
|
-
// Update canvas dimensions
|
|
331
|
-
this.canvas.width = width;
|
|
332
|
-
this.canvas.height = height;
|
|
333
|
-
|
|
334
|
-
// Redraw image at new size
|
|
335
|
-
this.drawImage();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
handleAction(action) {
|
|
339
|
-
switch(action) {
|
|
340
|
-
case 'upload':
|
|
341
|
-
this.container.querySelector('#swp-file-input').click();
|
|
342
|
-
break;
|
|
343
|
-
case 'rotate-left':
|
|
344
|
-
this.rotate(-90);
|
|
345
|
-
break;
|
|
346
|
-
case 'rotate-right':
|
|
347
|
-
this.rotate(90);
|
|
348
|
-
break;
|
|
349
|
-
case 'flip-h':
|
|
350
|
-
this.flip('horizontal');
|
|
351
|
-
break;
|
|
352
|
-
case 'flip-v':
|
|
353
|
-
this.flip('vertical');
|
|
354
|
-
break;
|
|
355
|
-
case 'toggle-adjustments':
|
|
356
|
-
this.togglePanel('.swp-adjustments-panel');
|
|
357
|
-
break;
|
|
358
|
-
case 'toggle-filters':
|
|
359
|
-
this.togglePanel('.swp-filters-panel');
|
|
360
|
-
break;
|
|
361
|
-
case 'toggle-resize':
|
|
362
|
-
this.togglePanel('.swp-resize-panel');
|
|
363
|
-
break;
|
|
364
|
-
case 'apply-resize':
|
|
365
|
-
this.applyResize();
|
|
366
|
-
break;
|
|
367
|
-
case 'reset':
|
|
368
|
-
this.reset();
|
|
369
|
-
break;
|
|
370
|
-
case 'download':
|
|
371
|
-
this.download();
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
togglePanel(selector) {
|
|
377
|
-
const panel = this.container.querySelector(selector);
|
|
378
|
-
if (panel) {
|
|
379
|
-
panel.classList.toggle('active');
|
|
380
|
-
|
|
381
|
-
// Close other panels
|
|
382
|
-
const panels = ['.swp-adjustments-panel', '.swp-filters-panel', '.swp-resize-panel'];
|
|
383
|
-
panels.forEach(p => {
|
|
384
|
-
if (p !== selector) {
|
|
385
|
-
this.container.querySelector(p)?.classList.remove('active');
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
loadImage(imageUrl) {
|
|
392
|
-
const img = new Image();
|
|
393
|
-
img.crossOrigin = 'anonymous';
|
|
394
|
-
|
|
395
|
-
img.onload = () => {
|
|
396
|
-
this.originalImage = img;
|
|
397
|
-
this.currentImage = img;
|
|
398
|
-
|
|
399
|
-
// Set canvas to exact image dimensions
|
|
400
|
-
this.canvas.width = img.width;
|
|
401
|
-
this.canvas.height = img.height;
|
|
402
|
-
|
|
403
|
-
// Update resize inputs
|
|
404
|
-
const widthInput = this.container.querySelector('#swp-resize-width');
|
|
405
|
-
const heightInput = this.container.querySelector('#swp-resize-height');
|
|
406
|
-
if (widthInput) widthInput.value = img.width;
|
|
407
|
-
if (heightInput) heightInput.value = img.height;
|
|
408
|
-
|
|
409
|
-
// Bind resize inputs with aspect ratio
|
|
410
|
-
this.bindResizeInputs();
|
|
411
|
-
|
|
412
|
-
this.drawImage();
|
|
413
|
-
this.emit('load');
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
img.onerror = () => {
|
|
417
|
-
console.error('Failed to load image');
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
img.src = imageUrl;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
drawImage() {
|
|
424
|
-
if (!this.currentImage) return;
|
|
425
|
-
|
|
426
|
-
const canvas = this.canvas;
|
|
427
|
-
const ctx = this.ctx;
|
|
428
|
-
|
|
429
|
-
// Clear canvas
|
|
430
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
431
|
-
|
|
432
|
-
// Apply transformations
|
|
433
|
-
ctx.save();
|
|
434
|
-
|
|
435
|
-
// Move to center for transformations
|
|
436
|
-
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
437
|
-
|
|
438
|
-
// Apply rotation
|
|
439
|
-
ctx.rotate(this.currentState.rotation * Math.PI / 180);
|
|
440
|
-
|
|
441
|
-
// Apply flips
|
|
442
|
-
ctx.scale(
|
|
443
|
-
this.currentState.flipH ? -1 : 1,
|
|
444
|
-
this.currentState.flipV ? -1 : 1
|
|
445
|
-
);
|
|
446
|
-
|
|
447
|
-
// Apply CSS filters
|
|
448
|
-
ctx.filter = this.getFilterString();
|
|
449
|
-
|
|
450
|
-
// Draw image at actual size (canvas matches image dimensions)
|
|
451
|
-
ctx.drawImage(
|
|
452
|
-
this.currentImage,
|
|
453
|
-
-canvas.width / 2,
|
|
454
|
-
-canvas.height / 2,
|
|
455
|
-
canvas.width,
|
|
456
|
-
canvas.height
|
|
457
|
-
);
|
|
458
|
-
|
|
459
|
-
ctx.restore();
|
|
460
|
-
|
|
461
|
-
this.emit('change');
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
getFilterString() {
|
|
465
|
-
let filters = [];
|
|
466
|
-
|
|
467
|
-
if (this.currentState.brightness !== 100) {
|
|
468
|
-
filters.push(`brightness(${this.currentState.brightness}%)`);
|
|
469
|
-
}
|
|
470
|
-
if (this.currentState.contrast !== 100) {
|
|
471
|
-
filters.push(`contrast(${this.currentState.contrast}%)`);
|
|
472
|
-
}
|
|
473
|
-
if (this.currentState.saturation !== 100) {
|
|
474
|
-
filters.push(`saturate(${this.currentState.saturation}%)`);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
switch(this.currentState.filter) {
|
|
478
|
-
case 'grayscale':
|
|
479
|
-
filters.push('grayscale(100%)');
|
|
480
|
-
break;
|
|
481
|
-
case 'sepia':
|
|
482
|
-
filters.push('sepia(100%)');
|
|
483
|
-
break;
|
|
484
|
-
case 'invert':
|
|
485
|
-
filters.push('invert(100%)');
|
|
486
|
-
break;
|
|
487
|
-
case 'blur':
|
|
488
|
-
filters.push('blur(5px)');
|
|
489
|
-
break;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return filters.length > 0 ? filters.join(' ') : 'none';
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
rotate(degrees) {
|
|
496
|
-
if (!this.currentImage) return;
|
|
497
|
-
|
|
498
|
-
this.currentState.rotation = (this.currentState.rotation + degrees) % 360;
|
|
499
|
-
this.drawImage();
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
flip(direction) {
|
|
503
|
-
if (!this.currentImage) return;
|
|
504
|
-
|
|
505
|
-
if (direction === 'horizontal') {
|
|
506
|
-
this.currentState.flipH = !this.currentState.flipH;
|
|
507
|
-
} else if (direction === 'vertical') {
|
|
508
|
-
this.currentState.flipV = !this.currentState.flipV;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
this.drawImage();
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
setAdjustment(adjustment, value) {
|
|
515
|
-
if (!this.currentImage) return;
|
|
516
|
-
|
|
517
|
-
this.currentState[adjustment] = value;
|
|
518
|
-
this.drawImage();
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
applyFilter(filterName) {
|
|
522
|
-
if (!this.currentImage) return;
|
|
523
|
-
|
|
524
|
-
this.currentState.filter = filterName;
|
|
525
|
-
this.drawImage();
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
applyResize() {
|
|
529
|
-
if (!this.currentImage) return;
|
|
530
|
-
|
|
531
|
-
const widthInput = this.container.querySelector('#swp-resize-width');
|
|
532
|
-
const heightInput = this.container.querySelector('#swp-resize-height');
|
|
533
|
-
|
|
534
|
-
const newWidth = parseInt(widthInput.value);
|
|
535
|
-
const newHeight = parseInt(heightInput.value);
|
|
536
|
-
|
|
537
|
-
if (!newWidth || !newHeight || newWidth < 1 || newHeight < 1) {
|
|
538
|
-
alert('Please enter valid dimensions');
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Canvas size is already updated by real-time preview
|
|
543
|
-
// Now we need to make the resize permanent by updating the currentImage
|
|
544
|
-
|
|
545
|
-
// Create temporary canvas with current canvas content
|
|
546
|
-
const tempCanvas = document.createElement('canvas');
|
|
547
|
-
tempCanvas.width = this.canvas.width;
|
|
548
|
-
tempCanvas.height = this.canvas.height;
|
|
549
|
-
const tempCtx = tempCanvas.getContext('2d');
|
|
550
|
-
|
|
551
|
-
// Copy current canvas content
|
|
552
|
-
tempCtx.drawImage(this.canvas, 0, 0);
|
|
553
|
-
|
|
554
|
-
// Load as new current image
|
|
555
|
-
const resizedImage = new Image();
|
|
556
|
-
resizedImage.onload = () => {
|
|
557
|
-
this.currentImage = resizedImage;
|
|
558
|
-
this.originalImage = resizedImage;
|
|
559
|
-
|
|
560
|
-
// Rebind resize inputs with new aspect ratio
|
|
561
|
-
this.bindResizeInputs();
|
|
562
|
-
|
|
563
|
-
this.drawImage();
|
|
564
|
-
};
|
|
565
|
-
resizedImage.src = tempCanvas.toDataURL();
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
crop(x, y, width, height) {
|
|
569
|
-
if (!this.currentImage) return;
|
|
570
|
-
|
|
571
|
-
// Create temporary canvas for cropping
|
|
572
|
-
const tempCanvas = document.createElement('canvas');
|
|
573
|
-
tempCanvas.width = width;
|
|
574
|
-
tempCanvas.height = height;
|
|
575
|
-
const tempCtx = tempCanvas.getContext('2d');
|
|
576
|
-
|
|
577
|
-
// Draw cropped area
|
|
578
|
-
tempCtx.drawImage(this.canvas, x, y, width, height, 0, 0, width, height);
|
|
579
|
-
|
|
580
|
-
// Load cropped image
|
|
581
|
-
const croppedImage = new Image();
|
|
582
|
-
croppedImage.onload = () => {
|
|
583
|
-
this.currentImage = croppedImage;
|
|
584
|
-
this.drawImage();
|
|
585
|
-
};
|
|
586
|
-
croppedImage.src = tempCanvas.toDataURL();
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
reset() {
|
|
590
|
-
// Reset all states
|
|
591
|
-
this.currentState = {
|
|
592
|
-
brightness: 100,
|
|
593
|
-
contrast: 100,
|
|
594
|
-
saturation: 100,
|
|
595
|
-
rotation: 0,
|
|
596
|
-
flipH: false,
|
|
597
|
-
flipV: false,
|
|
598
|
-
filter: 'none'
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
// Reset sliders
|
|
602
|
-
['brightness', 'contrast', 'saturation'].forEach(adj => {
|
|
603
|
-
const slider = this.container.querySelector(`#swp-${adj}`);
|
|
604
|
-
if (slider) {
|
|
605
|
-
slider.value = 100;
|
|
606
|
-
slider.nextElementSibling.textContent = '100%';
|
|
607
|
-
}
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
// Reset filter selection
|
|
611
|
-
this.container.querySelectorAll('.swp-filter-btn').forEach(btn => {
|
|
612
|
-
btn.classList.remove('active');
|
|
613
|
-
if (btn.getAttribute('data-filter') === 'none') {
|
|
614
|
-
btn.classList.add('active');
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
// Reset image
|
|
619
|
-
if (this.originalImage) {
|
|
620
|
-
this.currentImage = this.originalImage;
|
|
621
|
-
this.drawImage();
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
getImageData(format = 'jpeg', quality = 0.9) {
|
|
626
|
-
if (!this.canvas) return null;
|
|
627
|
-
|
|
628
|
-
const mimeType = format === 'png' ? 'image/png' : 'image/jpeg';
|
|
629
|
-
return this.canvas.toDataURL(mimeType, quality);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
download() {
|
|
633
|
-
const dataUrl = this.getImageData('png');
|
|
634
|
-
if (!dataUrl) return;
|
|
635
|
-
|
|
636
|
-
const link = document.createElement('a');
|
|
637
|
-
link.download = `swp-edited-${Date.now()}.png`;
|
|
638
|
-
link.href = dataUrl;
|
|
639
|
-
link.click();
|
|
640
|
-
|
|
641
|
-
this.emit('save');
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Event system
|
|
645
|
-
on(event, callback) {
|
|
646
|
-
if (!this.eventListeners[event]) {
|
|
647
|
-
this.eventListeners[event] = [];
|
|
648
|
-
}
|
|
649
|
-
this.eventListeners[event].push(callback);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
emit(event, data) {
|
|
653
|
-
if (this.eventListeners[event]) {
|
|
654
|
-
this.eventListeners[event].forEach(callback => {
|
|
655
|
-
callback(data);
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
}
|
|
22
|
+
constructor(container, options = {}) {
|
|
23
|
+
this.container = typeof container === 'string'
|
|
24
|
+
? document.querySelector(container)
|
|
25
|
+
: container;
|
|
26
|
+
|
|
27
|
+
if (!this.container) {
|
|
28
|
+
throw new Error('SWP: Container element not found');
|
|
659
29
|
}
|
|
660
30
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
console.error('Failed to parse data-swp-labels:', e);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
new SWP(container, options);
|
|
716
|
-
});
|
|
31
|
+
this.options = {
|
|
32
|
+
width: options.width || 1920,
|
|
33
|
+
height: options.height || 1080,
|
|
34
|
+
theme: options.theme || 'dark',
|
|
35
|
+
accentColor: options.accentColor || '#00FF99',
|
|
36
|
+
...options
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Core systems
|
|
40
|
+
this.events = new EventEmitter();
|
|
41
|
+
this.canvas = new Canvas(this, { width: this.options.width, height: this.options.height });
|
|
42
|
+
this.history = new History(this);
|
|
43
|
+
this.keyboard = new Keyboard(this);
|
|
44
|
+
|
|
45
|
+
// Managers
|
|
46
|
+
this.layers = new LayerManager(this);
|
|
47
|
+
this.tools = new ToolManager(this);
|
|
48
|
+
this.selection = new Selection(this);
|
|
49
|
+
this.filters = new FilterManager(this);
|
|
50
|
+
this.colors = new ColorManager(this);
|
|
51
|
+
this.file = new FileManager(this);
|
|
52
|
+
this.clipboard = new Clipboard(this);
|
|
53
|
+
|
|
54
|
+
// UI
|
|
55
|
+
this.ui = new UI(this);
|
|
56
|
+
|
|
57
|
+
this.init();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
init() {
|
|
61
|
+
// Initialize UI
|
|
62
|
+
this.ui.init(this.container);
|
|
63
|
+
|
|
64
|
+
// Apply theme class
|
|
65
|
+
this.applyTheme(this.options.theme);
|
|
66
|
+
|
|
67
|
+
// Apply accent color
|
|
68
|
+
this.applyAccentColor(this.options.accentColor);
|
|
69
|
+
|
|
70
|
+
// Initialize canvas in workspace
|
|
71
|
+
const workspace = this.ui.getWorkspace();
|
|
72
|
+
this.canvas.init(workspace);
|
|
73
|
+
|
|
74
|
+
// Bind tool events to canvas
|
|
75
|
+
this.tools.bindCanvasEvents(this.canvas.displayCanvas);
|
|
76
|
+
|
|
77
|
+
// Create initial document
|
|
78
|
+
this.file.newDocument({
|
|
79
|
+
width: this.options.width,
|
|
80
|
+
height: this.options.height
|
|
717
81
|
});
|
|
82
|
+
|
|
83
|
+
// Update UI
|
|
84
|
+
this.ui.updateLayersPanel();
|
|
85
|
+
this.ui.updateHistoryPanel();
|
|
86
|
+
this.ui.updateToolbox();
|
|
87
|
+
|
|
88
|
+
// Emit ready
|
|
89
|
+
this.events.emit(Events.READY);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Public API
|
|
93
|
+
|
|
94
|
+
loadImage(url) {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
const img = new Image();
|
|
97
|
+
img.crossOrigin = 'anonymous';
|
|
98
|
+
img.onload = () => {
|
|
99
|
+
this.file.newDocument({ width: img.width, height: img.height });
|
|
100
|
+
const layer = this.layers.getActiveLayer();
|
|
101
|
+
if (layer) {
|
|
102
|
+
layer.ctx.drawImage(img, 0, 0);
|
|
103
|
+
this.canvas.render();
|
|
104
|
+
this.history.pushState('Load Image');
|
|
105
|
+
}
|
|
106
|
+
resolve();
|
|
107
|
+
};
|
|
108
|
+
img.onerror = reject;
|
|
109
|
+
img.src = url;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
newDocument(width, height, background = '#ffffff') {
|
|
114
|
+
this.file.newDocument({ width, height, background });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getImageData(format = 'png', quality = 1) {
|
|
118
|
+
return this.canvas.toDataURL(`image/${format}`, quality);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export(format = 'png', quality = 1) {
|
|
122
|
+
return this.file.export(format, quality);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
undo() {
|
|
126
|
+
return this.history.undo();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
redo() {
|
|
130
|
+
return this.history.redo();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setTool(name) {
|
|
134
|
+
this.tools.setTool(name);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
applyFilter(name, options) {
|
|
138
|
+
this.filters.applyFilter(name, options);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
on(event, callback) {
|
|
142
|
+
return this.events.on(event, callback);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
off(event, callback) {
|
|
146
|
+
this.events.off(event, callback);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
cancelCurrentAction() {
|
|
150
|
+
this.filters.cancelPreview();
|
|
151
|
+
this.selection.clear();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
confirmCurrentAction() {
|
|
155
|
+
// Confirm crop, text, etc.
|
|
156
|
+
const tool = this.tools.currentTool;
|
|
157
|
+
if (tool?.name === 'crop' && tool.applyCrop) {
|
|
158
|
+
tool.applyCrop();
|
|
159
|
+
}
|
|
160
|
+
if (tool?.name === 'text' && tool.commitText) {
|
|
161
|
+
tool.commitText();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
applyTheme(theme) {
|
|
166
|
+
// The container itself has .swp-app class added by UI.init()
|
|
167
|
+
const app = this.container.classList.contains('swp-app')
|
|
168
|
+
? this.container
|
|
169
|
+
: this.container.querySelector('.swp-app');
|
|
170
|
+
if (app) {
|
|
171
|
+
app.classList.remove('swp-theme-dark', 'swp-theme-light');
|
|
172
|
+
app.classList.add(`swp-theme-${theme}`);
|
|
173
|
+
}
|
|
174
|
+
this.options.theme = theme;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
applyAccentColor(color) {
|
|
178
|
+
const app = this.container.classList.contains('swp-app')
|
|
179
|
+
? this.container
|
|
180
|
+
: this.container.querySelector('.swp-app');
|
|
181
|
+
if (app && color) {
|
|
182
|
+
// Parse hex color to RGB for variations
|
|
183
|
+
const hex = color.replace('#', '');
|
|
184
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
185
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
186
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
187
|
+
|
|
188
|
+
// Create lighter hover color (add 20% brightness)
|
|
189
|
+
const lighten = (val) => Math.min(255, Math.round(val + (255 - val) * 0.2));
|
|
190
|
+
const hoverR = lighten(r);
|
|
191
|
+
const hoverG = lighten(g);
|
|
192
|
+
const hoverB = lighten(b);
|
|
193
|
+
const hoverColor = `#${hoverR.toString(16).padStart(2, '0')}${hoverG.toString(16).padStart(2, '0')}${hoverB.toString(16).padStart(2, '0')}`;
|
|
194
|
+
|
|
195
|
+
// Calculate relative luminance for contrast (WCAG formula)
|
|
196
|
+
const toLinear = (val) => {
|
|
197
|
+
const sRGB = val / 255;
|
|
198
|
+
return sRGB <= 0.03928 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4);
|
|
199
|
+
};
|
|
200
|
+
const luminance = 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
201
|
+
|
|
202
|
+
// Choose contrasting text color (black for light backgrounds, white for dark)
|
|
203
|
+
// Threshold 0.179 is the crossover point where black/white have equal contrast
|
|
204
|
+
const contrastColor = luminance > 0.179 ? '#000000' : '#ffffff';
|
|
205
|
+
|
|
206
|
+
// Apply CSS variables
|
|
207
|
+
app.style.setProperty('--swp-accent', color);
|
|
208
|
+
app.style.setProperty('--swp-accent-hover', hoverColor);
|
|
209
|
+
app.style.setProperty('--swp-accent-dim', `rgba(${r}, ${g}, ${b}, 0.2)`);
|
|
210
|
+
app.style.setProperty('--swp-accent-contrast', contrastColor);
|
|
211
|
+
}
|
|
212
|
+
this.options.accentColor = color;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
setAccentColor(color) {
|
|
216
|
+
this.applyAccentColor(color);
|
|
217
|
+
this.events.emit(Events.CHANGE, { type: 'accentColor', color });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
setTheme(theme) {
|
|
221
|
+
if (theme === 'light' || theme === 'dark') {
|
|
222
|
+
this.applyTheme(theme);
|
|
223
|
+
this.events.emit(Events.CHANGE, { type: 'theme', theme });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
destroy() {
|
|
228
|
+
this.keyboard.destroy();
|
|
229
|
+
this.selection.destroy();
|
|
230
|
+
this.canvas.destroy();
|
|
231
|
+
this.container.innerHTML = '';
|
|
232
|
+
}
|
|
718
233
|
}
|
|
719
234
|
|
|
235
|
+
// Static method to parse data attributes
|
|
236
|
+
SWP.parseDataAttributes = function(element) {
|
|
237
|
+
const options = {};
|
|
238
|
+
|
|
239
|
+
// Parse data-swp-* attributes
|
|
240
|
+
const dataset = element.dataset;
|
|
241
|
+
|
|
242
|
+
// Width
|
|
243
|
+
if (dataset.swpWidth) {
|
|
244
|
+
options.width = parseInt(dataset.swpWidth, 10);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Height
|
|
248
|
+
if (dataset.swpHeight) {
|
|
249
|
+
options.height = parseInt(dataset.swpHeight, 10);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Theme
|
|
253
|
+
if (dataset.swpTheme) {
|
|
254
|
+
options.theme = dataset.swpTheme;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Any other data-swp-* attributes (convert kebab-case to camelCase)
|
|
258
|
+
for (const key in dataset) {
|
|
259
|
+
if (key.startsWith('swp') && key !== 'swp') {
|
|
260
|
+
// Remove 'swp' prefix and convert first char to lowercase
|
|
261
|
+
const optionKey = key.slice(3).charAt(0).toLowerCase() + key.slice(4);
|
|
262
|
+
if (!(optionKey in options)) {
|
|
263
|
+
// Try to parse as number or boolean
|
|
264
|
+
let value = dataset[key];
|
|
265
|
+
if (value === 'true') value = true;
|
|
266
|
+
else if (value === 'false') value = false;
|
|
267
|
+
else if (!isNaN(value) && value !== '') value = parseFloat(value);
|
|
268
|
+
options[optionKey] = value;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return options;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// Auto-initialize elements with data-swp attribute
|
|
277
|
+
SWP.autoInit = function() {
|
|
278
|
+
const elements = document.querySelectorAll('[data-swp]');
|
|
279
|
+
const instances = [];
|
|
280
|
+
|
|
281
|
+
elements.forEach(element => {
|
|
282
|
+
// Skip if already initialized
|
|
283
|
+
if (element.swpInstance) return;
|
|
284
|
+
|
|
285
|
+
const options = SWP.parseDataAttributes(element);
|
|
286
|
+
const instance = new SWP(element, options);
|
|
287
|
+
element.swpInstance = instance;
|
|
288
|
+
instances.push(instance);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return instances;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Store all auto-initialized instances
|
|
295
|
+
SWP.instances = [];
|
|
296
|
+
|
|
720
297
|
// Export
|
|
298
|
+
export { SWP, Events };
|
|
721
299
|
export default SWP;
|
|
722
300
|
|
|
723
|
-
//
|
|
301
|
+
// Global access
|
|
724
302
|
if (typeof window !== 'undefined') {
|
|
725
|
-
|
|
303
|
+
window.SWP = SWP;
|
|
304
|
+
|
|
305
|
+
// Auto-init on DOMContentLoaded
|
|
306
|
+
if (document.readyState === 'loading') {
|
|
307
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
308
|
+
SWP.instances = SWP.autoInit();
|
|
309
|
+
});
|
|
310
|
+
} else {
|
|
311
|
+
// DOM already loaded
|
|
312
|
+
SWP.instances = SWP.autoInit();
|
|
313
|
+
}
|
|
726
314
|
}
|