-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtwitter-custom-keys.lib.js
More file actions
156 lines (128 loc) · 5.85 KB
/
Copy pathtwitter-custom-keys.lib.js
File metadata and controls
156 lines (128 loc) · 5.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// twitter-custom-keys.lib.js — Shared library for registering custom keyboard shortcuts
// @require'd by individual hotkey userscripts. Injects a "Custom" section into Twitter's
// built-in keyboard shortcuts dialog (opened with ?).
//
// Usage: window.__twitterCustomKeys.register(key, description)
(function () {
'use strict';
// Singleton guard — only the first script to load initializes
if (window.__twitterCustomKeys) return;
const entries = [];
const SECTION_ID = 'tm-custom-keys-section';
window.__twitterCustomKeys = {
register(key, description) {
entries.push({ key, description });
},
isTyping(event) {
const isEditableElement = (el) => {
if (!(el instanceof Element)) return false;
if (el.matches('textarea, select, [role="textbox"], [role="searchbox"], [aria-multiline="true"]')) return true;
if (el.matches('input:not([type]), input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="tel"], input[type="password"], input[type="number"]')) return true;
if (el.isContentEditable) return true;
if (el.closest('[contenteditable]:not([contenteditable="false"])')) return true;
return false;
};
if (event?.isComposing) return true;
const path = typeof event?.composedPath === 'function' ? event.composedPath() : [];
for (const node of path) {
if (isEditableElement(node)) return true;
}
let active = document.activeElement;
while (active?.shadowRoot?.activeElement) {
active = active.shadowRoot.activeElement;
}
return isEditableElement(active);
}
};
// Find the container holding all shortcut sections.
// Desktop: [role="dialog"] > ... > [data-viewportview="true"] > sections
// Mobile: <main> > ... > scrollable div > sections
// Identified by the h2#modal-header heading near [role="table"] elements.
// Uses structural markers (id, roles) instead of text to support all languages.
function findSectionsContainer() {
const header = document.getElementById('modal-header');
if (!header) return null;
// Walk up from the header to find the ancestor containing [role="table"] sections
let el = header.parentElement;
while (el) {
if (el.querySelector('[role="table"]')) return el;
el = el.parentElement;
}
return null;
}
function renderSection(container) {
// Remove previous render if any
const old = document.getElementById(SECTION_ID);
if (old) old.remove();
if (entries.length === 0) return;
// Find an existing section to clone structure from.
// Each section wraps a heading + [role="table"]. The section is the
// table's parent (which may have varying CSS classes across views).
const existingTable = container.querySelector('[role="table"]');
if (!existingTable) return;
const existingRow = existingTable.querySelector('[role="row"]');
if (!existingRow) return;
const existingSection = existingTable.parentElement;
if (!existingSection) return;
// Clone the entire section as our template
const section = existingSection.cloneNode(true);
section.id = SECTION_ID;
// Update the heading text to "Custom"
const headingSpan = section.querySelector('h2[role="heading"] span');
if (headingSpan) {
headingSpan.textContent = 'Custom';
}
// Get reference to the table, clear its rows, and rebuild
const table = section.querySelector('[role="table"]');
table.innerHTML = '';
for (const { key, description } of entries) {
// Clone a row from the original dialog for correct classes
const row = existingRow.cloneNode(true);
// First cell = description
const cells = row.querySelectorAll('[role="cell"]');
const descCell = cells[0];
const keyCell = cells[1];
// Set description text
const descSpan = descCell.querySelector('span');
if (descSpan) {
descSpan.textContent = description;
} else {
descCell.textContent = description;
}
// Set key — clear existing content and rebuild from a single-key template
keyCell.innerHTML = '';
const existingKeyDiv = existingRow.querySelector('[role="cell"]:last-child > div');
if (existingKeyDiv) {
const keyDiv = existingKeyDiv.cloneNode(true);
keyDiv.textContent = key;
keyCell.appendChild(keyDiv);
} else {
keyCell.textContent = key;
}
table.appendChild(row);
}
// Force onto its own row in the desktop flex layout (which assumes 3 columns)
section.style.flexBasis = '100%';
// Append after the last existing section
existingSection.parentElement.appendChild(section);
}
function checkForShortcutsView() {
// Already injected
if (document.getElementById(SECTION_ID)) return;
const container = findSectionsContainer();
if (container) {
renderSection(container);
}
}
// Watch for shortcuts view appearance (dialog on desktop, page on mobile)
const observer = new MutationObserver(checkForShortcutsView);
function startObserver() {
observer.observe(document.body, { childList: true, subtree: true });
}
if (document.body) {
startObserver();
} else {
document.addEventListener('DOMContentLoaded', startObserver);
}
console.log('[CustomKeys] Shared library loaded');
})();