Getting Started

Registrazione scorciatoie

Pattern di registrazione scorciatoie globali e contestuali, modificatori cross-platform ed esempi pratici.

Panoramica

Le scorciatoie si registrano tramite il composable useShortcuts, che mantiene un registro globale singleton. Una scorciatoia è identificata da id univoco e può combinare un tasto principale con i modificatori ctrl, alt, shift, meta.

Definizione di una scorciatoia

import type { Shortcut } from '@pzeta/vue-scorciatoie'

const shortcut: Shortcut = {
  id: 'salva-record',
  key: 's',
  ctrl: true,
  description: 'Salva il record corrente',
  handler: () => salvaRecord(),
  enabled: true,
  allowInInput: false,
}
CampoTipoDefaultDescrizione
idstring—Identificatore univoco. Una seconda registrazione con lo stesso id viene ignorata
keystring—Tasto principale (case-insensitive, es. 's', 'Enter', 'ArrowUp')
ctrlbooleanfalseRichiede Ctrl (Windows/Linux) o Cmd (macOS)
altbooleanfalseRichiede Alt (Windows/Linux) o Ctrl (macOS)
shiftbooleanfalseRichiede Shift
metabooleanfalseRichiede tasto Meta esplicito (Windows/Linux). Su macOS il matching meta è sostituito dalla logica BR-18
descriptionstring—Descrizione testuale per cheat-sheet o tooltip
handler() => void | Promise<void>—Funzione invocata quando la scorciatoia è premuta. Un errore, sincrono o asincrono, non interrompe il listener e viene loggato in console solo in sviluppo
enabledbooleantrueSe false, la scorciatoia è ignorata senza essere rimossa
allowInInputbooleanfalseSe false, la scorciatoia non scatta quando il focus è in un elemento editabile (vedi Comportamento in input)

Scorciatoie globali

Registrate in un componente di alto livello (es. App.vue o un layout) restano attive per l'intera vita dell'applicazione finché lo scope non viene disposto.

<script setup lang="ts">
import { useShortcuts } from '@pzeta/vue-scorciatoie'

useShortcuts([
  {
    id: 'vai-home',
    key: 'h',
    alt: true,
    description: 'Vai alla home',
    handler: () => router.push('/'),
  },
  {
    id: 'cerca-globale',
    key: '/',
    description: 'Focus barra di ricerca globale',
    handler: () => document.getElementById('search')?.focus(),
    allowInInput: false,
  },
])
</script>

Ctrl+K è riservato alla command palette: useCommandPalette lo registra da sé alla prima chiamata, e per aprirla da codice si usa il suo open().

Scorciatoie contestuali

Registrate in un componente specifico vengono rimosse automaticamente quando il componente è smontato, grazie a onScopeDispose interno.

<script setup lang="ts">
// Solo nella vista dettaglio anagrafica
import { useShortcuts } from '@pzeta/vue-scorciatoie'

useShortcuts([
  {
    id: 'anagrafica-salva',
    key: 's',
    ctrl: true,
    description: 'Salva anagrafica',
    handler: handleSave,
    allowInInput: true, // Anche se il focus è su un campo del form
  },
  {
    id: 'anagrafica-elimina',
    key: 'Delete',
    shift: true,
    description: 'Elimina anagrafica',
    handler: handleDelete,
  },
])
</script>

Cross-platform: BR-17 e BR-18

Le scorciatoie si definiscono sempre con la semantica Windows/Linux. Su macOS il matching adatta automaticamente:

DefinizioneWindows/LinuxmacOS
alt: trueAlt + keyCtrl + key (BR-17)
ctrl: trueCtrl + keyCmd + key (BR-18)

Su macOS il tasto Option fisico non deve essere premuto per evitare conflitti con i caratteri speciali Option+key. Una scorciatoia con meta: true su macOS non scatta mai, perché Cmd è già usato da ctrl (avviso in console in sviluppo).

Il comportamento si cambia con macOSMapping della configurazione globale:

ValoreMatching su macOS
'auto' (default)alt → Ctrl, ctrl → Cmd, come in tabella
'native'Diretto: alt = Option, ctrl = Ctrl, meta = Cmd
(event, shortcut) => booleanDecide una funzione del consumer
useShortcuts([
  {
    id: 'nuova-anagrafica',
    key: 'n',
    alt: true, // Alt+N su Windows/Linux, Ctrl+N su macOS
    description: 'Nuova anagrafica',
    handler: () => apriDialogNuova(),
  },
])

Per visualizzare correttamente il modificatore nell'interfaccia, usare il composable useOS e le sue label primaryModifierLabel / paletteModifierLabel.

Configurazione globale

Il secondo parametro di useShortcuts accetta una ShortcutConfig che viene fusa nello state globale:

useShortcuts(scorciatoie, {
  preventDefault: true,    // Default true
  stopPropagation: true,   // Default true
  enabled: ref(isEnabled), // Accetta boolean o Ref<boolean>
  macOSMapping: 'auto',    // Default 'auto' (vedi sopra)
})

La configurazione è applicata a tutte le scorciatoie registrate, compreso il Ctrl+K della command palette. enabled accetta un Ref<boolean>, letto a ogni tasto, per disabilitare temporaneamente l'intero sistema (es. quando una modale ha il focus).

Aggiungere e rimuovere dinamicamente

useShortcuts espone API imperativa per registrazioni dinamiche:

const { addShortcut, removeShortcut, shortcuts } = useShortcuts()

addShortcut({
  id: 'temporanea',
  key: 'F2',
  description: 'Rinomina',
  handler: rinomina,
})

// In un secondo momento
removeShortcut('temporanea')

// Lista corrente (readonly)
console.log(shortcuts.value.length)

Priorità ed ordine di matching

Quando un evento keydown corrisponde a più scorciatoie, viene invocato l'handler della prima trovata nell'array (ordine di registrazione). Il loop si interrompe al primo match: usa l'ordine di registrazione come priorità implicita. In sviluppo, registrare una combinazione già usata da un'altra scorciatoia produce un avviso in console.

Comportamento in input

Per default le scorciatoie sono ignorate quando il focus è su un elemento editabile: input, textarea, select, un elemento contenteditable, o un nodo dentro un antenato [contenteditable] o [role="textbox"] (gli editor rich-text come CodeMirror, Monaco, ProseMirror e TipTap). Imposta allowInInput: true per permettere il match anche durante la digitazione (es. salvataggio con Ctrl+S mentre si compila un form).

Esempi pratici

Salva con Ctrl+S, anche durante la digitazione

useShortcuts([
  {
    id: 'salva',
    key: 's',
    ctrl: true,
    allowInInput: true,
    description: 'Salva',
    handler: salvaForm,
  },
])

Annulla con Esc fuori dagli input

useShortcuts([
  {
    id: 'annulla',
    key: 'Escape',
    description: 'Chiudi pannello',
    handler: () => panelStore.close(),
  },
])

Disabilitazione condizionale

const { addShortcut, removeShortcut } = useShortcuts()

watch(modalAperta, (aperta) => {
  if (aperta) {
    addShortcut({
      id: 'modal-chiudi',
      key: 'Escape',
      description: 'Chiudi modal',
      handler: chiudiModal,
    })
  } else {
    removeShortcut('modal-chiudi')
  }
})