@franzzemen/brokenstock-shell 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1865,6 +1865,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
1865
1865
|
args: [{ providedIn: 'root' }]
|
|
1866
1866
|
}], ctorParameters: () => [] });
|
|
1867
1867
|
|
|
1868
|
+
/**
|
|
1869
|
+
* Elements that mean "the user is typing", where an unmodified shortcut must not fire.
|
|
1870
|
+
*
|
|
1871
|
+
* Kept as a set of tag names plus a `contenteditable` check rather than a
|
|
1872
|
+
* selector string, so it is greppable and so the reasoning stays attached to it.
|
|
1873
|
+
*/
|
|
1874
|
+
const TEXT_ENTRY_TAGS = new Set(['INPUT', 'TEXTAREA', 'SELECT']);
|
|
1868
1875
|
class KeyboardShortcutService {
|
|
1869
1876
|
#groups;
|
|
1870
1877
|
#activeGroupId;
|
|
@@ -1880,6 +1887,18 @@ class KeyboardShortcutService {
|
|
|
1880
1887
|
this.#focusService = inject(TerminalFocusService);
|
|
1881
1888
|
this.helpVisible = signal(false, /* @ts-ignore */
|
|
1882
1889
|
...(ngDevMode ? [{ debugName: "helpVisible" }] : /* istanbul ignore next */ []));
|
|
1890
|
+
/**
|
|
1891
|
+
* True while unmodified shortcuts are being suppressed — the user is typing,
|
|
1892
|
+
* or a dialog is open.
|
|
1893
|
+
*
|
|
1894
|
+
* Exposed because a screen binding BARE keys needs to tell the user whether the
|
|
1895
|
+
* next keystroke acts or types, and that indicator must read the service's own
|
|
1896
|
+
* dispatch state. A screen keeping its own boolean will eventually disagree
|
|
1897
|
+
* with the service, and an indicator that lies about whether a keystroke sells
|
|
1898
|
+
* a thousand shares is worse than no indicator at all.
|
|
1899
|
+
*/
|
|
1900
|
+
this.suppressed = signal(false, /* @ts-ignore */
|
|
1901
|
+
...(ngDevMode ? [{ debugName: "suppressed" }] : /* istanbul ignore next */ []));
|
|
1883
1902
|
this.activeShortcuts = computed(() => {
|
|
1884
1903
|
const groupId = this.#activeGroupId();
|
|
1885
1904
|
if (!groupId)
|
|
@@ -1894,11 +1913,44 @@ class KeyboardShortcutService {
|
|
|
1894
1913
|
this.#document.addEventListener('keydown', (event) => {
|
|
1895
1914
|
this.#handleKeydown(event);
|
|
1896
1915
|
});
|
|
1916
|
+
// ⚠ THE WRITE IS DEFERRED, AND THAT IS NOT AN OPTIMISATION.
|
|
1917
|
+
//
|
|
1918
|
+
// `element.focus()` dispatches `focusin` SYNCHRONOUSLY. Angular calls
|
|
1919
|
+
// `.focus()` from template-driven code — an autofocused input, the shell's
|
|
1920
|
+
// terminal reclaiming focus — so this listener can run in the middle of a
|
|
1921
|
+
// render, and a signal write there throws NG0600, "writing to signals is
|
|
1922
|
+
// not allowed while Angular renders the template".
|
|
1923
|
+
//
|
|
1924
|
+
// That throw shipped: it aborted the login transition, so the sign-in POST
|
|
1925
|
+
// returned 200 and the app still showed "not logged in", with every
|
|
1926
|
+
// authenticated request that followed answering 401. Nothing in the failure
|
|
1927
|
+
// pointed at the keyboard.
|
|
1928
|
+
//
|
|
1929
|
+
// A microtask runs after the render completes, which is soon enough for an
|
|
1930
|
+
// indicator a human reads and late enough to be outside the reactive
|
|
1931
|
+
// context. The value is compared first so an unchanged focus move writes
|
|
1932
|
+
// nothing at all.
|
|
1933
|
+
const refresh = () => {
|
|
1934
|
+
queueMicrotask(() => {
|
|
1935
|
+
const next = this.#isSuppressed();
|
|
1936
|
+
if (next !== this.suppressed())
|
|
1937
|
+
this.suppressed.set(next);
|
|
1938
|
+
});
|
|
1939
|
+
};
|
|
1940
|
+
this.#document.addEventListener('focusin', refresh, true);
|
|
1941
|
+
this.#document.addEventListener('focusout', refresh, true);
|
|
1897
1942
|
});
|
|
1898
1943
|
}
|
|
1899
1944
|
registerGroup(groupId, shortcuts) {
|
|
1900
1945
|
this.#groups.set(groupId, shortcuts);
|
|
1901
1946
|
this.#activeGroupId.set(groupId);
|
|
1947
|
+
// Same hazard as the focus listener above: a screen registers from
|
|
1948
|
+
// `ngOnInit`, which Angular may run inside a render.
|
|
1949
|
+
queueMicrotask(() => {
|
|
1950
|
+
const next = this.#isSuppressed();
|
|
1951
|
+
if (next !== this.suppressed())
|
|
1952
|
+
this.suppressed.set(next);
|
|
1953
|
+
});
|
|
1902
1954
|
}
|
|
1903
1955
|
unregisterGroup(groupId) {
|
|
1904
1956
|
this.#groups.delete(groupId);
|
|
@@ -1910,17 +1962,61 @@ class KeyboardShortcutService {
|
|
|
1910
1962
|
toggleHelp() {
|
|
1911
1963
|
this.helpVisible.update(v => !v);
|
|
1912
1964
|
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Would an unmodified shortcut be ignored right now?
|
|
1967
|
+
*
|
|
1968
|
+
* Two cases: a text-entry element has focus, or a dialog is open. Alt
|
|
1969
|
+
* combinations are exempt — they do not collide with typing, which is why
|
|
1970
|
+
* every binding in the app before this guard existed used one.
|
|
1971
|
+
*/
|
|
1972
|
+
#isSuppressed() {
|
|
1973
|
+
const active = this.#document.activeElement;
|
|
1974
|
+
if (active) {
|
|
1975
|
+
if (TEXT_ENTRY_TAGS.has(active.tagName))
|
|
1976
|
+
return true;
|
|
1977
|
+
if (active.isContentEditable)
|
|
1978
|
+
return true;
|
|
1979
|
+
}
|
|
1980
|
+
// Angular Material puts every dialog in this container. A shortcut firing
|
|
1981
|
+
// behind an open dialog acts on a screen the user cannot currently see.
|
|
1982
|
+
return this.#document.querySelector('.cdk-overlay-container .cdk-overlay-pane') !== null;
|
|
1983
|
+
}
|
|
1984
|
+
#matches(shortcut, event) {
|
|
1985
|
+
// The physical key when `code` is given, the produced character otherwise.
|
|
1986
|
+
const hit = shortcut.code ? shortcut.code === event.code : shortcut.key === event.key;
|
|
1987
|
+
if (!hit)
|
|
1988
|
+
return false;
|
|
1989
|
+
if (shortcut.modifier === 'Alt')
|
|
1990
|
+
return event.altKey;
|
|
1991
|
+
if (shortcut.modifier === 'Shift') {
|
|
1992
|
+
return event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
1993
|
+
}
|
|
1994
|
+
// Unmodified. `ctrl`/`alt`/`meta` are excluded because the page still
|
|
1995
|
+
// receives a keystroke carrying the digit when the browser handles
|
|
1996
|
+
// `ctrl`+1 as a tab switch — without this, switching tabs would also fire
|
|
1997
|
+
// whatever a bare `1` is bound to.
|
|
1998
|
+
return !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
1999
|
+
}
|
|
1913
2000
|
#handleKeydown(event) {
|
|
1914
2001
|
const activeId = this.#activeGroupId();
|
|
1915
2002
|
if (activeId) {
|
|
1916
2003
|
const shortcuts = this.#groups.get(activeId);
|
|
1917
2004
|
if (shortcuts) {
|
|
1918
|
-
const match = shortcuts.find(s =>
|
|
1919
|
-
if (s.modifier === 'Alt')
|
|
1920
|
-
return event.altKey && s.key === event.key;
|
|
1921
|
-
return !event.altKey && !event.ctrlKey && !event.metaKey && s.key === event.key;
|
|
1922
|
-
});
|
|
2005
|
+
const match = shortcuts.find(s => this.#matches(s, event));
|
|
1923
2006
|
if (match) {
|
|
2007
|
+
const alt = match.modifier === 'Alt';
|
|
2008
|
+
// Alt bindings are exempt from both guards below, so nothing that
|
|
2009
|
+
// existed before them changes behaviour: Alt does not collide with
|
|
2010
|
+
// typing, and holding Alt+ArrowDown to walk a list is wanted.
|
|
2011
|
+
if (!alt) {
|
|
2012
|
+
// A key held down repeats at roughly thirty a second. On a screen
|
|
2013
|
+
// where a bare key places an order, that is fifteen orders in half a
|
|
2014
|
+
// second, and a synchronous action path will run every one of them.
|
|
2015
|
+
if (event.repeat)
|
|
2016
|
+
return false;
|
|
2017
|
+
if (this.#isSuppressed())
|
|
2018
|
+
return false;
|
|
2019
|
+
}
|
|
1924
2020
|
event.preventDefault();
|
|
1925
2021
|
this.#zone.run(() => match.action());
|
|
1926
2022
|
return true;
|