@squeditor/squeditor-framework 1.0.1 → 1.0.3
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 +7 -15
- package/package.json +1 -1
- package/project-template/package.json +3 -1
- package/project-template/squeditor.config.js +21 -9
- package/project-template/src/assets/js/_slider_dynamic.js +2 -0
- package/project-template/src/assets/js/gsap-init.js +28 -1
- package/project-template/src/assets/js/main.js +1 -0
- package/project-template/src/assets/js/modules/splide-init.js +207 -0
- package/project-template/src/assets/js/modules/swiper-init.js +216 -0
- package/project-template/src/assets/js/uikit-components.js +27 -21
- package/project-template/src/assets/scss/_swiper.scss +30 -0
- package/project-template/src/assets/scss/main.scss +2 -1
- package/project-template/src/assets/scss/themes/_two.scss +95 -0
- package/project-template/src/assets/static/images/og-default.png +0 -0
- package/project-template/src/assets/static/images/placeholder.png +0 -0
- package/project-template/src/index.php +5 -5
- package/project-template/src/init.php +38 -2
- package/project-template/src/page-templates/head.php +9 -1
- package/project-template/src/slider-test.php +87 -0
- package/project-template/src/template-parts/header.php +11 -11
- package/scripts/build-components.js +78 -1
- package/scripts/copy-static.js +26 -0
- package/scripts/dev-router.php +10 -1
- package/scripts/dev.js +30 -1
- package/scripts/package-customer.js +52 -21
- package/scripts/package-dist.js +1 -1
- package/scripts/scaffold.js +18 -5
- package/scripts/snapshot.js +134 -33
- package/scripts/utils/resolve-pages.js +47 -0
package/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
# ⚡️ Squeditor Framework
|
|
2
|
-
|
|
3
1
|
**Squeditor Framework** is a high-performance, developer-first framework for building lightning-fast, static websites. It combines the power of **PHP-style templating** with the modern performance of **Tailwind CSS**, the interactivity of **UIKit 3**, and the speed of **Vite**.
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
> Squeditor Framework is NOT a WordPress theme framework or a CMS. It is a build-time framework that uses PHP for development and outputs 100% flat, portable HTML/CSS/JS for production and compatible with Squeditor, Elementor, Gutenberg, and other page builders.
|
|
3
|
+
[Full Documentation](https://docs.squeditor.com)
|
|
7
4
|
|
|
8
5
|
## 🚀 Key Features
|
|
9
6
|
|
|
@@ -45,21 +42,16 @@ Every project includes an automatically generated `style-guide.php` page that re
|
|
|
45
42
|
|
|
46
43
|
## 🏁 Getting Started
|
|
47
44
|
|
|
48
|
-
### 1.
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
git clone https://github.com/unistudioco/squeditor-framework.git
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### 2. Scaffold a New Project
|
|
55
|
-
Squeditor includes a robust CLI scaffolding tool. It generates a completely clean, minimalist `project-template/` instance next to the core framework, fully pre-configured to utilize the advanced GSAP and UIKit engines without demo bloat.
|
|
45
|
+
### 1. Scaffold a New Project
|
|
46
|
+
Squeditor includes a robust CLI scaffolding tool. It generates a completely clean, minimalist project instance fully pre-configured to utilize the advanced GSAP and UIKit engines without demo bloat.
|
|
56
47
|
|
|
57
|
-
To scaffold your new project, run the following command
|
|
48
|
+
To scaffold your new project, simply run the following command in your terminal:
|
|
58
49
|
```bash
|
|
59
50
|
npx @squeditor/squeditor-framework your-project-name
|
|
60
51
|
```
|
|
52
|
+
*(Replace `your-project-name` with your desired folder name).*
|
|
61
53
|
|
|
62
|
-
###
|
|
54
|
+
### 2. Development
|
|
63
55
|
Navigate into your newly generated project folder, install its dependencies, and start the development server:
|
|
64
56
|
```bash
|
|
65
57
|
cd your-project-name
|
|
@@ -72,7 +64,7 @@ npm run dev
|
|
|
72
64
|
> Vite will show a link to `http://127.0.0.1:5173/` in your terminal. **Do not use that link.**
|
|
73
65
|
> Instead, always visit the PHP Server address: **`http://127.0.0.1:3001`**. This is because Squeditor Framework uses PHP for template rendering, and Vite only serves the static assets.
|
|
74
66
|
|
|
75
|
-
###
|
|
67
|
+
### 3. Build for Production
|
|
76
68
|
Generate the static `dist/` folder and a distributable ZIP archive:
|
|
77
69
|
```bash
|
|
78
70
|
npm run build
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@squeditor/squeditor-framework",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Squeditor Framework is a high-performance, developer-first framework for building lightning-fast, static websites. It combines the power of PHP-style templating with the modern performance of Tailwind CSS, the interactivity of UIKit 3, and the speed of Vite.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"publishConfig": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "new-project-name",
|
|
3
3
|
"private": true,
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "node ../squeditor-framework/scripts/dev.js",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"preview:vercel": "vercel dist --yes"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@splidejs/splide": "^4.1.4",
|
|
14
15
|
"gsap": "^3.13.0",
|
|
16
|
+
"swiper": "^12.1.2",
|
|
15
17
|
"uikit": "^3.21.0"
|
|
16
18
|
},
|
|
17
19
|
"devDependencies": {
|
|
@@ -1,8 +1,25 @@
|
|
|
1
|
-
//
|
|
1
|
+
// squeditor.config.js
|
|
2
2
|
module.exports = {
|
|
3
3
|
framework: '../squeditor-framework',
|
|
4
|
-
name: '
|
|
4
|
+
name: 'customer-bundle',
|
|
5
5
|
version: '1.0.0',
|
|
6
|
+
themes: {
|
|
7
|
+
'two': {
|
|
8
|
+
label: 'Two Theme',
|
|
9
|
+
bodyClass: 'theme-two',
|
|
10
|
+
scss: 'src/assets/scss/themes/_two.scss',
|
|
11
|
+
pages: ['404.php', 'slider-test.php'],
|
|
12
|
+
distSubfolder: '',
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
gsap: {
|
|
16
|
+
plugins: ['ScrollTrigger', 'SplitText', 'Flip', 'Observer'],
|
|
17
|
+
initScript: 'src/assets/js/gsap-init.js',
|
|
18
|
+
advancedScript: 'src/assets/js/gsap-advanced.js',
|
|
19
|
+
},
|
|
20
|
+
slider: {
|
|
21
|
+
library: 'splide', // 'swiper', 'splide', or false
|
|
22
|
+
},
|
|
6
23
|
components: [
|
|
7
24
|
'sticky',
|
|
8
25
|
'utility',
|
|
@@ -43,11 +60,6 @@ module.exports = {
|
|
|
43
60
|
'video',
|
|
44
61
|
'iframe',
|
|
45
62
|
],
|
|
46
|
-
gsap: {
|
|
47
|
-
plugins: ['ScrollTrigger', 'SplitText', 'Flip', 'Observer'],
|
|
48
|
-
initScript: 'src/assets/js/gsap-init.js',
|
|
49
|
-
advancedScript: 'src/assets/js/gsap-advanced.js',
|
|
50
|
-
},
|
|
51
63
|
output: {
|
|
52
64
|
css: 'src/assets/css',
|
|
53
65
|
js: 'src/assets/js',
|
|
@@ -55,7 +67,7 @@ module.exports = {
|
|
|
55
67
|
devServer: { port: 3001, root: 'src' },
|
|
56
68
|
snapshot: {
|
|
57
69
|
baseUrl: 'http://127.0.0.1:3001',
|
|
58
|
-
pages: ['
|
|
70
|
+
pages: ['*'],
|
|
59
71
|
outputDir: 'dist',
|
|
60
72
|
rewriteExtension: true,
|
|
61
73
|
},
|
|
@@ -75,7 +87,7 @@ module.exports = {
|
|
|
75
87
|
}
|
|
76
88
|
},
|
|
77
89
|
dist: {
|
|
78
|
-
zipName: '
|
|
90
|
+
zipName: 'squeditor-build.zip',
|
|
79
91
|
previewPlatform: 'netlify',
|
|
80
92
|
},
|
|
81
93
|
};
|
|
@@ -478,6 +478,19 @@ function initTimelines() {
|
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
// Initialize timeline with data-gsap-timeline parameters (e.g. {delay: 1, repeat: -1, yoyo: true})
|
|
481
|
+
// OPT-IN FOUC protection: if the developer set style="visibility: hidden" on the element,
|
|
482
|
+
// GSAP will manage show/hide. We only use onReverseComplete (reliable) and handle
|
|
483
|
+
// the "show" part directly in control handlers for bulletproof timing.
|
|
484
|
+
const isInitiallyHidden = timelineEl.style.visibility === 'hidden';
|
|
485
|
+
if (isInitiallyHidden) {
|
|
486
|
+
timelineEl._sqHidden = true; // Flag for control handlers
|
|
487
|
+
const originalOnReverseComplete = tlConfig.onReverseComplete;
|
|
488
|
+
tlConfig.onReverseComplete = function() {
|
|
489
|
+
timelineEl.style.visibility = 'hidden';
|
|
490
|
+
if (originalOnReverseComplete) originalOnReverseComplete.call(this);
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
481
494
|
const tl = gsap.timeline(tlConfig);
|
|
482
495
|
timelineEl._sqTimeline = tl; // Bind to DOM node so external buttons can trigger it.
|
|
483
496
|
|
|
@@ -548,7 +561,21 @@ function initTimelineControls() {
|
|
|
548
561
|
const tl = t._sqTimeline;
|
|
549
562
|
if (tl) {
|
|
550
563
|
if (action === 'toggle') {
|
|
551
|
-
|
|
564
|
+
if (t._sqOpen) {
|
|
565
|
+
tl.reverse();
|
|
566
|
+
t._sqOpen = false;
|
|
567
|
+
} else {
|
|
568
|
+
if (t._sqHidden) t.style.visibility = 'visible';
|
|
569
|
+
tl.play();
|
|
570
|
+
t._sqOpen = true;
|
|
571
|
+
}
|
|
572
|
+
} else if (action === 'play') {
|
|
573
|
+
if (t._sqHidden) t.style.visibility = 'visible';
|
|
574
|
+
t._sqOpen = true;
|
|
575
|
+
tl.play();
|
|
576
|
+
} else if (action === 'reverse') {
|
|
577
|
+
t._sqOpen = false;
|
|
578
|
+
tl.reverse();
|
|
552
579
|
} else {
|
|
553
580
|
tl[action]();
|
|
554
581
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/assets/js/main.js
|
|
2
2
|
import './gsap-init.js';
|
|
3
3
|
import './gsap-advanced.js';
|
|
4
|
+
import './_slider_dynamic.js';
|
|
4
5
|
// SCSS is imported directly in base.php for development (to prevent FOUC) and loaded via <link> in production.
|
|
5
6
|
|
|
6
7
|
// Remove the dev server FOUC shield
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import Splide, { EventInterface } from '@splidejs/splide';
|
|
2
|
+
|
|
3
|
+
// Splide CSS is handled separately by build-components.js → slider.min.css
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a generic Slide Effect Transition component for Splide,
|
|
7
|
+
* layering slides while preserving the physical list dimensions for native drag support,
|
|
8
|
+
* and transitioning specific CSS properties (scale, blur, clip-path).
|
|
9
|
+
*/
|
|
10
|
+
function createEffectTransition(duration, easing, applyStylesFn) {
|
|
11
|
+
return function CustomTransition(SplideInstance, Components) {
|
|
12
|
+
const { bind } = EventInterface(SplideInstance);
|
|
13
|
+
const { list } = Components.Elements;
|
|
14
|
+
let endCallback;
|
|
15
|
+
let activeIndex = SplideInstance.index;
|
|
16
|
+
|
|
17
|
+
function getBaseTransform(slideEl) {
|
|
18
|
+
const slides = Components.Elements.slides;
|
|
19
|
+
const idx = slides.indexOf(slideEl);
|
|
20
|
+
// Overlaps the slide dynamically without destroying the container's physical width flow math
|
|
21
|
+
const isVertical = SplideInstance.options.direction === 'ttb';
|
|
22
|
+
return isVertical ? `translateY(-${100 * idx}%)` : `translateX(-${100 * idx}%)`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mount() {
|
|
26
|
+
bind(list, 'transitionend', e => {
|
|
27
|
+
const slides = Components.Elements.slides;
|
|
28
|
+
if (endCallback && slides.includes(e.target)) {
|
|
29
|
+
endCallback();
|
|
30
|
+
endCallback = null;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const setupSlides = () => {
|
|
35
|
+
const slides = Components.Elements.slides;
|
|
36
|
+
activeIndex = SplideInstance.index; // Reset on setup/refresh
|
|
37
|
+
|
|
38
|
+
slides.forEach((slide, index) => {
|
|
39
|
+
slide.style.transition = 'none';
|
|
40
|
+
// We must NOT use absolute/grid because it destroys Splide's drag threshold math!
|
|
41
|
+
// Instead we shift the visual representation only, keeping the phantom DOM bounds intact:
|
|
42
|
+
const baseTransform = getBaseTransform(slide);
|
|
43
|
+
|
|
44
|
+
slide.style.width = '100%';
|
|
45
|
+
slide.style.margin = '0'; // override any Splide inline margins
|
|
46
|
+
slide.style.transformOrigin = 'center';
|
|
47
|
+
|
|
48
|
+
if (index === activeIndex) {
|
|
49
|
+
slide.style.opacity = '1';
|
|
50
|
+
slide.style.zIndex = '1';
|
|
51
|
+
slide.style.pointerEvents = 'auto'; // Active slide gets clicks
|
|
52
|
+
applyStylesFn(slide, 'in', baseTransform);
|
|
53
|
+
} else {
|
|
54
|
+
slide.style.opacity = '0';
|
|
55
|
+
slide.style.zIndex = '0';
|
|
56
|
+
slide.style.pointerEvents = 'none'; // Prevent invisible slides from absorbing clicks
|
|
57
|
+
applyStylesFn(slide, 'out', baseTransform);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Run immediately upon component mount and on every refresh
|
|
63
|
+
setupSlides();
|
|
64
|
+
SplideInstance.on('refresh', setupSlides);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function start(index, done) {
|
|
68
|
+
const slides = Components.Elements.slides;
|
|
69
|
+
const prevSlide = slides[activeIndex];
|
|
70
|
+
const nextSlide = slides[index];
|
|
71
|
+
|
|
72
|
+
// Update local tracker to the new index immediately
|
|
73
|
+
activeIndex = index;
|
|
74
|
+
|
|
75
|
+
if (!nextSlide || prevSlide === nextSlide) {
|
|
76
|
+
done();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Clean up unneeded slides right away to avoid visual glitches
|
|
81
|
+
slides.forEach(slide => {
|
|
82
|
+
if (slide !== prevSlide && slide !== nextSlide) {
|
|
83
|
+
const base = getBaseTransform(slide);
|
|
84
|
+
slide.style.transition = 'none';
|
|
85
|
+
slide.style.opacity = '0';
|
|
86
|
+
slide.style.zIndex = '0';
|
|
87
|
+
slide.style.pointerEvents = 'none';
|
|
88
|
+
applyStylesFn(slide, 'out', base);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Outgoing slide animation
|
|
93
|
+
if (prevSlide) {
|
|
94
|
+
const prevBase = getBaseTransform(prevSlide);
|
|
95
|
+
prevSlide.style.transition = `all ${duration}ms ${easing}`;
|
|
96
|
+
prevSlide.style.zIndex = '1';
|
|
97
|
+
prevSlide.style.opacity = '0';
|
|
98
|
+
prevSlide.style.pointerEvents = 'none';
|
|
99
|
+
applyStylesFn(prevSlide, 'out', prevBase);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Reset incoming slide to 'out' state before animating 'in'
|
|
103
|
+
const nextBase = getBaseTransform(nextSlide);
|
|
104
|
+
nextSlide.style.transition = 'none';
|
|
105
|
+
nextSlide.style.zIndex = '2';
|
|
106
|
+
nextSlide.style.opacity = '0';
|
|
107
|
+
nextSlide.style.pointerEvents = 'none';
|
|
108
|
+
applyStylesFn(nextSlide, 'out', nextBase);
|
|
109
|
+
|
|
110
|
+
// Force browser flow flush so the 'out' state is registered visually
|
|
111
|
+
void nextSlide.offsetWidth;
|
|
112
|
+
|
|
113
|
+
// Incoming slide animation
|
|
114
|
+
nextSlide.style.transition = `all ${duration}ms ${easing}`;
|
|
115
|
+
nextSlide.style.opacity = '1';
|
|
116
|
+
nextSlide.style.pointerEvents = 'auto'; // allow interaction
|
|
117
|
+
applyStylesFn(nextSlide, 'in', nextBase);
|
|
118
|
+
|
|
119
|
+
endCallback = done;
|
|
120
|
+
|
|
121
|
+
// Failsafe timeout in case transitionend event gets skipped/missed
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
if (endCallback) {
|
|
124
|
+
endCallback();
|
|
125
|
+
endCallback = null;
|
|
126
|
+
}
|
|
127
|
+
}, duration + 50);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function cancel() {
|
|
131
|
+
endCallback = null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { mount, start, cancel };
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Pre-defined custom transitions
|
|
140
|
+
* Developers can extend these following the Splide Docs.
|
|
141
|
+
* Transition properties manipulate opacity automatically; use `applyStylesFn` for transforms.
|
|
142
|
+
*/
|
|
143
|
+
const CustomTransitions = {
|
|
144
|
+
'scaleup': createEffectTransition(800, 'cubic-bezier(0.16, 1, 0.3, 1)', (slide, state, base) => {
|
|
145
|
+
const trans = state === 'in' ? 'scale(1)' : 'scale(0.85)';
|
|
146
|
+
slide.style.transform = `${base} ${trans}`;
|
|
147
|
+
}),
|
|
148
|
+
'scaledown': createEffectTransition(800, 'cubic-bezier(0.16, 1, 0.3, 1)', (slide, state, base) => {
|
|
149
|
+
const trans = state === 'in' ? 'scale(1)' : 'scale(1.15)';
|
|
150
|
+
slide.style.transform = `${base} ${trans}`;
|
|
151
|
+
}),
|
|
152
|
+
'blurfade': createEffectTransition(800, 'ease-in-out', (slide, state, base) => {
|
|
153
|
+
slide.style.filter = state === 'in' ? 'blur(0px)' : 'blur(10px)';
|
|
154
|
+
slide.style.transform = `${base} scale(1)`; // scale(1) required to prevent blur rendering bugs
|
|
155
|
+
}),
|
|
156
|
+
'reveal': createEffectTransition(1000, 'cubic-bezier(0.85, 0, 0.15, 1)', (slide, state, base) => {
|
|
157
|
+
slide.style.clipPath = state === 'in' ? 'inset(0 0 0 0)' : 'inset(100% 0 0 0)';
|
|
158
|
+
const trans = state === 'in' ? 'translateY(0%)' : 'translateY(15%)';
|
|
159
|
+
slide.style.transform = `${base} ${trans}`;
|
|
160
|
+
}),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Initializes all Splide sliders on the page
|
|
165
|
+
* Configuration is inherently read by Splide directly from the valid JSON `data-splide` attribute
|
|
166
|
+
*/
|
|
167
|
+
export function initSplide() {
|
|
168
|
+
const splides = document.querySelectorAll('.splide');
|
|
169
|
+
splides.forEach(el => {
|
|
170
|
+
try {
|
|
171
|
+
let optionsStr = el.getAttribute('data-splide');
|
|
172
|
+
let requestedType = null;
|
|
173
|
+
let overrideOptions = {};
|
|
174
|
+
|
|
175
|
+
if (optionsStr) {
|
|
176
|
+
const options = JSON.parse(optionsStr);
|
|
177
|
+
|
|
178
|
+
if (options.type && CustomTransitions[options.type]) {
|
|
179
|
+
requestedType = options.type;
|
|
180
|
+
|
|
181
|
+
// Force the slider into 'fade' mode so the core engine expects stacked slides natively,
|
|
182
|
+
// apply 'rewind' to emulate the infinite looping of our custom transitions.
|
|
183
|
+
overrideOptions = { type: 'fade', rewind: true };
|
|
184
|
+
|
|
185
|
+
// CRITICAL: Splide natively parses the DOM attribute directly inside its constructor!
|
|
186
|
+
// We must alter the original DOM data string before init so it doesn't overwrite 'fade' back
|
|
187
|
+
options.type = 'fade';
|
|
188
|
+
el.setAttribute('data-splide', JSON.stringify(options));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const splide = new Splide(el, overrideOptions);
|
|
193
|
+
|
|
194
|
+
if (requestedType && CustomTransitions[requestedType]) {
|
|
195
|
+
splide.mount({}, CustomTransitions[requestedType]);
|
|
196
|
+
} else {
|
|
197
|
+
splide.mount();
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
console.error('[Squeditor] Error initializing Splide slider', el, e);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
206
|
+
initSplide();
|
|
207
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import Swiper from 'swiper';
|
|
2
|
+
import { Navigation, Pagination, Autoplay, Parallax, Thumbs, EffectFade, FreeMode } from 'swiper/modules';
|
|
3
|
+
|
|
4
|
+
// Swiper CSS is handled separately by build-components.js → slider.min.css
|
|
5
|
+
|
|
6
|
+
function splitTopLevel(str, delimiters = [';', ',']) {
|
|
7
|
+
const result = [];
|
|
8
|
+
let current = '';
|
|
9
|
+
let braceDepth = 0;
|
|
10
|
+
|
|
11
|
+
for (let i = 0; i < str.length; i++) {
|
|
12
|
+
const char = str[i];
|
|
13
|
+
if (char === '{') braceDepth++;
|
|
14
|
+
if (char === '}') braceDepth--;
|
|
15
|
+
|
|
16
|
+
if (braceDepth === 0 && delimiters.includes(char)) {
|
|
17
|
+
result.push(current);
|
|
18
|
+
current = '';
|
|
19
|
+
} else {
|
|
20
|
+
current += char;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (current) result.push(current);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses a string like "slidesPerView: 1; md: {slidesPerView: 3}; loop: true"
|
|
29
|
+
* into a valid JavaScript object.
|
|
30
|
+
*/
|
|
31
|
+
function parseSwiperOptions(optionString) {
|
|
32
|
+
if (!optionString) return {};
|
|
33
|
+
|
|
34
|
+
const options = {};
|
|
35
|
+
const statements = splitTopLevel(optionString, [';', ',']);
|
|
36
|
+
|
|
37
|
+
statements.forEach(statement => {
|
|
38
|
+
statement = statement.trim();
|
|
39
|
+
if (!statement) return;
|
|
40
|
+
|
|
41
|
+
const firstColon = statement.indexOf(':');
|
|
42
|
+
if (firstColon > -1) {
|
|
43
|
+
const key = statement.slice(0, firstColon).trim();
|
|
44
|
+
let rawValue = statement.slice(firstColon + 1).trim();
|
|
45
|
+
|
|
46
|
+
// Handle nested objects
|
|
47
|
+
if (rawValue.startsWith('{') && rawValue.endsWith('}')) {
|
|
48
|
+
const innerString = rawValue.slice(1, -1);
|
|
49
|
+
options[key] = parseSwiperOptions(innerString);
|
|
50
|
+
} else {
|
|
51
|
+
options[key] = parsePrimitive(rawValue);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return options;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parsePrimitive(val) {
|
|
60
|
+
if (val === 'true') return true;
|
|
61
|
+
if (val === 'false') return false;
|
|
62
|
+
if (val === 'null') return null;
|
|
63
|
+
|
|
64
|
+
// Check if it's a number
|
|
65
|
+
if (!isNaN(vanillaParseFloat(val))) {
|
|
66
|
+
return vanillaParseFloat(val);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Remove wrapping quotes if they exist
|
|
70
|
+
if ((val.startsWith("'") && val.endsWith("'")) || (val.startsWith('"') && val.endsWith('"'))) {
|
|
71
|
+
return val.slice(1, -1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return val;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function vanillaParseFloat(val) {
|
|
78
|
+
if(typeof val === 'string' && val.trim() === '') return NaN;
|
|
79
|
+
return Number(val);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Maps Tailwind breakpoints to Swiper breakpoints
|
|
84
|
+
*/
|
|
85
|
+
function mapBreakpoints(options) {
|
|
86
|
+
const swiperBreakpoints = {};
|
|
87
|
+
const tailwindScreens = {
|
|
88
|
+
'sm': 640,
|
|
89
|
+
'md': 768,
|
|
90
|
+
'lg': 1024,
|
|
91
|
+
'xl': 1280,
|
|
92
|
+
'2xl': 1536
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
let hasBreakpoints = false;
|
|
96
|
+
|
|
97
|
+
// 1. Move root-level 'sm', 'md' directly into swiperBreakpoints
|
|
98
|
+
Object.keys(tailwindScreens).forEach(screen => {
|
|
99
|
+
if (options[screen]) {
|
|
100
|
+
hasBreakpoints = true;
|
|
101
|
+
swiperBreakpoints[tailwindScreens[screen]] = options[screen];
|
|
102
|
+
delete options[screen]; // Remove from root options
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// 2. Process nested options.breakpoints if it exists
|
|
107
|
+
if (options.breakpoints) {
|
|
108
|
+
hasBreakpoints = true;
|
|
109
|
+
Object.keys(options.breakpoints).forEach(key => {
|
|
110
|
+
if (tailwindScreens[key]) {
|
|
111
|
+
swiperBreakpoints[tailwindScreens[key]] = options.breakpoints[key];
|
|
112
|
+
} else {
|
|
113
|
+
swiperBreakpoints[key] = options.breakpoints[key];
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (hasBreakpoints && Object.keys(swiperBreakpoints).length > 0) {
|
|
119
|
+
options.breakpoints = swiperBreakpoints;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return options;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Initializes Swiper instance for a single element
|
|
127
|
+
*/
|
|
128
|
+
function initSwiperElement(el) {
|
|
129
|
+
// Prevent double initialization
|
|
130
|
+
if (el.swiper) return;
|
|
131
|
+
|
|
132
|
+
const rawOptions = el.getAttribute('data-sq-swiper') || '';
|
|
133
|
+
let parsedOptions = parseSwiperOptions(rawOptions);
|
|
134
|
+
parsedOptions = mapBreakpoints(parsedOptions);
|
|
135
|
+
|
|
136
|
+
// Default Configuration
|
|
137
|
+
const config = {
|
|
138
|
+
modules: [Navigation, Pagination, Autoplay, Parallax, Thumbs, EffectFade, FreeMode],
|
|
139
|
+
speed: 600,
|
|
140
|
+
// Optional navigation
|
|
141
|
+
navigation: {},
|
|
142
|
+
// Optional pagination
|
|
143
|
+
pagination: {
|
|
144
|
+
clickable: true
|
|
145
|
+
},
|
|
146
|
+
...parsedOptions
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Apply custom detached navigation if specified
|
|
150
|
+
if (parsedOptions.navNext || parsedOptions.navPrev) {
|
|
151
|
+
config.navigation = {
|
|
152
|
+
nextEl: parsedOptions.navNext || null,
|
|
153
|
+
prevEl: parsedOptions.navPrev || null
|
|
154
|
+
};
|
|
155
|
+
// Clean up root
|
|
156
|
+
delete config.navNext;
|
|
157
|
+
delete config.navPrev;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Apply custom detached pagination if specified
|
|
161
|
+
if (parsedOptions.paginationEl) {
|
|
162
|
+
config.pagination = {
|
|
163
|
+
el: parsedOptions.paginationEl,
|
|
164
|
+
clickable: true,
|
|
165
|
+
type: parsedOptions.paginationType || 'bullets'
|
|
166
|
+
};
|
|
167
|
+
delete config.paginationEl;
|
|
168
|
+
delete config.paginationType;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Handle Thumbs Gallery synchronization
|
|
172
|
+
if (parsedOptions.thumbs) {
|
|
173
|
+
const thumbTarget = document.querySelector(parsedOptions.thumbs);
|
|
174
|
+
if (thumbTarget) {
|
|
175
|
+
// First, ensure the thumb target is initialized
|
|
176
|
+
if (!thumbTarget.swiper) {
|
|
177
|
+
initSwiperElement(thumbTarget);
|
|
178
|
+
}
|
|
179
|
+
config.thumbs = {
|
|
180
|
+
swiper: thumbTarget.swiper
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Ensure slider has basic class structure required by CSS
|
|
186
|
+
if (!el.classList.contains('swiper')) {
|
|
187
|
+
el.classList.add('swiper');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const wrapper = el.querySelector(':scope > div');
|
|
191
|
+
if (wrapper && !wrapper.classList.contains('swiper-wrapper')) {
|
|
192
|
+
wrapper.classList.add('swiper-wrapper');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const slides = wrapper ? wrapper.children : el.children;
|
|
196
|
+
Array.from(slides).forEach(slide => {
|
|
197
|
+
if (!slide.classList.contains('swiper-slide')) {
|
|
198
|
+
slide.classList.add('swiper-slide');
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Initialize!
|
|
203
|
+
new Swiper(el, config);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Global Initialization
|
|
208
|
+
*/
|
|
209
|
+
export function initSwiper() {
|
|
210
|
+
const swipers = document.querySelectorAll('[data-sq-swiper]');
|
|
211
|
+
swipers.forEach(initSwiperElement);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
215
|
+
initSwiper();
|
|
216
|
+
});
|