UNPKG

nxt-sortablejs

Version:
239 lines (231 loc) 11.8 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, inject, ElementRef, ChangeDetectorRef, NgZone, Renderer2, input, output, computed, Directive } from '@angular/core'; import Sortable from 'sortablejs'; /** @internal */ const GLOBALS = new InjectionToken('Global config for sortablejs'); function provideGlobalSortableOptions(globalOptions) { return [ { provide: GLOBALS, useValue: globalOptions } ]; } /** @internal */ function isFormArray(target) { // we need this to identify that the target is a FormArray // we don't want to have a dependency on @angular/forms just for that // just checking for random FormArray methods not available on a standard array return !!target.at && !!target.insert && !!target.reset; } /** @internal */ class SortablejsBinding { constructor(target) { this.target = target; } insert(index, item) { if (isFormArray(this.target)) { this.target.insert(index, item); } else { this.target.splice(index, 0, item); } } get(index) { return isFormArray(this.target) ? this.target.at(index) : this.target[index]; } remove(index) { let item; if (isFormArray(this.target)) { item = this.target.at(index); this.target.removeAt(index); } else { item = this.target.splice(index, 1)[0]; } return item; } } /** @internal */ class SortablejsBindings { constructor(bindingTargets) { this.bindings = bindingTargets.map(target => new SortablejsBinding(target)); } injectIntoEvery(index, items) { this.bindings.forEach((b, i) => b.insert(index, items[i])); } getFromEvery(index) { return this.bindings.map(b => b.get(index)); } extractFromEvery(index) { return this.bindings.map(b => b.remove(index)); } get provided() { return !!this.bindings.length; } } /** @internal */ class SortablejsService { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SortablejsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SortablejsService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SortablejsService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class SortablejsDirective { constructor() { this.globalConfig = inject(GLOBALS, { optional: true }); this.service = inject(SortablejsService); this.element = inject(ElementRef); this.cdRef = inject(ChangeDetectorRef); this.zone = inject(NgZone); this.renderer = inject(Renderer2); /** Input data, can be Array or FormArray */ this.data = input(undefined, { ...(ngDevMode ? { debugName: "data" } : /* istanbul ignore next */ {}), alias: 'nxtSortablejs' }); /** * CSS selector for the sortable container * * Mostly required when used with custom components which wrap items in multiple containers. In that case, * this should be the selector for the direct HTML parent of sortable items. */ this.sortablejsContainer = input(/* @ts-ignore */ ...(ngDevMode ? [undefined, { debugName: "sortablejsContainer" }] : /* istanbul ignore next */ [])); /** Sortablejs configuration */ this.config = input(/* @ts-ignore */ ...(ngDevMode ? [undefined, { debugName: "config" }] : /* istanbul ignore next */ [])); /** A function invoked when cloning items from template dataset into target dataset */ this.cloneFunction = input(/* @ts-ignore */ ...(ngDevMode ? [undefined, { debugName: "cloneFunction" }] : /* istanbul ignore next */ [])); /** Initialised a new Sortablejs instance */ this.init = output(); this.bindings = computed(() => { const data = this.data(); if (!data) { return new SortablejsBindings([]); } else if (data instanceof SortablejsBindings) { return data; } else { return new SortablejsBindings([data]); } }, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "bindings" }] : /* istanbul ignore next */ [])); this.optionsWithoutEvents = computed(() => ({ ...(this.globalConfig || {}), ...(this.config() || {}) }), /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "optionsWithoutEvents" }] : /* istanbul ignore next */ [])); this.overridenOptions = computed(() => ({ // always intercept standard events but act only in case items are set (bindingEnabled) // allows to forget about tracking this.items changes onAdd: (event) => { this.service.transfer = (items) => { this.bindings().injectIntoEvery(event.newIndex, items); this.proxyEvent('onAdd', event); }; this.proxyEvent('onAddOriginal', event); this.cdRef.detectChanges(); }, onRemove: (event) => { const bindings = this.bindings(); if (bindings.provided) { if (this.isCloning) { this.service.transfer?.(bindings.getFromEvery(event.oldIndex).map(item => this.clone(item))); // great thanks to https://github.com/tauu // event.item is the original item from the source list which is moved to the target list // event.clone is a clone of the original item and will be added to source list // If bindings are provided, adding the item dom element to the target list causes artifacts // as it interferes with the rendering performed by the angular template. // Therefore we remove it immediately and also move the original item back to the source list. // (event handler may be attached to the original item and not its clone, therefore keeping // the original dom node, circumvents side effects ) this.renderer.removeChild(event.item.parentNode, event.item); this.renderer.insertBefore(event.clone.parentNode, event.item, event.clone); this.renderer.removeChild(event.clone.parentNode, event.clone); } else { this.service.transfer?.(bindings.extractFromEvery(event.oldIndex)); } this.service.transfer = undefined; } this.proxyEvent('onRemove', event); this.cdRef.detectChanges(); }, onUpdate: (event) => { const bindings = this.bindings(); bindings.injectIntoEvery(event.newIndex, bindings.extractFromEvery(event.oldIndex)); this.proxyEvent('onUpdate', event); this.cdRef.detectChanges(); } }), /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "overridenOptions" }] : /* istanbul ignore next */ [])); this.options = computed(() => ({ ...this.optionsWithoutEvents(), ...this.overridenOptions() }), /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ [])); } /** @internal */ ngAfterViewInit() { if (Sortable?.create) { // Sortable does not exist in angular universal (SSR) this.create(); } } /** @internal */ ngOnChanges(changes) { const optionsChange = changes.config; if (optionsChange && !optionsChange.isFirstChange()) { const previousOptions = optionsChange.previousValue; const currentOptions = optionsChange.currentValue; Object.keys(currentOptions).forEach(optionName => { if (currentOptions[optionName] !== previousOptions[optionName]) { // use low-level option setter this.sortableInstance?.option(optionName, this.options()[optionName]); } }); } } /** @internal */ ngOnDestroy() { if (this.sortableInstance) { this.sortableInstance.destroy(); } } create() { const sortablejsContainer = this.sortablejsContainer(); const container = sortablejsContainer ? this.element.nativeElement.querySelector(sortablejsContainer) : this.element.nativeElement; setTimeout(() => { if (typeof window != 'undefined') { this.sortableInstance = Sortable.create(container, this.options()); this.init.emit(this.sortableInstance); } }, 0); } proxyEvent(eventName, ...params) { this.zone.run(() => { const opts = this.optionsWithoutEvents(); if (opts?.[eventName]) { opts[eventName](...params); } }); } get isCloning() { const groupOptions = this.sortableInstance?.options.group; return groupOptions && (typeof groupOptions != 'string') ? groupOptions.checkPull?.(this.sortableInstance, this.sortableInstance, undefined, undefined) === 'clone' : groupOptions === 'clone'; } clone(item) { // by default pass the item through, no cloning performed return (this.cloneFunction() || (subitem => subitem))(item); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SortablejsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.6", type: SortablejsDirective, isStandalone: true, selector: "[nxtSortablejs]", inputs: { data: { classPropertyName: "data", publicName: "nxtSortablejs", isSignal: true, isRequired: false, transformFunction: null }, sortablejsContainer: { classPropertyName: "sortablejsContainer", publicName: "sortablejsContainer", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, cloneFunction: { classPropertyName: "cloneFunction", publicName: "cloneFunction", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { init: "init" }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SortablejsDirective, decorators: [{ type: Directive, args: [{ selector: '[nxtSortablejs]' }] }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "nxtSortablejs", required: false }] }], sortablejsContainer: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortablejsContainer", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], cloneFunction: [{ type: i0.Input, args: [{ isSignal: true, alias: "cloneFunction", required: false }] }], init: [{ type: i0.Output, args: ["init"] }] } }); /** * Generated bundle index. Do not edit. */ export { SortablejsDirective, provideGlobalSortableOptions }; //# sourceMappingURL=nxt-sortablejs.mjs.map