ion-item-sliding.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. /*!
  2. * (C) Ionic http://ionicframework.com - MIT License
  3. */
  4. import { proxyCustomElement, HTMLElement, createEvent, h, Host } from '@stencil/core/internal/client';
  5. import { a as findClosestIonContent, d as disableContentScrollY, r as resetContentScrollY } from './index8.js';
  6. import { m as isEndSide } from './helpers.js';
  7. import { a as printIonWarning } from './index4.js';
  8. import { w as watchForOptions } from './watch-options.js';
  9. import { b as getIonMode } from './ionic-global.js';
  10. const itemSlidingCss = "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}}";
  11. const IonItemSlidingStyle0 = itemSlidingCss;
  12. const SWIPE_MARGIN = 30;
  13. const ELASTIC_FACTOR = 0.55;
  14. let openSlidingItem;
  15. const ItemSliding = /*@__PURE__*/ proxyCustomElement(class ItemSliding extends HTMLElement {
  16. constructor() {
  17. super();
  18. this.__registerHost();
  19. this.ionDrag = createEvent(this, "ionDrag", 7);
  20. this.item = null;
  21. this.openAmount = 0;
  22. this.initialOpenAmount = 0;
  23. this.optsWidthRightSide = 0;
  24. this.optsWidthLeftSide = 0;
  25. this.sides = 0 /* ItemSide.None */;
  26. this.optsDirty = true;
  27. this.contentEl = null;
  28. this.initialContentScrollY = true;
  29. this.state = 2 /* SlidingState.Disabled */;
  30. this.disabled = false;
  31. }
  32. disabledChanged() {
  33. if (this.gesture) {
  34. this.gesture.enable(!this.disabled);
  35. }
  36. }
  37. async connectedCallback() {
  38. const { el } = this;
  39. this.item = el.querySelector('ion-item');
  40. this.contentEl = findClosestIonContent(el);
  41. /**
  42. * The MutationObserver needs to be added before we
  43. * call updateOptions below otherwise we may miss
  44. * ion-item-option elements that are added to the DOM
  45. * while updateOptions is running and before the MutationObserver
  46. * has been initialized.
  47. */
  48. this.mutationObserver = watchForOptions(el, 'ion-item-option', async () => {
  49. await this.updateOptions();
  50. });
  51. await this.updateOptions();
  52. this.gesture = (await import('./index3.js')).createGesture({
  53. el,
  54. gestureName: 'item-swipe',
  55. gesturePriority: 100,
  56. threshold: 5,
  57. canStart: (ev) => this.canStart(ev),
  58. onStart: () => this.onStart(),
  59. onMove: (ev) => this.onMove(ev),
  60. onEnd: (ev) => this.onEnd(ev),
  61. });
  62. this.disabledChanged();
  63. }
  64. disconnectedCallback() {
  65. if (this.gesture) {
  66. this.gesture.destroy();
  67. this.gesture = undefined;
  68. }
  69. this.item = null;
  70. this.leftOptions = this.rightOptions = undefined;
  71. if (openSlidingItem === this.el) {
  72. openSlidingItem = undefined;
  73. }
  74. if (this.mutationObserver) {
  75. this.mutationObserver.disconnect();
  76. this.mutationObserver = undefined;
  77. }
  78. }
  79. /**
  80. * Get the amount the item is open in pixels.
  81. */
  82. getOpenAmount() {
  83. return Promise.resolve(this.openAmount);
  84. }
  85. /**
  86. * Get the ratio of the open amount of the item compared to the width of the options.
  87. * If the number returned is positive, then the options on the right side are open.
  88. * If the number returned is negative, then the options on the left side are open.
  89. * If the absolute value of the number is greater than 1, the item is open more than
  90. * the width of the options.
  91. */
  92. getSlidingRatio() {
  93. return Promise.resolve(this.getSlidingRatioSync());
  94. }
  95. /**
  96. * Open the sliding item.
  97. *
  98. * @param side The side of the options to open. If a side is not provided, it will open the first set of options it finds within the item.
  99. */
  100. async open(side) {
  101. var _a;
  102. /**
  103. * It is possible for the item to be added to the DOM
  104. * after the item-sliding component was created. As a result,
  105. * if this.item is null, then we should attempt to
  106. * query for the ion-item again.
  107. * However, if the item is already defined then
  108. * we do not query for it again.
  109. */
  110. const item = (this.item = (_a = this.item) !== null && _a !== void 0 ? _a : this.el.querySelector('ion-item'));
  111. if (item === null) {
  112. return;
  113. }
  114. const optionsToOpen = this.getOptions(side);
  115. if (!optionsToOpen) {
  116. return;
  117. }
  118. /**
  119. * If side is not set, we need to infer the side
  120. * so we know which direction to move the options
  121. */
  122. if (side === undefined) {
  123. side = optionsToOpen === this.leftOptions ? 'start' : 'end';
  124. }
  125. // In RTL we want to switch the sides
  126. side = isEndSide(side) ? 'end' : 'start';
  127. const isStartOpen = this.openAmount < 0;
  128. const isEndOpen = this.openAmount > 0;
  129. /**
  130. * If a side is open and a user tries to
  131. * re-open the same side, we should not do anything
  132. */
  133. if (isStartOpen && optionsToOpen === this.leftOptions) {
  134. return;
  135. }
  136. if (isEndOpen && optionsToOpen === this.rightOptions) {
  137. return;
  138. }
  139. this.closeOpened();
  140. this.state = 4 /* SlidingState.Enabled */;
  141. requestAnimationFrame(() => {
  142. this.calculateOptsWidth();
  143. const width = side === 'end' ? this.optsWidthRightSide : -this.optsWidthLeftSide;
  144. openSlidingItem = this.el;
  145. this.setOpenAmount(width, false);
  146. this.state = side === 'end' ? 8 /* SlidingState.End */ : 16 /* SlidingState.Start */;
  147. });
  148. }
  149. /**
  150. * Close the sliding item. Items can also be closed from the [List](./list).
  151. */
  152. async close() {
  153. this.setOpenAmount(0, true);
  154. }
  155. /**
  156. * Close all of the sliding items in the list. Items can also be closed from the [List](./list).
  157. */
  158. async closeOpened() {
  159. if (openSlidingItem !== undefined) {
  160. openSlidingItem.close();
  161. openSlidingItem = undefined;
  162. return true;
  163. }
  164. return false;
  165. }
  166. /**
  167. * Given an optional side, return the ion-item-options element.
  168. *
  169. * @param side This side of the options to get. If a side is not provided it will
  170. * return the first one available.
  171. */
  172. getOptions(side) {
  173. if (side === undefined) {
  174. return this.leftOptions || this.rightOptions;
  175. }
  176. else if (side === 'start') {
  177. return this.leftOptions;
  178. }
  179. else {
  180. return this.rightOptions;
  181. }
  182. }
  183. async updateOptions() {
  184. const options = this.el.querySelectorAll('ion-item-options');
  185. let sides = 0;
  186. // Reset left and right options in case they were removed
  187. this.leftOptions = this.rightOptions = undefined;
  188. for (let i = 0; i < options.length; i++) {
  189. const item = options.item(i);
  190. /**
  191. * We cannot use the componentOnReady helper
  192. * util here since we need to wait for all of these items
  193. * to be ready before we set `this.sides` and `this.optsDirty`.
  194. */
  195. // eslint-disable-next-line custom-rules/no-component-on-ready-method
  196. const option = item.componentOnReady !== undefined ? await item.componentOnReady() : item;
  197. const side = isEndSide(option.side) ? 'end' : 'start';
  198. if (side === 'start') {
  199. this.leftOptions = option;
  200. sides |= 1 /* ItemSide.Start */;
  201. }
  202. else {
  203. this.rightOptions = option;
  204. sides |= 2 /* ItemSide.End */;
  205. }
  206. }
  207. this.optsDirty = true;
  208. this.sides = sides;
  209. }
  210. canStart(gesture) {
  211. /**
  212. * If very close to start of the screen
  213. * do not open left side so swipe to go
  214. * back will still work.
  215. */
  216. const rtl = document.dir === 'rtl';
  217. const atEdge = rtl ? window.innerWidth - gesture.startX < 15 : gesture.startX < 15;
  218. if (atEdge) {
  219. return false;
  220. }
  221. const selected = openSlidingItem;
  222. if (selected && selected !== this.el) {
  223. this.closeOpened();
  224. }
  225. return !!(this.rightOptions || this.leftOptions);
  226. }
  227. onStart() {
  228. /**
  229. * We need to query for the ion-item
  230. * every time the gesture starts. Developers
  231. * may toggle ion-item elements via *ngIf.
  232. */
  233. this.item = this.el.querySelector('ion-item');
  234. const { contentEl } = this;
  235. if (contentEl) {
  236. this.initialContentScrollY = disableContentScrollY(contentEl);
  237. }
  238. openSlidingItem = this.el;
  239. if (this.tmr !== undefined) {
  240. clearTimeout(this.tmr);
  241. this.tmr = undefined;
  242. }
  243. if (this.openAmount === 0) {
  244. this.optsDirty = true;
  245. this.state = 4 /* SlidingState.Enabled */;
  246. }
  247. this.initialOpenAmount = this.openAmount;
  248. if (this.item) {
  249. this.item.style.transition = 'none';
  250. }
  251. }
  252. onMove(gesture) {
  253. if (this.optsDirty) {
  254. this.calculateOptsWidth();
  255. }
  256. let openAmount = this.initialOpenAmount - gesture.deltaX;
  257. switch (this.sides) {
  258. case 2 /* ItemSide.End */:
  259. openAmount = Math.max(0, openAmount);
  260. break;
  261. case 1 /* ItemSide.Start */:
  262. openAmount = Math.min(0, openAmount);
  263. break;
  264. case 3 /* ItemSide.Both */:
  265. break;
  266. case 0 /* ItemSide.None */:
  267. return;
  268. default:
  269. printIonWarning('[ion-item-sliding] - invalid ItemSideFlags value', this.sides);
  270. break;
  271. }
  272. let optsWidth;
  273. if (openAmount > this.optsWidthRightSide) {
  274. optsWidth = this.optsWidthRightSide;
  275. openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR;
  276. }
  277. else if (openAmount < -this.optsWidthLeftSide) {
  278. optsWidth = -this.optsWidthLeftSide;
  279. openAmount = optsWidth + (openAmount - optsWidth) * ELASTIC_FACTOR;
  280. }
  281. this.setOpenAmount(openAmount, false);
  282. }
  283. onEnd(gesture) {
  284. const { contentEl, initialContentScrollY } = this;
  285. if (contentEl) {
  286. resetContentScrollY(contentEl, initialContentScrollY);
  287. }
  288. const velocity = gesture.velocityX;
  289. let restingPoint = this.openAmount > 0 ? this.optsWidthRightSide : -this.optsWidthLeftSide;
  290. // Check if the drag didn't clear the buttons mid-point
  291. // and we aren't moving fast enough to swipe open
  292. const isResetDirection = this.openAmount > 0 === !(velocity < 0);
  293. const isMovingFast = Math.abs(velocity) > 0.3;
  294. const isOnCloseZone = Math.abs(this.openAmount) < Math.abs(restingPoint / 2);
  295. if (swipeShouldReset(isResetDirection, isMovingFast, isOnCloseZone)) {
  296. restingPoint = 0;
  297. }
  298. const state = this.state;
  299. this.setOpenAmount(restingPoint, true);
  300. if ((state & 32 /* SlidingState.SwipeEnd */) !== 0 && this.rightOptions) {
  301. this.rightOptions.fireSwipeEvent();
  302. }
  303. else if ((state & 64 /* SlidingState.SwipeStart */) !== 0 && this.leftOptions) {
  304. this.leftOptions.fireSwipeEvent();
  305. }
  306. }
  307. calculateOptsWidth() {
  308. this.optsWidthRightSide = 0;
  309. if (this.rightOptions) {
  310. this.rightOptions.style.display = 'flex';
  311. this.optsWidthRightSide = this.rightOptions.offsetWidth;
  312. this.rightOptions.style.display = '';
  313. }
  314. this.optsWidthLeftSide = 0;
  315. if (this.leftOptions) {
  316. this.leftOptions.style.display = 'flex';
  317. this.optsWidthLeftSide = this.leftOptions.offsetWidth;
  318. this.leftOptions.style.display = '';
  319. }
  320. this.optsDirty = false;
  321. }
  322. setOpenAmount(openAmount, isFinal) {
  323. if (this.tmr !== undefined) {
  324. clearTimeout(this.tmr);
  325. this.tmr = undefined;
  326. }
  327. if (!this.item) {
  328. return;
  329. }
  330. const { el } = this;
  331. const style = this.item.style;
  332. this.openAmount = openAmount;
  333. if (isFinal) {
  334. style.transition = '';
  335. }
  336. if (openAmount > 0) {
  337. this.state =
  338. openAmount >= this.optsWidthRightSide + SWIPE_MARGIN
  339. ? 8 /* SlidingState.End */ | 32 /* SlidingState.SwipeEnd */
  340. : 8 /* SlidingState.End */;
  341. }
  342. else if (openAmount < 0) {
  343. this.state =
  344. openAmount <= -this.optsWidthLeftSide - SWIPE_MARGIN
  345. ? 16 /* SlidingState.Start */ | 64 /* SlidingState.SwipeStart */
  346. : 16 /* SlidingState.Start */;
  347. }
  348. else {
  349. /**
  350. * The sliding options should not be
  351. * clickable while the item is closing.
  352. */
  353. el.classList.add('item-sliding-closing');
  354. /**
  355. * Item sliding cannot be interrupted
  356. * while closing the item. If it did,
  357. * it would allow the item to get into an
  358. * inconsistent state where multiple
  359. * items are then open at the same time.
  360. */
  361. if (this.gesture) {
  362. this.gesture.enable(false);
  363. }
  364. this.tmr = setTimeout(() => {
  365. this.state = 2 /* SlidingState.Disabled */;
  366. this.tmr = undefined;
  367. if (this.gesture) {
  368. this.gesture.enable(!this.disabled);
  369. }
  370. el.classList.remove('item-sliding-closing');
  371. }, 600);
  372. openSlidingItem = undefined;
  373. style.transform = '';
  374. return;
  375. }
  376. style.transform = `translate3d(${-openAmount}px,0,0)`;
  377. this.ionDrag.emit({
  378. amount: openAmount,
  379. ratio: this.getSlidingRatioSync(),
  380. });
  381. }
  382. getSlidingRatioSync() {
  383. if (this.openAmount > 0) {
  384. return this.openAmount / this.optsWidthRightSide;
  385. }
  386. else if (this.openAmount < 0) {
  387. return this.openAmount / this.optsWidthLeftSide;
  388. }
  389. else {
  390. return 0;
  391. }
  392. }
  393. render() {
  394. const mode = getIonMode(this);
  395. return (h(Host, { key: 'f8aea4bb9802b111ef358cc6c172a635637ae1f8', class: {
  396. [mode]: true,
  397. 'item-sliding-active-slide': this.state !== 2 /* SlidingState.Disabled */,
  398. 'item-sliding-active-options-end': (this.state & 8 /* SlidingState.End */) !== 0,
  399. 'item-sliding-active-options-start': (this.state & 16 /* SlidingState.Start */) !== 0,
  400. 'item-sliding-active-swipe-end': (this.state & 32 /* SlidingState.SwipeEnd */) !== 0,
  401. 'item-sliding-active-swipe-start': (this.state & 64 /* SlidingState.SwipeStart */) !== 0,
  402. } }));
  403. }
  404. get el() { return this; }
  405. static get watchers() { return {
  406. "disabled": ["disabledChanged"]
  407. }; }
  408. static get style() { return IonItemSlidingStyle0; }
  409. }, [0, "ion-item-sliding", {
  410. "disabled": [4],
  411. "state": [32],
  412. "getOpenAmount": [64],
  413. "getSlidingRatio": [64],
  414. "open": [64],
  415. "close": [64],
  416. "closeOpened": [64]
  417. }, undefined, {
  418. "disabled": ["disabledChanged"]
  419. }]);
  420. const swipeShouldReset = (isResetDirection, isMovingFast, isOnResetZone) => {
  421. // The logic required to know when the sliding item should close (openAmount=0)
  422. // depends on three booleans (isResetDirection, isMovingFast, isOnResetZone)
  423. // and it ended up being too complicated to be written manually without errors
  424. // so the truth table is attached below: (0=false, 1=true)
  425. // isResetDirection | isMovingFast | isOnResetZone || shouldClose
  426. // 0 | 0 | 0 || 0
  427. // 0 | 0 | 1 || 1
  428. // 0 | 1 | 0 || 0
  429. // 0 | 1 | 1 || 0
  430. // 1 | 0 | 0 || 0
  431. // 1 | 0 | 1 || 1
  432. // 1 | 1 | 0 || 1
  433. // 1 | 1 | 1 || 1
  434. // The resulting expression was generated by resolving the K-map (Karnaugh map):
  435. return (!isMovingFast && isOnResetZone) || (isResetDirection && isMovingFast);
  436. };
  437. function defineCustomElement$1() {
  438. if (typeof customElements === "undefined") {
  439. return;
  440. }
  441. const components = ["ion-item-sliding"];
  442. components.forEach(tagName => { switch (tagName) {
  443. case "ion-item-sliding":
  444. if (!customElements.get(tagName)) {
  445. customElements.define(tagName, ItemSliding);
  446. }
  447. break;
  448. } });
  449. }
  450. const IonItemSliding = ItemSliding;
  451. const defineCustomElement = defineCustomElement$1;
  452. export { IonItemSliding, defineCustomElement };