├── .github
└── css-swipe-card.png
├── hacs.json
├── README.md
└── dist
└── css-swipe-card.js
/.github/css-swipe-card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nemuritor01/css-swipe-card/HEAD/.github/css-swipe-card.png
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CSS-Swipe-Card",
3 | "content_in_root": false,
4 | "render_readme": true,
5 | "filename": "css-swipe-card.js",
6 | "homeassistant": "2023.9.0"
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CSS-Swipe-Card
2 |
3 | 
4 |
5 | CSS-Swipe-Card is a minimalist and customizable card, that lets you flick through a slider-carousel of cards.
6 | This card is written in CSS and Java Script and not using any third party library.
7 |
8 | Please note, that I´ve created this card for my personal use in the first place.
9 | I´m not a full time developer, but an IT guy.
10 | The code might not follow best practise methods. Contributors are welcome.
11 |
12 |
13 |
14 | ## Table of contents
15 |
16 | **[`Installation`](#installation)** **[`Configuration`](#configuration)** **[`Styling`](#styling)** **[`Automations`](#automations)** **[`Credits`](#credits)**
17 |
18 |
19 |
20 | ## Installation
21 |
22 | **Home Assistant lowest supported version:** 2023.9.0
23 |
24 |
25 |
26 | With HACS
27 |
28 |
29 |
30 | 1. Open HACS (installation instructions are [here](https://hacs.xyz/docs/setup/prerequisites/).
31 | 2. Open the menu in the upper-right and select `Custom repositories`.
32 | 3. Enter the repository: `https://github.com/Nemuritor01/css-swipe-card`
33 | 4. Select the category `Lovelace`.
34 | 5. Select `ADD`.
35 | 6. Confirm the repository now appears in your HACS custom repositories list. Select `CANCEL` to close the custom repository window.
36 | 7. In the HACS search, type `CSS-Swipe-Card`.
37 | 8. Select the `CSS-Swipe-Card` Respository from the list.
38 | 9. Install the Repository.
39 | 10. Make sure to add to resources via one of the following:
40 | - If using the GUI Resource option, this should have been added automatically.
41 | - If using the `configuration.yaml`, open your `configuration.yaml` via File editor or other means and add:
42 | ```
43 | lovelace:
44 | mode: yaml
45 | resources:
46 | - url: /hacsfiles/css-swipe-card/css-swipe-card.js
47 | type: module
48 | ```
49 | 11. Reload your browser. If the card does not show, try to clear your browser cache.
50 |
51 |
52 |
53 |
54 |
55 | Without HACS
56 |
57 |
58 |
59 | 1. Download these files: [css-swipe-card.js](https://github.com/Nemuritor01/css-swipe-card/blob/main/src/css-swipe-card.js)
60 | 2. Add these files to your `/www` folder
61 | 3. On your dashboard click on the icon at the right top corner then on `Edit dashboard`
62 | 4. Click again on that icon and then click on `Manage resources`
63 | 5. Click on `Add resource`
64 | 6. Copy and paste this: `/local/css-swipe-card.js?v=1`
65 | 7. Click on `JavaScript Module` then `Create`
66 | 8. Go back and refresh your page
67 | 9. After any update of the file you will have to edit `/local/css-swipe-card.js?v=1` and change the version to any higher number
68 |
69 | If it's not working, just try to clear your browser cache.`
70 |
71 |
72 |
73 | ## Configuration:
74 |
75 | Add a card with type `custom:css-swipe-card`:
76 |
77 | ```yaml
78 | - type: custom:css-swipe-card
79 | cards: []
80 | ```
81 | ## Parameters
82 |
83 | | Name | Type | Default | Supported options | Description |
84 | | ---- | ---- | ------- | ----------------- | ----------- |
85 | | `cardId` | string | automatic calculation | a unique card ID you can use to trigger the card in automations |
86 | | `template` | string | slider-horizontal | slider-horizontal, slider-vertical |
87 | | `height` | string | | Any css option that fits in the `height` css value | Will force the height of the swiper container |
88 | | `auto_height` | boolean | false | true, false | force the same heigth, based on the tallest card |
89 | | `card_gap` | string | 0px | Any css option that fits in the `width` css value | |
90 | | `timer` | number | 0 | Any number | Will reset the swiper to the first card after `timer` seconds |
91 | | `pagination` | boolean | false | true, false | enable pagination bullets |
92 | | `navigation` | boolean | false | true, false | enable navigation buttons |
93 | | `navigation_next` | icon | none | any icon in home assistant (mdi:xxx; fas:xxx) | set icon in navigation button next |
94 | | `navigation_prev` | icon | none | any icon in home assistant (mdi:xxx; fas:xxx) | set icon in navigation button previous |
95 | | `custom_css` | | none | see [`Styling`](#styling) | customize design of the swipe card based on various shortcuts |
96 |
97 | ## Styles
98 |
99 | Option `custom_css:`gives the ability to customize lots of css variables
100 |
101 | | Variable | Default |
102 | | -------- | ------- |
103 | | `--slides-align-items` | center |
104 | | `--pagination-bullet-active-background-color` | var(--primary-text-color) |
105 | | `--pagination-bullet-background-color` | var(--primary-background-color) |
106 | | `--pagination-bullet-border` | 1px solid #999 |
107 | | `--pagination-bullet-distance` | 10px |
108 | | `--navigation-button-next-color` | var(--primary-text-color) |
109 | | `--navigation-button-next-background-color` | var(--primary-background-color) |
110 | | `--navigation-button-next-width` | 40px |
111 | | `--navigation-button-next-height` | 40px |
112 | | `--navigation-button-next-border-radius` | 100% |
113 | | `--navigation-button-next-border` | none |
114 | | `--navigation-button-prev-color` | var(--primary-text-color) |
115 | | `--navigation-button-prev-background-color` | var(--primary-background-color) |
116 | | `--navigation-button-prev-width` | 40px |
117 | | `--navigation-button-prev-height` | 40px |
118 | | `--navigation-button-prev-border-radius` | 100% |
119 | | `--navigation-button-prev-border` | none |
120 | | `--navigation-button-distance` | 10px |
121 |
122 |
123 | Example code
124 |
125 | ```yaml
126 | type: custom:css-swipe-card
127 | cardId: YourUniqueCardName
128 | template: slider-horizontal
129 | auto_height: true
130 | pagination: true
131 | navigation: true
132 | card_gap: 2rem
133 | timer: 3
134 | navigation_next: mdi:chevron-right
135 | navigation_prev: mdi:chevron-left
136 | height: 10rem
137 | cards:
138 | - type: entity
139 | entity: sensor.your_sensor
140 | - type: entity
141 | entity: sensor.your_sensor
142 | - type: entity
143 | entity: sensor.your_sensor
144 | custom_css:
145 | '--navigation-button-next-color': white
146 | '--navigation-button-next-background-color': cornflowerblue
147 | '--navigation-button-next-width': 50px
148 | '--navigation-button-next-height': 50px
149 | '--navigation-button-prev-color': white
150 | '--navigation-button-prev-background-color': orchid
151 | '--navigation-button-prev-width': 50px
152 | '--navigation-button-prev-height': 50px
153 | '--pagination-bullet-active-background-color': cornflowerblue
154 | '--pagination-bullet-distance': 5px
155 | ```
156 | ## Automations:
157 | Interactions with Home Assistant automations via input_number helper.
158 | CSS-Swipe-Card is able to monitor and interact with a user created input_number helper. The card monitors, if an input_number.YourCardId helper is availble. If an input number is set, the card will scroll to this card and reset the input_number helper.
159 |
160 | Instruction:
161 | 1. Define a unique cardId in your CSS-Swipe-Card config
162 |
163 | 2. Create an input_number helper.
164 | [create a number helper](https://www.home-assistant.io/integrations/input_number/)
165 |
166 | - name: cardId of your css-swipe-card
167 | - min: 0 (must be 0!)
168 | - max: amount of cards (if 4 cards enter 4)
169 | - step: 1
170 |
171 | 3. Use it in automations
172 | Define a trigger and use action: input_number.set_value. The value should be the card you want to scroll to
173 | - 1 = first card
174 | - 2 = second card
175 | etc.
176 |
177 | Example:
178 |
179 | ```yaml
180 | alias: your-automation-name
181 | description: ""
182 | trigger:
183 | - platform: state
184 | entity_id:
185 | - input_boolean.your_switch
186 | from: "off"
187 | to: "on"
188 | for:
189 | hours: 0
190 | minutes: 0
191 | seconds: 0
192 | condition: []
193 | - action: input_number.set_value
194 | metadata: {}
195 | data:
196 | value: 1
197 | target:
198 | entity_id: input_number.YourCardId
199 | mode: single
200 | ```
201 |
202 | ## Credits
203 |
204 | Credits to Bram Kragten. Some functions are based on his card code.
205 | https://github.com/bramkragten/swipe-card/commits?author=bramkragten
206 |
--------------------------------------------------------------------------------
/dist/css-swipe-card.js:
--------------------------------------------------------------------------------
1 | class CssSwipeCard extends HTMLElement {
2 | static get version() {
3 | return 'v0.8.0';
4 | }
5 |
6 | constructor() {
7 | super();
8 | this.attachShadow({ mode: 'open' });
9 | this.currentIndex = 0;
10 | this.resizeObserver = null;
11 | }
12 |
13 | // Core setup and rendering methods
14 | setConfig(config) {
15 | if (!config || !config.cards || !Array.isArray(config.cards)) {
16 | throw new Error('You need to define cards');
17 | }
18 |
19 | this.cardId = config.cardId || `css-swipe-card-${Math.random().toString(36).substr(2, 9)}`;
20 |
21 | this.config = {
22 | width: '100%',
23 | template: 'slider-horizontal',
24 | auto_height: false,
25 | card_gap: '0px',
26 | timer: 0,
27 | pagination: false,
28 | navigation: false,
29 | navigation_next: '',
30 | navigation_prev: '',
31 | custom_css: {},
32 | cardId: this.cardId,
33 | ...config
34 | };
35 |
36 | this.render();
37 | }
38 |
39 | async render() {
40 | const styles = this.getStyles();
41 | const html = this.getHtml();
42 |
43 | this.shadowRoot.innerHTML = `${html}`;
44 |
45 | const cardContainer = this.shadowRoot.querySelector(`.${this.config.template}`);
46 | this._cards = [];
47 |
48 | for (const [index, cardConfig] of this.config.cards.entries()) {
49 | const card = await this.createCardElement(cardConfig);
50 | const slide = document.createElement('div');
51 | slide.classList.add('slide');
52 | slide.style.width = '100%';
53 | slide.dataset.index = index;
54 | card.classList.add('card-element');
55 | slide.appendChild(card);
56 | cardContainer.appendChild(slide);
57 | this._cards.push(card);
58 | }
59 |
60 | if (this.config.auto_height) {
61 | this.setupResizeObserver();
62 | } else {
63 | await this.setManualHeight();
64 | }
65 |
66 | this.applyCustomStyles();
67 |
68 | if (this.config.pagination) {
69 | this.setupPagination();
70 | }
71 |
72 | if (this.config.navigation) {
73 | this.setupNavigation();
74 | }
75 |
76 | this.setupTimer();
77 |
78 | const slider = this.shadowRoot.querySelector(`.${this.config.template}`);
79 | slider.addEventListener('scroll', () => {
80 | this.updateCurrentIndex();
81 | this.updatePagination();
82 | });
83 |
84 | if (this._hass) {
85 | this.checkInputNumberState();
86 | }
87 | }
88 |
89 | // HTML and CSS generation methods
90 | getStyles() {
91 | return `
92 | :host {
93 | --slides-gap: ${this.config.card_gap};
94 | --slides-align-items: center;
95 | --pagination-bullet-active-background-color: var(--primary-text-color);
96 | --pagination-bullet-background-color: var(--primary-background-color);
97 | --pagination-bullet-border: 1px solid #999;
98 | --pagination-bullet-distance: 10px;
99 | --navigation-button-next-color: var(--primary-text-color);
100 | --navigation-button-next-background-color: var(--primary-background-color);
101 | --navigation-button-next-width: 40px;
102 | --navigation-button-next-height: 40px;
103 | --navigation-button-next-border-radius: 100%;
104 | --navigation-button-next-border: none;
105 | --navigation-button-prev-color: var(--primary-text-color);
106 | --navigation-button-prev-background-color: var(--primary-background-color);
107 | --navigation-button-prev-width: 40px;
108 | --navigation-button-prev-height: 40px;
109 | --navigation-button-prev-border-radius: 100%;
110 | --navigation-button-prev-border: none;
111 | --navigation-button-distance: 10px;
112 | }
113 | #${this.cardId} {
114 | position: relative;
115 | overflow: hidden;
116 |
117 | /* Force hardware acceleration with 3D transform */
118 | transform: translateZ(0);
119 | -webkit-transform: translateZ(0);
120 | -moz-transform: translateZ(0);
121 | -ms-transform: translateZ(0);
122 | -o-transform: translateZ(0);
123 |
124 | /* Existing properties */
125 | backface-visibility: hidden;
126 | perspective: 1000;
127 | -webkit-backface-visibility: hidden;
128 | -webkit-perspective: 1000;
129 | -moz-backface-visibility: hidden;
130 | -moz-perspective: 1000;
131 | -ms-backface-visibility: hidden;
132 | -ms-perspective: 1000;
133 |
134 | will-change: transform;
135 | -webkit-overflow-scrolling: touch;
136 | }
137 | #${this.cardId} .slider-horizontal {
138 | display: flex;
139 | overflow-x: auto;
140 | overflow-y: hidden;
141 | scroll-snap-type: x mandatory;
142 | scroll-behavior: smooth;
143 | position: relative;
144 | gap: var(--slides-gap);
145 | padding-inline: var(--slides-gap);
146 | }
147 | #${this.cardId} .slider-vertical {
148 | display: flex;
149 | flex-direction: column;
150 | overflow-y: auto;
151 | overflow-x: hidden;
152 | scroll-snap-type: y mandatory;
153 | scroll-behavior: smooth;
154 | position: relative;
155 | gap: var(--slides-gap);
156 | padding-block: var(--slides-gap);
157 | }
158 | #${this.cardId} .slider-horizontal,
159 | #${this.cardId} .slider-vertical {
160 | &::-webkit-scrollbar {
161 | display: none;
162 | }
163 | scrollbar-width: none;
164 | -ms-overflow-style: none;
165 |
166 | }
167 | #${this.cardId} .slide {
168 | display: flex;
169 | min-width: 100%;
170 | align-items: var(--slides-align-items);
171 | justify-content: center;
172 | scroll-snap-align: start;
173 | }
174 | #${this.cardId} .card-element {
175 | width: 100% !important;
176 | scroll-snap-align: start;
177 | scroll-snap-stop: always;
178 | }
179 | #${this.cardId} .pagination-control.horizontal {
180 | position: absolute;
181 | bottom: var(--pagination-bullet-distance);
182 | left: 50%;
183 | align-items: center;
184 | transform: translateX(-50%);
185 | display: flex;
186 | gap: 10px;
187 | }
188 | #${this.cardId} .pagination-control.vertical {
189 | position: absolute;
190 | top: 50%;
191 | right: var(--pagination-bullet-distance);
192 | align-items: center;
193 | transform: translateY(-50%);
194 | display: flex;
195 | flex-direction: column;
196 | gap: 10px;
197 | }
198 | #${this.cardId} .pagination-bullet {
199 | width: 10px;
200 | height: 10px;
201 | border-radius: 50%;
202 | background-color: var(--pagination-bullet-background-color, var(--primary-background-color));
203 | border: var(--pagination-bullet-border, 1px solid #999);
204 | cursor: pointer;
205 | padding: 0;
206 | transition: all 0.3s ease;
207 | }
208 | #${this.cardId} .pagination-bullet.active {
209 | background-color: var(--pagination-bullet-active-background-color, var(--primary-text-color));
210 | width: 12px;
211 | height: 12px;
212 | }
213 | #${this.cardId} .navigation-button {
214 | position: absolute;
215 | border: none;
216 | cursor: pointer;
217 | font-size: 24px;
218 | padding: 0;
219 | display: flex;
220 | align-items: center;
221 | justify-content: center;
222 | z-index: 1;
223 | transition: transform 0.1s;
224 | }
225 | #${this.cardId} .navigation-button:active {
226 | animation: buttonPress 0.2s ease-out;
227 | }
228 | #${this.cardId} .navigation-button.prev-horizontal {
229 | width: var(--navigation-button-prev-width);
230 | height: var(--navigation-button-prev-height);
231 | left: var(--navigation-button-distance);
232 | top: 50%;
233 | margin-top: calc(-1 * var(--navigation-button-prev-height) / 2);
234 | color: var(--navigation-button-prev-color);
235 | background: var(--navigation-button-prev-background-color);
236 | border-radius: var(--navigation-button-prev-border-radius);
237 | border: var(--navigation-button-prev-border);
238 | transition: transform 0.1s;
239 | }
240 | #${this.cardId} .navigation-button.next-horizontal {
241 | width: var(--navigation-button-next-width);
242 | height: var(--navigation-button-next-height);
243 | right: var(--navigation-button-distance);
244 | top: 50%;
245 | margin-top: calc(-1 * var(--navigation-button-next-height) / 2);
246 | color: var(--navigation-button-next-color);
247 | background: var(--navigation-button-next-background-color);
248 | border-radius: var(--navigation-button-next-border-radius);
249 | border: var(--navigation-button-next-border);
250 | transition: transform 0.1s;
251 | }
252 | #${this.cardId} .navigation-button.prev-vertical {
253 | width: var(--navigation-button-prev-width);
254 | height: var(--navigation-button-prev-height);
255 | top: var(--navigation-button-distance);
256 | left: 50%;
257 | margin-left: calc(-1 * var(--navigation-button-prev-width) / 2);
258 | color: var(--navigation-button-prev-color);
259 | background: var(--navigation-button-prev-background-color);
260 | border-radius: var(--navigation-button-prev-border-radius);
261 | border: var(--navigation-button-prev-border);
262 | transition: transform 0.1s;
263 | }
264 | #${this.cardId} .navigation-button.next-vertical {
265 | width: var(--navigation-button-next-width);
266 | height: var(--navigation-button-next-height);
267 | bottom: var(--navigation-button-distance);
268 | left: 50%;
269 | margin-left: calc(-1 * var(--navigation-button-next-width) / 2);
270 | color: var(--navigation-button-next-color);
271 | background: var(--navigation-button-next-background-color);
272 | border-radius: var(--navigation-button-next-border-radius);
273 | border: var(--navigation-button-next-border);
274 | transition: transform 0.1s;
275 | }
276 | #${this.cardId} .navigation-button ha-icon {
277 | width: 80%;
278 | height: 80%;
279 | display: flex;
280 | align-items: center;
281 | justify-content: center;
282 | }
283 | #${this.cardId} .navigation-button, #${this.cardId} .pagination-control label {
284 | -webkit-tap-highlight-color: transparent;
285 | outline: none;
286 | }
287 | #${this.cardId} .navigation-button ha-icon,
288 | #${this.cardId} .pagination-control label {
289 | pointer-events: none;
290 | }
291 | @keyframes buttonPress {
292 | 0% {
293 | transform: scale(1);
294 | }
295 | 50% {
296 | transform: scale(0.9);
297 | }
298 | 100% {
299 | transform: scale(1);
300 | }
301 | }
302 | `;
303 | }
304 |
305 | getHtml() {
306 | return `
307 |
308 |
309 | ${this.config.pagination ? `` : ''}
310 | ${this.config.navigation ? `
311 |
314 |
317 | ` : ''}
318 |
319 | `;
320 | }
321 |
322 | // Card creation and sizing methods
323 | async createCardElement(cardConfig) {
324 | const createCard = (await loadCardHelpers()).createCardElement;
325 | const element = createCard(cardConfig);
326 | element.hass = this._hass;
327 | return element;
328 | }
329 |
330 | async getCardSize() {
331 | if (!this._cards) {
332 | return 0;
333 | }
334 |
335 | let maxHeight = 0;
336 |
337 | for (const card of this._cards) {
338 | if (card.getCardSize) {
339 | const size = await card.getCardSize();
340 | maxHeight = Math.max(maxHeight, size * 50);
341 | } else {
342 | await card.updateComplete;
343 | const rect = card.getBoundingClientRect();
344 | maxHeight = Math.max(maxHeight, rect.height);
345 | }
346 | }
347 |
348 | return maxHeight || 140; // fallback to 140 if maxHeight is 0
349 | }
350 |
351 | async getMaxCardHeight() {
352 | let maxHeight = 0;
353 | for (const card of this._cards) {
354 | await card.updateComplete; // Ensure card is fully rendered
355 | const rect = card.getBoundingClientRect();
356 | maxHeight = Math.max(maxHeight, rect.height);
357 | }
358 | return maxHeight || 140; // Fallback height
359 | }
360 |
361 | // Card container height adjustment methods
362 | async adjustCardContainerHeight() {
363 | const cardContainer = this.shadowRoot.querySelector(`.${this.config.template}`);
364 | const slideContainer = this.shadowRoot.querySelector(`.slide`);
365 | const maxHeight = await this.getMaxCardHeight();
366 |
367 | if (this.config.auto_height) {
368 | this._cards.forEach(card => {
369 | card.style.height = `${maxHeight}px`;
370 | });
371 | cardContainer.style.height = `${maxHeight}px`;
372 | slideContainer.style.height = `${maxHeight}px`;
373 | } else {
374 | cardContainer.style.height = `${maxHeight}px`;
375 | slideContainer.style.height = `${maxHeight}px`;
376 | this._cards.forEach(card => {
377 | card.style.height = 'auto'; // Keeps native height for cards
378 | });
379 | }
380 |
381 | if (this.config.height && !this.config.auto_height) {
382 | cardContainer.style.height = this.config.height;
383 | slideContainer.style.height = this.config.height;
384 | this._cards.forEach(card => {
385 | card.style.height = this.config.height;
386 | });
387 | }
388 | }
389 |
390 | async setManualHeight() {
391 | const cardContainer = this.shadowRoot.querySelector(`.${this.config.template}`);
392 | const isHorizontal = this.config.template === 'slider-horizontal';
393 |
394 | if (isHorizontal) {
395 | cardContainer.style.height = this.config.height;
396 | cardContainer.style.overflowY = 'hidden';
397 | } else {
398 | // For vertical mode
399 | const maxHeight = await this.getMaxCardHeight();
400 | cardContainer.style.height = this.config.height || `${maxHeight}px`;
401 | cardContainer.style.overflowY = 'auto';
402 | }
403 |
404 | this._cards.forEach(card => {
405 | if (isHorizontal) {
406 | card.style.height = this.config.height;
407 | } else {
408 | card.style.height = 'auto'; // Keep native height for cards in vertical mode
409 | }
410 | });
411 | }
412 |
413 | // Resize observer setup
414 | setupResizeObserver() {
415 | if (this.resizeObserver) {
416 | this.resizeObserver.disconnect();
417 | }
418 |
419 | this.resizeObserver = new ResizeObserver(() => {
420 | this.adjustCardContainerHeight();
421 | this.updateCurrentIndex();
422 | this.updatePagination();
423 | });
424 |
425 | this._cards.forEach(card => {
426 | this.resizeObserver.observe(card);
427 | });
428 | }
429 |
430 | // Current index update method
431 | updateCurrentIndex() {
432 | const slider = this.shadowRoot.querySelector(`.${this.config.template}`);
433 | const isHorizontal = this.config.template === 'slider-horizontal';
434 | const scrollPosition = isHorizontal ? slider.scrollLeft : slider.scrollTop;
435 | const viewportSize = isHorizontal ? slider.clientWidth : slider.clientHeight;
436 |
437 | let accumulatedSize = 0;
438 | for (let i = 0; i < this._cards.length; i++) {
439 | const cardSize = isHorizontal ? this._cards[i].clientWidth : this._cards[i].clientHeight;
440 | if (scrollPosition < accumulatedSize + cardSize / 2) {
441 | this.currentIndex = i;
442 | break;
443 | }
444 | accumulatedSize += cardSize;
445 | }
446 | }
447 |
448 | // Home Assistant integration methods
449 | set hass(hass) {
450 | const oldHass = this._hass;
451 | this._hass = hass;
452 |
453 | if (!oldHass) {
454 | this.setupInputNumberListener();
455 | this.checkInputNumberState();
456 | }
457 |
458 | const cardContainer = this.shadowRoot.querySelector(`.${this.config.template}`);
459 | if (cardContainer) {
460 | cardContainer.childNodes.forEach((child) => {
461 | if (child.firstChild) {
462 | child.firstChild.hass = hass;
463 | }
464 | });
465 | }
466 |
467 | const inputNumberEntity = `input_number.${this.config.cardId}`;
468 | if (oldHass && hass.states[inputNumberEntity] !== oldHass.states[inputNumberEntity]) {
469 | this.checkInputNumberState();
470 | }
471 | }
472 |
473 | setupInputNumberListener() {
474 | const inputNumberEntity = `input_number.${this.config.cardId}`;
475 | this._hass.connection.subscribeEvents(
476 | (event) => this.handleInputNumberChange(event),
477 | 'state_changed',
478 | { entity_id: inputNumberEntity }
479 | );
480 | }
481 |
482 | checkInputNumberState() {
483 | const inputNumberEntity = `input_number.${this.config.cardId}`;
484 | const state = this._hass.states[inputNumberEntity];
485 | if (state) {
486 | const inputNumber = parseFloat(state.state);
487 | if (inputNumber !== 0) {
488 | const calcIndex = this.calcIndex(inputNumber);
489 | if (calcIndex >= 0) {
490 | requestAnimationFrame(() => {
491 | this.scrollToCardByIndex(calcIndex);
492 | setTimeout(() => {
493 | this.resetInputNumber();
494 | }, 500);
495 | });
496 | }
497 | }
498 | }
499 | }
500 |
501 | handleInputNumberChange(event) {
502 | if (event.data.entity_id === `input_number.${this.config.cardId}`) {
503 | const newState = event.data.new_state;
504 | if (newState && newState.state) {
505 | const inputNumber = parseFloat(newState.state);
506 | if (inputNumber !== 0) {
507 | const calcIndex = this.calcIndex(inputNumber);
508 | if (calcIndex >= 0) {
509 | this.scrollToCardByIndex(calcIndex).then(() => {
510 | this.resetInputNumber();
511 | });
512 | }
513 | }
514 | }
515 | }
516 | }
517 |
518 | resetInputNumber() {
519 | if (!this._hass) {
520 | console.error("HASS not available");
521 | return;
522 | }
523 |
524 | const inputNumberEntity = `input_number.${this.config.cardId}`;
525 | this._hass.callService("input_number", "set_value", {
526 | entity_id: inputNumberEntity,
527 | value: 0
528 | }).catch((error) => {
529 | console.error("Failed to reset input_number:", error);
530 | });
531 | }
532 |
533 | calcIndex(inputNumber) {
534 | return inputNumber - 1;
535 | }
536 |
537 | // Pagination setup and update methods
538 | setupPagination() {
539 | const paginationControl = this.shadowRoot.querySelector('.pagination-control');
540 | if (!paginationControl) return;
541 |
542 | // Clear existing pagination bullets
543 | paginationControl.innerHTML = '';
544 |
545 | this._cards.forEach((_, index) => {
546 | const bullet = document.createElement('button');
547 | bullet.classList.add('pagination-bullet');
548 | bullet.setAttribute('aria-label', `Go to slide ${index + 1}`);
549 | bullet.addEventListener('click', () => this.scrollToCard(index));
550 | paginationControl.appendChild(bullet);
551 | });
552 |
553 | this.updatePagination();
554 | }
555 |
556 | updatePagination() {
557 | const paginationControl = this.shadowRoot.querySelector('.pagination-control');
558 | if (!paginationControl) return;
559 |
560 | const bullets = paginationControl.querySelectorAll('.pagination-bullet');
561 | bullets.forEach((bullet, index) => {
562 | if (index === this.currentIndex) {
563 | bullet.classList.add('active');
564 | bullet.setAttribute('aria-current', 'true');
565 | } else {
566 | bullet.classList.remove('active');
567 | bullet.removeAttribute('aria-current');
568 | }
569 | });
570 | }
571 |
572 | // Navigation setup and methods
573 | setupNavigation() {
574 | const prevButton = this.shadowRoot.querySelector('.navigation-button.prev-horizontal, .navigation-button.prev-vertical');
575 | const nextButton = this.shadowRoot.querySelector('.navigation-button.next-horizontal, .navigation-button.next-vertical');
576 | if (prevButton) prevButton.addEventListener('click', () => this.navigate(-1));
577 | if (nextButton) nextButton.addEventListener('click', () => this.navigate(1));
578 | }
579 |
580 | navigate(direction) {
581 | const newIndex = Math.max(0, Math.min(this.currentIndex + direction, this._cards.length - 1));
582 | this.scrollToCard(newIndex);
583 | }
584 |
585 | // Card scrolling methods
586 | scrollToCard(index) {
587 | const slider = this.shadowRoot.querySelector(`.${this.config.template}`);
588 | if (!slider) return;
589 |
590 | const isHorizontal = this.config.template === 'slider-horizontal';
591 | let scrollPosition = 0;
592 |
593 | for (let i = 0; i < index; i++) {
594 | scrollPosition += isHorizontal ? this._cards[i].clientWidth : this._cards[i].clientHeight;
595 | }
596 |
597 | slider.scrollTo({
598 | [isHorizontal ? 'left' : 'top']: scrollPosition,
599 | behavior: 'smooth'
600 | });
601 |
602 | this.updateCurrentIndex();
603 | this.updatePagination();
604 |
605 | if (this.config.timer > 0) {
606 | this.resetTimer();
607 | }
608 | }
609 |
610 | scrollToCardByIndex(index) {
611 | return new Promise((resolve) => {
612 | const slider = this.shadowRoot.querySelector(`.${this.config.template}`);
613 | if (!slider) {
614 | resolve();
615 | return;
616 | }
617 | const isHorizontal = this.config.template === 'slider-horizontal';
618 | const maxIndex = this._cards.length - 1;
619 | const safeIndex = Math.max(0, Math.min(Math.round(index), maxIndex));
620 | const scrollPosition = safeIndex * (isHorizontal ? slider.clientWidth : slider.clientHeight);
621 |
622 | const scrollEndHandler = () => {
623 | slider.removeEventListener('scrollend', scrollEndHandler);
624 | this.updatePagination();
625 | resolve();
626 | };
627 |
628 | slider.addEventListener('scrollend', scrollEndHandler);
629 |
630 | slider.scrollTo({
631 | [isHorizontal ? 'left' : 'top']: scrollPosition,
632 | behavior: 'smooth'
633 | });
634 | });
635 | }
636 |
637 | // Timer setup and reset methods
638 | setupTimer() {
639 | if (this.config.timer > 0) {
640 | this.resetTimer();
641 | const slider = this.shadowRoot.querySelector(`.${this.config.template}`);
642 | slider.addEventListener('scroll', () => this.resetTimer());
643 | slider.addEventListener('click', () => this.resetTimer());
644 | slider.addEventListener('touchend', () => this.resetTimer());
645 | }
646 | }
647 |
648 | resetTimer() {
649 | if (this.timerInterval) {
650 | clearInterval(this.timerInterval);
651 | }
652 | this.timerInterval = setTimeout(() => {
653 | const slider = this.shadowRoot.querySelector(`.${this.config.template}`);
654 | this.scrollToCard(0);
655 | }, this.config.timer * 1000);
656 | }
657 |
658 | // Cleanup method
659 | disconnectedCallback() {
660 | if (this.resizeObserver) {
661 | this.resizeObserver.disconnect();
662 | }
663 | if (this.timerInterval) {
664 | clearInterval(this.timerInterval);
665 | }
666 | }
667 |
668 | // Custom styles application method
669 | applyCustomStyles() {
670 | const style = document.createElement('style');
671 | style.textContent = Object.entries(this.config.custom_css)
672 | .map(([property, value]) => `#${this.cardId} { ${property}: ${value}; }`)
673 | .join('\n');
674 | this.shadowRoot.appendChild(style);
675 | }
676 | }
677 |
678 | customElements.define('css-swipe-card', CssSwipeCard);
679 |
680 | window.customCards = window.customCards || [];
681 | window.customCards.push({
682 | type: "css-swipe-card",
683 | name: "CSS Swipe Card",
684 | description: "A custom swipe card and carousel"
685 | });
686 |
--------------------------------------------------------------------------------