summaryrefslogtreecommitdiff
path: root/source/features/comment-fields-keyboard-shortcuts.tsx
blob: 1e5b21b324e7e3ebe8a1373df7dd36ad2a6453e9 (plain) (blame)
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
import select from 'select-dom';
import delegate from 'delegate-it';
import indentTextarea from 'indent-textarea';
import insertText from 'insert-text-textarea';
import features from '../libs/features';

const formattingCharacters = ['`', '\'', '"', '[', '(', '{', '*', '_', '~'];
const matchingCharacters = ['`', '\'', '"', ']', ')', '}', '*', '_', '~'];

// Element.blur() will reset the tab focus to the start of the document.
// This places it back next to the blurred field
export function blurAccessibly(field: HTMLElement): void {
	field.blur();

	const range = new Range();
	const selection = getSelection()!;
	const focusHolder = new Text();
	field.after(focusHolder);
	range.selectNodeContents(focusHolder);
	selection.removeAllRanges();
	selection.addRange(range);
	focusHolder.remove();
}

function init(): void {
	delegate<HTMLTextAreaElement, KeyboardEvent>('.js-comment-field, #commit-description-textarea', 'keydown', event => {
		const field = event.delegateTarget;

		// Don't do anything if the suggester box is active
		if (select.exists('.suggester:not([hidden])', field.form!)) {
			return;
		}

		if (event.key === 'Tab' && !event.shiftKey) {
			indentTextarea(field);
			event.preventDefault();
		} else if (event.key === 'Escape') {
			// Cancel buttons have different classes for inline comments and editable comments
			const cancelButton = select<HTMLButtonElement>(`
				.js-hide-inline-comment-form,
				.js-comment-cancel-button
			`, field.form!);

			// Cancel if there is a button, else blur the field
			if (cancelButton) {
				cancelButton.click();
			} else {
				blurAccessibly(field);
			}

			event.stopImmediatePropagation();
			event.preventDefault();
		} else if (event.key === 'ArrowUp' && field.value === '') {
			const currentConversationContainer = field.closest([
				'.js-inline-comments-container', // Current review thread container
				'.discussion-timeline', // Or just ALL the comments in issues
				'#all_commit_comments' // Single commit comments at the bottom
			].join())!;
			const lastOwnComment = select
				.all<HTMLDetailsElement>('.js-comment.current-user', currentConversationContainer)
				.reverse()
				.find(comment => {
					const collapsible = comment.closest('details');
					return !collapsible || collapsible.open;
				});

			if (lastOwnComment) {
				select<HTMLButtonElement>('.js-comment-edit-button', lastOwnComment)!.click();
				const closeCurrentField = field
					.closest('form')!
					.querySelector<HTMLButtonElement>('.js-hide-inline-comment-form');

				if (closeCurrentField) {
					closeCurrentField.click();
				}

				// Move caret to end of field
				requestAnimationFrame(() => {
					select<HTMLTextAreaElement>('.js-comment-field', lastOwnComment)!.selectionStart = Number.MAX_SAFE_INTEGER;
				});
			}
		} else if (formattingCharacters.includes(event.key) && !event.isComposing) {
			const [start, end] = [field.selectionStart, field.selectionEnd];

			// If `start` and `end` of selection are the same, then no text is selected
			if (start === end) {
				return;
			}

			event.preventDefault();

			const formattingChar = event.key;
			const selectedText = field.value.slice(start, end);
			const matchingEndChar = matchingCharacters[formattingCharacters.indexOf(formattingChar)];
			insertText(field, formattingChar + selectedText + matchingEndChar);

			// Keep the selection as it is, to be able to chain shortcuts
			field.setSelectionRange(start + 1, end + 1);
		}
	});
}

features.add({
	id: __featureName__,
	description: 'Adds various shortcuts to comment fields: `↑` `tab` `esc` and Markdown formatting shortcuts',
	screenshot: false,
	shortcuts: {
		'↑': 'Edit your last comment'
	},
	init
});