skeleton-loader 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +401 -0
- data/app/assets/javascripts/skeleton_loader.js +2 -0
- data/app/assets/javascripts/skeleton_loader.js.LICENSE.txt +1 -0
- data/app/assets/stylesheets/skeleton_loader.css +83 -0
- data/app/controllers/skeleton_loader/skeleton_loader_controller.rb +60 -0
- data/app/javascript/client_skeleton_loader.js +234 -0
- data/app/javascript/server_skeleton_loader.js +113 -0
- data/app/javascript/skeleton_loader.js +6 -0
- data/config/routes.rb +5 -0
- data/lib/generators/skeleton_loader/add_templates_generator.rb +16 -0
- data/lib/generators/skeleton_loader/reset_templates_generator.rb +16 -0
- data/lib/generators/skeleton_loader/templates/_card.html.erb +31 -0
- data/lib/generators/skeleton_loader/templates/_comment.html.erb +61 -0
- data/lib/generators/skeleton_loader/templates/_default.html.erb +23 -0
- data/lib/generators/skeleton_loader/templates/_gallery.html.erb +19 -0
- data/lib/generators/skeleton_loader/templates/_paragraph.html.erb +28 -0
- data/lib/generators/skeleton_loader/templates/_product.html.erb +49 -0
- data/lib/generators/skeleton_loader/templates/_profile.html.erb +28 -0
- data/lib/skeleton-loader.rb +7 -0
- data/lib/skeleton_loader/configuration.rb +68 -0
- data/lib/skeleton_loader/engine.rb +42 -0
- data/lib/skeleton_loader/skeleton_element_generator.rb +49 -0
- data/lib/skeleton_loader/template_path_finder.rb +24 -0
- data/lib/skeleton_loader/template_renderer.rb +41 -0
- data/lib/skeleton_loader/version.rb +5 -0
- data/lib/skeleton_loader/view_helpers.rb +19 -0
- data/lib/skeleton_loader.rb +36 -0
- data/lib/tasks/skeleton_loader_tasks.rake +23 -0
- metadata +91 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c1f3870a383b99e72e3c7536797fba096526f8b9d72659f09f718c4a7790c16f
|
4
|
+
data.tar.gz: 19e17acfcf43058cba8ce8acc1040be050e078e14a73ee8bf2d30c0b6bb67ed3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d0c4b8979f8984131fb39a49fb95e459b090911cf96f78fd72dfa9e64cd9eaf6d8993900be5eccbb0a341200a586bb6e83c6f319b7f68eaf47ce7f8614c48f03
|
7
|
+
data.tar.gz: c8bbf8deb35d75bed0d5ed768a0ca5e8f36a411e85667c1cf594ba1edc93f386ec39f52ad52d9876374c09c2fab00f13f4bf1b73ffa08114b63ae039fbb75473
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Emad Rahimi
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,401 @@
|
|
1
|
+
# Skeleton Loader
|
2
|
+
|
3
|
+
![Gem Version](https://img.shields.io/badge/gem-v0.1.0-brightgreen)
|
4
|
+
[![CircleCI](https://dl.circleci.com/status-badge/img/circleci/8MamMcAVAVNWTcUqkjQk7R/Sh2DQkMWqqCv4MFvAmYWDL/tree/main.svg?style=svg&circle-token=CCIPRJ_PF8xu3Svcj2Ro4D8jhjCi7_71b7c0a7c781e09fc7194cd58cca67aecdc111b5)](https://dl.circleci.com/status-badge/redirect/circleci/8MamMcAVAVNWTcUqkjQk7R/Sh2DQkMWqqCv4MFvAmYWDL/tree/main)
|
5
|
+
![Test Coverage: 100%](https://img.shields.io/badge/Test%20Coverage-100%25-brightgreen)
|
6
|
+
[![MIT License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
|
7
|
+
|
8
|
+
**Skeleton Loader** is a Ruby on Rails gem for creating animated placeholders that enhance loading states. Whether rendered through Rails views or dynamically with JavaScript, these skeletons provide a seamless visual experience while content loads.
|
9
|
+
|
10
|
+
**⚠ Note:** Skeleton Loader is an experimental gem currently in early development, exploring the possibilities in the magical world of Rails gems. While loaders are ideally handled client-side, this gem aims to make it easy to add placeholders directly within the Rails realm.
|
11
|
+
|
12
|
+
---
|
13
|
+
|
14
|
+
## Table of Contents
|
15
|
+
|
16
|
+
- [Features](#features)
|
17
|
+
- [Installation](#installation)
|
18
|
+
- [Setup](#setup)
|
19
|
+
- [Rails Integration](#rails-integration)
|
20
|
+
- [JavaScript Integration](#javascript-integration)
|
21
|
+
- [Configuration](#configuration)
|
22
|
+
- [Code Quality](#code-quality)
|
23
|
+
- [Roadmap](#roadmap)
|
24
|
+
- [Contributing](#contributing)
|
25
|
+
- [License](#license)
|
26
|
+
|
27
|
+
---
|
28
|
+
|
29
|
+
![1](https://github.com/user-attachments/assets/f6f91f55-1bfa-42eb-9e8c-41606eb8afd5)
|
30
|
+
|
31
|
+
## Features
|
32
|
+
|
33
|
+
- 🚀 **Seamless Rails integration:** Enhances loading transitions with minimal setup.
|
34
|
+
- 🔨 **Universal skeletons:** Creates loading states in Rails views & JavaScript.
|
35
|
+
- ⚙️ **Consistency:** Unified templates & options across both environments.
|
36
|
+
- ⚡ **Lightweight & fast:** Designed to be fast and easy to implement with minimal dependencies.
|
37
|
+
|
38
|
+
---
|
39
|
+
|
40
|
+
## Installation
|
41
|
+
|
42
|
+
Add this line to your application’s Gemfile:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
gem "skeleton-loader"
|
46
|
+
```
|
47
|
+
|
48
|
+
Then, run:
|
49
|
+
|
50
|
+
```bash
|
51
|
+
bundle install
|
52
|
+
```
|
53
|
+
|
54
|
+
#### Requirements
|
55
|
+
|
56
|
+
- **Ruby** 2.5 or higher
|
57
|
+
- **Rails** 5.0 or higher
|
58
|
+
- **Asset Pipeline** (Required for CSS handling)
|
59
|
+
|
60
|
+
---
|
61
|
+
|
62
|
+
## Setup
|
63
|
+
|
64
|
+
### CSS Assets
|
65
|
+
|
66
|
+
The gem's CSS is always managed via the Asset Pipeline:
|
67
|
+
```javascript
|
68
|
+
/* app/assets/stylesheets/application.css */
|
69
|
+
*= require skeleton_loader
|
70
|
+
```
|
71
|
+
|
72
|
+
### JavaScript Assets
|
73
|
+
|
74
|
+
Include the JavaScript functionality using Asset Pipeline, Webpack, or Importmap, depending on your setup:
|
75
|
+
|
76
|
+
|
77
|
+
#### **Option 1: Asset Pipeline (Default for Rails 5)**
|
78
|
+
|
79
|
+
In `app/assets/javascripts/application.js`, add:
|
80
|
+
|
81
|
+
```javascript
|
82
|
+
//= require skeleton_loader
|
83
|
+
```
|
84
|
+
|
85
|
+
#### **Option 2: Webpack (Default for Rails 6)**
|
86
|
+
|
87
|
+
1. Install the package using Yarn:
|
88
|
+
|
89
|
+
```bash
|
90
|
+
yarn add "@ersync/skeleton-loader"
|
91
|
+
```
|
92
|
+
|
93
|
+
```javascript
|
94
|
+
import SkeletonLoader from "skeleton-loader"
|
95
|
+
```
|
96
|
+
|
97
|
+
#### **Option 3: Importmap (Default for Rails 7)**
|
98
|
+
|
99
|
+
1. Pin the package in `config/importmap.rb`:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
pin "skeleton-loader", to: "skeleton_loader.js"
|
103
|
+
```
|
104
|
+
|
105
|
+
```javascript
|
106
|
+
import SkeletonLoader from "skeleton-loader"
|
107
|
+
```
|
108
|
+
|
109
|
+
### Install Templates
|
110
|
+
|
111
|
+
To install predefined templates, run:
|
112
|
+
|
113
|
+
```bash
|
114
|
+
rails generate skeleton_loader:add_templates
|
115
|
+
```
|
116
|
+
|
117
|
+
This adds templates to `app/views/skeleton_loader/`. Customize them as needed.
|
118
|
+
|
119
|
+
To restore defaults later:
|
120
|
+
|
121
|
+
```bash
|
122
|
+
rails generate skeleton_loader:reset_templates
|
123
|
+
```
|
124
|
+
|
125
|
+
---
|
126
|
+
|
127
|
+
## Rails Integration
|
128
|
+
|
129
|
+
The gem provides a primary view helper, `skeleton_loader`, which generates a skeleton that is automatically replaced with content once loading completes. You can use it with predefined templates or define your own HTML blocks.
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
skeleton_loader(
|
133
|
+
content_id:, # Required. Target element ID for replacement
|
134
|
+
**options, # Optional. Customize the template
|
135
|
+
&block # Optional. Define custom skeleton HTML (excludes type/options)
|
136
|
+
)
|
137
|
+
```
|
138
|
+
|
139
|
+
### 1. Pre-defined Templates
|
140
|
+
|
141
|
+
Specify `content_id` and, optionally, a template `type` and customization `options`.
|
142
|
+
|
143
|
+
```erb
|
144
|
+
<%= skeleton_loader(content_id: 'content-element',
|
145
|
+
type: "card",
|
146
|
+
count: 5,
|
147
|
+
scale: 1.2,
|
148
|
+
animation_type: 'animation-pulse') %>
|
149
|
+
```
|
150
|
+
|
151
|
+
### 2. Custom Skeletons with a Block
|
152
|
+
|
153
|
+
Define your own HTML structure in a block. Note that `type` and `options` are not used.
|
154
|
+
|
155
|
+
```erb
|
156
|
+
<%= skeleton_loader(content_id: 'content-element') do %>
|
157
|
+
<div class="custom-skeleton">
|
158
|
+
<div class="avatar skeleton-circle"></div>
|
159
|
+
<div class="text-lines"></div>
|
160
|
+
</div>
|
161
|
+
<% end %>
|
162
|
+
```
|
163
|
+
|
164
|
+
See [Configuration](#configuration) for all available options.
|
165
|
+
|
166
|
+
---
|
167
|
+
|
168
|
+
## JavaScript Integration
|
169
|
+
|
170
|
+
Skeleton Loader also provides a JavaScript API to dynamically create skeletons independently of Rails views.
|
171
|
+
|
172
|
+
<details>
|
173
|
+
<summary>💡 You only need this API if you're loading content asynchronously i.e. AJAX calls. For standard Rails views, use the Rails helper method instead.</summary>
|
174
|
+
|
175
|
+
### 1. Basic Setup
|
176
|
+
|
177
|
+
First, initialize the SkeletonLoader in your JavaScript:
|
178
|
+
|
179
|
+
```javascript
|
180
|
+
const skeleton = new SkeletonLoader();
|
181
|
+
```
|
182
|
+
|
183
|
+
### 2. Creating Skeletons
|
184
|
+
|
185
|
+
There are two methods for creating skeletons using JavaScript:
|
186
|
+
|
187
|
+
#### Using Pre-defined Templates
|
188
|
+
|
189
|
+
```javascript
|
190
|
+
const loader = skeleton.render({
|
191
|
+
contentId, // Required. ID of the element to replace with skeleton
|
192
|
+
...options // Optional. See configuration section for available options
|
193
|
+
});
|
194
|
+
```
|
195
|
+
|
196
|
+
#### Creating Custom Skeletons
|
197
|
+
|
198
|
+
```javascript
|
199
|
+
const loader = skeleton.renderCustom({
|
200
|
+
contentId, // Required. ID of the element to replace with skeleton
|
201
|
+
markup // Required. Custom HTML string for skeleton content
|
202
|
+
});
|
203
|
+
```
|
204
|
+
|
205
|
+
### 3. Managing Loading States
|
206
|
+
|
207
|
+
Each skeleton instance (both `render()` and `renderCustom()`) returns an object with these methods:
|
208
|
+
|
209
|
+
- `isLoading()`: Returns the current loading state
|
210
|
+
- `reveal()`: Removes skeleton and displays content
|
211
|
+
|
212
|
+
```javascript
|
213
|
+
// Check if still loading
|
214
|
+
if (loader.isLoading()) {
|
215
|
+
// Do something while content loads
|
216
|
+
}
|
217
|
+
|
218
|
+
// Remove skeleton and show content
|
219
|
+
loader.reveal();
|
220
|
+
```
|
221
|
+
|
222
|
+
<details>
|
223
|
+
<summary><strong>Practical Examples</strong></summary>
|
224
|
+
|
225
|
+
Here's some example showing how to use the skeleton loader with an API call:
|
226
|
+
|
227
|
+
```javascript
|
228
|
+
async function loadUserProfile(userId) {
|
229
|
+
// Create skeleton while loading
|
230
|
+
const loader = skeleton.render({
|
231
|
+
contentId: 'content-element',
|
232
|
+
type: 'profile'
|
233
|
+
});
|
234
|
+
|
235
|
+
try {
|
236
|
+
// Fetch your data
|
237
|
+
const response = await fetch(`/api/users/${userId}`);
|
238
|
+
const userData = await response.json();
|
239
|
+
|
240
|
+
// Update the DOM with your content
|
241
|
+
document.getElementById('content-element').innerHTML = createUserProfile(userData);
|
242
|
+
|
243
|
+
// Remove the skeleton
|
244
|
+
loader.reveal();
|
245
|
+
} catch (error) {
|
246
|
+
console.error('Failed to load user profile:', error);
|
247
|
+
loader.reveal(); // Always cleanup the skeleton
|
248
|
+
}
|
249
|
+
}
|
250
|
+
```
|
251
|
+
|
252
|
+
```javascript
|
253
|
+
async function loadDashboard() {
|
254
|
+
// Create multiple skeletons
|
255
|
+
const loaders = {
|
256
|
+
profile: skeleton.render({ contentId: 'profile', type: 'profile', scale:1.2, width:250 }),
|
257
|
+
stats: skeleton.render({ contentId: 'stats', type: 'card', scale:1.3, animationType:"sl-flow" }),
|
258
|
+
activities: skeleton.render({ contentId: 'activities', type: 'list' })
|
259
|
+
};
|
260
|
+
|
261
|
+
// Load your data
|
262
|
+
const [profileData, statsData, activitiesData] = await Promise.all([
|
263
|
+
fetchProfile(),
|
264
|
+
fetchStats(),
|
265
|
+
fetchActivities()
|
266
|
+
]);
|
267
|
+
|
268
|
+
// Update content and reveal each section
|
269
|
+
updateProfile(profileData);
|
270
|
+
loaders.profile.reveal();
|
271
|
+
|
272
|
+
updateStats(statsData);
|
273
|
+
loaders.stats.reveal();
|
274
|
+
|
275
|
+
updateActivities(activitiesData);
|
276
|
+
loaders.activities.reveal();
|
277
|
+
}
|
278
|
+
```
|
279
|
+
</details>
|
280
|
+
</details>
|
281
|
+
|
282
|
+
---
|
283
|
+
|
284
|
+
## Configuration
|
285
|
+
|
286
|
+
### Options
|
287
|
+
|
288
|
+
The following options can be passed to both the Rails view helper and JavaScript API. Note that Rails uses snake_case (e.g., `animation_type`), while JavaScript uses camelCase (e.g., `animationType`).
|
289
|
+
|
290
|
+
|
291
|
+
| Option | Type | Description | Default | Example |
|
292
|
+
|--------|------|-------------|---------|---------|
|
293
|
+
| `content_id` | String | Unique identifier for the skeleton loader container | `nil` | `"content-element"` |
|
294
|
+
| `type` | String | Predefined template type (e.g., "product", "profile") | `"default"` | `"product"` |
|
295
|
+
| `width` | Integer | Base width of a single skeleton item in pixels | Depends on `type` | `250` |
|
296
|
+
| `count` | Integer | Number of skeleton items to render | Depends on `type` | `6` |
|
297
|
+
| `per_row` | Integer | Number of skeleton items displayed per row | Depends on `type` | `4` |
|
298
|
+
| `scale` | Float | Size multiplier for all skeleton item dimensions | `1.0` | `1.2` |
|
299
|
+
| `animation_type` | String | Type of loading animation | `"sl-gradient"` | `"sl-glow"` |
|
300
|
+
|
301
|
+
**Notes:**
|
302
|
+
- `type` determines default layout and styling
|
303
|
+
- `scale` affects width and spacing proportionally
|
304
|
+
- `animation_type` supports different loading effect styles
|
305
|
+
|
306
|
+
### Available Templates
|
307
|
+
|
308
|
+
Skeleton Loader comes with several pre-built templates, each with their **default** configurations:
|
309
|
+
|
310
|
+
| Template Type | Default Width | Count | Items Per Row |
|
311
|
+
|--------------|---------------|-------|---------------|
|
312
|
+
| `card` | 200px | 3 | 3 |
|
313
|
+
| `comment` | 900px | 2 | 1 |
|
314
|
+
| `default` | 900px | 1 | 1 |
|
315
|
+
| `gallery` | 320px | 3 | 3 |
|
316
|
+
| `paragraph` | 900px | 1 | 1 |
|
317
|
+
| `product` | 320px | 3 | 3 |
|
318
|
+
| `profile` | 320px | 3 | 3 |
|
319
|
+
|
320
|
+
For an interactive preview of available templates and animations, visit the [Live Demo](https://ersync.github.io/skeleton-loader/).
|
321
|
+
|
322
|
+
### Available Animations
|
323
|
+
|
324
|
+
Choose from several animation styles to match your design:
|
325
|
+
- `sl-gradient` (default): Smooth gradient movement
|
326
|
+
- `sl-shine`: Shimmer effect
|
327
|
+
- `sl-pulse`: Fade in/out
|
328
|
+
- `sl-flow`: Continuous flow
|
329
|
+
- `sl-neon`: Subtle glow
|
330
|
+
- `sl-breathing`: Gentle scaling
|
331
|
+
|
332
|
+
### Application Defaults
|
333
|
+
|
334
|
+
To set application-wide defaults, create `config/initializers/skeleton_loader.rb`:
|
335
|
+
|
336
|
+
```ruby
|
337
|
+
SkeletonLoader.configure do |config|
|
338
|
+
# Override default template settings
|
339
|
+
config.templates[:product] = {
|
340
|
+
width: 400,
|
341
|
+
count: 6,
|
342
|
+
per_row: 3
|
343
|
+
}
|
344
|
+
# Global settings
|
345
|
+
config.scale = 1.0 # Default: 1.0
|
346
|
+
config.animation_type = "sl-gradient" # Default: "sl-gradient"
|
347
|
+
end
|
348
|
+
```
|
349
|
+
|
350
|
+
### Advanced Configuration
|
351
|
+
|
352
|
+
For applications requiring custom HTML elements or styles:
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
SkeletonLoader.configure do |config|
|
356
|
+
config.additional_allowed_tags = [] # Default: []
|
357
|
+
config.additional_allowed_attributes = {} # Default: {}
|
358
|
+
config.additional_allowed_css_properties = [] # Default: []
|
359
|
+
end
|
360
|
+
```
|
361
|
+
|
362
|
+
---
|
363
|
+
|
364
|
+
## Code Quality
|
365
|
+
|
366
|
+
Skeleton Loader maintains code quality through:
|
367
|
+
|
368
|
+
- **RSpec**: Comprehensive tests covering core functionality.
|
369
|
+
- **CircleCI**: Continuous integration ensures all new changes meet quality standards.
|
370
|
+
- **Rubocop**: Consistent linting aligns code with Ruby community conventions.
|
371
|
+
|
372
|
+
---
|
373
|
+
|
374
|
+
## Roadmap
|
375
|
+
|
376
|
+
Here are some features I'd like to add when I have time:
|
377
|
+
|
378
|
+
- **New Templates & Animations**: Expanding template and animation options.
|
379
|
+
- **Turbo & Stimulus Support**: Enhancing compatibility with Rails Turbo and StimulusJS.
|
380
|
+
- **Builder Helper for Custom Skeletons:**: Introducing a helper to easily create custom skeletons (e.g., circles, rectangles, etc.) with customizable options, simplifying the process of designing custom skeletons.
|
381
|
+
|
382
|
+
Suggestions and contributions are welcome!
|
383
|
+
|
384
|
+
---
|
385
|
+
|
386
|
+
## Contributing
|
387
|
+
|
388
|
+
To contribute:
|
389
|
+
|
390
|
+
1. Fork the repository.
|
391
|
+
2. Create a new feature branch.
|
392
|
+
3. Add tests for new features.
|
393
|
+
4. Commit your changes and submit a pull request.
|
394
|
+
|
395
|
+
Please follow the [Code of Conduct](https://github.com/ersync/skeleton-loader/blob/main/CODE_OF_CONDUCT.md) for all contributions.
|
396
|
+
|
397
|
+
---
|
398
|
+
|
399
|
+
## License
|
400
|
+
|
401
|
+
Skeleton Loader is licensed under the MIT License. See [LICENSE](https://github.com/ersync/skeleton-loader/blob/main/LICENSE) for details.
|
@@ -0,0 +1,2 @@
|
|
1
|
+
/*! For license information please see skeleton_loader.js.LICENSE.txt */
|
2
|
+
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SkeletonLoader=e():t.SkeletonLoader=e()}(this,(()=>(()=>{"use strict";var t={d:(e,r)=>{for(var n in r)t.o(r,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:r[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e)},e={};t.d(e,{default:()=>E});var r=["contentId","type"],n=["contentId","mode","markup","type"],o=["contentId","mode","markup","type"];function i(t){return i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i(t)}function a(t,e){(null==e||e>t.length)&&(e=t.length);for(var r=0,n=Array(e);r<e;r++)n[r]=t[r];return n}function c(){c=function(){return e};var t,e={},r=Object.prototype,n=r.hasOwnProperty,o=Object.defineProperty||function(t,e,r){t[e]=r.value},a="function"==typeof Symbol?Symbol:{},u=a.iterator||"@@iterator",l=a.asyncIterator||"@@asyncIterator",s=a.toStringTag||"@@toStringTag";function f(t,e,r){return Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{f({},"")}catch(t){f=function(t,e,r){return t[e]=r}}function h(t,e,r,n){var i=e&&e.prototype instanceof b?e:b,a=Object.create(i.prototype),c=new I(n||[]);return o(a,"_invoke",{value:T(t,r,c)}),a}function p(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}e.wrap=h;var y="suspendedStart",d="suspendedYield",v="executing",m="completed",g={};function b(){}function w(){}function S(){}var k={};f(k,u,(function(){return this}));var E=Object.getPrototypeOf,L=E&&E(E(A([])));L&&L!==r&&n.call(L,u)&&(k=L);var O=S.prototype=b.prototype=Object.create(k);function x(t){["next","throw","return"].forEach((function(e){f(t,e,(function(t){return this._invoke(e,t)}))}))}function j(t,e){function r(o,a,c,u){var l=p(t[o],t,a);if("throw"!==l.type){var s=l.arg,f=s.value;return f&&"object"==i(f)&&n.call(f,"__await")?e.resolve(f.__await).then((function(t){r("next",t,c,u)}),(function(t){r("throw",t,c,u)})):e.resolve(f).then((function(t){s.value=t,c(s)}),(function(t){return r("throw",t,c,u)}))}u(l.arg)}var a;o(this,"_invoke",{value:function(t,n){function o(){return new e((function(e,o){r(t,n,e,o)}))}return a=a?a.then(o,o):o()}})}function T(e,r,n){var o=y;return function(i,a){if(o===v)throw Error("Generator is already running");if(o===m){if("throw"===i)throw a;return{value:t,done:!0}}for(n.method=i,n.arg=a;;){var c=n.delegate;if(c){var u=_(c,n);if(u){if(u===g)continue;return u}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(o===y)throw o=m,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);o=v;var l=p(e,r,n);if("normal"===l.type){if(o=n.done?m:d,l.arg===g)continue;return{value:l.arg,done:n.done}}"throw"===l.type&&(o=m,n.method="throw",n.arg=l.arg)}}}function _(e,r){var n=r.method,o=e.iterator[n];if(o===t)return r.delegate=null,"throw"===n&&e.iterator.return&&(r.method="return",r.arg=t,_(e,r),"throw"===r.method)||"return"!==n&&(r.method="throw",r.arg=new TypeError("The iterator does not provide a '"+n+"' method")),g;var i=p(o,e.iterator,r.arg);if("throw"===i.type)return r.method="throw",r.arg=i.arg,r.delegate=null,g;var a=i.arg;return a?a.done?(r[e.resultName]=a.value,r.next=e.nextLoc,"return"!==r.method&&(r.method="next",r.arg=t),r.delegate=null,g):a:(r.method="throw",r.arg=new TypeError("iterator result is not an object"),r.delegate=null,g)}function P(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function C(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function I(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(P,this),this.reset(!0)}function A(e){if(e||""===e){var r=e[u];if(r)return r.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var o=-1,a=function r(){for(;++o<e.length;)if(n.call(e,o))return r.value=e[o],r.done=!1,r;return r.value=t,r.done=!0,r};return a.next=a}}throw new TypeError(i(e)+" is not iterable")}return w.prototype=S,o(O,"constructor",{value:S,configurable:!0}),o(S,"constructor",{value:w,configurable:!0}),w.displayName=f(S,s,"GeneratorFunction"),e.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===w||"GeneratorFunction"===(e.displayName||e.name))},e.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,S):(t.__proto__=S,f(t,s,"GeneratorFunction")),t.prototype=Object.create(O),t},e.awrap=function(t){return{__await:t}},x(j.prototype),f(j.prototype,l,(function(){return this})),e.AsyncIterator=j,e.async=function(t,r,n,o,i){void 0===i&&(i=Promise);var a=new j(h(t,r,n,o),i);return e.isGeneratorFunction(r)?a:a.next().then((function(t){return t.done?t.value:a.next()}))},x(O),f(O,s,"Generator"),f(O,u,(function(){return this})),f(O,"toString",(function(){return"[object Generator]"})),e.keys=function(t){var e=Object(t),r=[];for(var n in e)r.push(n);return r.reverse(),function t(){for(;r.length;){var n=r.pop();if(n in e)return t.value=n,t.done=!1,t}return t.done=!0,t}},e.values=A,I.prototype={constructor:I,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=t,this.done=!1,this.delegate=null,this.method="next",this.arg=t,this.tryEntries.forEach(C),!e)for(var r in this)"t"===r.charAt(0)&&n.call(this,r)&&!isNaN(+r.slice(1))&&(this[r]=t)},stop:function(){this.done=!0;var t=this.tryEntries[0].completion;if("throw"===t.type)throw t.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var r=this;function o(n,o){return c.type="throw",c.arg=e,r.next=n,o&&(r.method="next",r.arg=t),!!o}for(var i=this.tryEntries.length-1;i>=0;--i){var a=this.tryEntries[i],c=a.completion;if("root"===a.tryLoc)return o("end");if(a.tryLoc<=this.prev){var u=n.call(a,"catchLoc"),l=n.call(a,"finallyLoc");if(u&&l){if(this.prev<a.catchLoc)return o(a.catchLoc,!0);if(this.prev<a.finallyLoc)return o(a.finallyLoc)}else if(u){if(this.prev<a.catchLoc)return o(a.catchLoc,!0)}else{if(!l)throw Error("try statement without catch or finally");if(this.prev<a.finallyLoc)return o(a.finallyLoc)}}}},abrupt:function(t,e){for(var r=this.tryEntries.length-1;r>=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev<o.finallyLoc){var i=o;break}}i&&("break"===t||"continue"===t)&&i.tryLoc<=e&&e<=i.finallyLoc&&(i=null);var a=i?i.completion:{};return a.type=t,a.arg=e,i?(this.method="next",this.next=i.finallyLoc,g):this.complete(a)},complete:function(t,e){if("throw"===t.type)throw t.arg;return"break"===t.type||"continue"===t.type?this.next=t.arg:"return"===t.type?(this.rval=this.arg=t.arg,this.method="return",this.next="end"):"normal"===t.type&&e&&(this.next=e),g},finish:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),C(r),g}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;C(r)}return o}}throw Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:A(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),g}},e}function u(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(t);e&&(n=n.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,n)}return r}function l(t){for(var e=1;e<arguments.length;e++){var r=null!=arguments[e]?arguments[e]:{};e%2?u(Object(r),!0).forEach((function(e){y(t,e,r[e])})):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(r)):u(Object(r)).forEach((function(e){Object.defineProperty(t,e,Object.getOwnPropertyDescriptor(r,e))}))}return t}function s(t,e){if(null==t)return{};var r,n,o=function(t,e){if(null==t)return{};var r={};for(var n in t)if({}.hasOwnProperty.call(t,n)){if(e.includes(n))continue;r[n]=t[n]}return r}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(n=0;n<i.length;n++)r=i[n],e.includes(r)||{}.propertyIsEnumerable.call(t,r)&&(o[r]=t[r])}return o}function f(t,e,r,n,o,i,a){try{var c=t[i](a),u=c.value}catch(t){return void r(t)}c.done?e(u):Promise.resolve(u).then(n,o)}function h(t){return function(){var e=this,r=arguments;return new Promise((function(n,o){var i=t.apply(e,r);function a(t){f(i,n,o,a,c,"next",t)}function c(t){f(i,n,o,a,c,"throw",t)}a(void 0)}))}}function p(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,d(n.key),n)}}function y(t,e,r){return(e=d(e))in t?Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}):t[e]=r,t}function d(t){var e=function(t){if("object"!=i(t)||!t)return t;var e=t[Symbol.toPrimitive];if(void 0!==e){var r=e.call(t,"string");if("object"!=i(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}(t);return"symbol"==i(e)?e:e+""}var v=function(){function t(){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.defaultDisplayStyles={},this.skeletonClass="skeleton-loader--client",this.contentRefAttr="data-content-id",this.apiEndpoint="/skeleton_loader/templates"}return e=t,i=[{key:"getSkeletonInstance",value:function(e){return t.activeSkeletons.has(e)||t.activeSkeletons.set(e,this.createSkeletonInstance(e)),t.activeSkeletons.get(e)}},{key:"createSkeletonInstance",value:function(e){var r=this;return{isLoading:function(){return t.loadingStates.get(e)||!1},reveal:function(){var t=document.querySelector("[".concat(r.contentRefAttr,'="').concat(e,'"]')),n=document.getElementById(e);t&&n&&r.revealContent(t,n)}}}},{key:"render",value:(m=h(c().mark((function t(){var e,n,o,i,a,u=arguments;return c().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=(e=u.length>0&&void 0!==u[0]?u[0]:{}).contentId,o=e.type,i=void 0===o?"default":o,a=s(e,r),t.abrupt("return",this.processRender(l({contentId:n,mode:"predefined",type:i},a)));case 2:case"end":return t.stop()}}),t,this)}))),function(){return m.apply(this,arguments)})},{key:"renderCustom",value:(v=h(c().mark((function t(){var e,r,n,o=arguments;return c().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(r=(e=o.length>0&&void 0!==o[0]?o[0]:{}).contentId,n=e.markup){t.next=3;break}return t.abrupt("return",this.logError("Custom markup is required"));case 3:return t.abrupt("return",this.processRender({contentId:r,mode:"custom",markup:n}));case 4:case"end":return t.stop()}}),t,this)}))),function(){return v.apply(this,arguments)})},{key:"processRender",value:(d=h(c().mark((function t(e){var r,n,o,i;return c().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(r=e.contentId){t.next=3;break}return t.abrupt("return",this.logError("contentId is required"));case 3:if(!(n=this.getSkeletonInstance(r)).isLoading()){t.next=7;break}return console.warn("Skeleton loading already in progress for ".concat(r)),t.abrupt("return",n);case 7:return t.prev=7,t.next=10,this.initializeLoading(r);case 10:if(o=t.sent){t.next=13;break}return t.abrupt("return",n);case 13:return t.next=15,this.createSkeletonElement(e);case 15:return(i=t.sent)&&this.showSkeleton(i,o),t.abrupt("return",n);case 20:return t.prev=20,t.t0=t.catch(7),console.error("[SkeletonLoader] Render failed:",t.t0),t.abrupt("return",n);case 24:case"end":return t.stop()}}),t,this,[[7,20]])}))),function(t){return d.apply(this,arguments)})},{key:"initializeLoading",value:(y=h(c().mark((function e(r){return c().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return t.loadingStates.set(r,!0),e.abrupt("return",this.hideContent(r));case 2:case"end":return e.stop()}}),e,this)}))),function(t){return y.apply(this,arguments)})},{key:"hideContent",value:function(t){var e=document.getElementById(t);if(!e)return console.error("Content not found: #".concat(t)),null;var r=getComputedStyle(e).display;return"none"!==r&&(this.defaultDisplayStyles[t]=r),e.style.display="none",e}},{key:"revealContent",value:function(e,r){var n=r.id;t.loadingStates.get(n)&&(e.remove(),r.style.display=this.defaultDisplayStyles[n],t.loadingStates.set(n,!1))}},{key:"showSkeleton",value:function(t,e){this.clearPreviousSkeleton(e.id),t.setAttribute(this.contentRefAttr,e.id),t.style.display="block",e.parentNode.insertBefore(t,e)}},{key:"clearPreviousSkeleton",value:function(t){document.querySelectorAll("[".concat(this.contentRefAttr,'="').concat(t,'"]')).forEach((function(t){return t.remove()}))}},{key:"createSkeletonElement",value:(f=h(c().mark((function t(e){var r,o,i,a,u,f,h,p;return c().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return r=e.contentId,o=e.mode,i=e.markup,a=void 0===i?null:i,u=e.type,f=s(e,n),h=this.buildParams(l({contentId:r,mode:o,markup:a,type:u},f)),t.prev=2,t.next=5,this.fetchTemplate(h);case 5:return p=t.sent,t.abrupt("return",this.parseTemplateToElement(p,r));case 9:return t.prev=9,t.t0=t.catch(2),console.error("Error creating skeleton:",t.t0),t.abrupt("return",null);case 13:case"end":return t.stop()}}),t,this,[[2,9]])}))),function(t){return f.apply(this,arguments)})},{key:"fetchTemplate",value:(u=h(c().mark((function t(e){var r;return c().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,fetch("".concat(this.apiEndpoint,"?").concat(e.toString()),{method:"GET",headers:{"X-Requested-With":"XMLHttpRequest"}});case 2:if((r=t.sent).ok){t.next=5;break}throw new Error("Failed to fetch skeleton template");case 5:return t.abrupt("return",r.text());case 6:case"end":return t.stop()}}),t,this)}))),function(t){return u.apply(this,arguments)})},{key:"parseTemplateToElement",value:function(t,e){return(new DOMParser).parseFromString(t,"text/html").body.firstElementChild||(console.error("Invalid skeleton template for contentId: ".concat(e)),null)}},{key:"buildParams",value:function(t){var e=t.contentId,r=t.mode,n=t.markup,i=t.type,a=s(t,o),c=new URLSearchParams(l({content_id:e,mode:r,type:i||"default"},this.formatOptions(a)));return n&&c.set("markup",n),c}},{key:"formatOptions",value:function(t){var e=this;return Object.fromEntries(Object.entries(t).map((function(t){var r=function(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var r=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null!=r){var n,o,i,a,c=[],u=!0,l=!1;try{if(i=(r=r.call(t)).next,0===e){if(Object(r)!==r)return;u=!1}else for(;!(u=(n=i.call(r)).done)&&(c.push(n.value),c.length!==e);u=!0);}catch(t){l=!0,o=t}finally{try{if(!u&&null!=r.return&&(a=r.return(),Object(a)!==a))return}finally{if(l)throw o}}return c}}(t,e)||function(t,e){if(t){if("string"==typeof t)return a(t,e);var r={}.toString.call(t).slice(8,-1);return"Object"===r&&t.constructor&&(r=t.constructor.name),"Map"===r||"Set"===r?Array.from(t):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?a(t,e):void 0}}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}(t,2),n=r[0],o=r[1];return[e.toSnakeCase(n),o]})))}},{key:"toSnakeCase",value:function(t){return t.replace(/([A-Z])/g,"_$1").toLowerCase()}},{key:"logError",value:function(t){return console.error("[SkeletonLoader] ".concat(t)),null}}],i&&p(e.prototype,i),Object.defineProperty(e,"prototype",{writable:!1}),e;var e,i,u,f,y,d,v,m}();function m(t){return m="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},m(t)}function g(){g=function(){return e};var t,e={},r=Object.prototype,n=r.hasOwnProperty,o=Object.defineProperty||function(t,e,r){t[e]=r.value},i="function"==typeof Symbol?Symbol:{},a=i.iterator||"@@iterator",c=i.asyncIterator||"@@asyncIterator",u=i.toStringTag||"@@toStringTag";function l(t,e,r){return Object.defineProperty(t,e,{value:r,enumerable:!0,configurable:!0,writable:!0}),t[e]}try{l({},"")}catch(t){l=function(t,e,r){return t[e]=r}}function s(t,e,r,n){var i=e&&e.prototype instanceof b?e:b,a=Object.create(i.prototype),c=new I(n||[]);return o(a,"_invoke",{value:T(t,r,c)}),a}function f(t,e,r){try{return{type:"normal",arg:t.call(e,r)}}catch(t){return{type:"throw",arg:t}}}e.wrap=s;var h="suspendedStart",p="suspendedYield",y="executing",d="completed",v={};function b(){}function w(){}function S(){}var k={};l(k,a,(function(){return this}));var E=Object.getPrototypeOf,L=E&&E(E(A([])));L&&L!==r&&n.call(L,a)&&(k=L);var O=S.prototype=b.prototype=Object.create(k);function x(t){["next","throw","return"].forEach((function(e){l(t,e,(function(t){return this._invoke(e,t)}))}))}function j(t,e){function r(o,i,a,c){var u=f(t[o],t,i);if("throw"!==u.type){var l=u.arg,s=l.value;return s&&"object"==m(s)&&n.call(s,"__await")?e.resolve(s.__await).then((function(t){r("next",t,a,c)}),(function(t){r("throw",t,a,c)})):e.resolve(s).then((function(t){l.value=t,a(l)}),(function(t){return r("throw",t,a,c)}))}c(u.arg)}var i;o(this,"_invoke",{value:function(t,n){function o(){return new e((function(e,o){r(t,n,e,o)}))}return i=i?i.then(o,o):o()}})}function T(e,r,n){var o=h;return function(i,a){if(o===y)throw Error("Generator is already running");if(o===d){if("throw"===i)throw a;return{value:t,done:!0}}for(n.method=i,n.arg=a;;){var c=n.delegate;if(c){var u=_(c,n);if(u){if(u===v)continue;return u}}if("next"===n.method)n.sent=n._sent=n.arg;else if("throw"===n.method){if(o===h)throw o=d,n.arg;n.dispatchException(n.arg)}else"return"===n.method&&n.abrupt("return",n.arg);o=y;var l=f(e,r,n);if("normal"===l.type){if(o=n.done?d:p,l.arg===v)continue;return{value:l.arg,done:n.done}}"throw"===l.type&&(o=d,n.method="throw",n.arg=l.arg)}}}function _(e,r){var n=r.method,o=e.iterator[n];if(o===t)return r.delegate=null,"throw"===n&&e.iterator.return&&(r.method="return",r.arg=t,_(e,r),"throw"===r.method)||"return"!==n&&(r.method="throw",r.arg=new TypeError("The iterator does not provide a '"+n+"' method")),v;var i=f(o,e.iterator,r.arg);if("throw"===i.type)return r.method="throw",r.arg=i.arg,r.delegate=null,v;var a=i.arg;return a?a.done?(r[e.resultName]=a.value,r.next=e.nextLoc,"return"!==r.method&&(r.method="next",r.arg=t),r.delegate=null,v):a:(r.method="throw",r.arg=new TypeError("iterator result is not an object"),r.delegate=null,v)}function P(t){var e={tryLoc:t[0]};1 in t&&(e.catchLoc=t[1]),2 in t&&(e.finallyLoc=t[2],e.afterLoc=t[3]),this.tryEntries.push(e)}function C(t){var e=t.completion||{};e.type="normal",delete e.arg,t.completion=e}function I(t){this.tryEntries=[{tryLoc:"root"}],t.forEach(P,this),this.reset(!0)}function A(e){if(e||""===e){var r=e[a];if(r)return r.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var o=-1,i=function r(){for(;++o<e.length;)if(n.call(e,o))return r.value=e[o],r.done=!1,r;return r.value=t,r.done=!0,r};return i.next=i}}throw new TypeError(m(e)+" is not iterable")}return w.prototype=S,o(O,"constructor",{value:S,configurable:!0}),o(S,"constructor",{value:w,configurable:!0}),w.displayName=l(S,u,"GeneratorFunction"),e.isGeneratorFunction=function(t){var e="function"==typeof t&&t.constructor;return!!e&&(e===w||"GeneratorFunction"===(e.displayName||e.name))},e.mark=function(t){return Object.setPrototypeOf?Object.setPrototypeOf(t,S):(t.__proto__=S,l(t,u,"GeneratorFunction")),t.prototype=Object.create(O),t},e.awrap=function(t){return{__await:t}},x(j.prototype),l(j.prototype,c,(function(){return this})),e.AsyncIterator=j,e.async=function(t,r,n,o,i){void 0===i&&(i=Promise);var a=new j(s(t,r,n,o),i);return e.isGeneratorFunction(r)?a:a.next().then((function(t){return t.done?t.value:a.next()}))},x(O),l(O,u,"Generator"),l(O,a,(function(){return this})),l(O,"toString",(function(){return"[object Generator]"})),e.keys=function(t){var e=Object(t),r=[];for(var n in e)r.push(n);return r.reverse(),function t(){for(;r.length;){var n=r.pop();if(n in e)return t.value=n,t.done=!1,t}return t.done=!0,t}},e.values=A,I.prototype={constructor:I,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=t,this.done=!1,this.delegate=null,this.method="next",this.arg=t,this.tryEntries.forEach(C),!e)for(var r in this)"t"===r.charAt(0)&&n.call(this,r)&&!isNaN(+r.slice(1))&&(this[r]=t)},stop:function(){this.done=!0;var t=this.tryEntries[0].completion;if("throw"===t.type)throw t.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var r=this;function o(n,o){return c.type="throw",c.arg=e,r.next=n,o&&(r.method="next",r.arg=t),!!o}for(var i=this.tryEntries.length-1;i>=0;--i){var a=this.tryEntries[i],c=a.completion;if("root"===a.tryLoc)return o("end");if(a.tryLoc<=this.prev){var u=n.call(a,"catchLoc"),l=n.call(a,"finallyLoc");if(u&&l){if(this.prev<a.catchLoc)return o(a.catchLoc,!0);if(this.prev<a.finallyLoc)return o(a.finallyLoc)}else if(u){if(this.prev<a.catchLoc)return o(a.catchLoc,!0)}else{if(!l)throw Error("try statement without catch or finally");if(this.prev<a.finallyLoc)return o(a.finallyLoc)}}}},abrupt:function(t,e){for(var r=this.tryEntries.length-1;r>=0;--r){var o=this.tryEntries[r];if(o.tryLoc<=this.prev&&n.call(o,"finallyLoc")&&this.prev<o.finallyLoc){var i=o;break}}i&&("break"===t||"continue"===t)&&i.tryLoc<=e&&e<=i.finallyLoc&&(i=null);var a=i?i.completion:{};return a.type=t,a.arg=e,i?(this.method="next",this.next=i.finallyLoc,v):this.complete(a)},complete:function(t,e){if("throw"===t.type)throw t.arg;return"break"===t.type||"continue"===t.type?this.next=t.arg:"return"===t.type?(this.rval=this.arg=t.arg,this.method="return",this.next="end"):"normal"===t.type&&e&&(this.next=e),v},finish:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),C(r),v}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;C(r)}return o}}throw Error("illegal catch attempt")},delegateYield:function(e,r,n){return this.delegate={iterator:A(e),resultName:r,nextLoc:n},"next"===this.method&&(this.arg=t),v}},e}function b(t,e,r,n,o,i,a){try{var c=t[i](a),u=c.value}catch(t){return void r(t)}c.done?e(u):Promise.resolve(u).then(n,o)}function w(t){return function(){var e=this,r=arguments;return new Promise((function(n,o){var i=t.apply(e,r);function a(t){b(i,n,o,a,c,"next",t)}function c(t){b(i,n,o,a,c,"throw",t)}a(void 0)}))}}function S(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,k(n.key),n)}}function k(t){var e=function(t){if("object"!=m(t)||!t)return t;var e=t[Symbol.toPrimitive];if(void 0!==e){var r=e.call(t,"string");if("object"!=m(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}(t);return"symbol"==m(e)?e:e+""}y(v,"loadingStates",new Map),y(v,"activeSkeletons",new Map),(new(function(){function t(){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.SKELETON_CLASS="skeleton-loader--server",this.CONTENT_ID_ATTR="data-content-id",this.contentsDisplayStyles={}}return e=t,r=[{key:"start",value:function(){return this.setupInitialLoading(),this.setupContentsSwap(),this}},{key:"setupInitialLoading",value:function(){var t=this;document.addEventListener("DOMContentLoaded",(function(){t.captureContentsDisplayStyles(),t.hideContents(),t.showSkeletons()}))}},{key:"setupContentsSwap",value:function(){var t=this;window.addEventListener("load",w(g().mark((function e(){return g().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,t.revealContent();case 2:return e.abrupt("return",e.sent);case 3:case"end":return e.stop()}}),e)}))))}},{key:"captureContentsDisplayStyles",value:function(){var e=this;document.querySelectorAll(".".concat(t.SKELETON_CLASS)).forEach((function(r){var n=r.getAttribute(t.CONTENT_ID_ATTR),o=document.getElementById(n);if(o){var i=getComputedStyle(o).display||"block";e.contentsDisplayStyles[n]=i}else console.warn('Content element with id "'.concat(n,'" not found'))}))}},{key:"showSkeletons",value:function(){document.querySelectorAll(".".concat(t.SKELETON_CLASS)).forEach((function(t){t.style.display="block"}))}},{key:"revealContent",value:(o=w(g().mark((function t(){return g().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:this.hideSkeletons(),this.showContents();case 2:case"end":return t.stop()}}),t,this)}))),function(){return o.apply(this,arguments)})},{key:"hideSkeletons",value:function(){document.querySelectorAll(".".concat(t.SKELETON_CLASS)).forEach((function(t){t.style.display="none"}))}},{key:"showContents",value:function(){var e=this;document.querySelectorAll(".".concat(t.SKELETON_CLASS)).forEach((function(r){var n=r.getAttribute(t.CONTENT_ID_ATTR),o=document.getElementById(n);if(o){var i=e.contentsDisplayStyles[n]||"block";o.style.display=i,o.style.visibility="visible"}else console.warn('Content element with id "'.concat(n,'" not found'))}))}},{key:"hideContents",value:function(){document.querySelectorAll(".".concat(t.SKELETON_CLASS)).forEach((function(e){var r=e.getAttribute(t.CONTENT_ID_ATTR),n=document.getElementById(r);n?n.style.display="none":console.warn('Content element with id "'.concat(r,'" not found'))}))}}],n=[{key:"SKELETON_CLASS",get:function(){return"skeleton-loader--server"}},{key:"CONTENT_ID_ATTR",get:function(){return"data-content-id"}}],r&&S(e.prototype,r),n&&S(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e;var e,r,n,o}())).start();const E=v;return e.default})()));
|
@@ -0,0 +1 @@
|
|
1
|
+
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
|
@@ -0,0 +1,83 @@
|
|
1
|
+
.skeleton-loading {
|
2
|
+
background-color: #e0e0e0;
|
3
|
+
border-radius: 10px;
|
4
|
+
overflow: hidden;
|
5
|
+
}
|
6
|
+
|
7
|
+
/* Animation Keyframes */
|
8
|
+
@keyframes skeleton-pulse {
|
9
|
+
0%, 100% { opacity: 1; }
|
10
|
+
50% { opacity: 0.5; }
|
11
|
+
}
|
12
|
+
|
13
|
+
@keyframes skeleton-flow {
|
14
|
+
0% { opacity: 0.6; }
|
15
|
+
50% { opacity: 0.3; }
|
16
|
+
100% { opacity: 0.6; }
|
17
|
+
}
|
18
|
+
|
19
|
+
@keyframes skeleton-shine {
|
20
|
+
0% { background-position: -201% center; }
|
21
|
+
100% { background-position: 198% center; }
|
22
|
+
}
|
23
|
+
|
24
|
+
@keyframes skeleton-gradient {
|
25
|
+
0% { background-position: 0% 50%; }
|
26
|
+
50% { background-position: 100% 50%; }
|
27
|
+
100% { background-position: 0% 50%; }
|
28
|
+
}
|
29
|
+
|
30
|
+
@keyframes skeleton-neon {
|
31
|
+
0% {
|
32
|
+
background: #e0e0e0;
|
33
|
+
box-shadow: 0 0 5px rgba(224, 224, 224, 0.5);
|
34
|
+
}
|
35
|
+
50% {
|
36
|
+
background: #e8e8e8;
|
37
|
+
box-shadow: 0 0 15px rgba(224, 224, 224, 0.8);
|
38
|
+
}
|
39
|
+
100% {
|
40
|
+
background: #e0e0e0;
|
41
|
+
box-shadow: 0 0 5px rgba(224, 224, 224, 0.5);
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
@keyframes skeleton-breathing {
|
46
|
+
0%, 100% {
|
47
|
+
transform: scale(1);
|
48
|
+
opacity: 0.7;
|
49
|
+
}
|
50
|
+
50% {
|
51
|
+
transform: scale(1.02);
|
52
|
+
opacity: 0.9;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
/* Animation Classes */
|
57
|
+
.sl-pulse {
|
58
|
+
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
59
|
+
}
|
60
|
+
|
61
|
+
.sl-flow {
|
62
|
+
animation: skeleton-flow 2s ease-in-out infinite;
|
63
|
+
}
|
64
|
+
|
65
|
+
.sl-shine {
|
66
|
+
background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 76%);
|
67
|
+
background-size: 200% 100%;
|
68
|
+
animation: skeleton-shine 1.5s ease-in-out infinite;
|
69
|
+
}
|
70
|
+
|
71
|
+
.sl-gradient {
|
72
|
+
background: linear-gradient(-45deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
|
73
|
+
background-size: 400% 400%;
|
74
|
+
animation: skeleton-gradient 2s ease-in-out infinite;
|
75
|
+
}
|
76
|
+
|
77
|
+
.sl-neon {
|
78
|
+
animation: skeleton-neon 2s ease-in-out infinite;
|
79
|
+
}
|
80
|
+
|
81
|
+
.sl-breathing {
|
82
|
+
animation: skeleton-breathing 2s ease-in-out infinite;
|
83
|
+
}
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SkeletonLoader
|
4
|
+
# Handles async requests to generate and render loading skeletons
|
5
|
+
class SkeletonLoaderController < ActionController::Base
|
6
|
+
def show
|
7
|
+
content_id = params[:content_id]
|
8
|
+
mode = params[:mode]
|
9
|
+
wrapped_content = mode == "custom" ? handle_custom_template(content_id) : handle_predefined_template(content_id)
|
10
|
+
render html: wrapped_content
|
11
|
+
rescue StandardError => e
|
12
|
+
handle_error(e)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def handle_predefined_template(content_id)
|
18
|
+
options = process_params
|
19
|
+
SkeletonElementGenerator.generate(
|
20
|
+
content_id: content_id,
|
21
|
+
options: options,
|
22
|
+
context: :controller
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def handle_custom_template(content_id)
|
27
|
+
markup = params[:markup]
|
28
|
+
SkeletonElementGenerator.generate(
|
29
|
+
content_id: content_id,
|
30
|
+
context: :controller
|
31
|
+
) { markup }
|
32
|
+
end
|
33
|
+
|
34
|
+
def process_params
|
35
|
+
params_to_process = params.to_unsafe_h
|
36
|
+
.except("controller", "action", "format", "content_id", "mode", "markup")
|
37
|
+
.transform_keys(&:to_sym)
|
38
|
+
|
39
|
+
convert_numeric_params(params_to_process)
|
40
|
+
params_to_process
|
41
|
+
end
|
42
|
+
|
43
|
+
def convert_numeric_params(params)
|
44
|
+
%i[scale count width per_row].each do |key|
|
45
|
+
params[key] = params[key].to_f if key == :scale && params[key]
|
46
|
+
params[key] = params[key].to_i if %i[count width per_row].include?(key) && params[key]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def handle_error(error)
|
51
|
+
Rails.logger.error "SkeletonLoader Error: #{error.message}"
|
52
|
+
Rails.logger.error error.backtrace.join("\n")
|
53
|
+
|
54
|
+
render json: {
|
55
|
+
error: error.message,
|
56
|
+
backtrace: Rails.env.development? ? error.backtrace : []
|
57
|
+
}, status: :unprocessable_entity
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|