Skip to content

Commit

Permalink
feat(lib): allow shortcuts to contain alphabetic characters
Browse files Browse the repository at this point in the history
  • Loading branch information
d3lm committed Oct 25, 2019
1 parent d7f7c4b commit f41c934
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 63 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,16 @@ import { DragToSelectModule } from 'ngx-drag-to-select';
DragToSelectModule.forRoot({
selectedClass: 'my-selected-item',
shortcuts: {
disableSelection: 'alt+meta'
disableSelection: 'alt+meta,d'
}
})
]
})
export class AppModule { }
```

This will override the `disableSelection` with **two** possible shortcuts, either `alt + meta` **or** just `d`. If you want to learn more about shortcut alternatives, check [this](shortcutAlternatives) section.

**Note**: If you override one of the shortcuts you have to make sure they do not interfear with one another to ensure a smooth selecting experience.

#### Modifiers
Expand All @@ -214,9 +216,11 @@ When overriding the default shortcuts you can use the following modifier keys:
**`ctrl`**
**`meta`**

Or you can use any key from `a - z`.

When using `meta`, it will be substituted with `ctrl` (for Windows) **and** `cmd` (for Mac). This allows for cross-platform shortcuts.

#### Shortcut alternatives
#### <a id="shortcutAlternatives"></a> Shortcut alternatives

You can also define alternative shortcuts. For that, simply chain the shortcuts with a comma. Here's an example:

Expand Down
9 changes: 5 additions & 4 deletions projects/ngx-drag-to-select/src/lib/drag-to-select.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
import { CommonModule } from '@angular/common';

import { ModuleWithProviders, NgModule } from '@angular/core';
import { DEFAULT_CONFIG } from './config';
import { KeyboardEventsService } from './keyboard-events.service';
import { DragToSelectConfig } from './models';
import { SelectContainerComponent } from './select-container.component';
import { SelectItemDirective } from './select-item.directive';
import { ShortcutService } from './shortcut.service';
import { DragToSelectConfig } from './models';
import { CONFIG, USER_CONFIG } from './tokens';
import { DEFAULT_CONFIG } from './config';
import { mergeDeep } from './utils';

const COMPONENTS = [SelectContainerComponent, SelectItemDirective];
Expand All @@ -26,6 +26,7 @@ export class DragToSelectModule {
ngModule: DragToSelectModule,
providers: [
ShortcutService,
KeyboardEventsService,
{ provide: USER_CONFIG, useValue: config },
{
provide: CONFIG,
Expand Down
26 changes: 26 additions & 0 deletions projects/ngx-drag-to-select/src/lib/keyboard-events.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { share } from 'rxjs/operators';
import { fromEvent } from 'rxjs';
import { distinctKeyEvents } from './operators';

@Injectable()
export class KeyboardEventsService {
keydown$ = fromEvent<KeyboardEvent>(window, 'keydown').pipe(share());
keyup$ = fromEvent<KeyboardEvent>(window, 'keyup').pipe(share());

// distinctKeyEvents is used to prevent multiple key events to be fired repeatedly
// on Windows when a key is being pressed

distinctKeydown$ = this.keydown$.pipe(
distinctKeyEvents(),
share()
);

distinctKeyup$ = this.keyup$.pipe(
distinctKeyEvents(),
share()
);

mouseup$ = fromEvent<MouseEvent>(window, 'mouseup').pipe(share());
mousemove$ = fromEvent<MouseEvent>(window, 'mousemove').pipe(share());
}
52 changes: 19 additions & 33 deletions projects/ngx-drag-to-select/src/lib/select-container.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
getMousePosition,
hasMinimumSize
} from './utils';
import { KeyboardEventsService } from './keyboard-events.service';

@Component({
selector: 'dts-select-container',
Expand Down Expand Up @@ -128,6 +129,7 @@ export class SelectContainerComponent implements AfterViewInit, OnDestroy, After
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
private shortcuts: ShortcutService,
private keyboardEvents: KeyboardEventsService,
private hostElementRef: ElementRef,
private renderer: Renderer2,
private ngZone: NgZone
Expand All @@ -143,45 +145,21 @@ export class SelectContainerComponent implements AfterViewInit, OnDestroy, After
this._observeBoundingRectChanges();
this._observeSelectableItems();

const keydown$ = fromEvent<KeyboardEvent>(window, 'keydown').pipe(share());
const keyup$ = fromEvent<KeyboardEvent>(window, 'keyup').pipe(share());

// distinctKeyEvents is used to prevent multiple key events to be fired repeatedly
// on Windows when a key is being pressed

const distinctKeydown$ = keydown$.pipe(
distinctKeyEvents(),
share()
);

const distinctKeyup$ = keyup$.pipe(
distinctKeyEvents(),
share()
);

const mouseup$ = fromEvent<MouseEvent>(window, 'mouseup').pipe(
const mouseup$ = this.keyboardEvents.mouseup$.pipe(
filter(() => !this.disabled),
tap(() => this._onMouseUp()),
share()
);

const mousemove$ = fromEvent<MouseEvent>(window, 'mousemove').pipe(
const mousemove$ = this.keyboardEvents.mousemove$.pipe(
filter(() => !this.disabled),
share()
);

const shortcuts$ = merge<KeyboardEvent | null>(keydown$, keyup$.pipe(mapTo(null))).pipe(
startWith(null),
distinctKeyEvents(),
distinctUntilChanged()
);

const mousedown$ = fromEvent<MouseEvent>(this.host, 'mousedown').pipe(
withLatestFrom(shortcuts$),
filter(([event]) => event.button === 0), // only emit left mouse
filter(event => event.button === 0), // only emit left mouse
filter(() => !this.disabled),
tap(([event, keyboardEvent]) => this._onMouseDown(event, keyboardEvent)),
map(([mouseEvent]) => mouseEvent),
tap(event => this._onMouseDown(event)),
share()
);

Expand All @@ -206,7 +184,12 @@ export class SelectContainerComponent implements AfterViewInit, OnDestroy, After
share()
);

this.selectBoxClasses$ = merge(dragging$, mouseup$, distinctKeydown$, distinctKeyup$).pipe(
this.selectBoxClasses$ = merge(
dragging$,
mouseup$,
this.keyboardEvents.distinctKeydown$,
this.keyboardEvents.distinctKeyup$
).pipe(
auditTime(AUDIT_TIME),
withLatestFrom(selectBox$),
map(([event, selectBox]) => {
Expand Down Expand Up @@ -241,7 +224,10 @@ export class SelectContainerComponent implements AfterViewInit, OnDestroy, After
map(({ event }) => event)
);

const selectOnKeyboardEvent$ = merge(distinctKeydown$, distinctKeyup$).pipe(
const selectOnKeyboardEvent$ = merge(
this.keyboardEvents.distinctKeydown$,
this.keyboardEvents.distinctKeyup$
).pipe(
auditTime(AUDIT_TIME),
whenSelectBoxVisible(selectBox$),
tap(event => {
Expand Down Expand Up @@ -423,7 +409,7 @@ export class SelectContainerComponent implements AfterViewInit, OnDestroy, After
this.renderer.removeClass(document.body, NO_SELECT_CLASS);
}

private _onMouseDown(event: MouseEvent, keyboardEvent: KeyboardEvent | null) {
private _onMouseDown(event: MouseEvent) {
if (this.shortcuts.disableSelection(event) || this.disabled) {
return;
}
Expand All @@ -443,7 +429,7 @@ export class SelectContainerComponent implements AfterViewInit, OnDestroy, After

let [startIndex, endIndex] = this._lastRange;

const isMoveRangeStart = this.shortcuts.moveRangeStart(event, keyboardEvent);
const isMoveRangeStart = this.shortcuts.moveRangeStart(event);

if (!this.shortcuts.extendedSelectionShortcut(event) || isMoveRangeStart) {
this._resetRange();
Expand All @@ -470,7 +456,7 @@ export class SelectContainerComponent implements AfterViewInit, OnDestroy, After
this._lastRange = [startIndex, endIndex];
}

if (this.shortcuts.moveRangeStart(event, keyboardEvent)) {
if (isMoveRangeStart) {
return;
}

Expand Down
7 changes: 4 additions & 3 deletions projects/ngx-drag-to-select/src/lib/shortcut.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TestBed } from '@angular/core/testing';
import { KeyboardEventsService } from './keyboard-events.service';
import { ShortcutService } from './shortcut.service';
import { CONFIG } from './tokens';

Expand All @@ -14,7 +15,7 @@ describe('ShortcutService', () => {
};

TestBed.configureTestingModule({
providers: [ShortcutService, { provide: CONFIG, useValue: config }]
providers: [KeyboardEventsService, ShortcutService, { provide: CONFIG, useValue: config }]
});

expect(() => {
Expand All @@ -30,7 +31,7 @@ describe('ShortcutService', () => {
};

TestBed.configureTestingModule({
providers: [ShortcutService, { provide: CONFIG, useValue: config }]
providers: [KeyboardEventsService, ShortcutService, { provide: CONFIG, useValue: config }]
});

expect(() => {
Expand All @@ -51,7 +52,7 @@ describe('ShortcutService', () => {

beforeEach(() => {
TestBed.configureTestingModule({
providers: [ShortcutService, { provide: CONFIG, useValue: CUSTOM_CONFIG }]
providers: [KeyboardEventsService, ShortcutService, { provide: CONFIG, useValue: CUSTOM_CONFIG }]
});

shortcutService = TestBed.get(ShortcutService);
Expand Down
82 changes: 61 additions & 21 deletions projects/ngx-drag-to-select/src/lib/shortcut.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Inject, Injectable } from '@angular/core';
import { merge } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { KeyboardEventsService } from './keyboard-events.service';
import { DragToSelectConfig } from './models';
import { CONFIG } from './tokens';

Expand Down Expand Up @@ -27,44 +30,73 @@ const SUPPORTED_SHORTCUTS = {

const ERROR_PREFIX = '[ShortcutService]';

interface KeyState {
code: string;
pressed: boolean;
}

@Injectable()
export class ShortcutService {
private _shortcuts: { [key: string]: string[][] } = {};

constructor(@Inject(CONFIG) config: DragToSelectConfig) {
this._shortcuts = this.createShortcutsFromConfig(config.shortcuts);
private _latestShortcut: Map<string, boolean> = new Map();

constructor(@Inject(CONFIG) config: DragToSelectConfig, private keyboardEvents: KeyboardEventsService) {
this._shortcuts = this._createShortcutsFromConfig(config.shortcuts);

const keydown$ = this.keyboardEvents.keydown$.pipe(
map<KeyboardEvent, KeyState>(event => ({ code: event.code, pressed: true }))
);

const keyup$ = this.keyboardEvents.keyup$.pipe(
map<KeyboardEvent, KeyState>(event => ({ code: event.code, pressed: false }))
);

merge<KeyState>(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: Event) {
return this.isShortcutPressed('disableSelection', event);
return this._isShortcutPressed('disableSelection', event);
}

moveRangeStart(mouseEvent: MouseEvent, keyboardEvent: KeyboardEvent) {
return this.isShortcutPressed('moveRangeStart', mouseEvent, keyboardEvent);
moveRangeStart(event: Event) {
return this._isShortcutPressed('moveRangeStart', event);
}

toggleSingleItem(event: Event) {
return this.isShortcutPressed('toggleSingleItem', event);
return this._isShortcutPressed('toggleSingleItem', event);
}

addToSelection(event: Event) {
return this.isShortcutPressed('addToSelection', event);
return this._isShortcutPressed('addToSelection', event);
}

removeFromSelection(event: Event) {
return this.isShortcutPressed('removeFromSelection', event);
return this._isShortcutPressed('removeFromSelection', event);
}

extendedSelectionShortcut(event: Event) {
return this.addToSelection(event) || this.removeFromSelection(event);
}

private createShortcutsFromConfig(shortcuts: { [key: string]: string }) {
private _createShortcutsFromConfig(shortcuts: { [key: string]: string }) {
const shortcutMap = {};

for (const [key, shortcutsForCommand] of Object.entries(shortcuts)) {
if (!this.isSupportedShortcut(key)) {
throw new Error(this.getErrorMessage(`Shortcut ${key} not supported`));
if (!this._isSupportedShortcut(key)) {
throw new Error(this._getErrorMessage(`Shortcut ${key} not supported`));
}

shortcutsForCommand
Expand All @@ -76,13 +108,13 @@ export class ShortcutService {
}

const combo = shortcut.split('+');
const cleanCombos = this.substituteKey(shortcut, combo, META_KEY);
const cleanCombos = this._substituteKey(shortcut, combo, META_KEY);

cleanCombos.forEach(cleanCombo => {
const unsupportedKey = this.isSupportedCombo(cleanCombo);
const unsupportedKey = this._isSupportedCombo(cleanCombo);

if (unsupportedKey) {
throw new Error(this.getErrorMessage(`Key '${unsupportedKey}' in shortcut ${shortcut} not supported`));
throw new Error(this._getErrorMessage(`Key '${unsupportedKey}' in shortcut ${shortcut} not supported`));
}

shortcutMap[key].push(
Expand All @@ -97,7 +129,7 @@ export class ShortcutService {
return shortcutMap;
}

private substituteKey(shortcut: string, combo: Array<string>, substituteKey: string) {
private _substituteKey(shortcut: string, combo: Array<string>, substituteKey: string) {
const hasSpecialKey = shortcut.includes(substituteKey);
const substitutedShortcut: string[][] = [];

Expand All @@ -114,23 +146,27 @@ export class ShortcutService {
return substitutedShortcut;
}

private getErrorMessage(message: string) {
private _getErrorMessage(message: string) {
return `${ERROR_PREFIX} ${message}`;
}

private isShortcutPressed(shortcutName: string, event: Event, keyboardEvent?: KeyboardEvent) {
private _isShortcutPressed(shortcutName: string, event: Event) {
const shortcuts = this._shortcuts[shortcutName];

return shortcuts.some(shortcut => {
return shortcut.every(key => (key.startsWith('Key') && keyboardEvent ? keyboardEvent.code === key : event[key]));
return shortcut.every(key => this._isKeyPressed(event, key));
});
}

private isSupportedCombo(combo: Array<string>) {
private _isKeyPressed(event: Event, key: string) {
return key.startsWith('Key') ? this._latestShortcut.has(key) : event[key];
}

private _isSupportedCombo(combo: Array<string>) {
let unsupportedKey = null;

combo.forEach(key => {
if (!SUPPORTED_META_KEYS[key] && !SUPPORTED_KEYS.test(key)) {
if (!SUPPORTED_META_KEYS[key] && (!SUPPORTED_KEYS.test(key) || this._isSingleChar(key))) {
unsupportedKey = key;
return;
}
Expand All @@ -139,7 +175,11 @@ export class ShortcutService {
return unsupportedKey;
}

private isSupportedShortcut(shortcut: string) {
private _isSingleChar(key: string) {
return key.length > 1;
}

private _isSupportedShortcut(shortcut: string) {
return SUPPORTED_SHORTCUTS[shortcut];
}
}

0 comments on commit f41c934

Please sign in to comment.