Skip to content

Commit 6b0b119

Browse files
authored
Merge pull request #66 from andrelsm/andrelsm-goto-plugin
GoTo plugin
2 parents 2f38815 + 4c7c239 commit 6b0b119

File tree

4 files changed

+249
-1
lines changed

4 files changed

+249
-1
lines changed

code-input.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,23 @@ export namespace plugins {
110110
constructor(delayMs: number);
111111
}
112112

113+
/**
114+
* Add basic Go-To-Line (ctrl-G by default) functionality to the code editor.
115+
* Files: go-to-line.js / go-to-line.css
116+
*/
117+
class GoToLine extends Plugin {
118+
/**
119+
* Create a go-to-line command plugin to pass into a template
120+
* @param {boolean} useCtrlG Should Ctrl+G be overriden for go-to-line functionality? If not, you can trigger it yourself using (instance of this plugin)`.showPrompt(code-input element)`.
121+
*/
122+
constructor(useCtrlG: boolean);
123+
/**
124+
* Show a search-like dialog prompting line number.
125+
* @param {codeInput.CodeInput} codeInput the `<code-input>` element.
126+
*/
127+
showPrompt(codeInput: CodeInput): void;
128+
}
129+
113130
/**
114131
* Adds indentation using the `Tab` key, and auto-indents after a newline, as well as making it
115132
* possible to indent/unindent multiple lines using Tab/Shift+Tab

plugins/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ Files: [debounce-update.js](./debounce-update.js)
2828

2929
[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/GRXyxzV)
3030

31+
### Go To Line
32+
Add a feature to go to a specific line when a line number is given (or column as well, in the format line no:column no) that appears when (optionally) Ctrl+G is pressed or when JavaScript triggers it.
33+
34+
Files: [go-to-line.js](./go-to-line.js) / [go-to-line.css](./go-to-line.css)
35+
36+
[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/YzBMOXP)
37+
3138
### Indent
3239
Adds indentation using the `Tab` key, and auto-indents after a newline, as well as making it possible to indent/unindent multiple lines using Tab/Shift+Tab. **Supports tab characters and custom numbers of spaces as indentation.**
3340

@@ -36,7 +43,7 @@ Files: [indent.js](./indent.js)
3643
[🚀 *CodePen Demo*](https://codepen.io/WebCoder49/pen/WNgdzar)
3744

3845
### Prism Line Numbers
39-
Allows code-input elements to be used with the Prism.js line-numbers plugin, as long as the code-input element or a parent element of it has the CSS class `line-numbers`. [Prism.js Plugin Docs](https://prismjs.com/plugins/line-numbers/)
46+
Allow code-input elements to be used with the Prism.js line-numbers plugin, as long as the code-input element or a parent element of it has the CSS class `line-numbers`. [Prism.js Plugin Docs](https://prismjs.com/plugins/line-numbers/)
4047

4148
Files: [prism-line-numbers.css](./prism-line-numbers.css) (NO JS FILE)
4249

plugins/go-to-line.css

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
@keyframes code-input_go-to_roll-in {
2+
0% {opacity: 0; transform: translateY(-34px);}
3+
100% {opacity: 1; transform: translateY(0px);}
4+
}
5+
6+
@keyframes code-input_go-to_roll-out {
7+
0% {opacity: 1; transform: translateY(0px);}
8+
100% {opacity: 0; transform: translateY(-34px);}
9+
}
10+
11+
.code-input_go-to_dialog {
12+
position: absolute;
13+
top: 0; right: 14px;
14+
height: 28px;
15+
padding: 6px;
16+
padding-top: 8px;
17+
border: solid 1px #00000044;
18+
background-color: white;
19+
border-radius: 6px;
20+
box-shadow: 0 .2em 1em .2em rgba(0, 0, 0, 0.16);
21+
animation: code-input_go-to_roll-in .2s;
22+
z-index: 10;
23+
}
24+
25+
.code-input_go-to_dialog.bye {
26+
animation: code-input_go-to_roll-out .2s;
27+
}
28+
29+
.code-input_go-to_dialog input::placeholder {
30+
font-size: 80%;
31+
}
32+
33+
.code-input_go-to_dialog input {
34+
position: relative;
35+
width: 240px; height: 32px; top: -3px;
36+
font-size: large;
37+
color: #000000aa;
38+
border: 0;
39+
}
40+
41+
.code-input_go-to_dialog input.error {
42+
color: #ff0000aa;
43+
}
44+
45+
.code-input_go-to_dialog input:focus {
46+
outline: none;
47+
}
48+
49+
.code-input_go-to_dialog span {
50+
display: inline-block;
51+
width: 24px;
52+
line-height: 24px;
53+
font-family: system-ui;
54+
font-size: 22px;
55+
font-weight: 500;
56+
text-align: center;
57+
border-radius: 50%;
58+
color: black;
59+
opacity: 0.6;
60+
vertical-align: top;
61+
}
62+
63+
.code-input_go-to_dialog span:before {
64+
content: "\00d7";
65+
}
66+
67+
.code-input_go-to_dialog span:hover {
68+
opacity: .8;
69+
background-color: #00000018;
70+
}

plugins/go-to-line.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* Add basic Go-To-Line (Ctrl+G by default) functionality to the code editor.
3+
* Files: go-to-line.js / go-to-line.css
4+
*/
5+
codeInput.plugins.GoToLine = class extends codeInput.Plugin {
6+
7+
/**
8+
* Create a go-to-line command plugin to pass into a template
9+
* @param {boolean} useCtrlG Should Ctrl+G be overriden for go-to-line functionality? If not, you can trigger it yourself using (instance of this plugin)`.showPrompt(code-input element)`.
10+
*/
11+
constructor(useCtrlG) {
12+
super([]); // No observed attributes
13+
}
14+
15+
/* Add keystroke events */
16+
afterElementsAdded(codeInput) {
17+
const textarea = codeInput.textareaElement;
18+
textarea.addEventListener('keydown', (event) => { this.checkCtrlG(codeInput, event); });
19+
}
20+
21+
blockSearch(dialog, event) {
22+
if (event.ctrlKey && event.key == 'g') {
23+
return event.preventDefault();
24+
}
25+
}
26+
27+
checkPrompt(dialog, event) {
28+
// Line number(:column number)
29+
const lines = dialog.textarea.value.split('\n');
30+
const maxLineNo = lines.length;
31+
const lineNo = Number(dialog.input.value.split(':')[0]);
32+
let columnNo = 0; // Means go to start of indented line
33+
let maxColumnNo = 1;
34+
const querySplitByColons = dialog.input.value.split(':');
35+
if(querySplitByColons.length > 2) return dialog.input.classList.add('error');
36+
37+
if(querySplitByColons.length >= 2) {
38+
columnNo = Number(querySplitByColons[1]);
39+
maxColumnNo = lines[lineNo-1].length;
40+
}
41+
42+
if (event.key == 'Escape') return this.cancelPrompt(dialog, event);
43+
44+
if (dialog.input.value) {
45+
if (!/^[0-9:]*$/.test(dialog.input.value) || lineNo < 1 || columnNo < 0 || lineNo > maxLineNo || columnNo > maxColumnNo) {
46+
return dialog.input.classList.add('error');
47+
} else {
48+
dialog.input.classList.remove('error');
49+
}
50+
}
51+
52+
if (event.key == 'Enter') {
53+
this.goTo(dialog.textarea, lineNo, columnNo);
54+
this.cancelPrompt(dialog, event);
55+
}
56+
}
57+
58+
cancelPrompt(dialog, event) {
59+
let delay;
60+
event.preventDefault();
61+
dialog.textarea.focus();
62+
63+
// Remove dialog after animation
64+
dialog.classList.add('bye');
65+
66+
if (dialog.computedStyleMap) {
67+
delay = 1000 * dialog.computedStyleMap().get('animation').toString().split('s')[0];
68+
} else {
69+
delay = 1000 * document.defaultView.getComputedStyle(dialog, null).getPropertyValue('animation').split('s')[0];
70+
}
71+
72+
setTimeout(() => { dialog.codeInput.removeChild(dialog); }, .9 * delay);
73+
}
74+
75+
/**
76+
* Show a search-like dialog prompting line number.
77+
* @param {codeInput.CodeInput} codeInput the `<code-input>` element.
78+
*/
79+
showPrompt(codeInput) {
80+
const textarea = codeInput.textareaElement;
81+
82+
const dialog = document.createElement('div');
83+
const input = document.createElement('input');
84+
const cancel = document.createElement('span');
85+
86+
dialog.appendChild(input);
87+
dialog.appendChild(cancel);
88+
89+
dialog.className = 'code-input_go-to_dialog';
90+
input.spellcheck = false;
91+
input.placeholder = "Line:Column / Line no. then Enter";
92+
dialog.codeInput = codeInput;
93+
dialog.textarea = textarea;
94+
dialog.input = input;
95+
96+
input.addEventListener('keydown', (event) => { this.blockSearch(dialog, event); });
97+
input.addEventListener('keyup', (event) => { this.checkPrompt(dialog, event); });
98+
cancel.addEventListener('click', (event) => { this.cancelPrompt(dialog, event); });
99+
100+
codeInput.appendChild(dialog);
101+
102+
input.focus();
103+
}
104+
105+
/* Set the cursor on the first non-space char of textarea's nth line; and scroll it into view */
106+
goTo(textarea, lineNo, columnNo = 0) {
107+
let fontSize;
108+
let lineHeight;
109+
let scrollAmount;
110+
let topPadding;
111+
let cursorPos = -1;
112+
let lines = textarea.value.split('\n');
113+
114+
if (lineNo > 0 && lineNo <= lines.length) {
115+
if (textarea.computedStyleMap) {
116+
fontSize = textarea.computedStyleMap().get('font-size').value;
117+
lineHeight = fontSize * textarea.computedStyleMap().get('line-height').value;
118+
} else {
119+
fontSize = document.defaultView.getComputedStyle(textarea, null).getPropertyValue('font-size').split('px')[0];
120+
lineHeight = document.defaultView.getComputedStyle(textarea, null).getPropertyValue('line-height').split('px')[0];
121+
}
122+
123+
// scroll amount and initial top padding (3 lines above, if possible)
124+
scrollAmount = (lineNo > 3 ? lineNo - 3 : 1) * lineHeight;
125+
topPadding = (lineHeight - fontSize) / 2;
126+
127+
if (lineNo > 1) {
128+
// cursor positon just after n - 1 full lines
129+
cursorPos = lines.slice(0, lineNo - 1).join('\n').length;
130+
}
131+
132+
// scan first non-space char in nth line
133+
if (columnNo == 0) {
134+
do cursorPos++; while (textarea.value[cursorPos] != '\n' && /\s/.test(textarea.value[cursorPos]));
135+
} else {
136+
cursorPos += 1 + columnNo - 1;
137+
}
138+
139+
textarea.scrollTop = scrollAmount - topPadding;
140+
textarea.setSelectionRange(cursorPos, cursorPos);
141+
textarea.click();
142+
}
143+
}
144+
145+
/* Event handlers */
146+
checkCtrlG(codeInput, event) {
147+
const textarea = codeInput.textareaElement;
148+
if (event.ctrlKey && event.key == 'g') {
149+
event.preventDefault();
150+
151+
this.showPrompt(codeInput);
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)