nxt-sortablejs
Version:
Angular sortablejs bindings.
239 lines (231 loc) • 11.8 kB
JavaScript
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