The Scope class represents a keyboard event scope that receives keyboard events and binds callbacks to hotkeys. Only one scope is active at a time, but scopes can inherit hotkeys from parent scopes.
Overview
Scopes are used throughout Obsidian to manage keyboard input:
- The main app has a global scope
- Modals create their own scope when opened
- Custom UI elements can create scopes for context-specific hotkeys
- Scopes are managed in a stack via the Keymap class
Constructor
const scope = new Scope(parent?: Scope);
Optional parent scope to inherit hotkeys from
Example
// Create a scope with no parent
const scope = new Scope();
// Create a scope that inherits from app scope
const childScope = new Scope(this.app.scope);
Methods
register
Register a keyboard event handler in this scope.
register(
modifiers: Modifier[] | null,
key: string | null,
func: KeymapEventListener
): KeymapEventHandler
modifiers
Modifier[] | null
required
Array of modifiers (‘Mod’, ‘Ctrl’, ‘Meta’, ‘Shift’, ‘Alt’) or null to match any modifiers
func
KeymapEventListener
required
Callback function that will be called when the hotkey is triggered. Return false to automatically call preventDefault()
A handler reference that can be used to unregister the hotkey
Examples
Basic Hotkey Registration
const scope = new Scope();
// Register Mod+K
scope.register(['Mod'], 'K', (evt, ctx) => {
console.log('Mod+K pressed');
new Notice('Hotkey triggered!');
return false; // preventDefault
});
// Register Mod+Shift+P
scope.register(['Mod', 'Shift'], 'P', () => {
console.log('Command palette shortcut');
return false;
});
Catch-All Handlers
// Catch any key press (no modifiers)
scope.register(null, null, (evt, ctx) => {
console.log('Any key pressed:', ctx.vkey);
// Don't return false - let other handlers process it
});
// Catch any key with Mod pressed
scope.register(['Mod'], null, (evt, ctx) => {
console.log('Mod + any key:', ctx.key);
});
Using Event Context
scope.register(['Mod'], 'Enter', (evt, ctx) => {
console.log('Key:', ctx.key);
console.log('Virtual key:', ctx.vkey);
console.log('Modifiers:', ctx.modifiers);
// Access the original keyboard event
if (evt.repeat) {
console.log('Key is being held down');
}
return false;
});
unregister
Remove a keyboard event handler from this scope.
unregister(handler: KeymapEventHandler): void
handler
KeymapEventHandler
required
The handler reference returned by register()
Example
const scope = new Scope();
// Register and keep the handler reference
const handler = scope.register(['Mod'], 'K', () => {
console.log('Handler called');
return false;
});
// Later, unregister the handler
scope.unregister(handler);
Working with Scopes
Activating and Deactivating Scopes
Scopes must be pushed onto the keymap stack to become active:
class MyPlugin extends Plugin {
customScope: Scope;
onload() {
this.customScope = new Scope();
this.customScope.register(['Mod'], 'K', () => {
new Notice('Custom hotkey!');
return false;
});
// Activate the scope
this.app.keymap.pushScope(this.customScope);
}
onunload() {
// Deactivate the scope
this.app.keymap.popScope(this.customScope);
}
}
Scope Inheritance
Scopes can inherit hotkeys from a parent scope:
class MyModal extends Modal {
onOpen() {
// Create a scope that inherits from the modal's scope
const customScope = new Scope(this.scope);
// Add custom hotkeys
customScope.register(['Mod'], 'Enter', () => {
this.submit();
return false;
});
customScope.register([], 'Escape', () => {
this.close();
return false;
});
// Push the custom scope
this.app.keymap.pushScope(customScope);
// Don't forget to pop it when done
this.onClose = () => {
this.app.keymap.popScope(customScope);
};
}
}
Modal and Component Scopes
Modals automatically have a scope that you can use:
class MyModal extends Modal {
onOpen() {
// The modal already has a scope available
this.scope.register(['Mod'], 'Enter', () => {
this.submit();
return false;
});
// Escape key is already handled by the modal
// but you can override it if needed
}
submit() {
// Handle form submission
this.close();
}
}
Complete Examples
class CustomInputModal extends Modal {
result: string = '';
onOpen() {
const { contentEl, scope } = this;
contentEl.createEl('h2', { text: 'Enter text' });
const inputEl = contentEl.createEl('input', {
type: 'text',
placeholder: 'Type something...'
});
// Focus the input
inputEl.focus();
// Register Mod+Enter to submit
scope.register(['Mod'], 'Enter', () => {
this.result = inputEl.value;
this.close();
return false;
});
// Register Escape to cancel
scope.register([], 'Escape', () => {
this.result = '';
this.close();
return false;
});
// Register arrow keys for navigation
scope.register([], 'ArrowUp', () => {
console.log('Up arrow pressed');
return false;
});
scope.register([], 'ArrowDown', () => {
console.log('Down arrow pressed');
return false;
});
}
}
Temporary Scope for Feature
class MyPlugin extends Plugin {
specialModeScope: Scope | null = null;
enableSpecialMode() {
if (this.specialModeScope) return;
// Create a new scope
this.specialModeScope = new Scope();
// Register special mode hotkeys
this.specialModeScope.register([], 'j', () => {
console.log('j - move down');
return false;
});
this.specialModeScope.register([], 'k', () => {
console.log('k - move up');
return false;
});
this.specialModeScope.register([], 'Escape', () => {
this.disableSpecialMode();
return false;
});
// Activate the scope
this.app.keymap.pushScope(this.specialModeScope);
new Notice('Special mode enabled (press Escape to exit)');
}
disableSpecialMode() {
if (!this.specialModeScope) return;
// Deactivate and clean up
this.app.keymap.popScope(this.specialModeScope);
this.specialModeScope = null;
new Notice('Special mode disabled');
}
}
Context-Sensitive Hotkeys
class MyPlugin extends Plugin {
onload() {
// Add to main app scope
this.app.scope.register(['Mod', 'Shift'], 'D', (evt) => {
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!activeView) {
new Notice('No active markdown view');
return false;
}
const editor = activeView.editor;
const selection = editor.getSelection();
if (selection) {
// Duplicate the selection
editor.replaceSelection(selection + '\n' + selection);
} else {
// Duplicate the line
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);
editor.replaceRange('\n' + line, { line: cursor.line, ch: 0 });
}
return false;
});
}
}
Best Practices
Always Clean Up Scopes
Remember to pop scopes when you’re done with them:
// Good
class MyFeature {
scope: Scope;
enable() {
this.scope = new Scope();
// Register handlers...
app.keymap.pushScope(this.scope);
}
disable() {
app.keymap.popScope(this.scope);
}
}
Use Appropriate Modifier Keys
Use 'Mod' for cross-platform compatibility:
// Good - works on all platforms
scope.register(['Mod'], 'K', handler);
// Bad - platform-specific
scope.register(['Ctrl'], 'K', handler);
Return false to preventDefault
Return false from your handler to automatically prevent default behavior:
scope.register(['Mod'], 'S', (evt, ctx) => {
// Handle save
saveDocument();
return false; // Prevents browser's save dialog
});
Use Modal/Component Scopes
When working with modals, use the built-in scope:
class MyModal extends Modal {
onOpen() {
// Use this.scope, don't create a new one
this.scope.register(['Mod'], 'Enter', () => {
this.submit();
return false;
});
}
}
See Also
- Keymap - For managing scope stacks
- Modal - Modals have built-in scopes
- Plugin - For registering global commands with hotkeys