@ionic/core 8.8.4-dev.11775576543.172b7b99 → 8.8.4-dev.11775666666.132201b7
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/components/ion-item-sliding.js +1 -1
- package/dist/cjs/ion-item-option_3.cjs.entry.js +137 -1
- package/dist/collection/components/item-sliding/item-sliding.js +137 -1
- package/dist/collection/utils/test/playwright/drag-element.js +3 -4
- package/dist/docs.json +1 -1
- package/dist/esm/ion-item-option_3.entry.js +137 -1
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/{p-727bb80c.entry.js → p-b0c54b4c.entry.js} +1 -1
- package/dist/types/components/item-sliding/item-sliding.d.ts +28 -1
- package/dist/types/utils/test/playwright/drag-element.d.ts +1 -1
- package/hydrate/index.js +137 -1
- package/hydrate/index.mjs +137 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* (C) Ionic http://ionicframework.com - MIT License
|
|
3
3
|
*/
|
|
4
|
-
import{p as t,H as i,e as s,f as e,h as n,t as o,d as r}from"./p-3Ni1Z654.js";import{a as h,d as a,r as d}from"./p-wCDzv5Q8.js";import{m as l}from"./p-BqDiJgC_.js";import{w as
|
|
4
|
+
import{p as t,H as i,e as s,f as e,h as n,t as o,d as r}from"./p-3Ni1Z654.js";import{a as h,d as a,r as d}from"./p-wCDzv5Q8.js";import{m as l}from"./p-BqDiJgC_.js";import{w as c}from"./p-Dtdm8lKC.js";import{b as m}from"./p-CVBkx7m1.js";let p;const u=t(class extends i{constructor(t){super(),!1!==t&&this.__registerHost(),this.ionDrag=s(this,"ionDrag",7),this.item=null,this.openAmount=0,this.initialOpenAmount=0,this.optsWidthRightSide=0,this.optsWidthLeftSide=0,this.sides=0,this.optsDirty=!0,this.contentEl=null,this.initialContentScrollY=!0,this.state=2,this.disabled=!1}disabledChanged(){this.gesture&&this.gesture.enable(!this.disabled)}async connectedCallback(){const{el:t}=this;this.item=t.querySelector("ion-item"),this.contentEl=h(t),this.mutationObserver=c(t,"ion-item-option",(async()=>{await this.updateOptions()})),await this.updateOptions(),this.gesture=(await import("./p-Cl0B-RWe.js")).createGesture({el:t,gestureName:"item-swipe",gesturePriority:100,threshold:5,canStart:t=>this.canStart(t),onStart:()=>this.onStart(),onMove:t=>this.onMove(t),onEnd:t=>this.onEnd(t)}),this.disabledChanged()}disconnectedCallback(){var t;this.gesture&&(this.gesture.destroy(),this.gesture=void 0),void 0!==this.tmr&&(clearTimeout(this.tmr),this.tmr=void 0),null===(t=this.animationAbortController)||void 0===t||t.abort(),this.item=null,this.leftOptions=this.rightOptions=void 0,p===this.el&&(p=void 0),this.mutationObserver&&(this.mutationObserver.disconnect(),this.mutationObserver=void 0)}getOpenAmount(){return Promise.resolve(this.openAmount)}getSlidingRatio(){return Promise.resolve(this.getSlidingRatioSync())}async open(t){var i;if(128&this.state)return;if(null===(this.item=null!==(i=this.item)&&void 0!==i?i:this.el.querySelector("ion-item")))return;const s=this.getOptions(t);s&&(void 0===t&&(t=s===this.leftOptions?"start":"end"),t=l(t)?"end":"start",this.openAmount<0&&s===this.leftOptions||this.openAmount>0&&s===this.rightOptions||(this.closeOpened(),this.state=4,requestAnimationFrame((()=>{this.calculateOptsWidth(),p=this.el,this.setOpenAmount("end"===t?this.optsWidthRightSide:-this.optsWidthLeftSide,!1),this.state="end"===t?8:16}))))}async close(){128&this.state||this.setOpenAmount(0,!0)}async closeOpened(){return void 0!==p&&(p.close(),p=void 0,!0)}getOptions(t){return void 0===t?this.leftOptions||this.rightOptions:"start"===t?this.leftOptions:this.rightOptions}hasExpandableOptions(t){if(!t)return!1;const i=t.querySelectorAll("ion-item-option");return Array.from(i).some((t=>!0===t.expandable&&!t.disabled))}delay(t,i){return new Promise(((s,e)=>{const n=setTimeout(s,t);i.addEventListener("abort",(()=>{clearTimeout(n),e(new DOMException("Animation cancelled","AbortError"))}),{once:!0})}))}animateToPosition(t,i,s){return new Promise(((e,n)=>{if(!this.item)return e();this.item.style.transition=`transform ${i}ms ease-out`,this.item.style.transform=`translate3d(${-t}px, 0, 0)`;const o=setTimeout(e,i);s.addEventListener("abort",(()=>{clearTimeout(o),n(new DOMException("Animation cancelled","AbortError"))}),{once:!0})}))}getSwipeThreshold(t){return("end"===t?this.optsWidthRightSide:this.optsWidthLeftSide)+30}async animateFullSwipe(t){const i=new AbortController;this.animationAbortController=i;const{signal:s}=i;this.gesture&&this.gesture.enable(!1);try{const i="end"===t?this.rightOptions:this.leftOptions;this.state="end"===t?168:208,await this.delay(100,s);const e="end"===t?window.innerWidth:-window.innerWidth;await this.animateToPosition(e,250,s),i&&i.fireSwipeEvent(),await this.delay(300,s),await this.animateToPosition(0,250,s)}catch(t){}finally{this.animationAbortController=void 0,this.item&&(this.item.style.transition="",this.item.style.transform=""),this.openAmount=0,this.state=2,p===this.el&&(p=void 0),this.gesture&&this.gesture.enable(!this.disabled)}}async updateOptions(){var t;const i=this.el.querySelectorAll("ion-item-options");let s=0;this.leftOptions=this.rightOptions=void 0;for(let e=0;e<i.length;e++){const n=i.item(e),o=void 0!==n.componentOnReady?await n.componentOnReady():n;"start"==(l(null!==(t=o.side)&&void 0!==t?t:o.getAttribute("side"))?"end":"start")?(this.leftOptions=o,s|=1):(this.rightOptions=o,s|=2)}this.optsDirty=!0,this.sides=s}canStart(t){return!("rtl"===document.dir?window.innerWidth-t.startX<15:t.startX<15)&&(p&&p!==this.el&&this.closeOpened(),!(!this.rightOptions&&!this.leftOptions))}onStart(){this.item=this.el.querySelector("ion-item");const{contentEl:t}=this;t&&(this.initialContentScrollY=a(t)),p=this.el,void 0!==this.tmr&&(clearTimeout(this.tmr),this.tmr=void 0),0===this.openAmount&&(this.optsDirty=!0,this.state=4),this.initialOpenAmount=this.openAmount,this.item&&(this.item.style.transition="none")}onMove(t){this.optsDirty&&this.calculateOptsWidth();let i,s=this.initialOpenAmount-t.deltaX;switch(this.sides){case 2:s=Math.max(0,s);break;case 1:s=Math.min(0,s);break;case 3:break;case 0:return;default:e("[ion-item-sliding] - invalid ItemSideFlags value",this.sides)}s>this.optsWidthRightSide?(i=this.optsWidthRightSide,s=i+.55*(s-i)):s<-this.optsWidthLeftSide&&(i=-this.optsWidthLeftSide,s=i+.55*(s-i)),this.setOpenAmount(s,!1)}onEnd(t){const{contentEl:i,initialContentScrollY:s}=this;i&&d(i,s);const e=Math.abs(t.deltaX),n=t.deltaX<0?"end":"start";if(this.hasExpandableOptions("end"===n?this.rightOptions:this.leftOptions)&&(e>this.getSwipeThreshold(n)||Math.abs(t.velocityX)>.5&&e>.5*("end"===n?this.optsWidthRightSide:this.optsWidthLeftSide)))return void this.animateFullSwipe(n).catch((()=>{this.gesture&&this.gesture.enable(!this.disabled)}));const o=t.velocityX;let r=this.openAmount>0?this.optsWidthRightSide:-this.optsWidthLeftSide;g(this.openAmount>0==!(o<0),Math.abs(o)>.3,Math.abs(this.openAmount)<Math.abs(r/2))&&(r=0);const h=this.state;this.setOpenAmount(r,!0),32&h&&this.rightOptions?this.rightOptions.fireSwipeEvent():64&h&&this.leftOptions&&this.leftOptions.fireSwipeEvent()}calculateOptsWidth(){this.optsWidthRightSide=0,this.rightOptions&&(this.rightOptions.style.display="flex",this.optsWidthRightSide=this.rightOptions.offsetWidth,this.rightOptions.style.display=""),this.optsWidthLeftSide=0,this.leftOptions&&(this.leftOptions.style.display="flex",this.optsWidthLeftSide=this.leftOptions.offsetWidth,this.leftOptions.style.display=""),this.optsDirty=!1}setOpenAmount(t,i){if(void 0!==this.tmr&&(clearTimeout(this.tmr),this.tmr=void 0),!this.item)return;const{el:s}=this,e=this.item.style;if(this.openAmount=t,i&&(e.transition=""),t>0)this.state=t>=this.optsWidthRightSide+30?40:8;else{if(!(t<0))return s.classList.add("item-sliding-closing"),this.gesture&&this.gesture.enable(!1),this.tmr=setTimeout((()=>{this.state=2,this.tmr=void 0,this.gesture&&this.gesture.enable(!this.disabled),s.classList.remove("item-sliding-closing")}),600),p=void 0,void(e.transform="");this.state=t<=-this.optsWidthLeftSide-30?80:16}e.transform=`translate3d(${-t}px,0,0)`,this.ionDrag.emit({amount:t,ratio:this.getSlidingRatioSync()})}getSlidingRatioSync(){return this.openAmount>0?this.openAmount/this.optsWidthRightSide:this.openAmount<0?this.openAmount/this.optsWidthLeftSide:0}render(){const t=m(this);return n(r,{key:"c945f30d9f7deb90d22064d4059e2b08f35614be",class:{[t]:!0,"item-sliding-active-slide":2!==this.state,"item-sliding-active-options-end":!!(8&this.state),"item-sliding-active-options-start":!!(16&this.state),"item-sliding-active-swipe-end":!!(32&this.state),"item-sliding-active-swipe-start":!!(64&this.state)}})}get el(){return this}static get watchers(){return{disabled:[{disabledChanged:0}]}}static get style(){return"ion-item-sliding{display:block;position:relative;width:100%;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}ion-item-sliding .item{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.item-sliding-active-slide .item{position:relative;-webkit-transition:-webkit-transform 500ms cubic-bezier(0.36, 0.66, 0.04, 1);transition:-webkit-transform 500ms cubic-bezier(0.36, 0.66, 0.04, 1);transition:transform 500ms cubic-bezier(0.36, 0.66, 0.04, 1);transition:transform 500ms cubic-bezier(0.36, 0.66, 0.04, 1), -webkit-transform 500ms cubic-bezier(0.36, 0.66, 0.04, 1);opacity:1;z-index:2;pointer-events:none;will-change:transform}.item-sliding-closing ion-item-options{pointer-events:none}.item-sliding-active-swipe-end .item-options-end .item-option-expandable{padding-left:100%;-ms-flex-order:1;order:1;-webkit-transition-duration:0.6s;transition-duration:0.6s;-webkit-transition-property:padding-left;transition-property:padding-left}:host-context([dir=rtl]) .item-sliding-active-swipe-end .item-options-end .item-option-expandable{-ms-flex-order:-1;order:-1}[dir=rtl] .item-sliding-active-swipe-end .item-options-end .item-option-expandable{-ms-flex-order:-1;order:-1}@supports selector(:dir(rtl)){.item-sliding-active-swipe-end .item-options-end .item-option-expandable:dir(rtl){-ms-flex-order:-1;order:-1}}.item-sliding-active-swipe-start .item-options-start .item-option-expandable{padding-right:100%;-ms-flex-order:-1;order:-1;-webkit-transition-duration:0.6s;transition-duration:0.6s;-webkit-transition-property:padding-right;transition-property:padding-right}:host-context([dir=rtl]) .item-sliding-active-swipe-start .item-options-start .item-option-expandable{-ms-flex-order:1;order:1}[dir=rtl] .item-sliding-active-swipe-start .item-options-start .item-option-expandable{-ms-flex-order:1;order:1}@supports selector(:dir(rtl)){.item-sliding-active-swipe-start .item-options-start .item-option-expandable:dir(rtl){-ms-flex-order:1;order:1}}"}},[0,"ion-item-sliding",{disabled:[4],state:[32],getOpenAmount:[64],getSlidingRatio:[64],open:[64],close:[64],closeOpened:[64]},void 0,{disabled:[{disabledChanged:0}]}]),g=(t,i,s)=>!i&&s||t&&i,v=u,b=function(){"undefined"!=typeof customElements&&["ion-item-sliding"].forEach((t=>{"ion-item-sliding"===t&&(customElements.get(o(t))||customElements.define(o(t),u))}))};export{v as IonItemSliding,b as defineCustomElement}
|
|
@@ -193,10 +193,18 @@ const ItemSliding = class {
|
|
|
193
193
|
this.disabledChanged();
|
|
194
194
|
}
|
|
195
195
|
disconnectedCallback() {
|
|
196
|
+
var _a;
|
|
196
197
|
if (this.gesture) {
|
|
197
198
|
this.gesture.destroy();
|
|
198
199
|
this.gesture = undefined;
|
|
199
200
|
}
|
|
201
|
+
if (this.tmr !== undefined) {
|
|
202
|
+
clearTimeout(this.tmr);
|
|
203
|
+
this.tmr = undefined;
|
|
204
|
+
}
|
|
205
|
+
// Abort any in-progress animation. The abort handler rejects the pending
|
|
206
|
+
// promise, causing animateFullSwipe's finally block to run cleanup.
|
|
207
|
+
(_a = this.animationAbortController) === null || _a === void 0 ? void 0 : _a.abort();
|
|
200
208
|
this.item = null;
|
|
201
209
|
this.leftOptions = this.rightOptions = undefined;
|
|
202
210
|
if (openSlidingItem === this.el) {
|
|
@@ -230,6 +238,9 @@ const ItemSliding = class {
|
|
|
230
238
|
*/
|
|
231
239
|
async open(side) {
|
|
232
240
|
var _a;
|
|
241
|
+
if ((this.state & 128 /* SlidingState.AnimatingFullSwipe */) !== 0) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
233
244
|
/**
|
|
234
245
|
* It is possible for the item to be added to the DOM
|
|
235
246
|
* after the item-sliding component was created. As a result,
|
|
@@ -281,6 +292,9 @@ const ItemSliding = class {
|
|
|
281
292
|
* Close the sliding item. Items can also be closed from the [List](./list).
|
|
282
293
|
*/
|
|
283
294
|
async close() {
|
|
295
|
+
if ((this.state & 128 /* SlidingState.AnimatingFullSwipe */) !== 0) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
284
298
|
this.setOpenAmount(0, true);
|
|
285
299
|
}
|
|
286
300
|
/**
|
|
@@ -311,6 +325,111 @@ const ItemSliding = class {
|
|
|
311
325
|
return this.rightOptions;
|
|
312
326
|
}
|
|
313
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Check if the given item options element contains at least one expandable, non-disabled option.
|
|
330
|
+
*/
|
|
331
|
+
hasExpandableOptions(options) {
|
|
332
|
+
if (!options)
|
|
333
|
+
return false;
|
|
334
|
+
const optionElements = options.querySelectorAll('ion-item-option');
|
|
335
|
+
return Array.from(optionElements).some((option) => {
|
|
336
|
+
return option.expandable === true && !option.disabled;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Returns a Promise that resolves after `ms` milliseconds, or rejects if the
|
|
341
|
+
* given AbortSignal is fired before the timer expires.
|
|
342
|
+
*/
|
|
343
|
+
delay(ms, signal) {
|
|
344
|
+
return new Promise((resolve, reject) => {
|
|
345
|
+
const id = setTimeout(resolve, ms);
|
|
346
|
+
signal.addEventListener('abort', () => {
|
|
347
|
+
clearTimeout(id);
|
|
348
|
+
reject(new DOMException('Animation cancelled', 'AbortError'));
|
|
349
|
+
}, { once: true });
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Animate the item to a specific position using CSS transitions.
|
|
354
|
+
* Returns a Promise that resolves when the animation completes, or rejects if
|
|
355
|
+
* the given AbortSignal is fired.
|
|
356
|
+
*/
|
|
357
|
+
animateToPosition(position, duration, signal) {
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
if (!this.item) {
|
|
360
|
+
return resolve();
|
|
361
|
+
}
|
|
362
|
+
this.item.style.transition = `transform ${duration}ms ease-out`;
|
|
363
|
+
this.item.style.transform = `translate3d(${-position}px, 0, 0)`;
|
|
364
|
+
const id = setTimeout(resolve, duration);
|
|
365
|
+
signal.addEventListener('abort', () => {
|
|
366
|
+
clearTimeout(id);
|
|
367
|
+
reject(new DOMException('Animation cancelled', 'AbortError'));
|
|
368
|
+
}, { once: true });
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Calculate the swipe threshold distance required to trigger a full swipe animation.
|
|
373
|
+
* Returns the maximum options width plus a margin to ensure it's achievable.
|
|
374
|
+
*/
|
|
375
|
+
getSwipeThreshold(direction) {
|
|
376
|
+
const maxWidth = direction === 'end' ? this.optsWidthRightSide : this.optsWidthLeftSide;
|
|
377
|
+
return maxWidth + SWIPE_MARGIN;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Animate the item through a full swipe sequence: off-screen → trigger action → return.
|
|
381
|
+
* This is used when an expandable option is swiped beyond the threshold.
|
|
382
|
+
*/
|
|
383
|
+
async animateFullSwipe(direction) {
|
|
384
|
+
const abortController = new AbortController();
|
|
385
|
+
this.animationAbortController = abortController;
|
|
386
|
+
const { signal } = abortController;
|
|
387
|
+
// Prevent interruption during animation
|
|
388
|
+
if (this.gesture) {
|
|
389
|
+
this.gesture.enable(false);
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const options = direction === 'end' ? this.rightOptions : this.leftOptions;
|
|
393
|
+
// Trigger expandable state without moving the item
|
|
394
|
+
// Set state directly so expandable option fills its container, starting from
|
|
395
|
+
// the exact position where the user released, without any visual snap.
|
|
396
|
+
this.state =
|
|
397
|
+
direction === 'end'
|
|
398
|
+
? 8 /* SlidingState.End */ | 32 /* SlidingState.SwipeEnd */ | 128 /* SlidingState.AnimatingFullSwipe */
|
|
399
|
+
: 16 /* SlidingState.Start */ | 64 /* SlidingState.SwipeStart */ | 128 /* SlidingState.AnimatingFullSwipe */;
|
|
400
|
+
await this.delay(100, signal);
|
|
401
|
+
// Animate off-screen while maintaining the expanded state
|
|
402
|
+
const offScreenDistance = direction === 'end' ? window.innerWidth : -window.innerWidth;
|
|
403
|
+
await this.animateToPosition(offScreenDistance, 250, signal);
|
|
404
|
+
// Trigger action
|
|
405
|
+
if (options) {
|
|
406
|
+
options.fireSwipeEvent();
|
|
407
|
+
}
|
|
408
|
+
// Small delay before returning
|
|
409
|
+
await this.delay(300, signal);
|
|
410
|
+
// Return to closed state
|
|
411
|
+
await this.animateToPosition(0, 250, signal);
|
|
412
|
+
}
|
|
413
|
+
catch (_a) {
|
|
414
|
+
// Animation was aborted (e.g. component disconnected). finally handles cleanup.
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
this.animationAbortController = undefined;
|
|
418
|
+
// Reset state
|
|
419
|
+
if (this.item) {
|
|
420
|
+
this.item.style.transition = '';
|
|
421
|
+
this.item.style.transform = '';
|
|
422
|
+
}
|
|
423
|
+
this.openAmount = 0;
|
|
424
|
+
this.state = 2 /* SlidingState.Disabled */;
|
|
425
|
+
if (openSlidingItem === this.el) {
|
|
426
|
+
openSlidingItem = undefined;
|
|
427
|
+
}
|
|
428
|
+
if (this.gesture) {
|
|
429
|
+
this.gesture.enable(!this.disabled);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
314
433
|
async updateOptions() {
|
|
315
434
|
var _a;
|
|
316
435
|
const options = this.el.querySelectorAll('ion-item-options');
|
|
@@ -417,6 +536,23 @@ const ItemSliding = class {
|
|
|
417
536
|
if (contentEl) {
|
|
418
537
|
index$1.resetContentScrollY(contentEl, initialContentScrollY);
|
|
419
538
|
}
|
|
539
|
+
// Check for full swipe conditions with expandable options
|
|
540
|
+
const rawSwipeDistance = Math.abs(gesture.deltaX);
|
|
541
|
+
const direction = gesture.deltaX < 0 ? 'end' : 'start';
|
|
542
|
+
const options = direction === 'end' ? this.rightOptions : this.leftOptions;
|
|
543
|
+
const hasExpandable = this.hasExpandableOptions(options);
|
|
544
|
+
const shouldTriggerFullSwipe = hasExpandable &&
|
|
545
|
+
(rawSwipeDistance > this.getSwipeThreshold(direction) ||
|
|
546
|
+
(Math.abs(gesture.velocityX) > 0.5 &&
|
|
547
|
+
rawSwipeDistance > (direction === 'end' ? this.optsWidthRightSide : this.optsWidthLeftSide) * 0.5));
|
|
548
|
+
if (shouldTriggerFullSwipe) {
|
|
549
|
+
this.animateFullSwipe(direction).catch(() => {
|
|
550
|
+
if (this.gesture) {
|
|
551
|
+
this.gesture.enable(!this.disabled);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
420
556
|
const velocity = gesture.velocityX;
|
|
421
557
|
let restingPoint = this.openAmount > 0 ? this.optsWidthRightSide : -this.optsWidthLeftSide;
|
|
422
558
|
// Check if the drag didn't clear the buttons mid-point
|
|
@@ -524,7 +660,7 @@ const ItemSliding = class {
|
|
|
524
660
|
}
|
|
525
661
|
render() {
|
|
526
662
|
const theme = ionicGlobal.getIonTheme(this);
|
|
527
|
-
return (index.h(index.Host, { key: '
|
|
663
|
+
return (index.h(index.Host, { key: 'c945f30d9f7deb90d22064d4059e2b08f35614be', class: {
|
|
528
664
|
[theme]: true,
|
|
529
665
|
'item-sliding-active-slide': this.state !== 2 /* SlidingState.Disabled */,
|
|
530
666
|
'item-sliding-active-options-end': (this.state & 8 /* SlidingState.End */) !== 0,
|
|
@@ -64,10 +64,18 @@ export class ItemSliding {
|
|
|
64
64
|
this.disabledChanged();
|
|
65
65
|
}
|
|
66
66
|
disconnectedCallback() {
|
|
67
|
+
var _a;
|
|
67
68
|
if (this.gesture) {
|
|
68
69
|
this.gesture.destroy();
|
|
69
70
|
this.gesture = undefined;
|
|
70
71
|
}
|
|
72
|
+
if (this.tmr !== undefined) {
|
|
73
|
+
clearTimeout(this.tmr);
|
|
74
|
+
this.tmr = undefined;
|
|
75
|
+
}
|
|
76
|
+
// Abort any in-progress animation. The abort handler rejects the pending
|
|
77
|
+
// promise, causing animateFullSwipe's finally block to run cleanup.
|
|
78
|
+
(_a = this.animationAbortController) === null || _a === void 0 ? void 0 : _a.abort();
|
|
71
79
|
this.item = null;
|
|
72
80
|
this.leftOptions = this.rightOptions = undefined;
|
|
73
81
|
if (openSlidingItem === this.el) {
|
|
@@ -101,6 +109,9 @@ export class ItemSliding {
|
|
|
101
109
|
*/
|
|
102
110
|
async open(side) {
|
|
103
111
|
var _a;
|
|
112
|
+
if ((this.state & 128 /* SlidingState.AnimatingFullSwipe */) !== 0) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
104
115
|
/**
|
|
105
116
|
* It is possible for the item to be added to the DOM
|
|
106
117
|
* after the item-sliding component was created. As a result,
|
|
@@ -152,6 +163,9 @@ export class ItemSliding {
|
|
|
152
163
|
* Close the sliding item. Items can also be closed from the [List](./list).
|
|
153
164
|
*/
|
|
154
165
|
async close() {
|
|
166
|
+
if ((this.state & 128 /* SlidingState.AnimatingFullSwipe */) !== 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
155
169
|
this.setOpenAmount(0, true);
|
|
156
170
|
}
|
|
157
171
|
/**
|
|
@@ -182,6 +196,111 @@ export class ItemSliding {
|
|
|
182
196
|
return this.rightOptions;
|
|
183
197
|
}
|
|
184
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Check if the given item options element contains at least one expandable, non-disabled option.
|
|
201
|
+
*/
|
|
202
|
+
hasExpandableOptions(options) {
|
|
203
|
+
if (!options)
|
|
204
|
+
return false;
|
|
205
|
+
const optionElements = options.querySelectorAll('ion-item-option');
|
|
206
|
+
return Array.from(optionElements).some((option) => {
|
|
207
|
+
return option.expandable === true && !option.disabled;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Returns a Promise that resolves after `ms` milliseconds, or rejects if the
|
|
212
|
+
* given AbortSignal is fired before the timer expires.
|
|
213
|
+
*/
|
|
214
|
+
delay(ms, signal) {
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
const id = setTimeout(resolve, ms);
|
|
217
|
+
signal.addEventListener('abort', () => {
|
|
218
|
+
clearTimeout(id);
|
|
219
|
+
reject(new DOMException('Animation cancelled', 'AbortError'));
|
|
220
|
+
}, { once: true });
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Animate the item to a specific position using CSS transitions.
|
|
225
|
+
* Returns a Promise that resolves when the animation completes, or rejects if
|
|
226
|
+
* the given AbortSignal is fired.
|
|
227
|
+
*/
|
|
228
|
+
animateToPosition(position, duration, signal) {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
if (!this.item) {
|
|
231
|
+
return resolve();
|
|
232
|
+
}
|
|
233
|
+
this.item.style.transition = `transform ${duration}ms ease-out`;
|
|
234
|
+
this.item.style.transform = `translate3d(${-position}px, 0, 0)`;
|
|
235
|
+
const id = setTimeout(resolve, duration);
|
|
236
|
+
signal.addEventListener('abort', () => {
|
|
237
|
+
clearTimeout(id);
|
|
238
|
+
reject(new DOMException('Animation cancelled', 'AbortError'));
|
|
239
|
+
}, { once: true });
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Calculate the swipe threshold distance required to trigger a full swipe animation.
|
|
244
|
+
* Returns the maximum options width plus a margin to ensure it's achievable.
|
|
245
|
+
*/
|
|
246
|
+
getSwipeThreshold(direction) {
|
|
247
|
+
const maxWidth = direction === 'end' ? this.optsWidthRightSide : this.optsWidthLeftSide;
|
|
248
|
+
return maxWidth + SWIPE_MARGIN;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Animate the item through a full swipe sequence: off-screen → trigger action → return.
|
|
252
|
+
* This is used when an expandable option is swiped beyond the threshold.
|
|
253
|
+
*/
|
|
254
|
+
async animateFullSwipe(direction) {
|
|
255
|
+
const abortController = new AbortController();
|
|
256
|
+
this.animationAbortController = abortController;
|
|
257
|
+
const { signal } = abortController;
|
|
258
|
+
// Prevent interruption during animation
|
|
259
|
+
if (this.gesture) {
|
|
260
|
+
this.gesture.enable(false);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const options = direction === 'end' ? this.rightOptions : this.leftOptions;
|
|
264
|
+
// Trigger expandable state without moving the item
|
|
265
|
+
// Set state directly so expandable option fills its container, starting from
|
|
266
|
+
// the exact position where the user released, without any visual snap.
|
|
267
|
+
this.state =
|
|
268
|
+
direction === 'end'
|
|
269
|
+
? 8 /* SlidingState.End */ | 32 /* SlidingState.SwipeEnd */ | 128 /* SlidingState.AnimatingFullSwipe */
|
|
270
|
+
: 16 /* SlidingState.Start */ | 64 /* SlidingState.SwipeStart */ | 128 /* SlidingState.AnimatingFullSwipe */;
|
|
271
|
+
await this.delay(100, signal);
|
|
272
|
+
// Animate off-screen while maintaining the expanded state
|
|
273
|
+
const offScreenDistance = direction === 'end' ? window.innerWidth : -window.innerWidth;
|
|
274
|
+
await this.animateToPosition(offScreenDistance, 250, signal);
|
|
275
|
+
// Trigger action
|
|
276
|
+
if (options) {
|
|
277
|
+
options.fireSwipeEvent();
|
|
278
|
+
}
|
|
279
|
+
// Small delay before returning
|
|
280
|
+
await this.delay(300, signal);
|
|
281
|
+
// Return to closed state
|
|
282
|
+
await this.animateToPosition(0, 250, signal);
|
|
283
|
+
}
|
|
284
|
+
catch (_a) {
|
|
285
|
+
// Animation was aborted (e.g. component disconnected). finally handles cleanup.
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
this.animationAbortController = undefined;
|
|
289
|
+
// Reset state
|
|
290
|
+
if (this.item) {
|
|
291
|
+
this.item.style.transition = '';
|
|
292
|
+
this.item.style.transform = '';
|
|
293
|
+
}
|
|
294
|
+
this.openAmount = 0;
|
|
295
|
+
this.state = 2 /* SlidingState.Disabled */;
|
|
296
|
+
if (openSlidingItem === this.el) {
|
|
297
|
+
openSlidingItem = undefined;
|
|
298
|
+
}
|
|
299
|
+
if (this.gesture) {
|
|
300
|
+
this.gesture.enable(!this.disabled);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
185
304
|
async updateOptions() {
|
|
186
305
|
var _a;
|
|
187
306
|
const options = this.el.querySelectorAll('ion-item-options');
|
|
@@ -288,6 +407,23 @@ export class ItemSliding {
|
|
|
288
407
|
if (contentEl) {
|
|
289
408
|
resetContentScrollY(contentEl, initialContentScrollY);
|
|
290
409
|
}
|
|
410
|
+
// Check for full swipe conditions with expandable options
|
|
411
|
+
const rawSwipeDistance = Math.abs(gesture.deltaX);
|
|
412
|
+
const direction = gesture.deltaX < 0 ? 'end' : 'start';
|
|
413
|
+
const options = direction === 'end' ? this.rightOptions : this.leftOptions;
|
|
414
|
+
const hasExpandable = this.hasExpandableOptions(options);
|
|
415
|
+
const shouldTriggerFullSwipe = hasExpandable &&
|
|
416
|
+
(rawSwipeDistance > this.getSwipeThreshold(direction) ||
|
|
417
|
+
(Math.abs(gesture.velocityX) > 0.5 &&
|
|
418
|
+
rawSwipeDistance > (direction === 'end' ? this.optsWidthRightSide : this.optsWidthLeftSide) * 0.5));
|
|
419
|
+
if (shouldTriggerFullSwipe) {
|
|
420
|
+
this.animateFullSwipe(direction).catch(() => {
|
|
421
|
+
if (this.gesture) {
|
|
422
|
+
this.gesture.enable(!this.disabled);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
291
427
|
const velocity = gesture.velocityX;
|
|
292
428
|
let restingPoint = this.openAmount > 0 ? this.optsWidthRightSide : -this.optsWidthLeftSide;
|
|
293
429
|
// Check if the drag didn't clear the buttons mid-point
|
|
@@ -395,7 +531,7 @@ export class ItemSliding {
|
|
|
395
531
|
}
|
|
396
532
|
render() {
|
|
397
533
|
const theme = getIonTheme(this);
|
|
398
|
-
return (h(Host, { key: '
|
|
534
|
+
return (h(Host, { key: 'c945f30d9f7deb90d22064d4059e2b08f35614be', class: {
|
|
399
535
|
[theme]: true,
|
|
400
536
|
'item-sliding-active-slide': this.state !== 2 /* SlidingState.Disabled */,
|
|
401
537
|
'item-sliding-active-options-end': (this.state & 8 /* SlidingState.End */) !== 0,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* See https://playwright.dev/docs/api/class-mouse#mouse-move for more information.
|
|
10
10
|
*/
|
|
11
|
-
export const dragElementBy = async (el, page, dragByX = 0, dragByY = 0, startXCoord, startYCoord, releaseDrag = true) => {
|
|
11
|
+
export const dragElementBy = async (el, page, dragByX = 0, dragByY = 0, startXCoord, startYCoord, releaseDrag = true, steps) => {
|
|
12
12
|
const boundingBox = await el.boundingBox();
|
|
13
13
|
if (!boundingBox) {
|
|
14
14
|
throw new Error('Cannot get a bounding box for an element that is not visible. See https://playwright.dev/docs/api/class-locator#locator-bounding-box for more information');
|
|
@@ -19,7 +19,7 @@ export const dragElementBy = async (el, page, dragByX = 0, dragByY = 0, startXCo
|
|
|
19
19
|
await page.mouse.move(startX, startY);
|
|
20
20
|
await page.mouse.down();
|
|
21
21
|
// Drag the element.
|
|
22
|
-
await moveElement(page, startX, startY, dragByX, dragByY);
|
|
22
|
+
await moveElement(page, startX, startY, dragByX, dragByY, steps);
|
|
23
23
|
if (releaseDrag) {
|
|
24
24
|
await page.mouse.up();
|
|
25
25
|
}
|
|
@@ -71,8 +71,7 @@ const validateDragByY = (startY, dragByY, viewportHeight) => {
|
|
|
71
71
|
throw new Error(`The element is being dragged past the top of the viewport. Update the dragByY value to prevent going out of bounds. A recommended value is ${recommendedDragByY}.`);
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
|
-
const moveElement = async (page, startX, startY, dragByX = 0, dragByY = 0) => {
|
|
75
|
-
const steps = 10;
|
|
74
|
+
const moveElement = async (page, startX, startY, dragByX = 0, dragByY = 0, steps = 10) => {
|
|
76
75
|
const browser = page.context().browser().browserType().name();
|
|
77
76
|
const viewport = page.viewportSize();
|
|
78
77
|
if (viewport === null) {
|
package/dist/docs.json
CHANGED
|
@@ -191,10 +191,18 @@ const ItemSliding = class {
|
|
|
191
191
|
this.disabledChanged();
|
|
192
192
|
}
|
|
193
193
|
disconnectedCallback() {
|
|
194
|
+
var _a;
|
|
194
195
|
if (this.gesture) {
|
|
195
196
|
this.gesture.destroy();
|
|
196
197
|
this.gesture = undefined;
|
|
197
198
|
}
|
|
199
|
+
if (this.tmr !== undefined) {
|
|
200
|
+
clearTimeout(this.tmr);
|
|
201
|
+
this.tmr = undefined;
|
|
202
|
+
}
|
|
203
|
+
// Abort any in-progress animation. The abort handler rejects the pending
|
|
204
|
+
// promise, causing animateFullSwipe's finally block to run cleanup.
|
|
205
|
+
(_a = this.animationAbortController) === null || _a === void 0 ? void 0 : _a.abort();
|
|
198
206
|
this.item = null;
|
|
199
207
|
this.leftOptions = this.rightOptions = undefined;
|
|
200
208
|
if (openSlidingItem === this.el) {
|
|
@@ -228,6 +236,9 @@ const ItemSliding = class {
|
|
|
228
236
|
*/
|
|
229
237
|
async open(side) {
|
|
230
238
|
var _a;
|
|
239
|
+
if ((this.state & 128 /* SlidingState.AnimatingFullSwipe */) !== 0) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
231
242
|
/**
|
|
232
243
|
* It is possible for the item to be added to the DOM
|
|
233
244
|
* after the item-sliding component was created. As a result,
|
|
@@ -279,6 +290,9 @@ const ItemSliding = class {
|
|
|
279
290
|
* Close the sliding item. Items can also be closed from the [List](./list).
|
|
280
291
|
*/
|
|
281
292
|
async close() {
|
|
293
|
+
if ((this.state & 128 /* SlidingState.AnimatingFullSwipe */) !== 0) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
282
296
|
this.setOpenAmount(0, true);
|
|
283
297
|
}
|
|
284
298
|
/**
|
|
@@ -309,6 +323,111 @@ const ItemSliding = class {
|
|
|
309
323
|
return this.rightOptions;
|
|
310
324
|
}
|
|
311
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Check if the given item options element contains at least one expandable, non-disabled option.
|
|
328
|
+
*/
|
|
329
|
+
hasExpandableOptions(options) {
|
|
330
|
+
if (!options)
|
|
331
|
+
return false;
|
|
332
|
+
const optionElements = options.querySelectorAll('ion-item-option');
|
|
333
|
+
return Array.from(optionElements).some((option) => {
|
|
334
|
+
return option.expandable === true && !option.disabled;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Returns a Promise that resolves after `ms` milliseconds, or rejects if the
|
|
339
|
+
* given AbortSignal is fired before the timer expires.
|
|
340
|
+
*/
|
|
341
|
+
delay(ms, signal) {
|
|
342
|
+
return new Promise((resolve, reject) => {
|
|
343
|
+
const id = setTimeout(resolve, ms);
|
|
344
|
+
signal.addEventListener('abort', () => {
|
|
345
|
+
clearTimeout(id);
|
|
346
|
+
reject(new DOMException('Animation cancelled', 'AbortError'));
|
|
347
|
+
}, { once: true });
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Animate the item to a specific position using CSS transitions.
|
|
352
|
+
* Returns a Promise that resolves when the animation completes, or rejects if
|
|
353
|
+
* the given AbortSignal is fired.
|
|
354
|
+
*/
|
|
355
|
+
animateToPosition(position, duration, signal) {
|
|
356
|
+
return new Promise((resolve, reject) => {
|
|
357
|
+
if (!this.item) {
|
|
358
|
+
return resolve();
|
|
359
|
+
}
|
|
360
|
+
this.item.style.transition = `transform ${duration}ms ease-out`;
|
|
361
|
+
this.item.style.transform = `translate3d(${-position}px, 0, 0)`;
|
|
362
|
+
const id = setTimeout(resolve, duration);
|
|
363
|
+
signal.addEventListener('abort', () => {
|
|
364
|
+
clearTimeout(id);
|
|
365
|
+
reject(new DOMException('Animation cancelled', 'AbortError'));
|
|
366
|
+
}, { once: true });
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Calculate the swipe threshold distance required to trigger a full swipe animation.
|
|
371
|
+
* Returns the maximum options width plus a margin to ensure it's achievable.
|
|
372
|
+
*/
|
|
373
|
+
getSwipeThreshold(direction) {
|
|
374
|
+
const maxWidth = direction === 'end' ? this.optsWidthRightSide : this.optsWidthLeftSide;
|
|
375
|
+
return maxWidth + SWIPE_MARGIN;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Animate the item through a full swipe sequence: off-screen → trigger action → return.
|
|
379
|
+
* This is used when an expandable option is swiped beyond the threshold.
|
|
380
|
+
*/
|
|
381
|
+
async animateFullSwipe(direction) {
|
|
382
|
+
const abortController = new AbortController();
|
|
383
|
+
this.animationAbortController = abortController;
|
|
384
|
+
const { signal } = abortController;
|
|
385
|
+
// Prevent interruption during animation
|
|
386
|
+
if (this.gesture) {
|
|
387
|
+
this.gesture.enable(false);
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const options = direction === 'end' ? this.rightOptions : this.leftOptions;
|
|
391
|
+
// Trigger expandable state without moving the item
|
|
392
|
+
// Set state directly so expandable option fills its container, starting from
|
|
393
|
+
// the exact position where the user released, without any visual snap.
|
|
394
|
+
this.state =
|
|
395
|
+
direction === 'end'
|
|
396
|
+
? 8 /* SlidingState.End */ | 32 /* SlidingState.SwipeEnd */ | 128 /* SlidingState.AnimatingFullSwipe */
|
|
397
|
+
: 16 /* SlidingState.Start */ | 64 /* SlidingState.SwipeStart */ | 128 /* SlidingState.AnimatingFullSwipe */;
|
|
398
|
+
await this.delay(100, signal);
|
|
399
|
+
// Animate off-screen while maintaining the expanded state
|
|
400
|
+
const offScreenDistance = direction === 'end' ? window.innerWidth : -window.innerWidth;
|
|
401
|
+
await this.animateToPosition(offScreenDistance, 250, signal);
|
|
402
|
+
// Trigger action
|
|
403
|
+
if (options) {
|
|
404
|
+
options.fireSwipeEvent();
|
|
405
|
+
}
|
|
406
|
+
// Small delay before returning
|
|
407
|
+
await this.delay(300, signal);
|
|
408
|
+
// Return to closed state
|
|
409
|
+
await this.animateToPosition(0, 250, signal);
|
|
410
|
+
}
|
|
411
|
+
catch (_a) {
|
|
412
|
+
// Animation was aborted (e.g. component disconnected). finally handles cleanup.
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
this.animationAbortController = undefined;
|
|
416
|
+
// Reset state
|
|
417
|
+
if (this.item) {
|
|
418
|
+
this.item.style.transition = '';
|
|
419
|
+
this.item.style.transform = '';
|
|
420
|
+
}
|
|
421
|
+
this.openAmount = 0;
|
|
422
|
+
this.state = 2 /* SlidingState.Disabled */;
|
|
423
|
+
if (openSlidingItem === this.el) {
|
|
424
|
+
openSlidingItem = undefined;
|
|
425
|
+
}
|
|
426
|
+
if (this.gesture) {
|
|
427
|
+
this.gesture.enable(!this.disabled);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
312
431
|
async updateOptions() {
|
|
313
432
|
var _a;
|
|
314
433
|
const options = this.el.querySelectorAll('ion-item-options');
|
|
@@ -415,6 +534,23 @@ const ItemSliding = class {
|
|
|
415
534
|
if (contentEl) {
|
|
416
535
|
resetContentScrollY(contentEl, initialContentScrollY);
|
|
417
536
|
}
|
|
537
|
+
// Check for full swipe conditions with expandable options
|
|
538
|
+
const rawSwipeDistance = Math.abs(gesture.deltaX);
|
|
539
|
+
const direction = gesture.deltaX < 0 ? 'end' : 'start';
|
|
540
|
+
const options = direction === 'end' ? this.rightOptions : this.leftOptions;
|
|
541
|
+
const hasExpandable = this.hasExpandableOptions(options);
|
|
542
|
+
const shouldTriggerFullSwipe = hasExpandable &&
|
|
543
|
+
(rawSwipeDistance > this.getSwipeThreshold(direction) ||
|
|
544
|
+
(Math.abs(gesture.velocityX) > 0.5 &&
|
|
545
|
+
rawSwipeDistance > (direction === 'end' ? this.optsWidthRightSide : this.optsWidthLeftSide) * 0.5));
|
|
546
|
+
if (shouldTriggerFullSwipe) {
|
|
547
|
+
this.animateFullSwipe(direction).catch(() => {
|
|
548
|
+
if (this.gesture) {
|
|
549
|
+
this.gesture.enable(!this.disabled);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
418
554
|
const velocity = gesture.velocityX;
|
|
419
555
|
let restingPoint = this.openAmount > 0 ? this.optsWidthRightSide : -this.optsWidthLeftSide;
|
|
420
556
|
// Check if the drag didn't clear the buttons mid-point
|
|
@@ -522,7 +658,7 @@ const ItemSliding = class {
|
|
|
522
658
|
}
|
|
523
659
|
render() {
|
|
524
660
|
const theme = getIonTheme(this);
|
|
525
|
-
return (h(Host, { key: '
|
|
661
|
+
return (h(Host, { key: 'c945f30d9f7deb90d22064d4059e2b08f35614be', class: {
|
|
526
662
|
[theme]: true,
|
|
527
663
|
'item-sliding-active-slide': this.state !== 2 /* SlidingState.Disabled */,
|
|
528
664
|
'item-sliding-active-options-end': (this.state & 8 /* SlidingState.End */) !== 0,
|