@simhub2/justflipit 4.0.0 → 4.0.1
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/CHANGELOG.md +6 -0
- package/README.md +243 -99
- package/dist/justFlipIt-combined.js +60 -28
- package/dist/justFlipIt-combined.min.js +12 -10
- package/dist/justFlipIt-modern.js +60 -28
- package/dist/justFlipIt-modern.min.js +12 -10
- package/dist/justFlipIt-vanilla.js +60 -28
- package/dist/justFlipIt-vanilla.min.js +12 -10
- package/dist/justFlipIt.cjs +60 -28
- package/dist/justFlipIt.cjs.map +2 -2
- package/dist/justFlipIt.js +60 -28
- package/dist/justFlipIt.js.map +2 -2
- package/dist/justFlipIt.min.js +12 -10
- package/dist/justFlipIt.min.js.map +3 -3
- package/dist/justFlipIt.mjs +60 -28
- package/dist/justFlipIt.mjs.map +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.1
|
|
4
|
+
|
|
5
|
+
- Enable 3D tilt on touch and reset it when the finger lifts or scrolling takes over.
|
|
6
|
+
- Keep tap-to-flip active across flip, fade and scale without immediately flipping back on touch release.
|
|
7
|
+
- Restrict axis rotation to the flip effect so fade also works with `FlipX: true`.
|
|
8
|
+
|
|
3
9
|
## 4.0.0
|
|
4
10
|
|
|
5
11
|
- One SSR-safe, dependency-free core with real ESM/CommonJS/browser exports.
|
package/README.md
CHANGED
|
@@ -1,168 +1,312 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="img/readme-banner.svg" alt="justFlipIt v4 — A little motion. A new perspective." width="100%">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@simhub2/justflipit"><img src="https://img.shields.io/npm/v/%40simhub2%2Fjustflipit?style=flat-square&color=b4df83" alt="npm version"></a>
|
|
7
|
+
<a href="https://github.com/SimHub/justFlipIt/actions/workflows/ci.yml"><img src="https://github.com/SimHub/justFlipIt/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI status"></a>
|
|
8
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-d9ccff?style=flat-square" alt="MIT license"></a>
|
|
9
|
+
<a href="#frameworks"><img src="https://img.shields.io/badge/TypeScript-included-93c5fd?style=flat-square" alt="TypeScript declarations included"></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
Responsive flip and tilt animations for your UI.<br>
|
|
14
|
+
One dependency-free core. Native framework lifecycles. Model-independent agent tools.
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="#quick-start">Quick start</a> ·
|
|
19
|
+
<a href="#frameworks">React / Vue / Svelte</a> ·
|
|
20
|
+
<a href="#agent-tools">Agent tools</a> ·
|
|
21
|
+
<a href="#options">Options</a> ·
|
|
22
|
+
<a href="#upgrading-from-v3">Upgrade from v3</a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## A card with more to say
|
|
28
|
+
|
|
29
|
+
Build product cards, reveal details, or let an agent control a card through explicit tool calls. Choose a 3D flip, a soft fade, or a scale transition, then add pointer-driven tilt.
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
**[Browse the demo source →](example/index.html)** · Run locally with `npm run build` and `python3 -m http.server 4173`, then open [localhost:4173/example/](http://localhost:4173/example/).
|
|
34
|
+
|
|
35
|
+
| Motion that responds | Fits your application | Control when you need it |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| Flip on either axis, fade, and scale | React hook, Vue directive, Svelte action | `flip`, `toggle`, `update`, and state inspection |
|
|
38
|
+
| Pointer tilt batched per animation frame | Existing framework-owned faces stay in place | Model-independent JSON-schema tool definition |
|
|
39
|
+
| Touch, keyboard, and reduced-motion support | Responsive layout and SSR-safe imports | Typed API and change events |
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
5
42
|
|
|
6
43
|
```sh
|
|
7
44
|
npm install @simhub2/justflipit
|
|
8
45
|
```
|
|
9
46
|
|
|
47
|
+
Add a container to your page, then initialize it after the element exists:
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<div class="product-card">
|
|
51
|
+
<h2>A different perspective</h2>
|
|
52
|
+
<p>Click to discover the other side.</p>
|
|
53
|
+
</div>
|
|
54
|
+
```
|
|
55
|
+
|
|
10
56
|
```js
|
|
11
57
|
import justFlipIt from '@simhub2/justflipit';
|
|
12
58
|
|
|
13
|
-
const card = justFlipIt(document.querySelector('.card'), {
|
|
59
|
+
const card = justFlipIt(document.querySelector('.product-card'), {
|
|
14
60
|
Click: true,
|
|
15
|
-
Template: '<p>
|
|
16
|
-
Tilt: { max:
|
|
17
|
-
|
|
18
|
-
Duration: 450
|
|
61
|
+
Template: '<h2>There is more to see.</h2><p>Your details go here.</p>',
|
|
62
|
+
Tilt: { max: 10, perspective: 1000, scale: 1.03 },
|
|
63
|
+
Duration: 450,
|
|
19
64
|
});
|
|
20
|
-
card.flip();
|
|
21
|
-
card.flip(false);
|
|
22
|
-
card.toggle();
|
|
23
|
-
card.update({ Effect: 'fade' });
|
|
24
|
-
card.getState();
|
|
25
|
-
card.destroy();
|
|
26
65
|
```
|
|
27
66
|
|
|
28
|
-
|
|
29
|
-
Repeated initialization returns the existing controller. `justFlipIt.get(element)`
|
|
30
|
-
retrieves it; `justFlipIt.destroy(target)` cleans up. Original nodes and their event
|
|
31
|
-
listeners are preserved. The grid layout adapts to both faces without fixed dimensions.
|
|
32
|
-
The card needs an HTML container, such as a `div`; do not initialize a void element.
|
|
67
|
+
The original node and its event listeners are preserved. Both faces participate in a responsive grid, so the card adapts to its content. Style the faces to match your design; the core supplies the motion and layout.
|
|
33
68
|
|
|
34
|
-
|
|
35
|
-
`/modern`, `/vanilla`, `/combined`, and their historical browser filenames are aliases
|
|
36
|
-
of the same core in v4. There is no jQuery plugin in v4.
|
|
69
|
+
> `Template` accepts trusted application HTML. Use the agent bridge's `setText` action for model-generated text. With a framework, render content through the framework instead.
|
|
37
70
|
|
|
38
|
-
|
|
71
|
+
<details>
|
|
72
|
+
<summary><strong>Using a browser script or CommonJS?</strong></summary>
|
|
39
73
|
|
|
40
|
-
|
|
41
|
-
|---|---|---|
|
|
42
|
-
| `Click` | `false` | Hover; `true` for click; selector or selector array for delegated clicks |
|
|
43
|
-
| `FlipX` | `false` | Rotate on X instead of Y |
|
|
44
|
-
| `Template` | cloned original | Trusted HTML for the generated back face |
|
|
45
|
-
| `Duration` | `400` | Non-negative milliseconds |
|
|
46
|
-
| `Easing` | `cubic-bezier(.22,.61,.36,1)` | CSS easing |
|
|
47
|
-
| `Effect` | `'flip'` | `'flip'`, `'fade'`, or `'scale'` |
|
|
48
|
-
| `Tilt` | `false` | `true`, `'only'`, or `{ max, perspective, scale }` |
|
|
49
|
-
| `ReducedMotion` | `true` | Respect live `prefers-reduced-motion` preferences |
|
|
50
|
-
| `Faces` | omitted | Select two existing direct children: `{ front, back }` |
|
|
51
|
-
| `preserveClasses` | `true` | Copy classes onto generated container |
|
|
52
|
-
| `preserveStyles` | `true` | Copy inline styles onto generated container |
|
|
53
|
-
| `Style` | `[]` | Array of `{ el: 'self' or selector, style: {...} }` |
|
|
74
|
+
**Browser script — no bundler:**
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
```html
|
|
77
|
+
<div class="card">Front of the card</div>
|
|
78
|
+
<script src="https://cdn.jsdelivr.net/npm/@simhub2/justflipit@4.0.1/dist/justFlipIt.min.js"></script>
|
|
79
|
+
<script>
|
|
80
|
+
justFlipIt('.card', { Click: true, Template: '<p>Back of the card</p>' });
|
|
81
|
+
</script>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**CommonJS:**
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
const justFlipIt = require('@simhub2/justflipit');
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The browser bundle exposes `window.justFlipIt`. ESM and CommonJS imports include matching TypeScript declarations.
|
|
91
|
+
|
|
92
|
+
</details>
|
|
60
93
|
|
|
61
94
|
## Frameworks
|
|
62
95
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
96
|
+
Use two direct child elements as the front and back faces. The adapters attach behavior to those existing nodes, so reactive content remains owned by your framework.
|
|
97
|
+
|
|
98
|
+
| Framework | Import | Integration | Cleanup |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| React 18+ | `@simhub2/justflipit/react` | `useFlip()` hook | Effect cleanup; StrictMode compatible |
|
|
101
|
+
| Vue 3 | `@simhub2/justflipit/vue` | `vFlip` directive | `beforeUnmount` |
|
|
102
|
+
| Svelte 4/5 | `@simhub2/justflipit/svelte` | `flip` action | Action `destroy()` |
|
|
103
|
+
| Other frameworks | `@simhub2/justflipit` | Core with `Faces` selectors | Call `destroy()` before unmount |
|
|
67
104
|
|
|
68
|
-
|
|
69
|
-
|
|
105
|
+
<details open>
|
|
106
|
+
<summary><strong>React</strong> — hook, ref and controller</summary>
|
|
70
107
|
|
|
71
108
|
```jsx
|
|
72
109
|
import { useFlip } from '@simhub2/justflipit/react';
|
|
73
110
|
|
|
74
|
-
export function
|
|
111
|
+
export function ProductCard({ title, details }) {
|
|
75
112
|
const { ref, controller } = useFlip({ Click: true, Tilt: true });
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<div
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<section>
|
|
116
|
+
<div ref={ref}>
|
|
117
|
+
<div data-flip-front>{title}</div>
|
|
118
|
+
<div data-flip-back>{details}</div>
|
|
119
|
+
</div>
|
|
120
|
+
<button onClick={() => controller.current?.toggle()}>Flip card</button>
|
|
121
|
+
</section>
|
|
122
|
+
);
|
|
83
123
|
}
|
|
84
124
|
```
|
|
85
125
|
|
|
86
|
-
|
|
87
|
-
|
|
126
|
+
In Next.js App Router, put this hook in a Client Component with `'use client'`.
|
|
127
|
+
|
|
128
|
+
</details>
|
|
129
|
+
|
|
130
|
+
<details>
|
|
131
|
+
<summary><strong>Vue 3</strong> — directive with reactive options</summary>
|
|
88
132
|
|
|
89
133
|
```vue
|
|
90
134
|
<script setup>
|
|
91
135
|
import { vFlip } from '@simhub2/justflipit/vue';
|
|
136
|
+
|
|
137
|
+
defineProps({ title: String, details: String });
|
|
92
138
|
</script>
|
|
139
|
+
|
|
93
140
|
<template>
|
|
94
141
|
<div v-flip="{ Click: true, Effect: 'scale' }">
|
|
95
|
-
<div data-flip-front>
|
|
96
|
-
<div data-flip-back>
|
|
142
|
+
<div data-flip-front>{{ title }}</div>
|
|
143
|
+
<div data-flip-back>{{ details }}</div>
|
|
97
144
|
</div>
|
|
98
145
|
</template>
|
|
99
146
|
```
|
|
100
147
|
|
|
101
|
-
|
|
148
|
+
</details>
|
|
149
|
+
|
|
150
|
+
<details>
|
|
151
|
+
<summary><strong>Svelte</strong> — action with lifecycle updates</summary>
|
|
102
152
|
|
|
103
153
|
```svelte
|
|
104
154
|
<script>
|
|
105
155
|
import { flip } from '@simhub2/justflipit/svelte';
|
|
106
156
|
</script>
|
|
157
|
+
|
|
107
158
|
<div use:flip={{ Click: true, Tilt: true }}>
|
|
108
159
|
<div data-flip-front>Front</div>
|
|
109
|
-
<div data-flip-back>
|
|
160
|
+
<div data-flip-back>Details</div>
|
|
110
161
|
</div>
|
|
111
162
|
```
|
|
112
163
|
|
|
113
|
-
|
|
114
|
-
and call `destroy()` before unmounting.
|
|
164
|
+
</details>
|
|
115
165
|
|
|
116
|
-
|
|
166
|
+
**Lifecycle notes:** Imports are safe during server rendering; initialize DOM controllers on the client. Keep `Faces` selectors stable while mounted. Render framework content through application state and omit `Template`. Avoid simultaneously managing the plugin's style, class and accessibility attributes from your framework.
|
|
117
167
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
agent runtime,
|
|
121
|
-
No API key or model SDK is required by this package.
|
|
168
|
+
## Agent tools
|
|
169
|
+
|
|
170
|
+
Connect any agent runtime that supports tool calls. Register the cards your agent may control, supply the exported schema to your runtime, and pass parsed tool arguments to the executor.
|
|
122
171
|
|
|
123
172
|
```js
|
|
173
|
+
import justFlipIt from '@simhub2/justflipit';
|
|
124
174
|
import { createAgentBridge } from '@simhub2/justflipit/agent';
|
|
125
|
-
const bridge = createAgentBridge({ product: card });
|
|
126
175
|
|
|
127
|
-
|
|
128
|
-
|
|
176
|
+
const product = justFlipIt(document.querySelector('.product-card'), {
|
|
177
|
+
Click: true,
|
|
178
|
+
});
|
|
179
|
+
const bridge = createAgentBridge({ product });
|
|
180
|
+
|
|
181
|
+
// Wrap this definition in the tool format expected by your agent runtime.
|
|
182
|
+
const tool = bridge.tool;
|
|
183
|
+
|
|
184
|
+
// Execute the parsed arguments from a tool call.
|
|
185
|
+
bridge.execute({ id: 'product', action: 'setText', text: 'Selected for you.' });
|
|
129
186
|
const result = bridge.execute({ id: 'product', action: 'flip' });
|
|
130
|
-
|
|
187
|
+
// { id: 'product', flipped: true, destroyed: false, effect: 'flip' }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Action | Result |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `state` | Read the card's current state |
|
|
193
|
+
| `flip` | Show the back |
|
|
194
|
+
| `reset` | Show the front |
|
|
195
|
+
| `toggle` | Switch sides |
|
|
196
|
+
| `setText` | Replace the generated back face with plain text |
|
|
197
|
+
|
|
198
|
+
The bridge validates commands and accepts only registered card IDs. `setText` uses `textContent`, accepts up to 100,000 characters, and rejects framework-managed cards; update framework content through application state.
|
|
199
|
+
|
|
200
|
+
The executor runs in the browser alongside the DOM. Relay server-side tool calls through your application. The package provides a schema and executor; your application supplies the agent runtime and any model credentials.
|
|
201
|
+
|
|
202
|
+
## Controller API
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
card.flip(); // Show back
|
|
206
|
+
card.flip(false); // Show front
|
|
207
|
+
card.toggle(); // Switch sides
|
|
208
|
+
card.update({ Effect: 'fade' }); // Change options in place
|
|
209
|
+
card.getState(); // { flipped, destroyed, effect }
|
|
210
|
+
card.destroy(); // Remove listeners and restore the element
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
| Call | Returns |
|
|
214
|
+
|---|---|
|
|
215
|
+
| `justFlipIt(element, options)` | One controller |
|
|
216
|
+
| `justFlipIt(selectorOrCollection, options)` | An array of controllers |
|
|
217
|
+
| `justFlipIt.get(element)` | Existing controller, or `undefined` |
|
|
218
|
+
| `justFlipIt.destroy(target)` | Destroys matching instances |
|
|
219
|
+
|
|
220
|
+
Repeated initialization returns the existing instance. Use `update()` to change its options. `destroy()` is safe to call more than once. Initialize an HTML container, such as a `div`, rather than a void element such as `img`.
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
card.hoverPanel.addEventListener('justflipit:change', ({ detail }) => {
|
|
224
|
+
console.log(detail.flipped, detail.effect);
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Options
|
|
229
|
+
|
|
230
|
+
| Option | Default | What it controls |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| `Click` | `false` | Hover by default; `true` for click; a selector or selector array for delegated clicks |
|
|
233
|
+
| `FlipX` | `false` | X-axis rotation instead of Y-axis |
|
|
234
|
+
| `Template` | Clone of original | Trusted HTML for a generated back face |
|
|
235
|
+
| `Duration` | `400` | Non-negative duration in milliseconds |
|
|
236
|
+
| `Easing` | `cubic-bezier(.22,.61,.36,1)` | CSS easing function |
|
|
237
|
+
| `Effect` | `'flip'` | `'flip'`, `'fade'`, or `'scale'` |
|
|
238
|
+
| `Tilt` | `false` | `true`, `'only'`, or `{ max, perspective, scale }` |
|
|
239
|
+
| `ReducedMotion` | `true` | Respect live `prefers-reduced-motion` preferences |
|
|
240
|
+
| `Faces` | Omitted | Existing direct children: `{ front: selector, back: selector }` |
|
|
241
|
+
| `preserveClasses` | `true` | Copy original classes onto a generated container |
|
|
242
|
+
| `preserveStyles` | `true` | Copy original inline styles onto a generated container |
|
|
243
|
+
| `Style` | `[]` | Apply `{ el: 'self' or selector, style: {...} }` entries |
|
|
244
|
+
|
|
245
|
+
<details>
|
|
246
|
+
<summary><strong>Style existing faces and choose a different effect</strong></summary>
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
justFlipIt(document.querySelector('.card'), {
|
|
250
|
+
Faces: { front: '[data-front]', back: '[data-back]' },
|
|
251
|
+
Click: true,
|
|
252
|
+
FlipX: true,
|
|
253
|
+
Effect: 'scale',
|
|
254
|
+
Duration: 500,
|
|
255
|
+
Style: [
|
|
256
|
+
{ el: '[data-front]', style: { background: '#243b2b', borderRadius: '20px' } },
|
|
257
|
+
{ el: '[data-back]', style: { background: '#c1f384', borderRadius: '20px' } },
|
|
258
|
+
],
|
|
259
|
+
});
|
|
131
260
|
```
|
|
132
261
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
262
|
+
</details>
|
|
263
|
+
|
|
264
|
+
## Motion and accessibility
|
|
265
|
+
|
|
266
|
+
- **Reversible transitions:** Rapid input reverses the current CSS transition without accumulating animation objects.
|
|
267
|
+
- **Efficient tilt.** Mouse and finger movement share one animation frame. With `Tilt: true` or custom tilt settings, touch tilts the card until release or until the browser takes over a scroll gesture.
|
|
268
|
+
- **Keyboard:** Focus the card, then use Enter or Space to toggle and Escape to reset.
|
|
269
|
+
- **Touch.** Tap to switch sides with any effect. Hover mode distinguishes a tap from a drag, and click mode uses the browser’s native click. Use `Tilt: 'only'` for touch tilt without tap-to-flip.
|
|
270
|
+
- **Hidden faces:** The inactive face is inert and marked `aria-hidden`.
|
|
271
|
+
- **Reduced motion:** System preference changes disable transitions and tilt automatically. Set `ReducedMotion: false` only when your application intentionally overrides this preference.
|
|
272
|
+
|
|
273
|
+
Targets modern browsers with CSS Grid, Pointer Events, and native `inert` support. Add application-level polyfills when targeting older browsers. SSR safety covers importing the package; DOM initialization requires a browser.
|
|
274
|
+
|
|
275
|
+
## Upgrading from v3
|
|
138
276
|
|
|
139
|
-
|
|
277
|
+
**v4 is a major release.** Check these changes before upgrading an existing integration:
|
|
140
278
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
279
|
+
| v3 integration | v4 equivalent |
|
|
280
|
+
|---|---|
|
|
281
|
+
| UMD-only npm entry | Default ESM import or callable CommonJS export |
|
|
282
|
+
| Optional jQuery plugin registration | Call `justFlipIt(...)` directly |
|
|
283
|
+
| Separate modern / vanilla / combined implementations | One core; historical import paths and browser filenames remain aliases |
|
|
284
|
+
| `.front` / `.backY` generated face selectors | `[data-jfi-face="front"]` / `[data-jfi-face="back"]` |
|
|
285
|
+
| `._flipY_` / `._flip_` state classes | `[data-jfi-flipped="true"]` |
|
|
286
|
+
| Fixed measured dimensions | Responsive grid; add explicit sizes in your design where needed |
|
|
287
|
+
| Framework DOM replacement | Existing direct-child faces via an adapter or `Faces` |
|
|
150
288
|
|
|
151
|
-
|
|
289
|
+
Option names retain PascalCase. `Faces` cannot change after initialization. Teardown restores plugin-owned attributes to their initial values.
|
|
152
290
|
|
|
153
|
-
|
|
291
|
+
See the [changelog](CHANGELOG.md) for the release overview.
|
|
292
|
+
|
|
293
|
+
## Development
|
|
294
|
+
|
|
295
|
+
Use Node.js 22 or newer.
|
|
154
296
|
|
|
155
297
|
```sh
|
|
156
298
|
npm ci
|
|
157
|
-
npm run check
|
|
158
|
-
npm
|
|
299
|
+
npm run check # Lifecycle tests and TypeScript consumer checks
|
|
300
|
+
npm run build # ESM, CommonJS, browser bundles, source maps, declarations
|
|
301
|
+
npm pack --dry-run # Inspect published files
|
|
159
302
|
```
|
|
160
303
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
present in the registry.
|
|
304
|
+
Tests cover original node preservation, teardown, delegated events, agent command validation, reduced motion, tilt batching, React StrictMode, Vue updates, and a compiled Svelte 5 component.
|
|
305
|
+
|
|
306
|
+
See the [release guide](https://github.com/SimHub/justFlipIt/blob/main/RELEASING.md) for the GitHub and npm steps.
|
|
307
|
+
|
|
308
|
+
---
|
|
167
309
|
|
|
168
|
-
|
|
310
|
+
<p align="center">
|
|
311
|
+
Built by <a href="https://github.com/SimHub">SimHub</a> · <a href="LICENSE">MIT license</a> · <a href="https://github.com/SimHub/justFlipIt/issues">Report an issue</a> · <a href="https://www.buymeacoffee.com/simhub">Buy me a coffee</a>
|
|
312
|
+
</p>
|
|
@@ -28,14 +28,16 @@ var JustFlipItModule = (() => {
|
|
|
28
28
|
var css = `
|
|
29
29
|
._justFlipIt_panel{position:relative;display:grid;perspective:var(--jfi-perspective,1000px);isolation:isolate}
|
|
30
30
|
._justFlipIt_panel>[data-jfi-face]{grid-area:1/1;min-width:0;backface-visibility:hidden;-webkit-backface-visibility:hidden;transition:transform var(--jfi-duration) var(--jfi-easing),opacity var(--jfi-duration) var(--jfi-easing);transform-origin:center}
|
|
31
|
-
._justFlipIt_panel>[data-jfi-face=front]{
|
|
32
|
-
._justFlipIt_panel>[data-jfi-face=back]{
|
|
33
|
-
._justFlipIt_panel[data-jfi-
|
|
34
|
-
._justFlipIt_panel[data-jfi-
|
|
35
|
-
._justFlipIt_panel[data-jfi-
|
|
36
|
-
._justFlipIt_panel[data-jfi-
|
|
37
|
-
._justFlipIt_panel[data-jfi-
|
|
38
|
-
._justFlipIt_panel[data-jfi-effect=
|
|
31
|
+
._justFlipIt_panel>[data-jfi-face=back],._justFlipIt_panel[data-jfi-flipped=true]>[data-jfi-face=front]{pointer-events:none}
|
|
32
|
+
._justFlipIt_panel[data-jfi-flipped=true]>[data-jfi-face=back]{pointer-events:auto}
|
|
33
|
+
._justFlipIt_panel[data-jfi-effect=flip]>[data-jfi-face=front]{transform:rotateY(0deg)}
|
|
34
|
+
._justFlipIt_panel[data-jfi-effect=flip]>[data-jfi-face=back]{transform:rotateY(-180deg)}
|
|
35
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-flipped=true]>[data-jfi-face=front]{transform:rotateY(180deg)}
|
|
36
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-flipped=true]>[data-jfi-face=back]{transform:rotateY(0deg)}
|
|
37
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-axis=x]>[data-jfi-face=front]{transform:rotateX(0deg)}
|
|
38
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-axis=x]>[data-jfi-face=back]{transform:rotateX(-180deg)}
|
|
39
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-axis=x][data-jfi-flipped=true]>[data-jfi-face=front]{transform:rotateX(180deg)}
|
|
40
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-axis=x][data-jfi-flipped=true]>[data-jfi-face=back]{transform:rotateX(0deg)}
|
|
39
41
|
._justFlipIt_panel[data-jfi-effect=fade]>[data-jfi-face=back],._justFlipIt_panel[data-jfi-effect=scale]>[data-jfi-face=back]{opacity:0}
|
|
40
42
|
._justFlipIt_panel[data-jfi-effect=fade][data-jfi-flipped=true]>[data-jfi-face=front],._justFlipIt_panel[data-jfi-effect=scale][data-jfi-flipped=true]>[data-jfi-face=front]{opacity:0}
|
|
41
43
|
._justFlipIt_panel[data-jfi-effect=fade][data-jfi-flipped=true]>[data-jfi-face=back],._justFlipIt_panel[data-jfi-effect=scale][data-jfi-flipped=true]>[data-jfi-face=back]{opacity:1}
|
|
@@ -163,33 +165,63 @@ var JustFlipItModule = (() => {
|
|
|
163
165
|
this.listen("pointerenter", (event) => {
|
|
164
166
|
if (!this.settings.Click && this.settings.Tilt !== "only" && event.pointerType !== "touch") this.flip(true);
|
|
165
167
|
});
|
|
166
|
-
this.listen("pointerleave", () => {
|
|
168
|
+
this.listen("pointerleave", (event) => {
|
|
169
|
+
if (event.pointerType === "touch") return;
|
|
167
170
|
this.resetTilt();
|
|
168
171
|
if (!this.settings.Click && this.settings.Tilt !== "only") this.flip(false);
|
|
169
172
|
});
|
|
170
|
-
this.listen("
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
});
|
|
173
|
+
this.listen("pointerdown", (event) => {
|
|
174
|
+
if (event.pointerType !== "touch" || event.isPrimary === false || this.touch) return;
|
|
175
|
+
this.resetTilt();
|
|
176
|
+
this.touch = { id: event.pointerId, x: event.clientX, y: event.clientY, moved: false };
|
|
177
|
+
this.queueTilt(event, this.touch);
|
|
178
|
+
}, { passive: true });
|
|
177
179
|
this.listen("pointermove", (event) => {
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const x = Math.max(-1, Math.min(1, (this.point[0] - rect.left) / rect.width * 2 - 1));
|
|
187
|
-
const y = Math.max(-1, Math.min(1, (this.point[1] - rect.top) / rect.height * 2 - 1));
|
|
188
|
-
this.hoverPanel.style.transform = `perspective(${t.perspective}px) rotateX(${-y * t.max}deg) rotateY(${x * t.max}deg) scale(${t.scale})`;
|
|
189
|
-
});
|
|
180
|
+
if (event.pointerType !== "touch") {
|
|
181
|
+
if (!this.touch) this.queueTilt(event);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const touch = this.touch;
|
|
185
|
+
if (!touch || touch.id !== event.pointerId) return;
|
|
186
|
+
touch.moved || (touch.moved = this.touchMoved(event, touch));
|
|
187
|
+
this.queueTilt(event, touch);
|
|
190
188
|
}, { passive: true });
|
|
189
|
+
this.listen("pointerup", (event) => {
|
|
190
|
+
const touch = this.finishTouch(event);
|
|
191
|
+
if (!touch || touch.moved || this.touchMoved(event, touch)) return;
|
|
192
|
+
if (!this.settings.Click && this.settings.Tilt !== "only") this.toggle();
|
|
193
|
+
});
|
|
194
|
+
this.listen("pointercancel", (event) => {
|
|
195
|
+
if (event.pointerType === "touch") this.finishTouch(event);
|
|
196
|
+
else this.resetTilt();
|
|
197
|
+
});
|
|
198
|
+
this.listen("lostpointercapture", (event) => this.finishTouch(event));
|
|
191
199
|
this.baseTransform = this.hoverPanel.style.transform;
|
|
192
200
|
}
|
|
201
|
+
touchMoved(event, touch) {
|
|
202
|
+
return Math.hypot(event.clientX - touch.x, event.clientY - touch.y) > 10;
|
|
203
|
+
}
|
|
204
|
+
finishTouch(event) {
|
|
205
|
+
const touch = this.touch;
|
|
206
|
+
if (event.pointerType !== "touch" || !touch || touch.id !== event.pointerId) return;
|
|
207
|
+
this.touch = null;
|
|
208
|
+
this.resetTilt();
|
|
209
|
+
return touch;
|
|
210
|
+
}
|
|
211
|
+
queueTilt(event, touch) {
|
|
212
|
+
if (!this.settings.Tilt || this.reduced) return;
|
|
213
|
+
this.point = [event.clientX, event.clientY];
|
|
214
|
+
if (this.frame) return;
|
|
215
|
+
this.frame = this.win.requestAnimationFrame(() => {
|
|
216
|
+
this.frame = 0;
|
|
217
|
+
const rect = touch ? touch.rect ?? (touch.rect = this.hoverPanel.getBoundingClientRect()) : this.hoverPanel.getBoundingClientRect();
|
|
218
|
+
if (!rect.width || !rect.height) return;
|
|
219
|
+
const { max = 10, perspective = 1e3, scale = 1.03 } = this.settings.Tilt;
|
|
220
|
+
const x = Math.max(-1, Math.min(1, (this.point[0] - rect.left) / rect.width * 2 - 1));
|
|
221
|
+
const y = Math.max(-1, Math.min(1, (this.point[1] - rect.top) / rect.height * 2 - 1));
|
|
222
|
+
this.hoverPanel.style.transform = `perspective(${perspective}px) rotateX(${-y * max}deg) rotateY(${x * max}deg) scale(${scale})`;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
193
225
|
resetTilt() {
|
|
194
226
|
if (this.frame) this.win.cancelAnimationFrame(this.frame);
|
|
195
227
|
this.frame = 0;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
var JustFlipItModule=(()=>{var
|
|
1
|
+
var JustFlipItModule=(()=>{var d=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var v=Object.prototype.hasOwnProperty;var b=(i,t)=>{for(var e in t)d(i,e,{get:t[e],enumerable:!0})},_=(i,t,e,a)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of T(t))!v.call(i,s)&&s!==e&&d(i,s,{get:()=>t[s],enumerable:!(a=g(t,s))||a.enumerable});return i};var w=i=>_(d({},"__esModule",{value:!0}),i);var I={};b(I,{FlipController:()=>c,default:()=>h});var n=new WeakMap,k={Click:!1,FlipX:!1,Duration:400,Easing:"cubic-bezier(.22,.61,.36,1)",Tilt:!1,Effect:"flip",ReducedMotion:!0,preserveClasses:!0,preserveStyles:!0,Style:[]},F=`
|
|
2
2
|
._justFlipIt_panel{position:relative;display:grid;perspective:var(--jfi-perspective,1000px);isolation:isolate}
|
|
3
3
|
._justFlipIt_panel>[data-jfi-face]{grid-area:1/1;min-width:0;backface-visibility:hidden;-webkit-backface-visibility:hidden;transition:transform var(--jfi-duration) var(--jfi-easing),opacity var(--jfi-duration) var(--jfi-easing);transform-origin:center}
|
|
4
|
-
._justFlipIt_panel>[data-jfi-face=front]{
|
|
5
|
-
._justFlipIt_panel>[data-jfi-face=back]{
|
|
6
|
-
._justFlipIt_panel[data-jfi-
|
|
7
|
-
._justFlipIt_panel[data-jfi-
|
|
8
|
-
._justFlipIt_panel[data-jfi-
|
|
9
|
-
._justFlipIt_panel[data-jfi-
|
|
10
|
-
._justFlipIt_panel[data-jfi-
|
|
11
|
-
._justFlipIt_panel[data-jfi-effect=
|
|
4
|
+
._justFlipIt_panel>[data-jfi-face=back],._justFlipIt_panel[data-jfi-flipped=true]>[data-jfi-face=front]{pointer-events:none}
|
|
5
|
+
._justFlipIt_panel[data-jfi-flipped=true]>[data-jfi-face=back]{pointer-events:auto}
|
|
6
|
+
._justFlipIt_panel[data-jfi-effect=flip]>[data-jfi-face=front]{transform:rotateY(0deg)}
|
|
7
|
+
._justFlipIt_panel[data-jfi-effect=flip]>[data-jfi-face=back]{transform:rotateY(-180deg)}
|
|
8
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-flipped=true]>[data-jfi-face=front]{transform:rotateY(180deg)}
|
|
9
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-flipped=true]>[data-jfi-face=back]{transform:rotateY(0deg)}
|
|
10
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-axis=x]>[data-jfi-face=front]{transform:rotateX(0deg)}
|
|
11
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-axis=x]>[data-jfi-face=back]{transform:rotateX(-180deg)}
|
|
12
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-axis=x][data-jfi-flipped=true]>[data-jfi-face=front]{transform:rotateX(180deg)}
|
|
13
|
+
._justFlipIt_panel[data-jfi-effect=flip][data-jfi-axis=x][data-jfi-flipped=true]>[data-jfi-face=back]{transform:rotateX(0deg)}
|
|
12
14
|
._justFlipIt_panel[data-jfi-effect=fade]>[data-jfi-face=back],._justFlipIt_panel[data-jfi-effect=scale]>[data-jfi-face=back]{opacity:0}
|
|
13
15
|
._justFlipIt_panel[data-jfi-effect=fade][data-jfi-flipped=true]>[data-jfi-face=front],._justFlipIt_panel[data-jfi-effect=scale][data-jfi-flipped=true]>[data-jfi-face=front]{opacity:0}
|
|
14
16
|
._justFlipIt_panel[data-jfi-effect=fade][data-jfi-flipped=true]>[data-jfi-face=back],._justFlipIt_panel[data-jfi-effect=scale][data-jfi-flipped=true]>[data-jfi-face=back]{opacity:1}
|
|
@@ -16,6 +18,6 @@ var JustFlipItModule=(()=>{var p=Object.defineProperty;var g=Object.getOwnProper
|
|
|
16
18
|
._justFlipIt_panel[data-jfi-effect=scale][data-jfi-flipped=true]>[data-jfi-face=back]{transform:scale(1)}
|
|
17
19
|
._justFlipIt_panel[data-jfi-reduced=true]>[data-jfi-face]{transition:none!important}
|
|
18
20
|
@media(prefers-reduced-motion:reduce){._justFlipIt_panel:not([data-jfi-motion=allow])>[data-jfi-face]{transition:none!important}}
|
|
19
|
-
`;function
|
|
21
|
+
`;function E(i){if(i.querySelector('style[data-justflipitstyle="v4"]'))return;let t=i.createElement("style");t.dataset.justflipitstyle="v4",t.textContent=F,i.head.append(t)}function j(i){if(i.Duration!==void 0&&(!Number.isFinite(i.Duration)||i.Duration<0))throw new TypeError("Duration must be a non-negative finite number");if(i.Effect!==void 0&&!["flip","fade","scale"].includes(i.Effect))throw new TypeError("Unknown Effect");if(i.Tilt&&typeof i.Tilt=="object"){for(let t of["max","perspective","scale"])if(i.Tilt[t]!==void 0&&(!Number.isFinite(i.Tilt[t])||i.Tilt[t]<0))throw new TypeError(`Invalid Tilt.${t}`)}}var c=class{constructor(t,e={}){if(j(e),!t||t.nodeType!==1)throw new TypeError("Expected an HTML element");this.element=t,this.settings={...k,...e},this.flipped=!1,this.destroyed=!1,this.cleanups=[],this.restores=[],this.win=t.ownerDocument.defaultView;let a=t.ownerDocument;if(E(a),this.managed=!!e.Faces,this.managed){if(this.hoverPanel=t,this.frontWrapper=t.querySelector(e.Faces.front),this.backWrapper=t.querySelector(e.Faces.back),!this.frontWrapper||!this.backWrapper||this.frontWrapper===this.backWrapper||this.frontWrapper.parentNode!==t||this.backWrapper.parentNode!==t)throw new TypeError("Faces must select two distinct direct children")}else{if(!t.parentNode)throw new TypeError("Element must have a parent");if(this.hoverPanel=a.createElement("div"),this.frontWrapper=a.createElement("div"),this.backWrapper=a.createElement("div"),this.settings.preserveClasses&&(this.hoverPanel.className=t.className),this.settings.preserveStyles&&(this.hoverPanel.style.cssText=t.style.cssText),t.before(this.hoverPanel),this.hoverPanel.append(this.frontWrapper,this.backWrapper),this.frontWrapper.append(t),e.Template!==void 0)this.backWrapper.innerHTML=e.Template;else{let r=t.cloneNode(!0);r.removeAttribute("id"),r.querySelectorAll("[id]").forEach(l=>l.removeAttribute("id")),this.backWrapper.append(r)}}let s=this.hoverPanel;for(let r of[s,this.frontWrapper,this.backWrapper]){let p=(r===s?["class","style","tabindex","data-jfi-axis","data-jfi-effect","data-jfi-motion","data-jfi-flipped","data-jfi-reduced"]:["style","data-jfi-face","aria-hidden","inert"]).map(o=>[o,r.getAttribute(o)]),y=r.inert||!1;this.restores.push(()=>{r.inert=y;for(let[o,u]of p)u===null?r.removeAttribute(o):r.setAttribute(o,u)})}s.classList.add("_justFlipIt_panel"),this.frontWrapper.dataset.jfiFace="front",this.backWrapper.dataset.jfiFace="back",this.motion=this.win.matchMedia?.("(prefers-reduced-motion: reduce)");let f=()=>{this.resetTilt(),this.render()};this.motion?.addEventListener("change",f),this.cleanups.push(()=>this.motion?.removeEventListener("change",f)),this.configure(),this.events(),this.render(),n.set(t,this),n.set(s,this)}listen(t,e,a){this.hoverPanel.addEventListener(t,e,a),this.cleanups.push(()=>this.hoverPanel.removeEventListener(t,e,a))}get reduced(){return this.settings.ReducedMotion!==!1&&!!this.motion?.matches}configure(){let t=this.settings,e=this.hoverPanel;e.style.setProperty("--jfi-duration",`${t.Duration}ms`),e.style.setProperty("--jfi-easing",t.Easing),e.dataset.jfiAxis=t.FlipX?"x":"y",e.dataset.jfiEffect=t.Effect,e.dataset.jfiMotion=t.ReducedMotion===!1?"allow":"respect";for(let a of t.Style||[])(a.el==="self"?[e]:e.querySelectorAll(a.el)).forEach(f=>Object.assign(f.style,a.style));t.Tilt!=="only"&&!e.hasAttribute("tabindex")&&(e.tabIndex=0)}events(){this.listen("click",t=>{if(this.settings.Tilt==="only")return;let e=this.settings.Click;(e===!0||e&&[].concat(e).some(a=>t.target.closest?.(a)&&this.hoverPanel.contains(t.target.closest(a))))&&this.toggle()}),this.listen("keydown",t=>{t.target!==this.hoverPanel||this.settings.Tilt==="only"||((t.key==="Enter"||t.key===" ")&&(t.preventDefault(),this.toggle()),t.key==="Escape"&&this.flip(!1))}),this.listen("pointerenter",t=>{!this.settings.Click&&this.settings.Tilt!=="only"&&t.pointerType!=="touch"&&this.flip(!0)}),this.listen("pointerleave",t=>{t.pointerType!=="touch"&&(this.resetTilt(),!this.settings.Click&&this.settings.Tilt!=="only"&&this.flip(!1))}),this.listen("pointerdown",t=>{t.pointerType!=="touch"||t.isPrimary===!1||this.touch||(this.resetTilt(),this.touch={id:t.pointerId,x:t.clientX,y:t.clientY,moved:!1},this.queueTilt(t,this.touch))},{passive:!0}),this.listen("pointermove",t=>{if(t.pointerType!=="touch"){this.touch||this.queueTilt(t);return}let e=this.touch;!e||e.id!==t.pointerId||(e.moved||(e.moved=this.touchMoved(t,e)),this.queueTilt(t,e))},{passive:!0}),this.listen("pointerup",t=>{let e=this.finishTouch(t);!e||e.moved||this.touchMoved(t,e)||!this.settings.Click&&this.settings.Tilt!=="only"&&this.toggle()}),this.listen("pointercancel",t=>{t.pointerType==="touch"?this.finishTouch(t):this.resetTilt()}),this.listen("lostpointercapture",t=>this.finishTouch(t)),this.baseTransform=this.hoverPanel.style.transform}touchMoved(t,e){return Math.hypot(t.clientX-e.x,t.clientY-e.y)>10}finishTouch(t){let e=this.touch;if(!(t.pointerType!=="touch"||!e||e.id!==t.pointerId))return this.touch=null,this.resetTilt(),e}queueTilt(t,e){!this.settings.Tilt||this.reduced||(this.point=[t.clientX,t.clientY],!this.frame&&(this.frame=this.win.requestAnimationFrame(()=>{this.frame=0;let a=e?e.rect??(e.rect=this.hoverPanel.getBoundingClientRect()):this.hoverPanel.getBoundingClientRect();if(!a.width||!a.height)return;let{max:s=10,perspective:f=1e3,scale:r=1.03}=this.settings.Tilt,l=Math.max(-1,Math.min(1,(this.point[0]-a.left)/a.width*2-1)),p=Math.max(-1,Math.min(1,(this.point[1]-a.top)/a.height*2-1));this.hoverPanel.style.transform=`perspective(${f}px) rotateX(${-p*s}deg) rotateY(${l*s}deg) scale(${r})`})))}resetTilt(){this.frame&&this.win.cancelAnimationFrame(this.frame),this.frame=0,this.hoverPanel.style.transform=this.baseTransform||""}render(){this.hoverPanel.dataset.jfiFlipped=String(this.flipped),this.hoverPanel.dataset.jfiReduced=String(this.reduced);for(let[t,e]of[[this.frontWrapper,this.flipped],[this.backWrapper,!this.flipped]])t.setAttribute("aria-hidden",String(e)),t.inert=e}flip(t=!0){if(this.destroyed||this.flipped===!!t)return this;let e=this.element.ownerDocument.activeElement;return(t?this.frontWrapper:this.backWrapper).contains(e)&&this.hoverPanel.focus({preventScroll:!0}),this.flipped=!!t,this.render(),this.hoverPanel.dispatchEvent(new this.win.CustomEvent("justflipit:change",{bubbles:!0,detail:this.getState()})),this}toggle(){return this.flip(!this.flipped)}update(t={}){if(this.destroyed)return this;if("Faces"in t)throw new TypeError("Faces cannot change after initialization");if(j(t),this.managed&&"Template"in t)throw new TypeError("Render framework face content with the framework");if(this.settings={...this.settings,...t},"Template"in t){if(this.managed)throw new TypeError("Render framework face content with the framework");this.backWrapper.innerHTML=t.Template}return this.resetTilt(),this.configure(),this.render(),this}getState(){return{flipped:this.flipped,destroyed:this.destroyed,effect:this.settings.Effect}}destroy(){this.destroyed||(this.resetTilt(),this.cleanups.forEach(t=>t()),this.restores.forEach(t=>t()),this.managed||this.hoverPanel.replaceWith(this.element),n.delete(this.element),n.delete(this.hoverPanel),this.destroyed=!0)}};function m(i){return typeof i=="string"?typeof document>"u"?[]:Array.from(document.querySelectorAll(i)):i?.nodeType===1?[i]:i?Array.from(i):[]}function h(i,t){let e=m(i).map(a=>n.get(a)||new c(a,t));return i?.nodeType===1?e[0]:e}h.destroy=i=>m(i).forEach(t=>n.get(t)?.destroy());h.get=i=>n.get(i);return w(I);})();
|
|
20
22
|
globalThis.justFlipIt = JustFlipItModule.default;
|
|
21
23
|
//# sourceMappingURL=justFlipIt.min.js.map
|