senangwebs-photobooth 1.0.2 → 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 +220 -236
- package/dist/swp.css +790 -256
- 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 +10 -6
- package/src/css/swp.css +790 -256
- 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 +247 -761
- 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/swp_preview.png +0 -0
package/src/js/swp.js
CHANGED
|
@@ -1,828 +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
|
-
import
|
|
8
|
-
import
|
|
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';
|
|
9
20
|
|
|
10
21
|
class SWP {
|
|
11
22
|
constructor(container, options = {}) {
|
|
12
|
-
this.container = container
|
|
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');
|
|
29
|
+
}
|
|
30
|
+
|
|
13
31
|
this.options = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
labels: {
|
|
20
|
-
upload:
|
|
21
|
-
options.labels?.upload !== undefined
|
|
22
|
-
? options.labels.upload
|
|
23
|
-
: "Upload",
|
|
24
|
-
rotateLeft:
|
|
25
|
-
options.labels?.rotateLeft !== undefined
|
|
26
|
-
? options.labels.rotateLeft
|
|
27
|
-
: null,
|
|
28
|
-
rotateRight:
|
|
29
|
-
options.labels?.rotateRight !== undefined
|
|
30
|
-
? options.labels.rotateRight
|
|
31
|
-
: null,
|
|
32
|
-
flipH:
|
|
33
|
-
options.labels?.flipH !== undefined ? options.labels.flipH : null,
|
|
34
|
-
flipV:
|
|
35
|
-
options.labels?.flipV !== undefined ? options.labels.flipV : null,
|
|
36
|
-
resize:
|
|
37
|
-
options.labels?.resize !== undefined
|
|
38
|
-
? options.labels.resize
|
|
39
|
-
: "Resize",
|
|
40
|
-
adjust:
|
|
41
|
-
options.labels?.adjust !== undefined
|
|
42
|
-
? options.labels.adjust
|
|
43
|
-
: "Adjust",
|
|
44
|
-
filters:
|
|
45
|
-
options.labels?.filters !== undefined
|
|
46
|
-
? options.labels.filters
|
|
47
|
-
: "Filters",
|
|
48
|
-
reset:
|
|
49
|
-
options.labels?.reset !== undefined ? options.labels.reset : "Reset",
|
|
50
|
-
save: options.labels?.save !== undefined ? options.labels.save : "Save",
|
|
51
|
-
},
|
|
32
|
+
width: options.width || 1920,
|
|
33
|
+
height: options.height || 1080,
|
|
34
|
+
theme: options.theme || 'dark',
|
|
35
|
+
accentColor: options.accentColor || '#00FF99',
|
|
36
|
+
...options
|
|
52
37
|
};
|
|
53
38
|
|
|
54
|
-
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
this.
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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);
|
|
69
56
|
|
|
70
57
|
this.init();
|
|
71
58
|
}
|
|
72
59
|
|
|
73
60
|
init() {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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);
|
|
78
90
|
}
|
|
79
91
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const controlContainer = document.createElement("div");
|
|
100
|
-
controlContainer.className = "swp-control-container";
|
|
101
|
-
|
|
102
|
-
// Create canvas container
|
|
103
|
-
const canvasContainer = document.createElement("div");
|
|
104
|
-
canvasContainer.className = "swp-canvas-container";
|
|
105
|
-
|
|
106
|
-
// Create canvas
|
|
107
|
-
this.canvas = document.createElement("canvas");
|
|
108
|
-
this.canvas.width = this.options.width;
|
|
109
|
-
this.canvas.height = this.options.height;
|
|
110
|
-
this.canvas.className = "swp-canvas";
|
|
111
|
-
this.ctx = this.canvas.getContext("2d");
|
|
112
|
-
|
|
113
|
-
canvasContainer.appendChild(this.canvas);
|
|
114
|
-
|
|
115
|
-
// Create adjustments panel
|
|
116
|
-
const adjustmentsPanel = document.createElement("div");
|
|
117
|
-
adjustmentsPanel.className = "swp-adjustments-panel";
|
|
118
|
-
adjustmentsPanel.innerHTML = this.createAdjustmentsPanelHTML();
|
|
119
|
-
|
|
120
|
-
// Create filters panel
|
|
121
|
-
const filtersPanel = document.createElement("div");
|
|
122
|
-
filtersPanel.className = "swp-filters-panel";
|
|
123
|
-
filtersPanel.innerHTML = this.createFiltersPanelHTML();
|
|
124
|
-
|
|
125
|
-
// Create resize panel
|
|
126
|
-
const resizePanel = document.createElement("div");
|
|
127
|
-
resizePanel.className = "swp-resize-panel";
|
|
128
|
-
resizePanel.innerHTML = this.createResizePanelHTML();
|
|
129
|
-
|
|
130
|
-
// Append elements
|
|
131
|
-
wrapper.appendChild(toolbar);
|
|
132
|
-
wrapper.appendChild(layoutContainer);
|
|
133
|
-
|
|
134
|
-
layoutContainer.appendChild(canvasContainer);
|
|
135
|
-
layoutContainer.appendChild(controlContainer);
|
|
136
|
-
|
|
137
|
-
controlContainer.appendChild(adjustmentsPanel);
|
|
138
|
-
controlContainer.appendChild(filtersPanel);
|
|
139
|
-
controlContainer.appendChild(resizePanel);
|
|
140
|
-
this.container.appendChild(wrapper);
|
|
141
|
-
|
|
142
|
-
// Bind events
|
|
143
|
-
this.bindEvents();
|
|
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
|
+
});
|
|
144
111
|
}
|
|
145
112
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const createButton = (action, icon, label, title) => {
|
|
150
|
-
const showIcon = showIcons
|
|
151
|
-
? `<span class="swp-icon"><ss-icon icon="${icon}" thickness="2.6"></ss-icon></span>`
|
|
152
|
-
: "";
|
|
153
|
-
const showLabel =
|
|
154
|
-
showLabels && label !== null ? `<span>${label}</span>` : "";
|
|
155
|
-
return `<button class="swp-btn${
|
|
156
|
-
!showLabel ? " swp-btn-icon-only" : ""
|
|
157
|
-
}" data-action="${action}" title="${title}">${showIcon}${showLabel}</button>`;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
return `
|
|
161
|
-
<div class="swp-toolbar-group">
|
|
162
|
-
${createButton(
|
|
163
|
-
"upload",
|
|
164
|
-
"folder",
|
|
165
|
-
labels.upload,
|
|
166
|
-
"Upload Image"
|
|
167
|
-
)}
|
|
168
|
-
<input type="file" id="swp-file-input" accept="image/*" style="display: none;">
|
|
169
|
-
</div>
|
|
170
|
-
|
|
171
|
-
<div class="swp-toolbar-group" style="margin: 0px auto;">
|
|
172
|
-
${createButton(
|
|
173
|
-
"rotate-left",
|
|
174
|
-
"arrow-rotate-ccw",
|
|
175
|
-
labels.rotateLeft,
|
|
176
|
-
"Rotate Left"
|
|
177
|
-
)}
|
|
178
|
-
${createButton(
|
|
179
|
-
"rotate-right",
|
|
180
|
-
"arrow-rotate-cw",
|
|
181
|
-
labels.rotateRight,
|
|
182
|
-
"Rotate Right"
|
|
183
|
-
)}
|
|
184
|
-
${createButton(
|
|
185
|
-
"flip-h",
|
|
186
|
-
"arrow-left-right",
|
|
187
|
-
labels.flipH,
|
|
188
|
-
"Flip Horizontal"
|
|
189
|
-
)}
|
|
190
|
-
${createButton(
|
|
191
|
-
"flip-v",
|
|
192
|
-
"arrow-up-down",
|
|
193
|
-
labels.flipV,
|
|
194
|
-
"Flip Vertical"
|
|
195
|
-
)}
|
|
196
|
-
</div>
|
|
197
|
-
|
|
198
|
-
<div class="swp-toolbar-group">
|
|
199
|
-
${createButton(
|
|
200
|
-
"reset",
|
|
201
|
-
"time-reset",
|
|
202
|
-
labels.reset,
|
|
203
|
-
"Reset"
|
|
204
|
-
)}
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
<div class="swp-toolbar-group">
|
|
208
|
-
${createButton(
|
|
209
|
-
"toggle-resize",
|
|
210
|
-
"maximize",
|
|
211
|
-
labels.resize,
|
|
212
|
-
"Resize"
|
|
213
|
-
)}
|
|
214
|
-
${createButton(
|
|
215
|
-
"toggle-adjustments",
|
|
216
|
-
"sliders-vertical",
|
|
217
|
-
labels.adjust,
|
|
218
|
-
"Adjustments"
|
|
219
|
-
)}
|
|
220
|
-
${createButton(
|
|
221
|
-
"toggle-filters",
|
|
222
|
-
"magic-wand",
|
|
223
|
-
labels.filters,
|
|
224
|
-
"Filters"
|
|
225
|
-
)}
|
|
226
|
-
</div>
|
|
227
|
-
<div class="swp-toolbar-group">
|
|
228
|
-
<button class="swp-btn swp-btn-primary${
|
|
229
|
-
!showLabels || labels.save === null
|
|
230
|
-
? " swp-btn-icon-only"
|
|
231
|
-
: ""
|
|
232
|
-
}" data-action="download" title="Download">
|
|
233
|
-
${
|
|
234
|
-
showIcons
|
|
235
|
-
? '<span class="swp-icon"><ss-icon icon="save"></ss-icon></span>'
|
|
236
|
-
: ""
|
|
237
|
-
}
|
|
238
|
-
${
|
|
239
|
-
showLabels && labels.save !== null
|
|
240
|
-
? `<span>${labels.save}</span>`
|
|
241
|
-
: ""
|
|
242
|
-
}
|
|
243
|
-
</button>
|
|
244
|
-
</div>
|
|
245
|
-
`;
|
|
113
|
+
newDocument(width, height, background = '#ffffff') {
|
|
114
|
+
this.file.newDocument({ width, height, background });
|
|
246
115
|
}
|
|
247
116
|
|
|
248
|
-
|
|
249
|
-
return `
|
|
250
|
-
<h3>Adjustments</h3>
|
|
251
|
-
<div class="swp-adjustment">
|
|
252
|
-
<label>Brightness</label>
|
|
253
|
-
<input type="range" id="swp-brightness" min="0" max="200" value="100">
|
|
254
|
-
<span class="swp-value">100%</span>
|
|
255
|
-
</div>
|
|
256
|
-
<div class="swp-adjustment">
|
|
257
|
-
<label>Contrast</label>
|
|
258
|
-
<input type="range" id="swp-contrast" min="0" max="200" value="100">
|
|
259
|
-
<span class="swp-value">100%</span>
|
|
260
|
-
</div>
|
|
261
|
-
<div class="swp-adjustment">
|
|
262
|
-
<label>Saturation</label>
|
|
263
|
-
<input type="range" id="swp-saturation" min="0" max="200" value="100">
|
|
264
|
-
<span class="swp-value">100%</span>
|
|
265
|
-
</div>
|
|
266
|
-
`;
|
|
117
|
+
getImageData(format = 'png', quality = 1) {
|
|
118
|
+
return this.canvas.toDataURL(`image/${format}`, quality);
|
|
267
119
|
}
|
|
268
120
|
|
|
269
|
-
|
|
270
|
-
return
|
|
271
|
-
<h3>Resize Image</h3>
|
|
272
|
-
<div class="swp-resize-controls">
|
|
273
|
-
<div class="swp-adjustment">
|
|
274
|
-
<label>Width (px)</label>
|
|
275
|
-
<input type="number" id="swp-resize-width" min="1" max="5000" value="800">
|
|
276
|
-
</div>
|
|
277
|
-
<div class="swp-adjustment">
|
|
278
|
-
<label>Height (px)</label>
|
|
279
|
-
<input type="number" id="swp-resize-height" min="1" max="5000" value="600">
|
|
280
|
-
</div>
|
|
281
|
-
<div class="swp-adjustment">
|
|
282
|
-
<label>
|
|
283
|
-
<input type="checkbox" id="swp-maintain-ratio" checked>
|
|
284
|
-
Maintain aspect ratio
|
|
285
|
-
</label>
|
|
286
|
-
</div>
|
|
287
|
-
<button class="swp-btn swp-btn-primary" data-action="apply-resize">Apply Resize</button>
|
|
288
|
-
</div>
|
|
289
|
-
`;
|
|
121
|
+
export(format = 'png', quality = 1) {
|
|
122
|
+
return this.file.export(format, quality);
|
|
290
123
|
}
|
|
291
124
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
{ name: "none", label: "None" },
|
|
295
|
-
{ name: "grayscale", label: "Grayscale" },
|
|
296
|
-
{ name: "sepia", label: "Sepia" },
|
|
297
|
-
{ name: "invert", label: "Invert" },
|
|
298
|
-
{ name: "blur", label: "Blur" },
|
|
299
|
-
];
|
|
300
|
-
|
|
301
|
-
return `
|
|
302
|
-
<h3>Filters</h3>
|
|
303
|
-
<div class="swp-filters-grid">
|
|
304
|
-
${filters
|
|
305
|
-
.map(
|
|
306
|
-
(filter) => `
|
|
307
|
-
<button class="swp-filter-btn ${
|
|
308
|
-
filter.name === "none" ? "active" : ""
|
|
309
|
-
}"
|
|
310
|
-
data-filter="${filter.name}">
|
|
311
|
-
<span class="swp-filter-preview" data-filter="${
|
|
312
|
-
filter.name
|
|
313
|
-
}"></span>
|
|
314
|
-
<span>${filter.label}</span>
|
|
315
|
-
</button>
|
|
316
|
-
`
|
|
317
|
-
)
|
|
318
|
-
.join("")}
|
|
319
|
-
</div>
|
|
320
|
-
`;
|
|
125
|
+
undo() {
|
|
126
|
+
return this.history.undo();
|
|
321
127
|
}
|
|
322
128
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
this.container.querySelectorAll("[data-action]").forEach((btn) => {
|
|
326
|
-
btn.addEventListener("click", (e) => {
|
|
327
|
-
const action = e.currentTarget.getAttribute("data-action");
|
|
328
|
-
this.handleAction(action);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// File input
|
|
333
|
-
const fileInput = this.container.querySelector("#swp-file-input");
|
|
334
|
-
if (fileInput) {
|
|
335
|
-
fileInput.addEventListener("change", (e) => {
|
|
336
|
-
const file = e.target.files[0];
|
|
337
|
-
if (file) {
|
|
338
|
-
const reader = new FileReader();
|
|
339
|
-
reader.onload = (event) => {
|
|
340
|
-
this.loadImage(event.target.result);
|
|
341
|
-
};
|
|
342
|
-
reader.readAsDataURL(file);
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Adjustment sliders
|
|
348
|
-
["brightness", "contrast", "saturation"].forEach((adj) => {
|
|
349
|
-
const slider = this.container.querySelector(`#swp-${adj}`);
|
|
350
|
-
if (slider) {
|
|
351
|
-
slider.addEventListener("input", (e) => {
|
|
352
|
-
const value = parseInt(e.target.value);
|
|
353
|
-
e.target.nextElementSibling.textContent = value + "%";
|
|
354
|
-
this.setAdjustment(adj, value);
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// Filter buttons
|
|
360
|
-
this.container.querySelectorAll("[data-filter]").forEach((btn) => {
|
|
361
|
-
if (btn.classList.contains("swp-filter-btn")) {
|
|
362
|
-
btn.addEventListener("click", (e) => {
|
|
363
|
-
const filter = e.currentTarget.getAttribute("data-filter");
|
|
364
|
-
this.applyFilter(filter);
|
|
365
|
-
|
|
366
|
-
// Update active state
|
|
367
|
-
this.container.querySelectorAll(".swp-filter-btn").forEach((b) => {
|
|
368
|
-
b.classList.remove("active");
|
|
369
|
-
});
|
|
370
|
-
e.currentTarget.classList.add("active");
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
bindResizeInputs() {
|
|
377
|
-
const widthInput = this.container.querySelector("#swp-resize-width");
|
|
378
|
-
const heightInput = this.container.querySelector("#swp-resize-height");
|
|
379
|
-
const maintainRatio = this.container.querySelector("#swp-maintain-ratio");
|
|
380
|
-
|
|
381
|
-
if (!widthInput || !heightInput || !this.currentImage) return;
|
|
382
|
-
|
|
383
|
-
// Remove old event listeners by cloning and replacing
|
|
384
|
-
const newWidthInput = widthInput.cloneNode(true);
|
|
385
|
-
const newHeightInput = heightInput.cloneNode(true);
|
|
386
|
-
widthInput.parentNode.replaceChild(newWidthInput, widthInput);
|
|
387
|
-
heightInput.parentNode.replaceChild(newHeightInput, heightInput);
|
|
388
|
-
|
|
389
|
-
const aspectRatio = this.currentImage.width / this.currentImage.height;
|
|
390
|
-
|
|
391
|
-
// Real-time width adjustment with aspect ratio
|
|
392
|
-
newWidthInput.addEventListener("input", (e) => {
|
|
393
|
-
const newWidth = parseInt(e.target.value);
|
|
394
|
-
|
|
395
|
-
if (maintainRatio.checked) {
|
|
396
|
-
const newHeight = Math.round(newWidth / aspectRatio);
|
|
397
|
-
newHeightInput.value = newHeight;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Real-time preview
|
|
401
|
-
this.updateCanvasSize(
|
|
402
|
-
parseInt(newWidthInput.value),
|
|
403
|
-
parseInt(newHeightInput.value)
|
|
404
|
-
);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
// Real-time height adjustment with aspect ratio
|
|
408
|
-
newHeightInput.addEventListener("input", (e) => {
|
|
409
|
-
const newHeight = parseInt(e.target.value);
|
|
410
|
-
|
|
411
|
-
if (maintainRatio.checked) {
|
|
412
|
-
const newWidth = Math.round(newHeight * aspectRatio);
|
|
413
|
-
newWidthInput.value = newWidth;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Real-time preview
|
|
417
|
-
this.updateCanvasSize(
|
|
418
|
-
parseInt(newWidthInput.value),
|
|
419
|
-
parseInt(newHeightInput.value)
|
|
420
|
-
);
|
|
421
|
-
});
|
|
129
|
+
redo() {
|
|
130
|
+
return this.history.redo();
|
|
422
131
|
}
|
|
423
132
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
return;
|
|
427
|
-
|
|
428
|
-
// Update canvas dimensions
|
|
429
|
-
this.canvas.width = width;
|
|
430
|
-
this.canvas.height = height;
|
|
431
|
-
|
|
432
|
-
// Redraw image at new size
|
|
433
|
-
this.drawImage();
|
|
133
|
+
setTool(name) {
|
|
134
|
+
this.tools.setTool(name);
|
|
434
135
|
}
|
|
435
136
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
case "upload":
|
|
439
|
-
this.container.querySelector("#swp-file-input").click();
|
|
440
|
-
break;
|
|
441
|
-
case "rotate-left":
|
|
442
|
-
this.rotate(-90);
|
|
443
|
-
break;
|
|
444
|
-
case "rotate-right":
|
|
445
|
-
this.rotate(90);
|
|
446
|
-
break;
|
|
447
|
-
case "flip-h":
|
|
448
|
-
this.flip("horizontal");
|
|
449
|
-
break;
|
|
450
|
-
case "flip-v":
|
|
451
|
-
this.flip("vertical");
|
|
452
|
-
break;
|
|
453
|
-
case "toggle-adjustments":
|
|
454
|
-
this.togglePanel(".swp-adjustments-panel");
|
|
455
|
-
break;
|
|
456
|
-
case "toggle-filters":
|
|
457
|
-
this.togglePanel(".swp-filters-panel");
|
|
458
|
-
break;
|
|
459
|
-
case "toggle-resize":
|
|
460
|
-
this.togglePanel(".swp-resize-panel");
|
|
461
|
-
break;
|
|
462
|
-
case "apply-resize":
|
|
463
|
-
this.applyResize();
|
|
464
|
-
break;
|
|
465
|
-
case "reset":
|
|
466
|
-
this.reset();
|
|
467
|
-
break;
|
|
468
|
-
case "download":
|
|
469
|
-
this.download();
|
|
470
|
-
break;
|
|
471
|
-
}
|
|
137
|
+
applyFilter(name, options) {
|
|
138
|
+
this.filters.applyFilter(name, options);
|
|
472
139
|
}
|
|
473
140
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (panel) {
|
|
477
|
-
panel.classList.toggle("active");
|
|
478
|
-
|
|
479
|
-
// Close other panels
|
|
480
|
-
const panels = [
|
|
481
|
-
".swp-adjustments-panel",
|
|
482
|
-
".swp-filters-panel",
|
|
483
|
-
".swp-resize-panel",
|
|
484
|
-
];
|
|
485
|
-
panels.forEach((p) => {
|
|
486
|
-
if (p !== selector) {
|
|
487
|
-
this.container.querySelector(p)?.classList.remove("active");
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
}
|
|
141
|
+
on(event, callback) {
|
|
142
|
+
return this.events.on(event, callback);
|
|
491
143
|
}
|
|
492
144
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
img.crossOrigin = "anonymous";
|
|
496
|
-
|
|
497
|
-
img.onload = () => {
|
|
498
|
-
this.originalImage = img;
|
|
499
|
-
this.currentImage = img;
|
|
500
|
-
|
|
501
|
-
// Set canvas to exact image dimensions
|
|
502
|
-
this.canvas.width = img.width;
|
|
503
|
-
this.canvas.height = img.height;
|
|
504
|
-
|
|
505
|
-
// Update resize inputs
|
|
506
|
-
const widthInput = this.container.querySelector("#swp-resize-width");
|
|
507
|
-
const heightInput = this.container.querySelector("#swp-resize-height");
|
|
508
|
-
if (widthInput) widthInput.value = img.width;
|
|
509
|
-
if (heightInput) heightInput.value = img.height;
|
|
510
|
-
|
|
511
|
-
// Bind resize inputs with aspect ratio
|
|
512
|
-
this.bindResizeInputs();
|
|
513
|
-
|
|
514
|
-
this.drawImage();
|
|
515
|
-
this.emit("load");
|
|
516
|
-
};
|
|
517
|
-
|
|
518
|
-
img.onerror = () => {
|
|
519
|
-
console.error("Failed to load image");
|
|
520
|
-
};
|
|
521
|
-
|
|
522
|
-
img.src = imageUrl;
|
|
145
|
+
off(event, callback) {
|
|
146
|
+
this.events.off(event, callback);
|
|
523
147
|
}
|
|
524
148
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const canvas = this.canvas;
|
|
529
|
-
const ctx = this.ctx;
|
|
530
|
-
|
|
531
|
-
// Clear canvas
|
|
532
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
533
|
-
|
|
534
|
-
// Apply transformations
|
|
535
|
-
ctx.save();
|
|
536
|
-
|
|
537
|
-
// Move to center for transformations
|
|
538
|
-
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
539
|
-
|
|
540
|
-
// Apply rotation
|
|
541
|
-
ctx.rotate((this.currentState.rotation * Math.PI) / 180);
|
|
542
|
-
|
|
543
|
-
// Apply flips
|
|
544
|
-
ctx.scale(
|
|
545
|
-
this.currentState.flipH ? -1 : 1,
|
|
546
|
-
this.currentState.flipV ? -1 : 1
|
|
547
|
-
);
|
|
548
|
-
|
|
549
|
-
// Apply CSS filters
|
|
550
|
-
ctx.filter = this.getFilterString();
|
|
551
|
-
|
|
552
|
-
// Draw image at actual size (canvas matches image dimensions)
|
|
553
|
-
ctx.drawImage(
|
|
554
|
-
this.currentImage,
|
|
555
|
-
-canvas.width / 2,
|
|
556
|
-
-canvas.height / 2,
|
|
557
|
-
canvas.width,
|
|
558
|
-
canvas.height
|
|
559
|
-
);
|
|
560
|
-
|
|
561
|
-
ctx.restore();
|
|
562
|
-
|
|
563
|
-
this.emit("change");
|
|
149
|
+
cancelCurrentAction() {
|
|
150
|
+
this.filters.cancelPreview();
|
|
151
|
+
this.selection.clear();
|
|
564
152
|
}
|
|
565
153
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
if (this.currentState.contrast !== 100) {
|
|
573
|
-
filters.push(`contrast(${this.currentState.contrast}%)`);
|
|
154
|
+
confirmCurrentAction() {
|
|
155
|
+
// Confirm crop, text, etc.
|
|
156
|
+
const tool = this.tools.currentTool;
|
|
157
|
+
if (tool?.name === 'crop' && tool.applyCrop) {
|
|
158
|
+
tool.applyCrop();
|
|
574
159
|
}
|
|
575
|
-
if (
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
switch (this.currentState.filter) {
|
|
580
|
-
case "grayscale":
|
|
581
|
-
filters.push("grayscale(100%)");
|
|
582
|
-
break;
|
|
583
|
-
case "sepia":
|
|
584
|
-
filters.push("sepia(100%)");
|
|
585
|
-
break;
|
|
586
|
-
case "invert":
|
|
587
|
-
filters.push("invert(100%)");
|
|
588
|
-
break;
|
|
589
|
-
case "blur":
|
|
590
|
-
filters.push("blur(5px)");
|
|
591
|
-
break;
|
|
160
|
+
if (tool?.name === 'text' && tool.commitText) {
|
|
161
|
+
tool.commitText();
|
|
592
162
|
}
|
|
593
|
-
|
|
594
|
-
return filters.length > 0 ? filters.join(" ") : "none";
|
|
595
163
|
}
|
|
596
164
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if (!this.currentImage) return;
|
|
606
|
-
|
|
607
|
-
if (direction === "horizontal") {
|
|
608
|
-
this.currentState.flipH = !this.currentState.flipH;
|
|
609
|
-
} else if (direction === "vertical") {
|
|
610
|
-
this.currentState.flipV = !this.currentState.flipV;
|
|
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}`);
|
|
611
173
|
}
|
|
612
|
-
|
|
613
|
-
this.drawImage();
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
setAdjustment(adjustment, value) {
|
|
617
|
-
if (!this.currentImage) return;
|
|
618
|
-
|
|
619
|
-
this.currentState[adjustment] = value;
|
|
620
|
-
this.drawImage();
|
|
174
|
+
this.options.theme = theme;
|
|
621
175
|
}
|
|
622
176
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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);
|
|
642
211
|
}
|
|
643
|
-
|
|
644
|
-
// Canvas size is already updated by real-time preview
|
|
645
|
-
// Now we need to make the resize permanent by updating the currentImage
|
|
646
|
-
|
|
647
|
-
// Create temporary canvas with current canvas content
|
|
648
|
-
const tempCanvas = document.createElement("canvas");
|
|
649
|
-
tempCanvas.width = this.canvas.width;
|
|
650
|
-
tempCanvas.height = this.canvas.height;
|
|
651
|
-
const tempCtx = tempCanvas.getContext("2d");
|
|
652
|
-
|
|
653
|
-
// Copy current canvas content
|
|
654
|
-
tempCtx.drawImage(this.canvas, 0, 0);
|
|
655
|
-
|
|
656
|
-
// Load as new current image
|
|
657
|
-
const resizedImage = new Image();
|
|
658
|
-
resizedImage.onload = () => {
|
|
659
|
-
this.currentImage = resizedImage;
|
|
660
|
-
this.originalImage = resizedImage;
|
|
661
|
-
|
|
662
|
-
// Rebind resize inputs with new aspect ratio
|
|
663
|
-
this.bindResizeInputs();
|
|
664
|
-
|
|
665
|
-
this.drawImage();
|
|
666
|
-
};
|
|
667
|
-
resizedImage.src = tempCanvas.toDataURL();
|
|
212
|
+
this.options.accentColor = color;
|
|
668
213
|
}
|
|
669
214
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
// Create temporary canvas for cropping
|
|
674
|
-
const tempCanvas = document.createElement("canvas");
|
|
675
|
-
tempCanvas.width = width;
|
|
676
|
-
tempCanvas.height = height;
|
|
677
|
-
const tempCtx = tempCanvas.getContext("2d");
|
|
678
|
-
|
|
679
|
-
// Draw cropped area
|
|
680
|
-
tempCtx.drawImage(this.canvas, x, y, width, height, 0, 0, width, height);
|
|
681
|
-
|
|
682
|
-
// Load cropped image
|
|
683
|
-
const croppedImage = new Image();
|
|
684
|
-
croppedImage.onload = () => {
|
|
685
|
-
this.currentImage = croppedImage;
|
|
686
|
-
this.drawImage();
|
|
687
|
-
};
|
|
688
|
-
croppedImage.src = tempCanvas.toDataURL();
|
|
215
|
+
setAccentColor(color) {
|
|
216
|
+
this.applyAccentColor(color);
|
|
217
|
+
this.events.emit(Events.CHANGE, { type: 'accentColor', color });
|
|
689
218
|
}
|
|
690
219
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
contrast: 100,
|
|
696
|
-
saturation: 100,
|
|
697
|
-
rotation: 0,
|
|
698
|
-
flipH: false,
|
|
699
|
-
flipV: false,
|
|
700
|
-
filter: "none",
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
// Reset sliders
|
|
704
|
-
["brightness", "contrast", "saturation"].forEach((adj) => {
|
|
705
|
-
const slider = this.container.querySelector(`#swp-${adj}`);
|
|
706
|
-
if (slider) {
|
|
707
|
-
slider.value = 100;
|
|
708
|
-
slider.nextElementSibling.textContent = "100%";
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
// Reset filter selection
|
|
713
|
-
this.container.querySelectorAll(".swp-filter-btn").forEach((btn) => {
|
|
714
|
-
btn.classList.remove("active");
|
|
715
|
-
if (btn.getAttribute("data-filter") === "none") {
|
|
716
|
-
btn.classList.add("active");
|
|
717
|
-
}
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
// Reset image
|
|
721
|
-
if (this.originalImage) {
|
|
722
|
-
this.currentImage = this.originalImage;
|
|
723
|
-
this.drawImage();
|
|
220
|
+
setTheme(theme) {
|
|
221
|
+
if (theme === 'light' || theme === 'dark') {
|
|
222
|
+
this.applyTheme(theme);
|
|
223
|
+
this.events.emit(Events.CHANGE, { type: 'theme', theme });
|
|
724
224
|
}
|
|
725
225
|
}
|
|
726
226
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
227
|
+
destroy() {
|
|
228
|
+
this.keyboard.destroy();
|
|
229
|
+
this.selection.destroy();
|
|
230
|
+
this.canvas.destroy();
|
|
231
|
+
this.container.innerHTML = '';
|
|
732
232
|
}
|
|
233
|
+
}
|
|
733
234
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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);
|
|
744
245
|
}
|
|
745
|
-
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
this.eventListeners[event] = [];
|
|
750
|
-
}
|
|
751
|
-
this.eventListeners[event].push(callback);
|
|
246
|
+
|
|
247
|
+
// Height
|
|
248
|
+
if (dataset.swpHeight) {
|
|
249
|
+
options.height = parseInt(dataset.swpHeight, 10);
|
|
752
250
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
callback(data);
|
|
758
|
-
});
|
|
759
|
-
}
|
|
251
|
+
|
|
252
|
+
// Theme
|
|
253
|
+
if (dataset.swpTheme) {
|
|
254
|
+
options.theme = dataset.swpTheme;
|
|
760
255
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
if (
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
options
|
|
774
|
-
}
|
|
775
|
-
if (container.dataset.swpWidth) {
|
|
776
|
-
options.width = parseInt(container.dataset.swpWidth);
|
|
777
|
-
}
|
|
778
|
-
if (container.dataset.swpHeight) {
|
|
779
|
-
options.height = parseInt(container.dataset.swpHeight);
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// Parse boolean options
|
|
783
|
-
if (container.dataset.swpShowIcons !== undefined) {
|
|
784
|
-
options.showIcons = container.dataset.swpShowIcons !== "false";
|
|
785
|
-
}
|
|
786
|
-
if (container.dataset.swpShowLabels !== undefined) {
|
|
787
|
-
options.showLabels = container.dataset.swpShowLabels !== "false";
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Parse labels object
|
|
791
|
-
if (container.dataset.swpLabels) {
|
|
792
|
-
try {
|
|
793
|
-
// Support both JSON and simple key:value format
|
|
794
|
-
let labelsStr = container.dataset.swpLabels.trim();
|
|
795
|
-
|
|
796
|
-
// If it looks like JSON, parse as JSON
|
|
797
|
-
if (labelsStr.startsWith("{")) {
|
|
798
|
-
options.labels = JSON.parse(labelsStr);
|
|
799
|
-
} else {
|
|
800
|
-
// Parse simple format: "upload: 'text'; resize: 'text'"
|
|
801
|
-
options.labels = {};
|
|
802
|
-
const pairs = labelsStr.split(";");
|
|
803
|
-
pairs.forEach((pair) => {
|
|
804
|
-
const [key, value] = pair.split(":").map((s) => s.trim());
|
|
805
|
-
if (key && value) {
|
|
806
|
-
// Remove quotes and parse null
|
|
807
|
-
const cleanValue = value.replace(/^['"]|['"]$/g, "");
|
|
808
|
-
options.labels[key] = cleanValue === "null" ? null : cleanValue;
|
|
809
|
-
}
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
} catch (e) {
|
|
813
|
-
console.error("Failed to parse data-swp-labels:", e);
|
|
814
|
-
}
|
|
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;
|
|
815
269
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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);
|
|
819
289
|
});
|
|
820
|
-
|
|
290
|
+
|
|
291
|
+
return instances;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// Store all auto-initialized instances
|
|
295
|
+
SWP.instances = [];
|
|
821
296
|
|
|
822
297
|
// Export
|
|
298
|
+
export { SWP, Events };
|
|
823
299
|
export default SWP;
|
|
824
300
|
|
|
825
|
-
//
|
|
826
|
-
if (typeof window !==
|
|
301
|
+
// Global access
|
|
302
|
+
if (typeof window !== 'undefined') {
|
|
827
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
|
+
}
|
|
828
314
|
}
|