mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-21 11:28:12 +01:00
Merge 0cabec3cfa409ea67ff73063d11d87ff9dee61fb into 87f729190918e957b1d80c5e94c4e3ff440a387c
This commit is contained in:
commit
03424ea997
41
templates/devtest/keyboard-shortcut.tmpl
Normal file
41
templates/devtest/keyboard-shortcut.tmpl
Normal file
@ -0,0 +1,41 @@
|
||||
{{template "devtest/devtest-header"}}
|
||||
<div class="page-content devtest ui container">
|
||||
<h1>Keyboard Shortcut</h1>
|
||||
<p>
|
||||
A <code><kbd data-global-keyboard-shortcut="key"></code> element next to an <code><input></code> declares a keyboard shortcut.
|
||||
Pressing the key focuses the input. Pressing <kbd>Escape</kbd> clears and blurs it.
|
||||
The hint is hidden automatically when the input is focused or has a value.
|
||||
</p>
|
||||
|
||||
<h2>Input with "s" shortcut</h2>
|
||||
<div style="position: relative; display: inline-flex; align-items: center;">
|
||||
<input class="ui input" placeholder="Press S to focus" style="padding-right: 36px;">
|
||||
<kbd data-global-keyboard-shortcut="s" class="devtest-shortcut-hint">S</kbd>
|
||||
</div>
|
||||
|
||||
<h2>Input with "f" shortcut</h2>
|
||||
<div style="position: relative; display: inline-flex; align-items: center;">
|
||||
<input class="ui input" placeholder="Press F to focus" style="padding-right: 36px;">
|
||||
<kbd data-global-keyboard-shortcut="f" class="devtest-shortcut-hint">F</kbd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.devtest-shortcut-hint {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
color: var(--color-text-light-2);
|
||||
background-color: var(--color-box-body);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: inset 0 -1px 0 var(--color-secondary);
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
{{template "devtest/devtest-footer"}}
|
||||
@ -1,7 +1,9 @@
|
||||
<div class="repo-home-sidebar-top">
|
||||
<form class="ignore-dirty tw-flex tw-flex-1" action="{{.RepoLink}}/search" method="get">
|
||||
<div class="ui small action input tw-flex-1">
|
||||
<input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}"> {{template "shared/search/button"}}
|
||||
<div class="ui small action input tw-flex-1 repo-code-search-input-wrapper">
|
||||
<input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}" class="code-search-input" aria-keyshortcuts="s">
|
||||
<kbd data-global-keyboard-shortcut="s" class="repo-search-shortcut-hint" aria-hidden="true">S</kbd>
|
||||
{{template "shared/search/button"}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
137
tests/e2e/repo-shortcuts.test.e2e.ts
Normal file
137
tests/e2e/repo-shortcuts.test.e2e.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login_user, load_logged_in_context} from './utils_e2e.ts';
|
||||
|
||||
test.beforeAll(async ({browser}, workerInfo) => {
|
||||
await login_user(browser, workerInfo, 'user2');
|
||||
});
|
||||
|
||||
test.describe('Repository Keyboard Shortcuts', () => {
|
||||
test('T key focuses file search input', async ({browser}, workerInfo) => {
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
const page = await context.newPage();
|
||||
|
||||
// Navigate to a repository page with file listing
|
||||
await page.goto('/user2/repo1');
|
||||
|
||||
// Verify the file search input exists and has the keyboard hint
|
||||
const fileSearchInput = page.getByPlaceholder('Go to file');
|
||||
await expect(fileSearchInput).toBeVisible();
|
||||
|
||||
// Verify the keyboard hint is visible
|
||||
const kbdHint = page.locator('.repo-file-search-input-wrapper kbd');
|
||||
await expect(kbdHint).toBeVisible();
|
||||
await expect(kbdHint).toHaveText('T');
|
||||
|
||||
// Press T key to focus the file search input
|
||||
await page.keyboard.press('t');
|
||||
|
||||
// Verify the input is focused
|
||||
await expect(fileSearchInput).toBeFocused();
|
||||
});
|
||||
|
||||
test('T key does not trigger when typing in input', async ({browser}, workerInfo) => {
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
const page = await context.newPage();
|
||||
|
||||
// Navigate to a repository page
|
||||
await page.goto('/user2/repo1');
|
||||
|
||||
// Focus on file search first
|
||||
const fileSearchInput = page.getByPlaceholder('Go to file');
|
||||
await fileSearchInput.click();
|
||||
|
||||
// Type something including 't'
|
||||
await page.keyboard.type('test');
|
||||
|
||||
// Verify the input still has focus and contains the typed text
|
||||
await expect(fileSearchInput).toBeFocused();
|
||||
await expect(fileSearchInput).toHaveValue('test');
|
||||
});
|
||||
|
||||
test('S key focuses code search input on repo home', async ({browser}, workerInfo) => {
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
const page = await context.newPage();
|
||||
|
||||
// Navigate to repo home page where code search is available
|
||||
await page.goto('/user2/repo1');
|
||||
|
||||
// The code search input is in the sidebar
|
||||
const codeSearchInput = page.getByPlaceholder('Search code…');
|
||||
await expect(codeSearchInput).toBeVisible();
|
||||
|
||||
// Verify the keyboard hint is visible
|
||||
const kbdHint = page.locator('.repo-code-search-input-wrapper .repo-search-shortcut-hint');
|
||||
await expect(kbdHint).toBeVisible();
|
||||
await expect(kbdHint).toHaveText('S');
|
||||
|
||||
// Press S key to focus the code search input
|
||||
await page.keyboard.press('s');
|
||||
|
||||
// Verify the input is focused
|
||||
await expect(codeSearchInput).toBeFocused();
|
||||
});
|
||||
|
||||
test('File search keyboard hint hides when input has value', async ({browser}, workerInfo) => {
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
const page = await context.newPage();
|
||||
|
||||
// Navigate to a repository page
|
||||
await page.goto('/user2/repo1');
|
||||
|
||||
// Check file search kbd hint
|
||||
const fileSearchInput = page.getByPlaceholder('Go to file');
|
||||
const fileKbdHint = page.locator('.repo-file-search-input-wrapper kbd');
|
||||
|
||||
// Initially the hint should be visible
|
||||
await expect(fileKbdHint).toBeVisible();
|
||||
|
||||
// Focus and type in the file search
|
||||
await fileSearchInput.click();
|
||||
await page.keyboard.type('test');
|
||||
|
||||
// The hint should now be hidden (generic handler hides kbd on focus)
|
||||
await expect(fileKbdHint).toBeHidden();
|
||||
});
|
||||
|
||||
test('Code search keyboard hint hides when input has value', async ({browser}, workerInfo) => {
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
const page = await context.newPage();
|
||||
|
||||
// Navigate to a repository page
|
||||
await page.goto('/user2/repo1');
|
||||
|
||||
const codeSearchInput = page.getByPlaceholder('Search code…');
|
||||
await expect(codeSearchInput).toBeVisible();
|
||||
|
||||
const codeKbdHint = page.locator('.repo-code-search-input-wrapper .repo-search-shortcut-hint');
|
||||
|
||||
// Initially the hint should be visible
|
||||
await expect(codeKbdHint).toBeVisible();
|
||||
|
||||
// Focus and type in the code search
|
||||
await codeSearchInput.click();
|
||||
await page.keyboard.type('search');
|
||||
|
||||
// The hint should now be hidden
|
||||
await expect(codeKbdHint).toBeHidden();
|
||||
});
|
||||
|
||||
test('Shortcuts do not trigger with modifier keys', async ({browser}, workerInfo) => {
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
const page = await context.newPage();
|
||||
|
||||
// Navigate to a repository page
|
||||
await page.goto('/user2/repo1');
|
||||
|
||||
const fileSearchInput = page.getByPlaceholder('Go to file');
|
||||
|
||||
// Click somewhere else first to ensure nothing is focused
|
||||
await page.locator('body').click();
|
||||
|
||||
// Press Ctrl+T (should not focus file search - this is typically "new tab" in browsers)
|
||||
await page.keyboard.press('Control+t');
|
||||
|
||||
// The file search input should NOT be focused
|
||||
await expect(fileSearchInput).not.toBeFocused();
|
||||
});
|
||||
});
|
||||
@ -2064,3 +2064,41 @@ tbody.commit-list {
|
||||
.branch-selector-dropdown .scrolling.menu .loading-indicator {
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
/* Keyboard shortcut hint styles for repo search inputs */
|
||||
.repo-code-search-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.repo-code-search-input-wrapper input {
|
||||
padding-right: 32px !important;
|
||||
}
|
||||
|
||||
.repo-search-shortcut-hint {
|
||||
position: absolute;
|
||||
right: 40px; /* account for the search button */
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
color: var(--color-text-light-2);
|
||||
background-color: var(--color-box-body);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: inset 0 -1px 0 var(--color-secondary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Override Fomantic UI action input styles for file search - need high specificity */
|
||||
.repo-file-search-input-wrapper.ui.input input,
|
||||
.repo-file-search-input-wrapper.ui.input input:hover {
|
||||
border-right: 1px solid var(--color-input-border) !important;
|
||||
border-top-right-radius: var(--border-radius) !important;
|
||||
border-bottom-right-radius: var(--border-radius) !important;
|
||||
}
|
||||
|
||||
.repo-file-search-input-wrapper.ui.input input:focus {
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@ const allFiles = ref<string[]>([]);
|
||||
const selectedIndex = ref(0);
|
||||
const isLoadingFileList = ref(false);
|
||||
const hasLoadedFileList = ref(false);
|
||||
|
||||
const showPopup = computed(() => searchQuery.value.length > 0);
|
||||
|
||||
const filteredFiles = computed(() => {
|
||||
@ -45,8 +44,8 @@ const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.isComposing) return;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
clearSearch();
|
||||
nextTick(() => refElemInput.value.blur());
|
||||
return;
|
||||
}
|
||||
if (!searchQuery.value || filteredFiles.value.length === 0) return;
|
||||
@ -145,12 +144,14 @@ watch([searchQuery, filteredFiles], async () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="ui small input">
|
||||
<div class="ui small input repo-file-search-input-wrapper">
|
||||
<input
|
||||
ref="searchInput" :placeholder="placeholder" autocomplete="off"
|
||||
role="combobox" aria-autocomplete="list" :aria-expanded="searchQuery ? 'true' : 'false'"
|
||||
aria-keyshortcuts="t"
|
||||
@input="handleSearchInput" @keydown="handleKeyDown"
|
||||
>
|
||||
<kbd data-global-keyboard-shortcut="t" class="repo-file-search-shortcut-hint" aria-hidden="true">T</kbd>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
@ -183,6 +184,37 @@ watch([searchQuery, filteredFiles], async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.repo-file-search-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.repo-file-search-input-wrapper input {
|
||||
padding-right: 32px !important;
|
||||
border-right: 1px solid var(--color-input-border) !important;
|
||||
border-top-right-radius: var(--border-radius) !important;
|
||||
border-bottom-right-radius: var(--border-radius) !important;
|
||||
}
|
||||
|
||||
.repo-file-search-input-wrapper input:focus {
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.repo-file-search-shortcut-hint {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: inline-block;
|
||||
padding: 2px 5px;
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
color: var(--color-text-light-2);
|
||||
background-color: var(--color-box-body);
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-search-popup {
|
||||
position: absolute;
|
||||
background: var(--color-box-body);
|
||||
|
||||
148
web_src/js/features/repo-shortcuts.test.ts
Normal file
148
web_src/js/features/repo-shortcuts.test.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import {describe, test, expect, beforeEach, afterEach} from 'vitest';
|
||||
|
||||
// The keyboard shortcut mechanism is driven by global event delegation in observer.ts.
|
||||
// These tests set up the same event listeners to verify the behavior in isolation.
|
||||
|
||||
function setupKeyboardShortcutListeners() {
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
if (e.key === 'Escape' && target.matches('input, textarea, select')) {
|
||||
const kbd = target.parentElement?.querySelector<HTMLElement>('kbd[data-global-keyboard-shortcut]');
|
||||
if (kbd) {
|
||||
(target as HTMLInputElement).value = '';
|
||||
(target as HTMLInputElement).blur();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (target.matches('input, textarea, select') || target.isContentEditable) return;
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
|
||||
const key = e.key.toLowerCase();
|
||||
const escapedKey = CSS.escape(key);
|
||||
const kbd = document.querySelector<HTMLElement>(`kbd[data-global-keyboard-shortcut="${escapedKey}"]`);
|
||||
if (!kbd) return;
|
||||
|
||||
e.preventDefault();
|
||||
const input = kbd.parentElement?.querySelector<HTMLInputElement>('input, textarea, select');
|
||||
if (input) input.focus();
|
||||
});
|
||||
|
||||
document.addEventListener('focusin', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.matches('input, textarea, select')) return;
|
||||
const kbd = target.parentElement?.querySelector<HTMLElement>('kbd[data-global-keyboard-shortcut]');
|
||||
if (kbd) kbd.style.display = 'none';
|
||||
});
|
||||
|
||||
document.addEventListener('focusout', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.matches('input, textarea, select')) return;
|
||||
const kbd = target.parentElement?.querySelector<HTMLElement>('kbd[data-global-keyboard-shortcut]');
|
||||
if (kbd) kbd.style.display = (target as HTMLInputElement).value ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
||||
describe('Keyboard Shortcut Mechanism', () => {
|
||||
let input: HTMLInputElement;
|
||||
let kbd: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = `
|
||||
<div>
|
||||
<input placeholder="Search" type="text">
|
||||
<kbd data-global-keyboard-shortcut="s">S</kbd>
|
||||
</div>
|
||||
`;
|
||||
input = document.querySelector('input')!;
|
||||
kbd = document.querySelector('kbd')!;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
// Register listeners once for all tests (they persist across tests on document)
|
||||
setupKeyboardShortcutListeners();
|
||||
|
||||
test('Shortcut key focuses the associated input', () => {
|
||||
expect(document.activeElement).not.toBe(input);
|
||||
|
||||
document.body.dispatchEvent(new KeyboardEvent('keydown', {key: 's', bubbles: true}));
|
||||
|
||||
expect(document.activeElement).toBe(input);
|
||||
});
|
||||
|
||||
test('Kbd hint hides when input is focused', () => {
|
||||
expect(kbd.style.display).toBe('');
|
||||
|
||||
input.focus();
|
||||
|
||||
expect(kbd.style.display).toBe('none');
|
||||
});
|
||||
|
||||
test('Kbd hint shows when input is blurred with empty value', () => {
|
||||
input.focus();
|
||||
expect(kbd.style.display).toBe('none');
|
||||
|
||||
input.blur();
|
||||
|
||||
expect(kbd.style.display).toBe('');
|
||||
});
|
||||
|
||||
test('Kbd hint stays hidden when input is blurred with a value', () => {
|
||||
input.focus();
|
||||
input.value = 'test';
|
||||
|
||||
input.blur();
|
||||
|
||||
expect(kbd.style.display).toBe('none');
|
||||
});
|
||||
|
||||
test('Escape key clears and blurs the input', () => {
|
||||
input.focus();
|
||||
input.value = 'test';
|
||||
|
||||
const event = new KeyboardEvent('keydown', {key: 'Escape', bubbles: true});
|
||||
input.dispatchEvent(event);
|
||||
|
||||
expect(input.value).toBe('');
|
||||
expect(document.activeElement).not.toBe(input);
|
||||
});
|
||||
|
||||
test('Escape key shows kbd hint after clearing', () => {
|
||||
input.focus();
|
||||
input.value = 'test';
|
||||
expect(kbd.style.display).toBe('none');
|
||||
|
||||
const event = new KeyboardEvent('keydown', {key: 'Escape', bubbles: true});
|
||||
input.dispatchEvent(event);
|
||||
|
||||
expect(kbd.style.display).toBe('');
|
||||
});
|
||||
|
||||
test('Shortcut does not trigger with modifier keys', () => {
|
||||
document.body.dispatchEvent(new KeyboardEvent('keydown', {key: 's', ctrlKey: true, bubbles: true}));
|
||||
expect(document.activeElement).not.toBe(input);
|
||||
|
||||
document.body.dispatchEvent(new KeyboardEvent('keydown', {key: 's', metaKey: true, bubbles: true}));
|
||||
expect(document.activeElement).not.toBe(input);
|
||||
|
||||
document.body.dispatchEvent(new KeyboardEvent('keydown', {key: 's', altKey: true, bubbles: true}));
|
||||
expect(document.activeElement).not.toBe(input);
|
||||
});
|
||||
|
||||
test('Shortcut does not trigger when typing in another input', () => {
|
||||
// Add a second input without a shortcut
|
||||
const otherInput = document.createElement('input');
|
||||
document.body.append(otherInput);
|
||||
otherInput.focus();
|
||||
|
||||
const event = new KeyboardEvent('keydown', {key: 's', bubbles: true});
|
||||
otherInput.dispatchEvent(event);
|
||||
|
||||
expect(document.activeElement).toBe(otherInput);
|
||||
expect(document.activeElement).not.toBe(input);
|
||||
});
|
||||
});
|
||||
@ -55,6 +55,13 @@ function callGlobalInitFunc(el: HTMLElement) {
|
||||
func(el);
|
||||
}
|
||||
|
||||
function initKeyboardShortcutKbd(kbd: HTMLElement) {
|
||||
// Handle initial state: hide the kbd hint if the associated input already has a value
|
||||
// (e.g., from browser autofill or back/forward navigation cache)
|
||||
const input = kbd.parentElement?.querySelector<HTMLInputElement>('input, textarea, select');
|
||||
if (input?.value) kbd.style.display = 'none';
|
||||
}
|
||||
|
||||
function attachGlobalEvents() {
|
||||
// add global "[data-global-click]" event handler
|
||||
document.addEventListener('click', (e) => {
|
||||
@ -65,6 +72,60 @@ function attachGlobalEvents() {
|
||||
if (!func) throw new Error(`Global event function "click:${funcName}" not found`);
|
||||
func(elem, e);
|
||||
});
|
||||
|
||||
// add global "kbd[data-global-keyboard-shortcut]" event handlers
|
||||
// A <kbd> element next to an <input> declares a keyboard shortcut for that input.
|
||||
// When the matching key is pressed, the sibling input is focused.
|
||||
// When Escape is pressed inside such an input, the input is cleared and blurred.
|
||||
// The <kbd> element is shown/hidden automatically based on input focus and value.
|
||||
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Handle Escape: clear and blur inputs that have an associated keyboard shortcut
|
||||
if (e.key === 'Escape' && target.matches('input, textarea, select')) {
|
||||
const kbd = target.parentElement?.querySelector<HTMLElement>('kbd[data-global-keyboard-shortcut]');
|
||||
if (kbd) {
|
||||
(target as HTMLInputElement).value = '';
|
||||
(target as HTMLInputElement).blur();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't trigger shortcuts when typing in input fields or contenteditable areas
|
||||
if (target.matches('input, textarea, select') || target.isContentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't trigger shortcuts when modifier keys are pressed
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find kbd element with matching shortcut (case-insensitive), then focus its sibling input
|
||||
const key = e.key.toLowerCase();
|
||||
const escapedKey = CSS.escape(key);
|
||||
const kbd = document.querySelector<HTMLElement>(`kbd[data-global-keyboard-shortcut="${escapedKey}"]`);
|
||||
if (!kbd) return;
|
||||
|
||||
e.preventDefault();
|
||||
const input = kbd.parentElement?.querySelector<HTMLInputElement>('input, textarea, select');
|
||||
if (input) input.focus();
|
||||
});
|
||||
|
||||
// Toggle kbd shortcut hint visibility on input focus/blur
|
||||
document.addEventListener('focusin', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.matches('input, textarea, select')) return;
|
||||
const kbd = target.parentElement?.querySelector<HTMLElement>('kbd[data-global-keyboard-shortcut]');
|
||||
if (kbd) kbd.style.display = 'none';
|
||||
});
|
||||
|
||||
document.addEventListener('focusout', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.matches('input, textarea, select')) return;
|
||||
const kbd = target.parentElement?.querySelector<HTMLElement>('kbd[data-global-keyboard-shortcut]');
|
||||
if (kbd) kbd.style.display = (target as HTMLInputElement).value ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
||||
export function initGlobalSelectorObserver(perfTracer: InitPerformanceTracer | null): void {
|
||||
@ -74,6 +135,7 @@ export function initGlobalSelectorObserver(perfTracer: InitPerformanceTracer | n
|
||||
attachGlobalEvents();
|
||||
|
||||
selectorHandlers.push({selector: '[data-global-init]', handler: callGlobalInitFunc});
|
||||
selectorHandlers.push({selector: 'kbd[data-global-keyboard-shortcut]', handler: initKeyboardShortcutKbd});
|
||||
const observer = new MutationObserver((mutationList) => {
|
||||
const len = mutationList.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user