File

src/core/src/components/keyboard-key/keyboard-key.component.ts

Implements

OnInit

Metadata

changeDetection ChangeDetectionStrategy.OnPush
preserveWhitespaces false
selector mat-keyboard-key
styleUrls keyboard-key.component.scss
templateUrl ./keyboard-key.component.html

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor(_deadkeys: IKeyboardDeadkeys, _icons: IKeyboardIcons)
Parameters :
Name Type Optional
_deadkeys IKeyboardDeadkeys No
_icons IKeyboardIcons No

Inputs

active

Type : boolean

control

Type : FormControl

input

Type : ElementRef

key

Type : string | KeyboardClassKey

pressed

Type : boolean

Outputs

altClick $event Type: EventEmitter
bkspClick $event Type: EventEmitter
capsClick $event Type: EventEmitter
enterClick $event Type: EventEmitter
genericClick $event Type: EventEmitter
keyClick $event Type: EventEmitter
shiftClick $event Type: EventEmitter
spaceClick $event Type: EventEmitter
tabClick $event Type: EventEmitter

Methods

Private _getCursorPosition
_getCursorPosition()
Returns : number
Private _getSelectionLength
_getSelectionLength()
Returns : number
Private _isTextarea
_isTextarea()
Returns : boolean
Private _setCursorPosition
_setCursorPosition(position: number)
Parameters :
Name Type Optional
position number No
Returns : boolean
Private _triggerKeyEvent
_triggerKeyEvent()
Returns : Event
Private deleteSelectedText
deleteSelectedText()
Returns : void
ngOnInit
ngOnInit()
Returns : void
onClick
onClick(event: MouseEvent)
Parameters :
Name Type Optional
event MouseEvent No
Returns : void
Private replaceSelectedText
replaceSelectedText(char: string)
Parameters :
Name Type Optional
char string No
Returns : void

Properties

Private _deadkeyKeys
_deadkeyKeys: string[]
Type : string[]
Default value : []
Private _iconKeys
_iconKeys: string[]
Type : string[]
Default value : []
active$
active$: BehaviorSubject<boolean>
Type : BehaviorSubject<boolean>
Default value : new BehaviorSubject(false)
pressed$
pressed$: BehaviorSubject<boolean>
Type : BehaviorSubject<boolean>
Default value : new BehaviorSubject(false)

Accessors

active
getactive()
setactive(active: boolean)
Parameters :
Name Type Optional
active boolean No
Returns : void
pressed
getpressed()
setpressed(pressed: boolean)
Parameters :
Name Type Optional
pressed boolean No
Returns : void
lowerKey
getlowerKey()
charCode
getcharCode()
isClassKey
getisClassKey()
isDeadKey
getisDeadKey()
hasIcon
gethasIcon()
icon
geticon()
cssClass
getcssClass()
inputValue
getinputValue()
setinputValue(inputValue: string)
Parameters :
Name Type Optional
inputValue string No
Returns : void
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
import { FormControl } from '@angular/forms';

import { BehaviorSubject } from 'rxjs/BehaviorSubject';

import { MAT_KEYBOARD_DEADKEYS } from '../../configs/keyboard-deadkey.config';
import { MAT_KEYBOARD_ICONS } from '../../configs/keyboard-icons.config';
import { KeyboardClassKey } from '../../enums/keyboard-class-key.enum';
import { IKeyboardDeadkeys } from '../../interfaces/keyboard-deadkeys.interface';
import { IKeyboardIcons } from '../../interfaces/keyboard-icons.interface';

export const VALUE_NEWLINE = '\n\r';
export const VALUE_SPACE = ' ';
export const VALUE_TAB = '\t';

@Component({
  selector: 'mat-keyboard-key',
  templateUrl: './keyboard-key.component.html',
  styleUrls: ['./keyboard-key.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  preserveWhitespaces: false
})
export class MatKeyboardKeyComponent implements OnInit {

  private _deadkeyKeys: string[] = [];

  private _iconKeys: string[] = [];

  active$: BehaviorSubject<boolean> = new BehaviorSubject(false);

  pressed$: BehaviorSubject<boolean> = new BehaviorSubject(false);

  @Input()
  key: string | KeyboardClassKey;

  @Input()
  set active(active: boolean) {
    this.active$.next(active);
  }

  get active(): boolean {
    return this.active$.getValue();
  }

  @Input()
  set pressed(pressed: boolean) {
    this.pressed$.next(pressed);
  }

  get pressed(): boolean {
    return this.pressed$.getValue();
  }

  @Input()
  input?: ElementRef;

  @Input()
  control?: FormControl;

  @Output()
  genericClick = new EventEmitter<MouseEvent>();

  @Output()
  enterClick = new EventEmitter<MouseEvent>();

  @Output()
  bkspClick = new EventEmitter<MouseEvent>();

  @Output()
  capsClick = new EventEmitter<MouseEvent>();

  @Output()
  altClick = new EventEmitter<MouseEvent>();

  @Output()
  shiftClick = new EventEmitter<MouseEvent>();

  @Output()
  spaceClick = new EventEmitter<MouseEvent>();

  @Output()
  tabClick = new EventEmitter<MouseEvent>();

  @Output()
  keyClick = new EventEmitter<MouseEvent>();

  get lowerKey(): string {
    return `${this.key}`.toLowerCase();
  }

  get charCode(): number {
    return `${this.key}`.charCodeAt(0);
  }

  get isClassKey(): boolean {
    return this.key in KeyboardClassKey;
  }

  get isDeadKey(): boolean {
    return this._deadkeyKeys.some((deadKey: string) => deadKey === `${this.key}`);
  }

  get hasIcon(): boolean {
    return this._iconKeys.some((iconKey: string) => iconKey === `${this.key}`);
  }

  get icon(): string {
    return this._icons[this.key];
  }

  get cssClass(): string {
    const classes = [];

    if (this.hasIcon) {
      classes.push('mat-keyboard-key-modifier');
      classes.push(`mat-keyboard-key-${this.lowerKey}`);
    }

    if (this.isDeadKey) {
      classes.push('mat-keyboard-key-deadkey');
    }

    return classes.join(' ');
  }

  get inputValue(): string {
    if (this.control) {
      return this.control.value;
    } else if (this.input && this.input.nativeElement && this.input.nativeElement.value) {
      return this.input.nativeElement.value;
    } else {
      return '';
    }
  }

  set inputValue(inputValue: string) {
    if (this.control) {
      this.control.setValue(inputValue);
    } else if (this.input && this.input.nativeElement) {
      this.input.nativeElement.value = inputValue;
    }
  }

  // Inject dependencies
  constructor(@Inject(MAT_KEYBOARD_DEADKEYS) private _deadkeys: IKeyboardDeadkeys,
              @Inject(MAT_KEYBOARD_ICONS) private _icons: IKeyboardIcons) {}

  ngOnInit() {
    // read the deadkeys
    this._deadkeyKeys = Object.keys(this._deadkeys);

    // read the icons
    this._iconKeys = Object.keys(this._icons);
  }

  onClick(event: MouseEvent) {
    // Trigger a global key event
    // TODO: investigate
    this._triggerKeyEvent();

    // Trigger generic click event
    this.genericClick.emit(event);

    // Manipulate the focused input / textarea value
    const value = this.inputValue;
    const caret = this.input ? this._getCursorPosition() : 0;

    let char: string;
    switch (this.key) {
      // this keys have no actions yet
      // TODO: add deadkeys and modifiers
      case KeyboardClassKey.Alt:
      case KeyboardClassKey.AltGr:
      case KeyboardClassKey.AltLk:
        this.altClick.emit(event);
        break;

      case KeyboardClassKey.Bksp:
        this.deleteSelectedText();
        this.bkspClick.emit(event);
        break;

      case KeyboardClassKey.Caps:
        this.capsClick.emit(event);
        break;

      case KeyboardClassKey.Enter:
        if (this._isTextarea()) {
          char = VALUE_NEWLINE;
        } else {
          this.enterClick.emit(event);
          // TODO: trigger submit / onSubmit / ngSubmit properly (for the time being this has to be handled by the user himself)
          // console.log(this.control.ngControl.control.root)
          // this.input.nativeElement.form.submit();
        }
        break;

      case KeyboardClassKey.Shift:
        this.shiftClick.emit(event);
        break;

      case KeyboardClassKey.Space:
        char = VALUE_SPACE;
        this.spaceClick.emit(event);
        break;

      case KeyboardClassKey.Tab:
        char = VALUE_TAB;
        this.tabClick.emit(event);
        break;

      default:
        // the key is not mapped or a string
        char = `${this.key}`;
        this.keyClick.emit(event);
        break;
    }

    if (char && this.input) {
      this.replaceSelectedText(char);
      this._setCursorPosition(caret + 1);
    }
  }

  private deleteSelectedText(): void {
    const value = this.inputValue;
    let caret = this.input ? this._getCursorPosition() : 0;
    let selectionLength = this._getSelectionLength();
    if (selectionLength === 0) {
      if (caret === 0) {
        return;
      }

      caret--;
      selectionLength = 1;
    }

    const headPart = value.slice(0, caret);
    const endPart = value.slice(caret + selectionLength);

    this.inputValue = [headPart, endPart].join('');
    this._setCursorPosition(caret);
  }

  private replaceSelectedText(char: string): void {
    const value = this.inputValue;
    const caret = this.input ? this._getCursorPosition() : 0;
    const selectionLength = this._getSelectionLength();
    const headPart = value.slice(0, caret);
    const endPart = value.slice(caret + selectionLength);

    this.inputValue = [headPart, char, endPart].join('');
  }

  private _triggerKeyEvent(): Event {
    const keyboardEvent = new KeyboardEvent('keydown');
    //
    // keyboardEvent[initMethod](
    //   true, // bubbles
    //   true, // cancelable
    //   window, // viewArg: should be window
    //   false, // ctrlKeyArg
    //   false, // altKeyArg
    //   false, // shiftKeyArg
    //   false, // metaKeyArg
    //   this.charCode, // keyCodeArg : unsigned long - the virtual key code, else 0
    //   0 // charCodeArgs : unsigned long - the Unicode character associated with the depressed key, else 0
    // );
    //
    // window.document.dispatchEvent(keyboardEvent);

    return keyboardEvent;
  }

  // inspired by:
  // ref https://stackoverflow.com/a/2897510/1146207
  private _getCursorPosition(): number {
    if (!this.input) {
      return;
    }

    if ('selectionStart' in this.input.nativeElement) {
      // Standard-compliant browsers
      return this.input.nativeElement.selectionStart;
    } else if ('selection' in window.document) {
      // IE
      this.input.nativeElement.focus();
      const sel = window.document['selection'].createRange();
      const selLen = window.document['selection'].createRange().text.length;
      sel.moveStart('character', -this.control.value.length);

      return sel.text.length - selLen;
    }
  }

  private _getSelectionLength(): number {
    if (!this.input) {
      return;
    }

    if ('selectionEnd' in this.input.nativeElement) {
      // Standard-compliant browsers
      return this.input.nativeElement.selectionEnd - this.input.nativeElement.selectionStart;
    }

    if ('selection' in window.document) {
      // IE
      this.input.nativeElement.focus();

      return window.document['selection'].createRange().text.length;
    }
  }

  // inspired by:
  // ref https://stackoverflow.com/a/12518737/1146207
  // tslint:disable one-line
  private _setCursorPosition(position: number): boolean {
    if (!this.input) {
      return;
    }

    this.inputValue = this.control.value;
    // ^ this is used to not only get "focus", but
    // to make sure we don't have it everything -selected-
    // (it causes an issue in chrome, and having it doesn't hurt any other browser)

    if ('createTextRange' in this.input.nativeElement) {
      const range = this.input.nativeElement.createTextRange();
      range.move('character', position);
      range.select();
      return true;
    } else {
      // (el.selectionStart === 0 added for Firefox bug)
      if (this.input.nativeElement.selectionStart || this.input.nativeElement.selectionStart === 0) {
        this.input.nativeElement.focus();
        this.input.nativeElement.setSelectionRange(position, position);
        return true;
      }
      // fail city, fortunately this never happens (as far as I've tested) :)
      else {
        this.input.nativeElement.focus();
        return false;
      }
    }
  }

  private _isTextarea(): boolean {
    return this.input && this.input.nativeElement && this.input.nativeElement.tagName === 'TEXTAREA';
  }

}
<button mat-raised-button
        class="mat-keyboard-key"
        tabindex="-1"
        [class.mat-keyboard-key-active]="active$ | async"
        [class.mat-keyboard-key-pressed]="pressed$ | async"
        [ngClass]="cssClass"
        (click)="onClick($event)"
>
  <mat-icon *ngIf="hasIcon">{{ icon }}</mat-icon>
  <ng-container *ngIf="!hasIcon">{{ key }}</ng-container>
</button>
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""