ngx-drag-to-select
Version:
A lightweight, fast, configurable and reactive drag-to-select component for Angular 8 and beyond
933 lines (918 loc) • 41.2 kB
JavaScript
import * as i3 from '@angular/common';
import { isPlatformBrowser, CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { PLATFORM_ID, Injectable, Inject, InjectionToken, Directive, HostBinding, Input, EventEmitter, Component, ViewChild, ContentChildren, Output, NgModule } from '@angular/core';
import { fromEvent, merge, BehaviorSubject, Subject, combineLatest, from, asyncScheduler } from 'rxjs';
import { map, withLatestFrom, filter, distinctUntilChanged, share, tap, switchMap, takeUntil, mapTo, auditTime, first, observeOn, startWith, concatMapTo } from 'rxjs/operators';
const DEFAULT_CONFIG = {
selectedClass: 'selected',
shortcuts: {
moveRangeStart: 'shift+r',
disableSelection: 'alt',
toggleSingleItem: 'meta',
addToSelection: 'shift',
removeFromSelection: 'shift+meta',
},
};
const AUDIT_TIME = 16;
const MIN_WIDTH = 5;
const MIN_HEIGHT = 5;
const NO_SELECT_CLASS = 'dts-no-select';
const isObject = (item) => {
return item && typeof item === 'object' && !Array.isArray(item) && item !== null;
};
function mergeDeep(target, source) {
if (isObject(target) && isObject(source)) {
Object.keys(source).forEach((key) => {
if (isObject(source[key])) {
if (!target[key]) {
Object.assign(target, { [key]: {} });
}
mergeDeep(target[key], source[key]);
}
else {
Object.assign(target, { [key]: source[key] });
}
});
}
return target;
}
const hasMinimumSize = (selectBox, minWidth = MIN_WIDTH, minHeight = MIN_HEIGHT) => {
return selectBox.width > minWidth || selectBox.height > minHeight;
};
const clearSelection = (window) => {
const selection = window.getSelection();
if (!selection) {
return;
}
if (selection.removeAllRanges) {
selection.removeAllRanges();
}
else if (selection.empty) {
selection.empty();
}
};
const inBoundingBox = (point, box) => {
return (box.left <= point.x && point.x <= box.left + box.width && box.top <= point.y && point.y <= box.top + box.height);
};
const boxIntersects = (boxA, boxB) => {
return (boxA.left <= boxB.left + boxB.width &&
boxA.left + boxA.width >= boxB.left &&
boxA.top <= boxB.top + boxB.height &&
boxA.top + boxA.height >= boxB.top);
};
const calculateBoundingClientRect = (element) => {
return element.getBoundingClientRect();
};
const getMousePosition = (event) => {
return {
x: event.clientX,
y: event.clientY,
};
};
const getScroll = () => {
if (!document || !document.documentElement) {
return {
x: 0,
y: 0,
};
}
return {
x: document.documentElement.scrollLeft || document.body.scrollLeft,
y: document.documentElement.scrollTop || document.body.scrollTop,
};
};
const getRelativeMousePosition = (event, container) => {
const { x: clientX, y: clientY } = getMousePosition(event);
const scroll = getScroll();
const borderSize = (container.boundingClientRect.width - container.clientWidth) / 2;
const offsetLeft = container.boundingClientRect.left + scroll.x;
const offsetTop = container.boundingClientRect.top + scroll.y;
return {
x: clientX - borderSize - (offsetLeft - window.pageXOffset) + container.scrollLeft,
y: clientY - borderSize - (offsetTop - window.pageYOffset) + container.scrollTop,
};
};
const cursorWithinElement = (event, element) => {
const mousePoint = getMousePosition(event);
return inBoundingBox(mousePoint, calculateBoundingClientRect(element));
};
const createSelectBox = (container) => {
return (source) => {
return source.pipe(map(([event, opacity, { x, y }]) => {
// Type annotation is required here, because `getRelativeMousePosition` returns a `MousePosition`,
// the TS compiler cannot figure out the shape of this type.
const mousePosition = getRelativeMousePosition(event, container);
const width = opacity > 0 ? mousePosition.x - x : 0;
const height = opacity > 0 ? mousePosition.y - y : 0;
return {
top: height < 0 ? mousePosition.y : y,
left: width < 0 ? mousePosition.x : x,
width: Math.abs(width),
height: Math.abs(height),
opacity,
};
}));
};
};
const whenSelectBoxVisible = (selectBox$) => (source) => source.pipe(withLatestFrom(selectBox$), filter(([, selectBox]) => hasMinimumSize(selectBox, 0, 0)), map(([event, _]) => event));
const distinctKeyEvents = () => (source) => source.pipe(distinctUntilChanged((prev, curr) => {
return prev && curr && prev.code === curr.code;
}));
class KeyboardEventsService {
constructor(platformId) {
this.platformId = platformId;
if (isPlatformBrowser(this.platformId)) {
this._initializeKeyboardStreams();
}
}
_initializeKeyboardStreams() {
this.keydown$ = fromEvent(window, 'keydown').pipe(share());
this.keyup$ = fromEvent(window, 'keyup').pipe(share());
// distinctKeyEvents is used to prevent multiple key events to be fired repeatedly
// on Windows when a key is being pressed
this.distinctKeydown$ = this.keydown$.pipe(distinctKeyEvents(), share());
this.distinctKeyup$ = this.keyup$.pipe(distinctKeyEvents(), share());
this.mouseup$ = fromEvent(window, 'mouseup').pipe(share());
this.mousemove$ = fromEvent(window, 'mousemove').pipe(share());
}
}
KeyboardEventsService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: KeyboardEventsService, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable });
KeyboardEventsService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: KeyboardEventsService });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: KeyboardEventsService, decorators: [{
type: Injectable
}], ctorParameters: function () {
return [{ type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }];
} });
const CONFIG = new InjectionToken('DRAG_TO_SELECT_CONFIG');
const USER_CONFIG = new InjectionToken('USER_CONFIG');
const SELECT_ITEM_INSTANCE = Symbol();
class SelectItemDirective {
constructor(config, platformId, host, renderer) {
this.config = config;
this.platformId = platformId;
this.host = host;
this.renderer = renderer;
this.selected = false;
this.rangeStart = false;
this.hostClass = true;
this.dtsDisabled = false;
}
get value() {
return this.dtsSelectItem ? this.dtsSelectItem : this;
}
ngOnInit() {
this.nativeElememnt[SELECT_ITEM_INSTANCE] = this;
}
ngDoCheck() {
this.applySelectedClass();
}
toggleRangeStart() {
this.rangeStart = !this.rangeStart;
}
get nativeElememnt() {
return this.host.nativeElement;
}
getBoundingClientRect() {
if (isPlatformBrowser(this.platformId) && !this._boundingClientRect) {
this.calculateBoundingClientRect();
}
return this._boundingClientRect;
}
calculateBoundingClientRect() {
const boundingBox = calculateBoundingClientRect(this.host.nativeElement);
this._boundingClientRect = boundingBox;
return boundingBox;
}
_select() {
this.selected = true;
}
_deselect() {
this.selected = false;
}
applySelectedClass() {
if (this.selected) {
this.renderer.addClass(this.host.nativeElement, this.config.selectedClass);
}
else {
this.renderer.removeClass(this.host.nativeElement, this.config.selectedClass);
}
}
}
SelectItemDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: SelectItemDirective, deps: [{ token: CONFIG }, { token: PLATFORM_ID }, { token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
SelectItemDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "13.0.3", type: SelectItemDirective, selector: "[dtsSelectItem]", inputs: { dtsSelectItem: "dtsSelectItem", dtsDisabled: "dtsDisabled" }, host: { properties: { "class.dts-range-start": "this.rangeStart", "class.dts-select-item": "this.hostClass", "class.dts-disabled": "this.dtsDisabled" } }, exportAs: ["dtsSelectItem"], ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: SelectItemDirective, decorators: [{
type: Directive,
args: [{
selector: '[dtsSelectItem]',
exportAs: 'dtsSelectItem',
}]
}], ctorParameters: function () {
return [{ type: undefined, decorators: [{
type: Inject,
args: [CONFIG]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }, { type: i0.ElementRef }, { type: i0.Renderer2 }];
}, propDecorators: { rangeStart: [{
type: HostBinding,
args: ['class.dts-range-start']
}], hostClass: [{
type: HostBinding,
args: ['class.dts-select-item']
}], dtsSelectItem: [{
type: Input
}], dtsDisabled: [{
type: Input
}, {
type: HostBinding,
args: ['class.dts-disabled']
}] } });
var UpdateActions;
(function (UpdateActions) {
UpdateActions[UpdateActions["Add"] = 0] = "Add";
UpdateActions[UpdateActions["Remove"] = 1] = "Remove";
})(UpdateActions || (UpdateActions = {}));
var Action;
(function (Action) {
Action[Action["Add"] = 0] = "Add";
Action[Action["Delete"] = 1] = "Delete";
Action[Action["None"] = 2] = "None";
})(Action || (Action = {}));
const SUPPORTED_META_KEYS = {
alt: true,
shift: true,
meta: true,
ctrl: true,
};
const SUPPORTED_KEYS = /[a-z]/;
const META_KEY = 'meta';
const KEY_ALIASES = {
[META_KEY]: ['ctrl', 'meta'],
};
const SUPPORTED_SHORTCUTS = {
moveRangeStart: true,
disableSelection: true,
toggleSingleItem: true,
addToSelection: true,
removeFromSelection: true,
};
const ERROR_PREFIX = '[ShortcutService]';
class ShortcutService {
constructor(platformId, config, keyboardEvents) {
this.platformId = platformId;
this.keyboardEvents = keyboardEvents;
this._shortcuts = {};
this._latestShortcut = new Map();
this._shortcuts = this._createShortcutsFromConfig(config.shortcuts);
if (isPlatformBrowser(this.platformId)) {
const keydown$ = this.keyboardEvents.keydown$.pipe(map((event) => ({ code: event.code, pressed: true })));
const keyup$ = this.keyboardEvents.keyup$.pipe(map((event) => ({ code: event.code, pressed: false })));
merge(keydown$, keyup$)
.pipe(distinctUntilChanged((prev, curr) => {
return prev.pressed === curr.pressed && prev.code === curr.code;
}))
.subscribe((keyState) => {
if (keyState.pressed) {
this._latestShortcut.set(keyState.code, true);
}
else {
this._latestShortcut.delete(keyState.code);
}
});
}
}
disableSelection(event) {
return this._isShortcutPressed('disableSelection', event);
}
moveRangeStart(event) {
return this._isShortcutPressed('moveRangeStart', event);
}
toggleSingleItem(event) {
return this._isShortcutPressed('toggleSingleItem', event);
}
addToSelection(event) {
return this._isShortcutPressed('addToSelection', event);
}
removeFromSelection(event) {
return this._isShortcutPressed('removeFromSelection', event);
}
extendedSelectionShortcut(event) {
return this.addToSelection(event) || this.removeFromSelection(event);
}
_createShortcutsFromConfig(shortcuts) {
const shortcutMap = {};
for (const [key, shortcutsForCommand] of Object.entries(shortcuts)) {
if (!this._isSupportedShortcut(key)) {
throw new Error(this._getErrorMessage(`Shortcut ${key} not supported`));
}
shortcutsForCommand
.replace(/ /g, '')
.split(',')
.forEach((shortcut) => {
if (!shortcutMap[key]) {
shortcutMap[key] = [];
}
const combo = shortcut.split('+');
const cleanCombos = this._substituteKey(shortcut, combo, META_KEY);
cleanCombos.forEach((cleanCombo) => {
const unsupportedKey = this._isSupportedCombo(cleanCombo);
if (unsupportedKey) {
throw new Error(this._getErrorMessage(`Key '${unsupportedKey}' in shortcut ${shortcut} not supported`));
}
shortcutMap[key].push(cleanCombo.map((comboKey) => {
return SUPPORTED_META_KEYS[comboKey] ? `${comboKey}Key` : `Key${comboKey.toUpperCase()}`;
}));
});
});
}
return shortcutMap;
}
_substituteKey(shortcut, combo, substituteKey) {
const hasSpecialKey = shortcut.includes(substituteKey);
const substitutedShortcut = [];
if (hasSpecialKey) {
const cleanShortcut = combo.filter((element) => element !== META_KEY);
KEY_ALIASES.meta.forEach((alias) => {
substitutedShortcut.push([...cleanShortcut, alias]);
});
}
else {
substitutedShortcut.push(combo);
}
return substitutedShortcut;
}
_getErrorMessage(message) {
return `${ERROR_PREFIX} ${message}`;
}
_isShortcutPressed(shortcutName, event) {
const shortcuts = this._shortcuts[shortcutName];
return shortcuts.some((shortcut) => {
return shortcut.every((key) => this._isKeyPressed(event, key));
});
}
_isKeyPressed(event, key) {
return key.startsWith('Key') ? this._latestShortcut.has(key) : event[key];
}
_isSupportedCombo(combo) {
let unsupportedKey = null;
combo.forEach((key) => {
if (!SUPPORTED_META_KEYS[key] && (!SUPPORTED_KEYS.test(key) || this._isSingleChar(key))) {
unsupportedKey = key;
return;
}
});
return unsupportedKey;
}
_isSingleChar(key) {
return key.length > 1;
}
_isSupportedShortcut(shortcut) {
return SUPPORTED_SHORTCUTS[shortcut];
}
}
ShortcutService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: ShortcutService, deps: [{ token: PLATFORM_ID }, { token: CONFIG }, { token: KeyboardEventsService }], target: i0.ɵɵFactoryTarget.Injectable });
ShortcutService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: ShortcutService });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: ShortcutService, decorators: [{
type: Injectable
}], ctorParameters: function () {
return [{ type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [CONFIG]
}] }, { type: KeyboardEventsService }];
} });
class SelectContainerComponent {
constructor(platformId, shortcuts, keyboardEvents, hostElementRef, renderer, ngZone) {
this.platformId = platformId;
this.shortcuts = shortcuts;
this.keyboardEvents = keyboardEvents;
this.hostElementRef = hostElementRef;
this.renderer = renderer;
this.ngZone = ngZone;
this.selectOnDrag = true;
this.disabled = false;
this.disableDrag = false;
this.selectOnClick = true;
this.dragOverItems = true;
this.disableRangeSelection = false;
this.selectMode = false;
this.selectWithShortcut = false;
this.custom = false;
this.hostClass = true;
this.selectedItemsChange = new EventEmitter();
this.select = new EventEmitter();
this.itemSelected = new EventEmitter();
this.itemDeselected = new EventEmitter();
this.selectionStarted = new EventEmitter();
this.selectionEnded = new EventEmitter();
this._tmpItems = new Map();
this._selectedItems$ = new BehaviorSubject([]);
this._selectableItems = [];
this.updateItems$ = new Subject();
this.destroy$ = new Subject();
this._lastRange = [-1, -1];
this._lastStartIndex = undefined;
this._newRangeStart = false;
this._lastRangeSelection = new Map();
}
ngAfterViewInit() {
if (isPlatformBrowser(this.platformId)) {
this.host = this.hostElementRef.nativeElement;
this._initSelectedItemsChange();
this._calculateBoundingClientRect();
this._observeBoundingRectChanges();
this._observeSelectableItems();
const mouseup$ = this.keyboardEvents.mouseup$.pipe(filter(() => !this.disabled), tap(() => this._onMouseUp()), share());
const mousemove$ = this.keyboardEvents.mousemove$.pipe(filter(() => !this.disabled), share());
const mousedown$ = fromEvent(this.host, 'mousedown').pipe(filter((event) => event.button === 0), // only emit left mouse
filter(() => !this.disabled), filter((event) => this.selectOnClick || event.target === this.host), tap((event) => this._onMouseDown(event)), share());
const dragging$ = mousedown$.pipe(filter((event) => !this.shortcuts.disableSelection(event)), filter(() => !this.selectMode), filter(() => !this.disableDrag), filter((event) => this.dragOverItems || event.target === this.host), switchMap(() => mousemove$.pipe(takeUntil(mouseup$))), share());
const currentMousePosition$ = mousedown$.pipe(map((event) => getRelativeMousePosition(event, this.host)));
const show$ = dragging$.pipe(mapTo(1));
const hide$ = mouseup$.pipe(mapTo(0));
const opacity$ = merge(show$, hide$).pipe(distinctUntilChanged());
const selectBox$ = combineLatest([dragging$, opacity$, currentMousePosition$]).pipe(createSelectBox(this.host), share());
this.selectBoxClasses$ = merge(dragging$, mouseup$, this.keyboardEvents.distinctKeydown$, this.keyboardEvents.distinctKeyup$).pipe(auditTime(AUDIT_TIME), withLatestFrom(selectBox$), map(([event, selectBox]) => {
return {
'dts-adding': hasMinimumSize(selectBox, 0, 0) && !this.shortcuts.removeFromSelection(event),
'dts-removing': this.shortcuts.removeFromSelection(event),
};
}), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)));
const selectOnMouseUp$ = dragging$.pipe(filter(() => !this.selectOnDrag), filter(() => !this.selectMode), filter((event) => this._cursorWithinHost(event)), switchMap((_) => mouseup$.pipe(first())), filter((event) => (!this.shortcuts.disableSelection(event) && !this.shortcuts.toggleSingleItem(event)) ||
this.shortcuts.removeFromSelection(event)));
const selectOnDrag$ = selectBox$.pipe(auditTime(AUDIT_TIME), withLatestFrom(mousemove$, (selectBox, event) => ({
selectBox,
event,
})), filter(() => this.selectOnDrag), filter(({ selectBox }) => hasMinimumSize(selectBox)), map(({ event }) => event));
const selectOnKeyboardEvent$ = merge(this.keyboardEvents.distinctKeydown$, this.keyboardEvents.distinctKeyup$).pipe(auditTime(AUDIT_TIME), whenSelectBoxVisible(selectBox$), tap((event) => {
if (this._isExtendedSelection(event)) {
this._tmpItems.clear();
}
else {
this._flushItems();
}
}));
merge(selectOnMouseUp$, selectOnDrag$, selectOnKeyboardEvent$)
.pipe(takeUntil(this.destroy$))
.subscribe((event) => this._selectItems(event));
this.selectBoxStyles$ = selectBox$.pipe(map((selectBox) => ({
top: `${selectBox.top}px`,
left: `${selectBox.left}px`,
width: `${selectBox.width}px`,
height: `${selectBox.height}px`,
opacity: selectBox.opacity,
})));
this._initSelectionOutputs(mousedown$, mouseup$);
}
}
ngAfterContentInit() {
this._selectableItems = this.$selectableItems.toArray();
}
selectAll() {
this.$selectableItems.forEach((item) => {
this._selectItem(item);
});
}
toggleItems(predicate) {
this._filterSelectableItems(predicate).subscribe((item) => this._toggleItem(item));
}
selectItems(predicate) {
this._filterSelectableItems(predicate).subscribe((item) => this._selectItem(item));
}
deselectItems(predicate) {
this._filterSelectableItems(predicate).subscribe((item) => this._deselectItem(item));
}
clearSelection() {
this.$selectableItems.forEach((item) => {
this._deselectItem(item);
});
}
update() {
this._calculateBoundingClientRect();
this.$selectableItems.forEach((item) => item.calculateBoundingClientRect());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
_filterSelectableItems(predicate) {
// Wrap select items in an observable for better efficiency as
// no intermediate arrays are created and we only need to process
// every item once.
return from(this._selectableItems).pipe(filter((item) => predicate(item.value)));
}
_initSelectedItemsChange() {
this._selectedItems$.pipe(auditTime(AUDIT_TIME), takeUntil(this.destroy$)).subscribe({
next: (selectedItems) => {
this.selectedItemsChange.emit(selectedItems);
this.select.emit(selectedItems);
},
complete: () => {
this.selectedItemsChange.emit([]);
},
});
}
_observeSelectableItems() {
// Listen for updates and either select or deselect an item
this.updateItems$
.pipe(withLatestFrom(this._selectedItems$), takeUntil(this.destroy$), filter(([update]) => !update.item.dtsDisabled))
.subscribe(([update, selectedItems]) => {
const item = update.item;
switch (update.type) {
case UpdateActions.Add:
if (this._addItem(item, selectedItems)) {
item._select();
}
break;
case UpdateActions.Remove:
if (this._removeItem(item, selectedItems)) {
item._deselect();
}
break;
}
});
// Update the container as well as all selectable items if the list has changed
this.$selectableItems.changes
.pipe(withLatestFrom(this._selectedItems$), observeOn(asyncScheduler), takeUntil(this.destroy$))
.subscribe(([items, selectedItems]) => {
const newList = items.toArray();
this._selectableItems = newList;
const newValues = newList.map((item) => item.value);
const removedItems = selectedItems.filter((item) => !newValues.includes(item));
if (removedItems.length) {
removedItems.forEach((item) => this._removeItem(item, selectedItems));
}
this.update();
});
}
_observeBoundingRectChanges() {
this.ngZone.runOutsideAngular(() => {
const resize$ = fromEvent(window, 'resize');
const windowScroll$ = fromEvent(window, 'scroll');
const containerScroll$ = fromEvent(this.host, 'scroll');
merge(resize$, windowScroll$, containerScroll$)
.pipe(startWith('INITIAL_UPDATE'), auditTime(AUDIT_TIME), takeUntil(this.destroy$))
.subscribe(() => {
this.update();
});
});
}
_initSelectionOutputs(mousedown$, mouseup$) {
mousedown$
.pipe(filter((event) => this._cursorWithinHost(event)), tap(() => this.selectionStarted.emit()), concatMapTo(mouseup$.pipe(first())), withLatestFrom(this._selectedItems$), map(([, items]) => items), takeUntil(this.destroy$))
.subscribe((items) => {
this.selectionEnded.emit(items);
});
}
_calculateBoundingClientRect() {
this.host.boundingClientRect = calculateBoundingClientRect(this.host);
}
_cursorWithinHost(event) {
return cursorWithinElement(event, this.host);
}
_onMouseUp() {
this._flushItems();
this.renderer.removeClass(document.body, NO_SELECT_CLASS);
}
_onMouseDown(event) {
if (this.shortcuts.disableSelection(event) || this.disabled) {
return;
}
clearSelection(window);
if (!this.disableDrag) {
this.renderer.addClass(document.body, NO_SELECT_CLASS);
}
if (this.shortcuts.removeFromSelection(event)) {
return;
}
const mousePoint = getMousePosition(event);
const [currentIndex, clickedItem] = this._getClosestSelectItem(event);
let [startIndex, endIndex] = this._lastRange;
const isMoveRangeStart = this.shortcuts.moveRangeStart(event);
const shouldResetRangeSelection = !this.shortcuts.extendedSelectionShortcut(event) || isMoveRangeStart || this.disableRangeSelection;
if (shouldResetRangeSelection) {
this._resetRangeStart();
}
// move range start
if (shouldResetRangeSelection && !this.disableRangeSelection) {
if (currentIndex > -1) {
this._newRangeStart = true;
this._lastStartIndex = currentIndex;
clickedItem.toggleRangeStart();
this._lastRangeSelection.clear();
}
else {
this._lastStartIndex = -1;
}
}
if (currentIndex > -1) {
startIndex = Math.min(this._lastStartIndex, currentIndex);
endIndex = Math.max(this._lastStartIndex, currentIndex);
this._lastRange = [startIndex, endIndex];
}
if (isMoveRangeStart) {
return;
}
this.$selectableItems.forEach((item, index) => {
const itemRect = item.getBoundingClientRect();
const withinBoundingBox = inBoundingBox(mousePoint, itemRect);
if (this.shortcuts.extendedSelectionShortcut(event) && this.disableRangeSelection) {
return;
}
const withinRange = this.shortcuts.extendedSelectionShortcut(event) &&
startIndex > -1 &&
endIndex > -1 &&
index >= startIndex &&
index <= endIndex &&
startIndex !== endIndex;
const shouldAdd = (withinBoundingBox &&
!this.shortcuts.toggleSingleItem(event) &&
!this.selectMode &&
!this.selectWithShortcut) ||
(this.shortcuts.extendedSelectionShortcut(event) && item.selected && !this._lastRangeSelection.get(item)) ||
withinRange ||
(withinBoundingBox && this.shortcuts.toggleSingleItem(event) && !item.selected) ||
(!withinBoundingBox && this.shortcuts.toggleSingleItem(event) && item.selected) ||
(withinBoundingBox && !item.selected && this.selectMode) ||
(!withinBoundingBox && item.selected && this.selectMode);
const shouldRemove = (!withinBoundingBox &&
!this.shortcuts.toggleSingleItem(event) &&
!this.selectMode &&
!this.shortcuts.extendedSelectionShortcut(event) &&
!this.selectWithShortcut) ||
(this.shortcuts.extendedSelectionShortcut(event) && currentIndex > -1) ||
(!withinBoundingBox && this.shortcuts.toggleSingleItem(event) && !item.selected) ||
(withinBoundingBox && this.shortcuts.toggleSingleItem(event) && item.selected) ||
(!withinBoundingBox && !item.selected && this.selectMode) ||
(withinBoundingBox && item.selected && this.selectMode);
if (shouldAdd) {
this._selectItem(item);
}
else if (shouldRemove) {
this._deselectItem(item);
}
if (withinRange && !this._lastRangeSelection.get(item)) {
this._lastRangeSelection.set(item, true);
}
else if (!withinRange && !this._newRangeStart && !item.selected) {
this._lastRangeSelection.delete(item);
}
});
// if we don't toggle a single item, we set `newRangeStart` to `false`
// meaning that we are building up a range
if (!this.shortcuts.toggleSingleItem(event)) {
this._newRangeStart = false;
}
}
_selectItems(event) {
const selectionBox = calculateBoundingClientRect(this.$selectBox.nativeElement);
this.$selectableItems.forEach((item, index) => {
if (this._isExtendedSelection(event)) {
this._extendedSelectionMode(selectionBox, item, event);
}
else {
this._normalSelectionMode(selectionBox, item, event);
if (this._lastStartIndex < 0 && item.selected) {
item.toggleRangeStart();
this._lastStartIndex = index;
}
}
});
}
_isExtendedSelection(event) {
return this.shortcuts.extendedSelectionShortcut(event) && this.selectOnDrag;
}
_normalSelectionMode(selectBox, item, event) {
const inSelection = boxIntersects(selectBox, item.getBoundingClientRect());
const shouldAdd = inSelection && !item.selected && !this.shortcuts.removeFromSelection(event);
const shouldRemove = (!inSelection && item.selected && !this.shortcuts.addToSelection(event)) ||
(inSelection && item.selected && this.shortcuts.removeFromSelection(event));
if (shouldAdd) {
this._selectItem(item);
}
else if (shouldRemove) {
this._deselectItem(item);
}
}
_extendedSelectionMode(selectBox, item, event) {
const inSelection = boxIntersects(selectBox, item.getBoundingClientRect());
const shoudlAdd = (inSelection && !item.selected && !this.shortcuts.removeFromSelection(event) && !this._tmpItems.has(item)) ||
(inSelection && item.selected && this.shortcuts.removeFromSelection(event) && !this._tmpItems.has(item));
const shouldRemove = (!inSelection && item.selected && this.shortcuts.addToSelection(event) && this._tmpItems.has(item)) ||
(!inSelection && !item.selected && this.shortcuts.removeFromSelection(event) && this._tmpItems.has(item));
if (shoudlAdd) {
if (item.selected) {
item._deselect();
}
else {
item._select();
}
const action = this.shortcuts.removeFromSelection(event)
? Action.Delete
: this.shortcuts.addToSelection(event)
? Action.Add
: Action.None;
this._tmpItems.set(item, action);
}
else if (shouldRemove) {
if (this.shortcuts.removeFromSelection(event)) {
item._select();
}
else {
item._deselect();
}
this._tmpItems.delete(item);
}
}
_flushItems() {
this._tmpItems.forEach((action, item) => {
if (action === Action.Add) {
this._selectItem(item);
}
if (action === Action.Delete) {
this._deselectItem(item);
}
});
this._tmpItems.clear();
}
_addItem(item, selectedItems) {
let success = false;
if (!this._hasItem(item, selectedItems)) {
success = true;
selectedItems.push(item.value);
this._selectedItems$.next(selectedItems);
this.itemSelected.emit(item.value);
}
return success;
}
_removeItem(item, selectedItems) {
let success = false;
const value = item instanceof SelectItemDirective ? item.value : item;
const index = selectedItems.indexOf(value);
if (index > -1) {
success = true;
selectedItems.splice(index, 1);
this._selectedItems$.next(selectedItems);
this.itemDeselected.emit(value);
}
return success;
}
_toggleItem(item) {
if (item.selected) {
this._deselectItem(item);
}
else {
this._selectItem(item);
}
}
_selectItem(item) {
this.updateItems$.next({ type: UpdateActions.Add, item });
}
_deselectItem(item) {
this.updateItems$.next({ type: UpdateActions.Remove, item });
}
_hasItem(item, selectedItems) {
return selectedItems.includes(item.value);
}
_getClosestSelectItem(event) {
const target = event.target.closest('.dts-select-item');
let index = -1;
let targetItem = null;
if (target) {
targetItem = target[SELECT_ITEM_INSTANCE];
index = this._selectableItems.indexOf(targetItem);
}
return [index, targetItem];
}
_resetRangeStart() {
this._lastRange = [-1, -1];
const lastRangeStart = this._getLastRangeSelection();
if (lastRangeStart && lastRangeStart.rangeStart) {
lastRangeStart.toggleRangeStart();
}
}
_getLastRangeSelection() {
if (this._lastStartIndex >= 0) {
return this._selectableItems[this._lastStartIndex];
}
return null;
}
}
SelectContainerComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: SelectContainerComponent, deps: [{ token: PLATFORM_ID }, { token: ShortcutService }, { token: KeyboardEventsService }, { token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
SelectContainerComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.0.3", type: SelectContainerComponent, selector: "dts-select-container", inputs: { selectedItems: "selectedItems", selectOnDrag: "selectOnDrag", disabled: "disabled", disableDrag: "disableDrag", selectOnClick: "selectOnClick", dragOverItems: "dragOverItems", disableRangeSelection: "disableRangeSelection", selectMode: "selectMode", selectWithShortcut: "selectWithShortcut", custom: "custom" }, outputs: { selectedItemsChange: "selectedItemsChange", select: "select", itemSelected: "itemSelected", itemDeselected: "itemDeselected", selectionStarted: "selectionStarted", selectionEnded: "selectionEnded" }, host: { properties: { "class.dts-custom": "this.custom", "class.dts-select-container": "this.hostClass" } }, queries: [{ propertyName: "$selectableItems", predicate: SelectItemDirective, descendants: true }], viewQueries: [{ propertyName: "$selectBox", first: true, predicate: ["selectBox"], descendants: true, static: true }], exportAs: ["dts-select-container"], ngImport: i0, template: `
<ng-content></ng-content>
<div
class="dts-select-box"
#selectBox
[ngClass]="selectBoxClasses$ | async"
[ngStyle]="selectBoxStyles$ | async"
></div>
`, isInline: true, styles: [":host{display:block;position:relative}\n"], directives: [{ type: i3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], pipes: { "async": i3.AsyncPipe } });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: SelectContainerComponent, decorators: [{
type: Component,
args: [{ selector: 'dts-select-container', exportAs: 'dts-select-container', template: `
<ng-content></ng-content>
<div
class="dts-select-box"
#selectBox
[ngClass]="selectBoxClasses$ | async"
[ngStyle]="selectBoxStyles$ | async"
></div>
`, styles: [":host{display:block;position:relative}\n"] }]
}], ctorParameters: function () {
return [{ type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }, { type: ShortcutService }, { type: KeyboardEventsService }, { type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }];
}, propDecorators: { $selectBox: [{
type: ViewChild,
args: ['selectBox', { static: true }]
}], $selectableItems: [{
type: ContentChildren,
args: [SelectItemDirective, { descendants: true }]
}], selectedItems: [{
type: Input
}], selectOnDrag: [{
type: Input
}], disabled: [{
type: Input
}], disableDrag: [{
type: Input
}], selectOnClick: [{
type: Input
}], dragOverItems: [{
type: Input
}], disableRangeSelection: [{
type: Input
}], selectMode: [{
type: Input
}], selectWithShortcut: [{
type: Input
}], custom: [{
type: Input
}, {
type: HostBinding,
args: ['class.dts-custom']
}], hostClass: [{
type: HostBinding,
args: ['class.dts-select-container']
}], selectedItemsChange: [{
type: Output
}], select: [{
type: Output
}], itemSelected: [{
type: Output
}], itemDeselected: [{
type: Output
}], selectionStarted: [{
type: Output
}], selectionEnded: [{
type: Output
}] } });
const COMPONENTS = [SelectContainerComponent, SelectItemDirective];
function configFactory(config) {
return mergeDeep(DEFAULT_CONFIG, config);
}
class DragToSelectModule {
static forRoot(config = {}) {
return {
ngModule: DragToSelectModule,
providers: [
ShortcutService,
KeyboardEventsService,
{ provide: USER_CONFIG, useValue: config },
{
provide: CONFIG,
useFactory: configFactory,
deps: [USER_CONFIG],
},
],
};
}
}
DragToSelectModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: DragToSelectModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
DragToSelectModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: DragToSelectModule, declarations: [SelectContainerComponent, SelectItemDirective], imports: [CommonModule], exports: [SelectContainerComponent, SelectItemDirective] });
DragToSelectModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: DragToSelectModule, imports: [[CommonModule]] });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.0.3", ngImport: i0, type: DragToSelectModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule],
declarations: [...COMPONENTS],
exports: [...COMPONENTS],
}]
}] });
/*
* Public API Surface of ngx-drag-to-select
*/
/**
* Generated bundle index. Do not edit.
*/
export { DragToSelectModule, SELECT_ITEM_INSTANCE, SelectContainerComponent, SelectItemDirective };
//# sourceMappingURL=ngx-drag-to-select.mjs.map