← JavaScript/Клавиатура: keydown и keyup#140 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Клавиатура: keydown и keyup

Вы разрабатываете текстовый редактор. Нужно: Ctrl+S — сохранить, Ctrl+Z — отменить, Escape — закрыть диалог. Параллельно — браузерная игра, где стрелки двигают персонажа. Всё это строится на двух событиях: keydown и keyup.

Что решает эта тема

Клавиатурные события позволяют добавлять горячие клавиши, валидировать ввод в реальном времени и строить игровое управление — всё без сторонних библиотек.

На основе предыдущих уроков

  • события: addEventListener, preventDefault
  • Set: отслеживание нажатых клавиш через Set
  • Map: реестр горячих клавиш через Map
  • Три события клавиатуры

    element.addEventListener('keydown', handler)  // клавиша нажата
    element.addEventListener('keyup', handler)    // клавиша отпущена
    element.addEventListener('keypress', handler) // УСТАРЕВШЕЕ — не использовать

    keypress не поддерживает специальные клавиши (стрелки, Delete и т.д.) и считается устаревшим. Используй keydown вместо него.

    event.key vs event.code

    document.addEventListener('keydown', (event) => {
      console.log(event.key)  // 'a' / 'A' (зависит от Shift и раскладки)
      console.log(event.code) // 'KeyA' (физическое расположение клавиши)
    })

    | Клавиша | event.key | event.code |

    |---|---|---|

    | A в русской раскладке | 'ф' | 'KeyA' |

    | A в латинской | 'a' / 'A' | 'KeyA' |

    | Enter | 'Enter' | 'Enter' |

    | Стрелка вверх | 'ArrowUp' | 'ArrowUp' |

    | Цифра 1 | '1' | 'Digit1' |

    | Пробел | ' ' | 'Space' |

    Правило: используй event.code для горячих клавиш (не зависит от раскладки), event.key — для ввода символов.

    Модификаторы

    document.addEventListener('keydown', (event) => {
      console.log(event.ctrlKey)  // true если удержан Ctrl (Command на Mac)
      console.log(event.shiftKey) // true если удержан Shift
      console.log(event.altKey)   // true если удержан Alt (Option на Mac)
      console.log(event.metaKey)  // true если удержан Meta (Win/Command)
    })

    Автоповтор

    При удержании клавиши браузер посылает повторные события keydown:

    document.addEventListener('keydown', (event) => {
      if (event.repeat) {
        // Клавиша удерживается — это автоповтор
        return  // часто нужно игнорировать
      }
      // Первое нажатие
    })

    Горячие клавиши (hotkeys)

    document.addEventListener('keydown', (event) => {
      // Ctrl+S — сохранить
      if (event.ctrlKey && event.code === 'KeyS') {
        event.preventDefault()  // запрет стандартного сохранения страницы
        saveDocument()
        return
      }
    
      // Ctrl+Z — отменить
      if (event.ctrlKey && !event.shiftKey && event.code === 'KeyZ') {
        event.preventDefault()
        undoLastAction()
        return
      }
    
      // Ctrl+Shift+Z — повторить
      if (event.ctrlKey && event.shiftKey && event.code === 'KeyZ') {
        event.preventDefault()
        redoLastAction()
        return
      }
    })

    Паттерн: реестр горячих клавиш

    class HotkeyRegistry {
      constructor() {
        this.hotkeys = new Map()
    
        document.addEventListener('keydown', (event) => {
          const key = this.buildKey(event)
          const action = this.hotkeys.get(key)
          if (action) {
            event.preventDefault()
            action()
          }
        })
      }
    
      buildKey(event) {
        const parts = []
        if (event.ctrlKey) parts.push('Ctrl')
        if (event.shiftKey) parts.push('Shift')
        if (event.altKey) parts.push('Alt')
        parts.push(event.code)
        return parts.join('+')
      }
    
      register(combo, action) {
        this.hotkeys.set(combo, action)
      }
    }
    
    const hotkeys = new HotkeyRegistry()
    hotkeys.register('Ctrl+KeyS', () => console.log('Сохранение...'))
    hotkeys.register('Ctrl+KeyZ', () => console.log('Отмена...'))
    hotkeys.register('Ctrl+Shift+KeyZ', () => console.log('Повтор...'))

    Отслеживание нескольких клавиш одновременно

    const pressedKeys = new Set()
    
    document.addEventListener('keydown', (event) => {
      pressedKeys.add(event.code)
      // В игре: проверяем одновременное нажатие стрелок
      if (pressedKeys.has('ArrowUp') && pressedKeys.has('ArrowRight')) {
        movePlayerDiagonally()
      }
    })
    
    document.addEventListener('keyup', (event) => {
      pressedKeys.delete(event.code)
    })

    Запрет ввода цифр

    nameInput.addEventListener('keydown', (event) => {
      // Разрешить: Backspace, Delete, стрелки, Tab
      const allowed = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Space', 'Tab']
      if (allowed.includes(event.code)) return
    
      // Запретить цифры
      if (event.code.startsWith('Digit')) {
        event.preventDefault()
      }
    })

    Типичные ошибки

    Ошибка 1: используют event.key для горячих клавиш — ломается при смене раскладки

    // Сломано: при русской раскладке 's' становится 'ы' — Ctrl+S не работает
    if (event.ctrlKey && event.key === 's') {
      saveDocument()
    }
    
    // Исправлено: event.code не зависит от раскладки
    if (event.ctrlKey && event.code === 'KeyS') {
      saveDocument()
    }

    Ошибка 2: не предотвращают стандартное поведение браузера

    // Сломано: Ctrl+S открывает диалог "Сохранить страницу" браузера
    document.addEventListener('keydown', (e) => {
      if (e.ctrlKey && e.code === 'KeyS') {
        saveDocument()  // браузер тоже сохраняет!
      }
    })
    
    // Исправлено:
    if (e.ctrlKey && e.code === 'KeyS') {
      e.preventDefault()  // сначала отменить стандартное
      saveDocument()
    }

    Ошибка 3: не обрабатывают event.repeat при удержании клавиши

    // Сломано: при удержании клавиши функция вызывается сотни раз
    document.addEventListener('keydown', (e) => {
      if (e.code === 'Space') {
        shootBullet()  // выстрелит сотни раз!
      }
    })
    
    // Исправлено: проверяем repeat
    document.addEventListener('keydown', (e) => {
      if (e.code === 'Space' && !e.repeat) {
        shootBullet()  // только первое нажатие
      }
    })

    В реальных проектах

  • Текстовые редакторы (CodeMirror, Monaco): сотни горячих клавиш через реестр
  • Браузерные игры: Set нажатых клавиш для плавного движения персонажа
  • Формы: запрет ввода букв в числовые поля, валидация в реальном времени
  • Accessibility: навигация по интерфейсу с клавиатуры (Tab, Enter, Escape, стрелки)
  • Примеры

    Реестр горячих клавиш: Ctrl+S сохранить, Ctrl+Z отменить — через мок-события клавиатуры

    // Реестр горячих клавиш без DOM
    // Моделируем KeyboardEvent-объекты и обрабатываем их
    
    function buildHotkeyString(event) {
      const parts = []
      if (event.ctrlKey)  parts.push('Ctrl')
      if (event.shiftKey) parts.push('Shift')
      if (event.altKey)   parts.push('Alt')
      if (event.metaKey)  parts.push('Meta')
      parts.push(event.code)
      return parts.join('+')
    }
    
    class HotkeyRegistry {
      constructor() {
        this._hotkeys = new Map()
        this._history = []  // лог выполненных команд для демонстрации
      }
    
      register(combo, description, action) {
        this._hotkeys.set(combo, { description, action })
        return this
      }
    
      handle(event) {
        if (event.repeat) return 'repeat — ignored'
        const key = buildHotkeyString(event)
        const entry = this._hotkeys.get(key)
        if (!entry) return `no handler for "${key}"`
        const result = entry.action()
        this._history.push({ key, description: entry.description, result })
        return `executed: ${entry.description}`
      }
    
      getHistory() { return this._history }
    }
    
    // Создаём реестр и регистрируем горячие клавиши
    const registry = new HotkeyRegistry()
    
    // Симуляция состояния приложения
    const appState = {
      document: 'Привет, мир! Это тестовый документ.',
      history: [],
      saved: false,
    }
    
    registry
      .register('Ctrl+KeyS', 'Сохранить документ', () => {
        appState.saved = true
        return `сохранено: "${appState.document.slice(0, 20)}..."`
      })
      .register('Ctrl+KeyZ', 'Отменить действие', () => {
        const prev = appState.history.pop()
        if (prev) { appState.document = prev; return 'откат выполнен' }
        return 'нечего отменять'
      })
      .register('Ctrl+Shift+KeyZ', 'Повторить действие', () => {
        return 'повтор выполнен'
      })
      .register('Ctrl+KeyA', 'Выделить всё', () => {
        return `выделено ${appState.document.length} символов`
      })
      .register('Ctrl+KeyC', 'Копировать', () => {
        return `скопировано в буфер`
      })
    
    // Симулируем нажатия клавиш (мок-события)
    const mockKeyEvents = [
      { ctrlKey: true,  shiftKey: false, altKey: false, metaKey: false, code: 'KeyA', repeat: false },
      { ctrlKey: true,  shiftKey: false, altKey: false, metaKey: false, code: 'KeyC', repeat: false },
      { ctrlKey: true,  shiftKey: false, altKey: false, metaKey: false, code: 'KeyS', repeat: false },
      { ctrlKey: true,  shiftKey: false, altKey: false, metaKey: false, code: 'KeyZ', repeat: false },
      { ctrlKey: true,  shiftKey: true,  altKey: false, metaKey: false, code: 'KeyZ', repeat: false },
      { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false, code: 'KeyA', repeat: false },
      { ctrlKey: true,  shiftKey: false, altKey: false, metaKey: false, code: 'KeyS', repeat: true  }, // автоповтор
    ]
    
    console.log('=== Обработка горячих клавиш ===')
    mockKeyEvents.forEach(event => {
      const combo = buildHotkeyString(event) + (event.repeat ? ' [repeat]' : '')
      const result = registry.handle(event)
      console.log(`${combo.padEnd(25)} → ${result}`)
    })
    
    console.log('\n=== История выполненных команд ===')
    registry.getHistory().forEach(entry => {
      console.log(`[${entry.key}] ${entry.description}: ${entry.result}`)
    })
    
    // Демонстрация отслеживания нескольких клавиш
    console.log('\n=== Несколько клавиш одновременно (игровой паттерн) ===')
    const pressedKeys = new Set()
    
    const gameEvents = [
      { type: 'keydown', code: 'ArrowUp' },
      { type: 'keydown', code: 'ArrowRight' },
      { type: 'keyup',   code: 'ArrowUp' },
      { type: 'keyup',   code: 'ArrowRight' },
    ]
    
    gameEvents.forEach(event => {
      if (event.type === 'keydown') pressedKeys.add(event.code)
      else pressedKeys.delete(event.code)
    
      const direction = []
      if (pressedKeys.has('ArrowUp'))    direction.push('вверх')
      if (pressedKeys.has('ArrowDown'))  direction.push('вниз')
      if (pressedKeys.has('ArrowLeft'))  direction.push('влево')
      if (pressedKeys.has('ArrowRight')) direction.push('вправо')
    
      const move = direction.length ? direction.join('+') : 'стоим'
      console.log(`${event.type} ${event.code} → движение: ${move}`)
    })

    Клавиатура: keydown и keyup

    Вы разрабатываете текстовый редактор. Нужно: Ctrl+S — сохранить, Ctrl+Z — отменить, Escape — закрыть диалог. Параллельно — браузерная игра, где стрелки двигают персонажа. Всё это строится на двух событиях: keydown и keyup.

    Что решает эта тема

    Клавиатурные события позволяют добавлять горячие клавиши, валидировать ввод в реальном времени и строить игровое управление — всё без сторонних библиотек.

    На основе предыдущих уроков

  • события: addEventListener, preventDefault
  • Set: отслеживание нажатых клавиш через Set
  • Map: реестр горячих клавиш через Map
  • Три события клавиатуры

    element.addEventListener('keydown', handler)  // клавиша нажата
    element.addEventListener('keyup', handler)    // клавиша отпущена
    element.addEventListener('keypress', handler) // УСТАРЕВШЕЕ — не использовать

    keypress не поддерживает специальные клавиши (стрелки, Delete и т.д.) и считается устаревшим. Используй keydown вместо него.

    event.key vs event.code

    document.addEventListener('keydown', (event) => {
      console.log(event.key)  // 'a' / 'A' (зависит от Shift и раскладки)
      console.log(event.code) // 'KeyA' (физическое расположение клавиши)
    })

    | Клавиша | event.key | event.code |

    |---|---|---|

    | A в русской раскладке | 'ф' | 'KeyA' |

    | A в латинской | 'a' / 'A' | 'KeyA' |

    | Enter | 'Enter' | 'Enter' |

    | Стрелка вверх | 'ArrowUp' | 'ArrowUp' |

    | Цифра 1 | '1' | 'Digit1' |

    | Пробел | ' ' | 'Space' |

    Правило: используй event.code для горячих клавиш (не зависит от раскладки), event.key — для ввода символов.

    Модификаторы

    document.addEventListener('keydown', (event) => {
      console.log(event.ctrlKey)  // true если удержан Ctrl (Command на Mac)
      console.log(event.shiftKey) // true если удержан Shift
      console.log(event.altKey)   // true если удержан Alt (Option на Mac)
      console.log(event.metaKey)  // true если удержан Meta (Win/Command)
    })

    Автоповтор

    При удержании клавиши браузер посылает повторные события keydown:

    document.addEventListener('keydown', (event) => {
      if (event.repeat) {
        // Клавиша удерживается — это автоповтор
        return  // часто нужно игнорировать
      }
      // Первое нажатие
    })

    Горячие клавиши (hotkeys)

    document.addEventListener('keydown', (event) => {
      // Ctrl+S — сохранить
      if (event.ctrlKey && event.code === 'KeyS') {
        event.preventDefault()  // запрет стандартного сохранения страницы
        saveDocument()
        return
      }
    
      // Ctrl+Z — отменить
      if (event.ctrlKey && !event.shiftKey && event.code === 'KeyZ') {
        event.preventDefault()
        undoLastAction()
        return
      }
    
      // Ctrl+Shift+Z — повторить
      if (event.ctrlKey && event.shiftKey && event.code === 'KeyZ') {
        event.preventDefault()
        redoLastAction()
        return
      }
    })

    Паттерн: реестр горячих клавиш

    class HotkeyRegistry {
      constructor() {
        this.hotkeys = new Map()
    
        document.addEventListener('keydown', (event) => {
          const key = this.buildKey(event)
          const action = this.hotkeys.get(key)
          if (action) {
            event.preventDefault()
            action()
          }
        })
      }
    
      buildKey(event) {
        const parts = []
        if (event.ctrlKey) parts.push('Ctrl')
        if (event.shiftKey) parts.push('Shift')
        if (event.altKey) parts.push('Alt')
        parts.push(event.code)
        return parts.join('+')
      }
    
      register(combo, action) {
        this.hotkeys.set(combo, action)
      }
    }
    
    const hotkeys = new HotkeyRegistry()
    hotkeys.register('Ctrl+KeyS', () => console.log('Сохранение...'))
    hotkeys.register('Ctrl+KeyZ', () => console.log('Отмена...'))
    hotkeys.register('Ctrl+Shift+KeyZ', () => console.log('Повтор...'))

    Отслеживание нескольких клавиш одновременно

    const pressedKeys = new Set()
    
    document.addEventListener('keydown', (event) => {
      pressedKeys.add(event.code)
      // В игре: проверяем одновременное нажатие стрелок
      if (pressedKeys.has('ArrowUp') && pressedKeys.has('ArrowRight')) {
        movePlayerDiagonally()
      }
    })
    
    document.addEventListener('keyup', (event) => {
      pressedKeys.delete(event.code)
    })

    Запрет ввода цифр

    nameInput.addEventListener('keydown', (event) => {
      // Разрешить: Backspace, Delete, стрелки, Tab
      const allowed = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Space', 'Tab']
      if (allowed.includes(event.code)) return
    
      // Запретить цифры
      if (event.code.startsWith('Digit')) {
        event.preventDefault()
      }
    })

    Типичные ошибки

    Ошибка 1: используют event.key для горячих клавиш — ломается при смене раскладки

    // Сломано: при русской раскладке 's' становится 'ы' — Ctrl+S не работает
    if (event.ctrlKey && event.key === 's') {
      saveDocument()
    }
    
    // Исправлено: event.code не зависит от раскладки
    if (event.ctrlKey && event.code === 'KeyS') {
      saveDocument()
    }

    Ошибка 2: не предотвращают стандартное поведение браузера

    // Сломано: Ctrl+S открывает диалог "Сохранить страницу" браузера
    document.addEventListener('keydown', (e) => {
      if (e.ctrlKey && e.code === 'KeyS') {
        saveDocument()  // браузер тоже сохраняет!
      }
    })
    
    // Исправлено:
    if (e.ctrlKey && e.code === 'KeyS') {
      e.preventDefault()  // сначала отменить стандартное
      saveDocument()
    }

    Ошибка 3: не обрабатывают event.repeat при удержании клавиши

    // Сломано: при удержании клавиши функция вызывается сотни раз
    document.addEventListener('keydown', (e) => {
      if (e.code === 'Space') {
        shootBullet()  // выстрелит сотни раз!
      }
    })
    
    // Исправлено: проверяем repeat
    document.addEventListener('keydown', (e) => {
      if (e.code === 'Space' && !e.repeat) {
        shootBullet()  // только первое нажатие
      }
    })

    В реальных проектах

  • Текстовые редакторы (CodeMirror, Monaco): сотни горячих клавиш через реестр
  • Браузерные игры: Set нажатых клавиш для плавного движения персонажа
  • Формы: запрет ввода букв в числовые поля, валидация в реальном времени
  • Accessibility: навигация по интерфейсу с клавиатуры (Tab, Enter, Escape, стрелки)
  • Примеры

    Реестр горячих клавиш: Ctrl+S сохранить, Ctrl+Z отменить — через мок-события клавиатуры

    // Реестр горячих клавиш без DOM
    // Моделируем KeyboardEvent-объекты и обрабатываем их
    
    function buildHotkeyString(event) {
      const parts = []
      if (event.ctrlKey)  parts.push('Ctrl')
      if (event.shiftKey) parts.push('Shift')
      if (event.altKey)   parts.push('Alt')
      if (event.metaKey)  parts.push('Meta')
      parts.push(event.code)
      return parts.join('+')
    }
    
    class HotkeyRegistry {
      constructor() {
        this._hotkeys = new Map()
        this._history = []  // лог выполненных команд для демонстрации
      }
    
      register(combo, description, action) {
        this._hotkeys.set(combo, { description, action })
        return this
      }
    
      handle(event) {
        if (event.repeat) return 'repeat — ignored'
        const key = buildHotkeyString(event)
        const entry = this._hotkeys.get(key)
        if (!entry) return `no handler for "${key}"`
        const result = entry.action()
        this._history.push({ key, description: entry.description, result })
        return `executed: ${entry.description}`
      }
    
      getHistory() { return this._history }
    }
    
    // Создаём реестр и регистрируем горячие клавиши
    const registry = new HotkeyRegistry()
    
    // Симуляция состояния приложения
    const appState = {
      document: 'Привет, мир! Это тестовый документ.',
      history: [],
      saved: false,
    }
    
    registry
      .register('Ctrl+KeyS', 'Сохранить документ', () => {
        appState.saved = true
        return `сохранено: "${appState.document.slice(0, 20)}..."`
      })
      .register('Ctrl+KeyZ', 'Отменить действие', () => {
        const prev = appState.history.pop()
        if (prev) { appState.document = prev; return 'откат выполнен' }
        return 'нечего отменять'
      })
      .register('Ctrl+Shift+KeyZ', 'Повторить действие', () => {
        return 'повтор выполнен'
      })
      .register('Ctrl+KeyA', 'Выделить всё', () => {
        return `выделено ${appState.document.length} символов`
      })
      .register('Ctrl+KeyC', 'Копировать', () => {
        return `скопировано в буфер`
      })
    
    // Симулируем нажатия клавиш (мок-события)
    const mockKeyEvents = [
      { ctrlKey: true,  shiftKey: false, altKey: false, metaKey: false, code: 'KeyA', repeat: false },
      { ctrlKey: true,  shiftKey: false, altKey: false, metaKey: false, code: 'KeyC', repeat: false },
      { ctrlKey: true,  shiftKey: false, altKey: false, metaKey: false, code: 'KeyS', repeat: false },
      { ctrlKey: true,  shiftKey: false, altKey: false, metaKey: false, code: 'KeyZ', repeat: false },
      { ctrlKey: true,  shiftKey: true,  altKey: false, metaKey: false, code: 'KeyZ', repeat: false },
      { ctrlKey: false, shiftKey: false, altKey: false, metaKey: false, code: 'KeyA', repeat: false },
      { ctrlKey: true,  shiftKey: false, altKey: false, metaKey: false, code: 'KeyS', repeat: true  }, // автоповтор
    ]
    
    console.log('=== Обработка горячих клавиш ===')
    mockKeyEvents.forEach(event => {
      const combo = buildHotkeyString(event) + (event.repeat ? ' [repeat]' : '')
      const result = registry.handle(event)
      console.log(`${combo.padEnd(25)} → ${result}`)
    })
    
    console.log('\n=== История выполненных команд ===')
    registry.getHistory().forEach(entry => {
      console.log(`[${entry.key}] ${entry.description}: ${entry.result}`)
    })
    
    // Демонстрация отслеживания нескольких клавиш
    console.log('\n=== Несколько клавиш одновременно (игровой паттерн) ===')
    const pressedKeys = new Set()
    
    const gameEvents = [
      { type: 'keydown', code: 'ArrowUp' },
      { type: 'keydown', code: 'ArrowRight' },
      { type: 'keyup',   code: 'ArrowUp' },
      { type: 'keyup',   code: 'ArrowRight' },
    ]
    
    gameEvents.forEach(event => {
      if (event.type === 'keydown') pressedKeys.add(event.code)
      else pressedKeys.delete(event.code)
    
      const direction = []
      if (pressedKeys.has('ArrowUp'))    direction.push('вверх')
      if (pressedKeys.has('ArrowDown'))  direction.push('вниз')
      if (pressedKeys.has('ArrowLeft'))  direction.push('влево')
      if (pressedKeys.has('ArrowRight')) direction.push('вправо')
    
      const move = direction.length ? direction.join('+') : 'стоим'
      console.log(`${event.type} ${event.code} → движение: ${move}`)
    })

    Задание

    Напиши функцию parseHotkey(event), которая принимает объект мок-события клавиатуры { ctrlKey, shiftKey, altKey, metaKey, key, code } и возвращает строку горячей клавиши в формате "Ctrl+Shift+S". Модификаторы должны идти в порядке: Ctrl, Shift, Alt, Meta, затем event.key (не code). Если модификатор не нажат — его не включать.

    Подсказка

    [event.ctrlKey && "Ctrl", event.shiftKey && "Shift", event.altKey && "Alt", event.metaKey && "Meta", event.key].filter(Boolean).join("+")

    Загружаем среду выполнения...
    Загружаем AI-помощника...