import Service , { inject as service } from '@ember/service' ; import Macro , { MacroOptions } from '../utils/macro' ; import { TO_MODIFIER , TO_KEY } from '../utils/modifier-keys' ; import { warn } from '@ember/debug' ; import { isPresent } from '@ember/utils' ; import { MODIFIERS_ON_KEYUP as MODIFIERS_ON_KEYUP_WARNING , } from 'ember-key-manager/utils/warning-messages' ; import { A } from '@ember/array' ; import { capitalize } from '@ember/string' ; import KeyManagerConfig from 'ember-key-manager/utils/config' ; const inputElements = [ 'INPUT' , 'SELECT' , 'TEXTAREA' , ] ; const isInputElement = ( element ) => { const isContentEditable = element . isContentEditable ; const isInput = inputElements . includes ( element . tagName ) ; return isContentEditable || isInput ; } ; export default class KeyManagerService extends Service { @ service config ! : KeyManagerConfig ; isDisabledOnInput = false ; macros = A ( ) ; public get keydownMacros ( ) { return this . macros . filterBy ( 'keyEvent' , 'keydown' ) ; } public get keyupMacros ( ) { return this . macros . filterBy ( 'keyEvent' , 'keyup' ) ; } constructor ( ) { super ( ... arguments ) ; this . _registerConfigOptions ( ) ; } public addMacro ( options ) { options = this . _mergeConfigDefaults ( options ) ; const macro = new Macro ( options ) ; const keyEvent = macro . keyEvent ; this . _handleModifiersOnKeyup ( macro , keyEvent ) ; const element = macro . element ; this . _addEventListener ( element , keyEvent ) ; const macros = this . macros ; macros . pushObject ( macro ) ; return macro ; } public removeMacro ( macro ) { this . macros . removeObject ( macro ) ; this . _removeEventListenter ( macro . element , macro . keyEvent ) ; } private _handleModifiersOnKeyup ( { modifierKeys } , keyEvent ) { if ( keyEvent === 'keyup' && isPresent ( modifierKeys ) ) { warn ( MODIFIERS_ON_KEYUP_WARNING , false , { id : 'keyup-with-modifiers' } ) ; } } private _mergeConfigDefaults ( options ) { if ( options . isDisabledOnInput == undefined ) { options . isDisabledOnInput = this . isDisabledOnInput ; } return options ; } private _addEventListener ( element , keyEvent ) { const hasListenerForElementAndKeyEvent = this . _findMacroWithElementAndKeyEvent ( element , keyEvent ) ; if ( ! hasListenerForElementAndKeyEvent ) { element . addEventListener ( keyEvent , this ) ; } } private _removeEventListenter ( element , keyEvent ) { const hasListenerForElementAndKeyEvent = this . _findMacroWithElementAndKeyEvent ( element , keyEvent ) ; if ( ! hasListenerForElementAndKeyEvent ) { element . removeEventListener ( keyEvent , this ) ; } } disable ( recipient ) { this . _setDisabledState ( recipient , true ) ; } enable ( recipient ) { this . _setDisabledState ( recipient , false ) ; } public handleEvent ( event ) { if ( this . isDestroyed || this . isDestroying ) { return false ; } const isKeydown = event . type === 'keydown' ; const isKeyup = event . type === 'keyup' ; if ( isKeydown || isKeyup ) { const allEventModifierKeys = { altKey : event . altKey , ctrlKey : event . ctrlKey , metaKey : event . metaKey , shiftKey : event . shiftKey , } ; const eventModifierKeys = A ( Object . keys ( allEventModifierKeys ) . filter ( ( key ) => { return allEventModifierKeys [ key ] !== false ; } ) ) ; const targetIsElement = event . target instanceof HTMLElement ; const matchingMacros = targetIsElement ? this . _findMatchingMacros ( event . target as HTMLElement , event . key || '' , eventModifierKeys , event . type ) : [ ] ; if ( isPresent ( matchingMacros ) ) { const isTargetInput = targetIsElement && isInputElement ( event . target as HTMLElement ) ; event . stopPropagation ( ) ; matchingMacros . forEach ( ( matchingMacro ) => { const isDisabled = matchingMacro . isDisabled || ( matchingMacro . isDisabledOnInput && isTargetInput ) ; if ( ! isDisabled ) { matchingMacro . callback ( event ) ; } } ) ; } } return false ; } private _findMacroWithElementAndKeyEvent ( eventElement , eventKeyEvent ) { var events = eventKeyEvent === "keydown" ? this . keydownMacros : this . keyupMacros ; return events . find ( ( macro ) => { const element = macro . element ; return eventElement === element ; } ) ; } private _findMatchingMacros ( eventElement , eventExecutionKey , eventModifierKeys , eventKeyEvent ) { var events = eventKeyEvent === "keydown" ? this . keydownMacros : this . keyupMacros ; const matchingMacros = events . filter ( ( macro ) => { const element = macro . element ; const executionKey = macro . executionKey ; const modifierKeys = macro . modifierKeys ; const hasElementMatch = element === eventElement || element . contains ( eventElement ) ; const hasExecutionKeyMatch = eventExecutionKey . toLowerCase ( ) === executionKey . toLowerCase ( ) ; const onlyModifierKeys = A ( eventModifierKeys ) ; onlyModifierKeys . removeObject ( TO_MODIFIER [ eventExecutionKey ] ) ; const hasModifierKeysMatch = onlyModifierKeys . every ( ( key ) => { return modifierKeys . map ( k => capitalize ( k ) ) . includes ( TO_KEY [ key ] ) ; } ) ; const hasModifierKeyCount = eventModifierKeys . length === modifierKeys . length ; return hasElementMatch && hasExecutionKeyMatch && hasModifierKeysMatch && hasModifierKeyCount ; } ) ; const highestPriority = A ( matchingMacros ) . mapBy ( 'priority' ) . reduce ( ( max , priority ) => Math . max ( max , priority ) , - Infinity ) ; return matchingMacros . filter ( ( macro ) => macro . priority === highestPriority ) ; } private _registerConfigOptions ( ) { if ( this . config . isDisabledOnInput !== undefined ) { this . isDisabledOnInput = this . config . isDisabledOnInput ; } } private _setDisabledState ( recipient , isDisabled ) { if ( typeof recipient === 'string' ) { this . _setGroupDisabledState ( recipient , isDisabled ) ; } else { recipient . isDisabled = isDisabled ; } } private _setGroupDisabledState ( groupName , isDisabled ) { this . macros . filterBy ( 'groupName' , groupName ) . setEach ( 'isDisabled' , isDisabled ) ; } }