-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathAdd_kbd_sup_sub_shortcuts.user.js
315 lines (292 loc) · 13 KB
/
Add_kbd_sup_sub_shortcuts.user.js
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
// ==UserScript==
// @name StackExchange, Add kbd, sup, and sub shortcuts
// @description Adds buttons and keyboard shortcuts to add <kbd>, <sup>, <sub> tags, and more.
// @match *://*.askubuntu.com/*
// @match *://*.mathoverflow.net/*
// @match *://*.serverfault.com/*
// @match *://*.stackapps.com/*
// @match *://*.stackexchange.com/*
// @match *://*.stackoverflow.com/*
// @match *://*.superuser.com/*
// @exclude *://api.stackexchange.com/*
// @exclude *://blog.*.com/*
// @exclude *://chat.*.com/*
// @exclude *://data.stackexchange.com/*
// @exclude *://elections.stackexchange.com/*
// @exclude *://openid.stackexchange.com/*
// @exclude *://stackexchange.com/*
// @exclude *://*/review
// @grant none
// @version 4.3
// @history 4.3 Fix to work with throwback page layout of the user's "Edit your profile" page.
// @history 4.2 Speeded icon add when user elects to answer their own question.
// @history 4.1 Restored icons after SE layout changes; Added checks for layout changes; Code tweaks.
// @history 4.0 Refactor à la MVC, in prep for options dialog; Fix double markup on slow page loads; Add multi-word split for <kbd>.
// @history 3.0 SE changed positioning; Added hover highlites; Added <br>; Added <del>; Clear JSHint warnings.
// @history 2.3 Add mathoverflow.net.
// @history 2.2 Update test and minor text formatting.
// @history 2.1 No point in injecting the script anymore, due to Chrome and Firefox changes.
// @history 2.0 Update for SE changes (jQuery version, esp.), Added <sup> and <sub> support. Moved to GitHub proper.
// @history 1.2 SSL support
// @history 1.1 Style tweak
// @history 1.0 Standardized wrap logic to same as SE markup
// @history 1.0 Initial release on GitHubGist
// @author Brock Adams
// @homepage http://stackapps.com/q/3341/7653
// @updateURL https://github.com/BrockA/SE-misc/raw/master/Add_kbd_sup_sub_shortcuts.user.js
// ==/UserScript==
/* global $, StackExchange */
/* eslint-disable no-multi-spaces, curly */
var rootNode = $("#content");
var scConfig = [
// titleName, tagText, btnText, bSoloTag, bNotTag, keyTxt, keyCode, kbModifiers (Alt/Ctrl/Shift), kbModArry, bWrapByWord
// 0 1 2 3 4 5 6 7 8 9
["Keyboard", "kbd", "<kbd>kb</kbd>", false, false, "K", 75, ["Alt"], [], true],
["Superscript", "sup", "<sup>sup</sup>", false, false, "↑", 38, ["Alt"], [], false], // Up arrow
["Subscript", "sub", "<sub>sub</sub>", false, false, "↓", 40, ["Alt"], [], false], // Dwn arrow
["Del/strike", "del", "<del>del</del>", false, false, "X", 88, ["Alt"], [], false],
["Break", "br", "↵", true, false, "B", 66, ["Alt"], [], false],
["em-space", " ", "↔", true, true, "M", 77, ["Alt"], [], false],
];
let targetKeyCodes = [];
let targetCssClasses = [];
$("textarea.wmd-input").each (AddOurButtonsAsNeeded);
rootNode.on ("focus", "textarea.wmd-input", AddOurButtonsAsNeeded);
rootNode.on ("keydown", "textarea.wmd-input", InsertOurTagByKeypress);
rootNode.on ("click", ".tmAdded", InsertOurTagByClick);
rootNode.on ("click", "#self-answer-popup > .popup-actions .popup-submit", () => {
$("textarea.wmd-input").each (AddOurButtonsAsNeeded);
} );
/*--- Pre-build button HTML. It's like:
<li class="wmd-button tmAdded wmd-kbd-button" title="Keyboard tag <kbd> Alt+K">
<span><kbd>kb</kbd></span>
</li>
for each new button.
*/
let btnsHtml = "";
for (let btn of scConfig) {
let btnClssTxt = btn[1].replace (/\W/g, "");
btnClssTxt = `wmd-${btnClssTxt}-button`;
let btnTtlDetail = btn[4] ? btn[1] : `<${btn[1]}>`;
let btnKeyHint = btn[7].join ('+') + `+${btn[5]}`;
targetCssClasses.push (btnClssTxt);
btnsHtml += `
<li class="wmd-button tmAdded ${btnClssTxt}" title="${btn[0]} ${btnTtlDetail} ${btnKeyHint}">
<span>${btn[2]}</span>
</li>
`;
}
//--- Compile keyboard modifiers and quick-check list.
for (let btn of scConfig) {
let btnMods = btn[7];
for (let kbMod of btnMods) {
switch (kbMod.toLowerCase() ) {
case "alt": btn[8].push ("altKey"); break;
case "ctrl": btn[8].push ("ctrlKey"); break;
case "shift": btn[8].push ("shiftKey"); break;
default:
console.warn (`***Userscript error: Illegal keyboard modifier: "${kbMod}"`);
break;
}
}
targetKeyCodes.push (btn[6]);
}
function AddOurButtonsAsNeeded () {
var jThis = $(this);
if ( ! jThis.data ("hasKbdBttn") ) {
//--- Find the button bar and add our buttons after the last, not help, button.
var btnBar = jThis.closest (".wmd-container").find (".wmd-button-bar");
if (btnBar.length) {
//--- The button bar takes a while to AJAX-in.
jThis.data ("loopSafety", 0);
var bbListTimer = setInterval ( () => {
var lpCnt = jThis.data ("loopSafety") + 1;
jThis.data ("loopSafety", lpCnt);
if (lpCnt > 100) { // 100 ~= 15 seconds
clearInterval (bbListTimer);
if (jThis.is(":visible") ) { // Avoid triggering on unused self-answer textarea.
console.warn (`***Userscript error: Unable to find the wmd-button-row.`, jThis);
}
}
var bbList = btnBar.find (".wmd-button-row");
if (bbList.length) {
clearInterval (bbListTimer);
if (jThis.data ("hasKbdBttn") ) return; // Guard against multiple timer overlap on slow pages.
let insrtPnt = bbList.find (".wmd-button").not (".wmd-help-button").last ();
insrtPnt.after (btnsHtml);
jThis.data ("hasKbdBttn", true);
}
}, 150);
}
else {
console.warn (`***Userscript error: Unable to find the button bar.`);
}
}
}
function InsertOurTagByKeypress (zEvent) {
//--- At least one modifier must be set
if ( !zEvent.altKey && !zEvent.ctrlKey && !zEvent.shiftKey) {
return true;
}
let J = targetKeyCodes.indexOf (zEvent.which);
if (J < 0) return true;
let btn = scConfig[J];
let matchesEvent = true;
for (let kbMod of btn[8]) {
if ( ! zEvent[kbMod] ) {
matchesEvent = false;
break;
}
}
if (matchesEvent) {
let newHTML = btn[4] ? btn[1] : `<${btn[1]}>`;
InsertOurTag (this, newHTML, btn[3], btn[9]);
return false;
}
//--- Ignore all other keys.
return true;
}
function InsertOurTagByClick () {
//--- From the clicked button, find the matching textarea.
var jThis = $(this);
var targArea = jThis.closest (".wmd-button-bar").nextAll (".js-stacks-validation").find ("textarea.wmd-input");
if (targArea.length === 0) {
//-- The "Edit your profile" page currently uses a different (mostly throwback) layout.
targArea = jThis.closest (".wmd-button-bar").next ("textarea.wmd-input");
if (targArea.length === 0) {
console.warn (`***Userscript error: Unable to find the textarea from button.`);
return;
}
}
for (let J in targetCssClasses) {
if (jThis.hasClass (targetCssClasses[J] ) ) {
let btn = scConfig[J];
let newHTML = btn[4] ? btn[1] : `<${btn[1]}>`;
InsertOurTag (targArea[0], newHTML, btn[3], btn[9]);
targArea.focus ();
try {
//--- This is a utility function that SE currently provides on its pages.
StackExchange.MarkdownEditor.refreshAllPreviews ();
}
catch (e) {
console.error ("***Userscript error: refreshAllPreviews() is no longer defined!", e);
}
break;
}
}
}
function InsertOurTag (node, tagTxt, bTagHasNoEnd, bWrapByWord) {
//--- Wrap selected text or insert at curser.
var tagLength = tagTxt.length;
var endTag = tagTxt.replace (/</, "</");
var unwrapRegex = new RegExp ('^' + tagTxt + '((?:.|\\n|\\r)*)' + endTag + '$');
var oldText = node.value || node.textContent;
var newText;
var iTargetStart = node.selectionStart;
var iTargetEnd = node.selectionEnd;
var selectedText = oldText.slice (iTargetStart, iTargetEnd);
var possWrappedTxt;
if (bTagHasNoEnd) {
newText = oldText.slice (0, iTargetStart) + tagTxt + oldText.slice (iTargetStart);
iTargetStart += tagLength;
iTargetEnd += tagLength;
}
else {
try {
//--- Lazyman's overrun checking...
possWrappedTxt = oldText.slice (iTargetStart - tagLength, iTargetEnd + tagLength + 1);
}
catch (e) {
possWrappedTxt = "Text can't be wrapped, cause we overran the string.";
}
/*--- Is the current selection wrapped? If so, just unwrap it.
This works the same way as SE's bold, italic, code, etc...
"]text[" --> "<sup>]text[</sup>"
"<sup>]text[</sup>" --> "]text["
"]<sup>text</sup>[" --> "<sup>]<sup>text</sup>[</sup>"
Except that:
"][" --> "<sup>][</sup>"
"<sup>][</sup>" --> "]["
with no placeholder text.
And (Wrap by Word Mode):
"]Shift P[" --> "<kbd>]Shift</kbd> <kbd>P[</kbd>"
"<kbd>]Shift</kbd> <kbd>P[</kbd>" --> "]Shift P["
And: No wrapping or unwrapping is done on tags with no end tag, nor on non-tag text.
Note that `]` and `[` denote the selected text here.
*/
if (possWrappedTxt &&
selectedText == possWrappedTxt.replace (unwrapRegex, "$1")
) {
let coreText = selectedText;
if (bWrapByWord) {
let strpRE = new RegExp (`${tagTxt}|${endTag}`, 'g');
coreText = coreText.replace (strpRE, "");
}
iTargetStart -= tagLength;
iTargetEnd += tagLength + 1;
newText = oldText.slice (0, iTargetStart) + coreText + oldText.slice (iTargetEnd);
iTargetEnd = iTargetStart + coreText.length;
}
else {
/*--- Here we will wrap the selection in our tags, but there is one extra
condition. We don't want to wrap leading or trailing whitespace.
*/
var trimSelctd = selectedText.match (/^(\s*)(\S?(?:.|\n|\r)*\S)(\s*)$/) || ["", "", "", ""];
if (trimSelctd.length != 4) {
console.warn ("***Userscript error: unexpected failure of whitespace RE.");
}
else {
let wrappedText = tagTxt + trimSelctd[2] + endTag;
if (bWrapByWord && trimSelctd[2].length) {
let pieces = trimSelctd[2].split (/(\W+)/);
wrappedText = "";
for (let piece of pieces) {
if (piece.length && /\w/.test (piece[0]) )
wrappedText += tagTxt + piece + endTag;
else
wrappedText += piece;
}
}
newText = trimSelctd[1] //-- Leading whitespace, if any.
+ wrappedText
+ trimSelctd[3] //-- Trailing whitespace, if any.
;
newText = oldText.slice (0, iTargetStart)
+ newText + oldText.slice (iTargetEnd)
;
iTargetStart += tagLength + trimSelctd[1].length;
iTargetEnd += wrappedText.length - endTag.length - trimSelctd[2].length - trimSelctd[3].length;
}
}
}
node.value = newText;
//--- After using spelling corrector, this gets buggered, hence the multiple sets.
node.textContent = newText;
//--- Have to reset the selection, since we overwrote the text.
node.selectionStart = iTargetStart;
node.selectionEnd = iTargetEnd;
}
//--- Touch up styles...
var newStyle = document.createElement ('style');
newStyle.textContent = `
.tmAdded > span {
background-image: none;
}
.tmAdded:hover {
color: orange;
}
.wmd-kbd-button {
margin-right: 1ex;
}
.wmd-kbd-button > span > kbd {
border: 0px;
}
.wmd-kbd-button:hover > span > kbd {
background: orange;
}
.wmd-br-button > span {
font-size: 120%;
font-weight: bold;
}
`;
document.head.appendChild (newStyle);