Skip to content

GoTo plugin #66

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions code-input.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,23 @@ export namespace plugins {
constructor(delayMs: number);
}

/**
* Add basic Go-To-Line (ctrl-G by default) functionality to the code editor.
* Files: go-to-line.js / go-to-line.css
*/
class GoToLine extends Plugin {
/**
* Create a go-to-line command plugin to pass into a template
* @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)`.
*/
constructor(useCtrlG: boolean);
/**
* Show a search-like dialog prompting line number.
* @param {codeInput.CodeInput} codeInput the `<code-input>` element.
*/
showPrompt(codeInput: CodeInput): void;
}

/**
* 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
Expand Down
9 changes: 8 additions & 1 deletion plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ Files: [debounce-update.js](./debounce-update.js)

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

### Go To Line
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.

Files: [go-to-line.js](./go-to-line.js) / [go-to-line.css](./go-to-line.css)

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

### Indent
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.**

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

### Prism Line Numbers
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/)
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/)

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

Expand Down
70 changes: 70 additions & 0 deletions plugins/go-to-line.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@keyframes code-input_go-to_roll-in {
0% {opacity: 0; transform: translateY(-34px);}
100% {opacity: 1; transform: translateY(0px);}
}

@keyframes code-input_go-to_roll-out {
0% {opacity: 1; transform: translateY(0px);}
100% {opacity: 0; transform: translateY(-34px);}
}

.code-input_go-to_dialog {
position: absolute;
top: 0; right: 14px;
height: 28px;
padding: 6px;
padding-top: 8px;
border: solid 1px #00000044;
background-color: white;
border-radius: 6px;
box-shadow: 0 .2em 1em .2em rgba(0, 0, 0, 0.16);
animation: code-input_go-to_roll-in .2s;
z-index: 10;
}

.code-input_go-to_dialog.bye {
animation: code-input_go-to_roll-out .2s;
}

.code-input_go-to_dialog input::placeholder {
font-size: 80%;
}

.code-input_go-to_dialog input {
position: relative;
width: 240px; height: 32px; top: -3px;
font-size: large;
color: #000000aa;
border: 0;
}

.code-input_go-to_dialog input.error {
color: #ff0000aa;
}

.code-input_go-to_dialog input:focus {
outline: none;
}

.code-input_go-to_dialog span {
display: inline-block;
width: 24px;
line-height: 24px;
font-family: system-ui;
font-size: 22px;
font-weight: 500;
text-align: center;
border-radius: 50%;
color: black;
opacity: 0.6;
vertical-align: top;
}

.code-input_go-to_dialog span:before {
content: "\00d7";
}

.code-input_go-to_dialog span:hover {
opacity: .8;
background-color: #00000018;
}
154 changes: 154 additions & 0 deletions plugins/go-to-line.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* Add basic Go-To-Line (Ctrl+G by default) functionality to the code editor.
* Files: go-to-line.js / go-to-line.css
*/
codeInput.plugins.GoToLine = class extends codeInput.Plugin {

/**
* Create a go-to-line command plugin to pass into a template
* @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)`.
*/
constructor(useCtrlG) {
super([]); // No observed attributes
}

/* Add keystroke events */
afterElementsAdded(codeInput) {
const textarea = codeInput.textareaElement;
textarea.addEventListener('keydown', (event) => { this.checkCtrlG(codeInput, event); });
}

blockSearch(dialog, event) {
if (event.ctrlKey && event.key == 'g') {
return event.preventDefault();
}
}

checkPrompt(dialog, event) {
// Line number(:column number)
const lines = dialog.textarea.value.split('\n');
const maxLineNo = lines.length;
const lineNo = Number(dialog.input.value.split(':')[0]);
let columnNo = 0; // Means go to start of indented line
let maxColumnNo = 1;
const querySplitByColons = dialog.input.value.split(':');
if(querySplitByColons.length > 2) return dialog.input.classList.add('error');

if(querySplitByColons.length >= 2) {
columnNo = Number(querySplitByColons[1]);
maxColumnNo = lines[lineNo-1].length;
}

if (event.key == 'Escape') return this.cancelPrompt(dialog, event);

if (dialog.input.value) {
if (!/^[0-9:]*$/.test(dialog.input.value) || lineNo < 1 || columnNo < 0 || lineNo > maxLineNo || columnNo > maxColumnNo) {
return dialog.input.classList.add('error');
} else {
dialog.input.classList.remove('error');
}
}

if (event.key == 'Enter') {
this.goTo(dialog.textarea, lineNo, columnNo);
this.cancelPrompt(dialog, event);
}
}

cancelPrompt(dialog, event) {
let delay;
event.preventDefault();
dialog.textarea.focus();

// Remove dialog after animation
dialog.classList.add('bye');

if (dialog.computedStyleMap) {
delay = 1000 * dialog.computedStyleMap().get('animation').toString().split('s')[0];
} else {
delay = 1000 * document.defaultView.getComputedStyle(dialog, null).getPropertyValue('animation').split('s')[0];
}

setTimeout(() => { dialog.codeInput.removeChild(dialog); }, .9 * delay);
}

/**
* Show a search-like dialog prompting line number.
* @param {codeInput.CodeInput} codeInput the `<code-input>` element.
*/
showPrompt(codeInput) {
const textarea = codeInput.textareaElement;

const dialog = document.createElement('div');
const input = document.createElement('input');
const cancel = document.createElement('span');

dialog.appendChild(input);
dialog.appendChild(cancel);

dialog.className = 'code-input_go-to_dialog';
input.spellcheck = false;
input.placeholder = "Line:Column / Line no. then Enter";
dialog.codeInput = codeInput;
dialog.textarea = textarea;
dialog.input = input;

input.addEventListener('keydown', (event) => { this.blockSearch(dialog, event); });
input.addEventListener('keyup', (event) => { this.checkPrompt(dialog, event); });
cancel.addEventListener('click', (event) => { this.cancelPrompt(dialog, event); });

codeInput.appendChild(dialog);

input.focus();
}

/* Set the cursor on the first non-space char of textarea's nth line; and scroll it into view */
goTo(textarea, lineNo, columnNo = 0) {
let fontSize;
let lineHeight;
let scrollAmount;
let topPadding;
let cursorPos = -1;
let lines = textarea.value.split('\n');

if (lineNo > 0 && lineNo <= lines.length) {
if (textarea.computedStyleMap) {
fontSize = textarea.computedStyleMap().get('font-size').value;
lineHeight = fontSize * textarea.computedStyleMap().get('line-height').value;
} else {
fontSize = document.defaultView.getComputedStyle(textarea, null).getPropertyValue('font-size').split('px')[0];
lineHeight = document.defaultView.getComputedStyle(textarea, null).getPropertyValue('line-height').split('px')[0];
}

// scroll amount and initial top padding (3 lines above, if possible)
scrollAmount = (lineNo > 3 ? lineNo - 3 : 1) * lineHeight;
topPadding = (lineHeight - fontSize) / 2;

if (lineNo > 1) {
// cursor positon just after n - 1 full lines
cursorPos = lines.slice(0, lineNo - 1).join('\n').length;
}

// scan first non-space char in nth line
if (columnNo == 0) {
do cursorPos++; while (textarea.value[cursorPos] != '\n' && /\s/.test(textarea.value[cursorPos]));
} else {
cursorPos += 1 + columnNo - 1;
}

textarea.scrollTop = scrollAmount - topPadding;
textarea.setSelectionRange(cursorPos, cursorPos);
textarea.click();
}
}

/* Event handlers */
checkCtrlG(codeInput, event) {
const textarea = codeInput.textareaElement;
if (event.ctrlKey && event.key == 'g') {
event.preventDefault();

this.showPrompt(codeInput);
}
}
}