diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index 810079ef1c..228381e928 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -622,6 +622,7 @@ export class ReorderGroup { constructor(r: ElementRef) { const el = r.nativeElement; + proxyMethods(this, el, ['complete']); proxyInputs(this, el, ['disabled']); proxyOutputs(this, el, ['ionItemReorder']); } diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 309653be67..16d0656efd 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -26,6 +26,7 @@ import { InputChangeEvent, ItemHeightFn, ItemRenderFn, + ItemReorderDetail, LoadingOptions, MenuChangeEventDetail, MenuControllerI, @@ -39,6 +40,7 @@ import { PickerOptions, PopoverOptions, RangeValue, + RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, @@ -3515,7 +3517,7 @@ export namespace Components { /** * Emitted when the user lets go of the content and has pulled down further than the `pullMin` or pulls the content down and exceeds the pullMax. Updates the refresher state to `refreshing`. The `complete()` method should be called when the async operation has completed. */ - 'onIonRefresh'?: (event: CustomEvent) => void; + 'onIonRefresh'?: (event: CustomEvent) => void; /** * Emitted when the user begins to start pulling down. */ @@ -3535,6 +3537,7 @@ export namespace Components { } interface IonReorderGroup { + 'complete': (listOrReorder?: boolean | any[] | undefined) => Promise; /** * If true, the reorder will be hidden. Defaults to `true`. */ @@ -3545,7 +3548,10 @@ export namespace Components { * If true, the reorder will be hidden. Defaults to `true`. */ 'disabled'?: boolean; - 'onIonItemReorder'?: (event: CustomEvent) => void; + /** + * Event that needs to be listen to in order to respond to reorder action. `ion-reorder-group` uses this event to delegate to the user the reordering of data array. The complete() method exposed as + */ + 'onIonItemReorder'?: (event: CustomEvent) => void; } interface IonReorder {} diff --git a/core/src/components/refresher/refresher-interface.ts b/core/src/components/refresher/refresher-interface.ts new file mode 100644 index 0000000000..81e51b7681 --- /dev/null +++ b/core/src/components/refresher/refresher-interface.ts @@ -0,0 +1,4 @@ + +export interface RefresherEventDetail { + complete(): void; +} diff --git a/core/src/components/refresher/refresher.tsx b/core/src/components/refresher/refresher.tsx index 6568648582..05d69466f5 100644 --- a/core/src/components/refresher/refresher.tsx +++ b/core/src/components/refresher/refresher.tsx @@ -1,6 +1,6 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Prop, QueueApi, State, Watch } from '@stencil/core'; -import { Gesture, GestureDetail, Mode } from '../../interface'; +import { Gesture, GestureDetail, Mode, RefresherEventDetail } from '../../interface'; import { createThemedClasses } from '../../utils/theme'; @Component({ @@ -20,6 +20,8 @@ export class Refresher implements ComponentInterface { mode!: Mode; + @Element() el!: HTMLElement; + @Prop({ context: 'queue' }) queue!: QueueApi; /** @@ -34,8 +36,6 @@ export class Refresher implements ComponentInterface { */ @State() private state: RefresherState = RefresherState.Inactive; - @Element() el!: HTMLElement; - /** * The minimum distance the user must pull down until the * refresher will go into the `refreshing` state. Defaults to `60`. @@ -77,7 +77,7 @@ export class Refresher implements ComponentInterface { * Updates the refresher state to `refreshing`. The `complete()` method should be * called when the async operation has completed. */ - @Event() ionRefresh!: EventEmitter; + @Event() ionRefresh!: EventEmitter; /** * Emitted while the user is pulling down the content and exposing the refresher. @@ -307,7 +307,9 @@ export class Refresher implements ComponentInterface { // emit "refresh" because it was pulled down far enough // and they let go to begin refreshing - this.ionRefresh.emit(); + this.ionRefresh.emit({ + complete: this.complete.bind(this) + }); } private close(state: RefresherState, delay: string) { diff --git a/core/src/components/reorder-group/readme.md b/core/src/components/reorder-group/readme.md index d398ea1569..3a5364729c 100644 --- a/core/src/components/reorder-group/readme.md +++ b/core/src/components/reorder-group/readme.md @@ -1,6 +1,50 @@ # ion-reorder-group -The reorder group is a wrapper component for items with the `ion-reorder` component. For more information, see the [Reorder documentation](../reorder/). +The reorder group is a wrapper component for items with the `ion-reorder` component, Check [Reorder documentation](../reorder/) for further information about the anchor component that is used to drag items within the `ion-reorder-group` list. + +Once the user drags an item and drops it in a new position, the `ionItemReorder` event is dispatched. A handler for it must be implemented by the developer to commit changes. + +```js +reorderGroup.addEventListener('ionItemReorder', (ev) => { + console.log(`Moving item from ${ev.detail.from} to ${ev.detail.to}`); + + this.dataList = reorderArray(this.dataList, ev.detail.from, ev.detail.to); + ev.detail.complete(); +}); +``` + +The event's detail includes all the relevant information about the reorder operation, including the `from` and `to` indexes. The meaning of this indexes are pretty self-explanatory, the item **from** index X, moved **to** the index Y. + +For example, in this list we move the item at index `0` to the index `3`: + +``` +BEFORE | AFTER + 0 | 1 + 1 | 2 + 2 | 3 + 3 | 0 + 4 | 4 +``` + +``` +detail: { + from: 0 + to: 3 +} +``` + +Once the data structure has been updated to reflect the reorder change, the `complete()` method must be called. +This method finishes the reorder operation and resets all the CSS transforms applied by the `ion-reorder-group` component. + +Fortunately this `complete()` method can optionally accept an array as input and it will return a reordered copy, based in `detail.from` and `detail.to`. + +```ts +this.dataList = reorderGroup.complete(this.dataList); +``` + +This utility is really handy when + + @@ -14,9 +58,16 @@ The reorder group is a wrapper component for items with the `ion-reorder` compon ## Events -| Event | Description | -| ---------------- | ----------- | -| `ionItemReorder` | | +| Event | Description | +| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ionItemReorder` | Event that needs to be listen to in order to respond to reorder action. `ion-reorder-group` uses this event to delegate to the user the reordering of data array. The complete() method exposed as | + + +## Methods + +| Method | Description | +| ---------- | ----------- | +| `complete` | | ---------------------------------------------- diff --git a/core/src/components/reorder-group/reorder-group-interface.ts b/core/src/components/reorder-group/reorder-group-interface.ts new file mode 100644 index 0000000000..2720c9aab0 --- /dev/null +++ b/core/src/components/reorder-group/reorder-group-interface.ts @@ -0,0 +1,6 @@ + +export interface ItemReorderDetail { + from: number; + to: number; + complete: (data?: boolean | any[]) => any; +} diff --git a/core/src/components/reorder-group/reorder-group.tsx b/core/src/components/reorder-group/reorder-group.tsx index 57a96b2af1..276749d5ee 100644 --- a/core/src/components/reorder-group/reorder-group.tsx +++ b/core/src/components/reorder-group/reorder-group.tsx @@ -1,8 +1,14 @@ -import { Component, ComponentInterface, Element, Event, EventEmitter, Prop, QueueApi, State, Watch } from '@stencil/core'; +import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Prop, QueueApi, State, Watch } from '@stencil/core'; -import { Gesture, GestureDetail } from '../../interface'; +import { Gesture, GestureDetail, ItemReorderDetail } from '../../interface'; import { hapticSelectionChanged, hapticSelectionEnd, hapticSelectionStart } from '../../utils/haptic'; +const enum ReordeGroupState { + Idle = 0, + Active = 1, + Complete = 2 +} + @Component({ tag: 'ion-reorder-group', styleUrl: 'reorder-group.scss' @@ -23,7 +29,7 @@ export class ReorderGroup implements ComponentInterface { private containerTop = 0; private containerBottom = 0; - @State() activated = false; + @State() state = ReordeGroupState.Idle; @Element() el!: HTMLElement; @@ -43,7 +49,14 @@ export class ReorderGroup implements ComponentInterface { delete a.a; } - @Event() ionItemReorder!: EventEmitter; + /** + * Event that needs to be listen to in order to respond to reorder action. + * `ion-reorder-group` uses this event to delegate to the user the reordering of data array. + * + * + * The complete() method exposed as + */ + @Event({ bubbles: false }) ionItemReorder!: EventEmitter; async componentDidLoad() { const contentEl = this.el.closest('ion-content'); @@ -73,8 +86,13 @@ export class ReorderGroup implements ComponentInterface { this.onEnd(); } + @Method() + complete(listOrReorder?: boolean | any[]): Promise { + return Promise.resolve(this.completeSync(listOrReorder)); + } + private canStart(ev: GestureDetail): boolean { - if (this.selectedItemEl) { + if (this.selectedItemEl || this.state !== ReordeGroupState.Idle) { return false; } const target = ev.event.target as HTMLElement; @@ -111,7 +129,7 @@ export class ReorderGroup implements ComponentInterface { child.$ionIndex = i; } - const box = this.el.getBoundingClientRect(); + const box = el.getBoundingClientRect(); this.containerTop = box.top; this.containerBottom = box.bottom; @@ -128,7 +146,7 @@ export class ReorderGroup implements ComponentInterface { this.lastToIndex = indexForItem(item); this.selectedItemHeight = item.offsetHeight; - this.activated = true; + this.state = ReordeGroupState.Active; item.classList.add(ITEM_REORDER_SELECTED); @@ -163,48 +181,62 @@ export class ReorderGroup implements ComponentInterface { } private onEnd() { - this.activated = false; const selectedItem = this.selectedItemEl; + this.state = ReordeGroupState.Complete; if (!selectedItem) { + this.state = ReordeGroupState.Idle; return; } - const children = this.el.children as any; const toIndex = this.lastToIndex; const fromIndex = indexForItem(selectedItem); - const ref = (fromIndex < toIndex) - ? children[toIndex + 1] - : children[toIndex]; - - this.el.insertBefore(selectedItem, ref); - - const len = children.length; - for (let i = 0; i < len; i++) { - children[i].style['transform'] = ''; - } - - const reorderInactive = () => { - if (this.selectedItemEl) { - this.selectedItemEl.style.transition = ''; - this.selectedItemEl.classList.remove(ITEM_REORDER_SELECTED); - this.selectedItemEl = undefined; - } - }; if (toIndex === fromIndex) { selectedItem.style.transition = 'transform 200ms ease-in-out'; - setTimeout(reorderInactive, 200); + setTimeout(() => this.completeSync(), 200); } else { - reorderInactive(); this.ionItemReorder.emit({ from: fromIndex, - to: toIndex + to: toIndex, + complete: this.completeSync.bind(this) }); } hapticSelectionEnd(); } + private completeSync(listOrReorder?: boolean | any[]): any { + const selectedItemEl = this.selectedItemEl; + if (selectedItemEl && this.state === ReordeGroupState.Complete) { + const children = this.el.children as any; + const len = children.length; + const toIndex = this.lastToIndex; + const fromIndex = indexForItem(selectedItemEl); + + if (listOrReorder === true) { + const ref = (fromIndex < toIndex) + ? children[toIndex + 1] + : children[toIndex]; + + this.el.insertBefore(selectedItemEl, ref); + } + + if (Array.isArray(listOrReorder)) { + listOrReorder = reorderArray(listOrReorder, fromIndex, toIndex); + } + + for (let i = 0; i < len; i++) { + children[i].style['transform'] = ''; + } + + selectedItemEl.style.transition = ''; + selectedItemEl.classList.remove(ITEM_REORDER_SELECTED); + this.selectedItemEl = undefined; + this.state = ReordeGroupState.Idle; + } + return listOrReorder; + } + private itemIndexForTop(deltaY: number): number { const heights = this.cachedHeights; let i = 0; @@ -257,7 +289,7 @@ export class ReorderGroup implements ComponentInterface { return { class: { 'reorder-enabled': !this.disabled, - 'reorder-list-active': this.activated, + 'reorder-list-active': this.state !== ReordeGroupState.Idle, } }; } @@ -284,3 +316,10 @@ function findReorderItem(node: HTMLElement, container: HTMLElement): HTMLElement const AUTO_SCROLL_MARGIN = 60; const SCROLL_JUMP = 10; const ITEM_REORDER_SELECTED = 'reorder-selected'; + +function reorderArray(array: any[], from: number, to: number): any[] { + const element = array[from]; + array.splice(from, 1); + array.splice(to, 0, element); + return array.slice(); +} diff --git a/core/src/components/reorder/test/basic/e2e.ts b/core/src/components/reorder-group/test/basic/e2e.ts similarity index 100% rename from core/src/components/reorder/test/basic/e2e.ts rename to core/src/components/reorder-group/test/basic/e2e.ts diff --git a/core/src/components/reorder/test/basic/index.html b/core/src/components/reorder-group/test/basic/index.html similarity index 96% rename from core/src/components/reorder/test/basic/index.html rename to core/src/components/reorder-group/test/basic/index.html index 5279b891ea..7de69c11a0 100644 --- a/core/src/components/reorder/test/basic/index.html +++ b/core/src/components/reorder-group/test/basic/index.html @@ -114,6 +114,9 @@ function toggleEdit() { const reorderGroup = document.getElementById('reorder'); reorderGroup.disabled = !reorderGroup.disabled; + reorderGroup.addEventListener('ionItemReorder', ({detail}) => { + detail.complete(true); + }); } diff --git a/core/src/components/reorder/test/preview/index.html b/core/src/components/reorder-group/test/preview/index.html similarity index 100% rename from core/src/components/reorder/test/preview/index.html rename to core/src/components/reorder-group/test/preview/index.html diff --git a/core/src/components/reorder/test/standalone/e2e.ts b/core/src/components/reorder-group/test/standalone/e2e.ts similarity index 100% rename from core/src/components/reorder/test/standalone/e2e.ts rename to core/src/components/reorder-group/test/standalone/e2e.ts diff --git a/core/src/components/reorder/test/standalone/index.html b/core/src/components/reorder-group/test/standalone/index.html similarity index 100% rename from core/src/components/reorder/test/standalone/index.html rename to core/src/components/reorder-group/test/standalone/index.html diff --git a/core/src/components/reorder-group/usage/angular.md b/core/src/components/reorder-group/usage/angular.md new file mode 100644 index 0000000000..2b5304a755 --- /dev/null +++ b/core/src/components/reorder-group/usage/angular.md @@ -0,0 +1,49 @@ +```html + + + + + + + Item 1 + + + + + + + Item 2 (default ion-reorder slot="start") + + + + + + + Item 3 (custom ion-reorder) + + + + + + + + + Item 4 (custom ion-reorder slot="start") + + + + + + + + + + Item 5 (the whole item can be dragged) + + + + + + + +``` diff --git a/core/src/components/reorder-group/usage/javascript.md b/core/src/components/reorder-group/usage/javascript.md new file mode 100644 index 0000000000..7056fb390a --- /dev/null +++ b/core/src/components/reorder-group/usage/javascript.md @@ -0,0 +1,60 @@ +```html + + + + + + + Item 1 + + + + + + + Item 2 (default ion-reorder slot="start") + + + + + + + Item 3 (custom ion-reorder) + + + + + + + + + Item 4 (custom ion-reorder slot="start") + + + + + + + + + + Item 5 (the whole item can be dragged) + + + + + + + +``` + +```javascript +const reorderGroup = document.querySelector('ion-reorder-group'); +reorderGroup.addEventListener('ionItemReorder', ({detail}) => { + // finishing the reorder, true means ion-reorder-group with reorder the DOM + detail.complete(true); + + // or: + // reorderGroup.complete(true) +}); +``` \ No newline at end of file diff --git a/core/src/components/reorder/readme.md b/core/src/components/reorder/readme.md index bde4144e1d..edea0ce9f6 100644 --- a/core/src/components/reorder/readme.md +++ b/core/src/components/reorder/readme.md @@ -1,6 +1,19 @@ # ion-reorder -Reorder is a component that allows an item to be dragged to change its order. It can be used within an `ion-reorder-group` to provide a visual drag and drop interface. +Reorder is a component that allows an item to be dragged to change its order. It must be used within an `ion-reorder-group` to provide a visual drag and drop interface. + +`ion-reorder` is the anchor users will use to drag and drop items inside the `ion-reorder-group`. It must be added to `ion-item` in order for them to be draggable. + +```html + + + Item + + + +``` + +The position of the diff --git a/core/src/components/reorder/usage/angular.md b/core/src/components/reorder/usage/angular.md deleted file mode 100644 index be27939097..0000000000 --- a/core/src/components/reorder/usage/angular.md +++ /dev/null @@ -1,88 +0,0 @@ -```html - - - - - - - Item 1 - - - - - - - Item 2 - - - - - - - Item 3 (default ion-reorder slot="start") - - - - - - - Item 4 (default ion-reorder slot="start") - - - - - - - Item 5 (custom ion-reorder) - - - - - - - - - Item 6 (custom ion-reorder) - - - - - - - - - Item 7 (custom ion-reorder slot="start") - - - - - - - - - - Item 8 (the whole item can be dragged) - - - - - - - - Item 9 (the whole item can be dragged) - - - - - - - - Item 10 (the whole item can be dragged) - - - - - - - -``` diff --git a/core/src/components/reorder/usage/javascript.md b/core/src/components/reorder/usage/javascript.md deleted file mode 100644 index be27939097..0000000000 --- a/core/src/components/reorder/usage/javascript.md +++ /dev/null @@ -1,88 +0,0 @@ -```html - - - - - - - Item 1 - - - - - - - Item 2 - - - - - - - Item 3 (default ion-reorder slot="start") - - - - - - - Item 4 (default ion-reorder slot="start") - - - - - - - Item 5 (custom ion-reorder) - - - - - - - - - Item 6 (custom ion-reorder) - - - - - - - - - Item 7 (custom ion-reorder slot="start") - - - - - - - - - - Item 8 (the whole item can be dragged) - - - - - - - - Item 9 (the whole item can be dragged) - - - - - - - - Item 10 (the whole item can be dragged) - - - - - - - -``` diff --git a/core/src/interface.d.ts b/core/src/interface.d.ts index 1d2bfd2bfa..50a0df9ad7 100644 --- a/core/src/interface.d.ts +++ b/core/src/interface.d.ts @@ -12,6 +12,8 @@ export * from './components/popover/popover-interface'; export * from './components/nav/nav-interface'; export * from './components/router/utils/interface'; export * from './components/range/range-interface'; +export * from './components/refresher/refresher-interface'; +export * from './components/reorder-group/reorder-group-interface'; export * from './components/content/content-interface'; export * from './components/select/select-interface'; export * from './components/select-popover/select-popover-interface';