UNPKG

maiaeditor

Version:

MaiaEditor (The MaiaStudio Editor) MaiaScript is a programming language aimed at developing adaptable and intelligent applications.

796 lines (744 loc) 29.2 kB
/** * @license * Copyright 2020 Roberto Luiz Souza Monteiro, * Renata Souza Barreto, * Hernane Borges de Barros Pereira. * * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at; * * http://www.apache.org/licenses/LICENSE-2.0; * * Unless required by applicable law or agreed to in writing, software; * distributed under the License is distributed on an 'AS IS' BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, eitherMath.express or implied. * See the License for the specific language governing permissions and; * limitations under the License. */ /* * The cursor positioning functions, getCursorPosition and setCursorPosition * were based on the CodeJar library positioning code: * https://github.com/antonmedv/codejar.git */ /** * MaiaScript code editor. * @class * @param {string} container - HTML element to setup as an editor. * @param {string} language - Programming language to highlight the syntax. * @param {object} options - Object containing options for configuring the editor. * @return {object} Element configured as source code editor. */ function MaiaEditor(container, language, options) { init(); /** * Creates the attributes of the class. */ function init() { // Class attributes goes here. } var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; var opts = { 'lineBreak': '\r\n', 'indentChars': ' ', 'commentChars': '//' } if (typeof options != 'undefined') { for (key in options) { opts[key] = options[key]; } } var maiaeditor = this; // History for undo and redo operations. var editorHistory = []; var editorHistoryBackup = []; var editorHistoryLength = 999; // Element that will contain the editor. var editorContainer = document.getElementById(container); var language = language; // Gets the code in the container. var code = editorContainer.textContent || ''; editorContainer.textContent = ''; // Creates the line number bar. var lineNumbers = document.createElement('pre'); editorContainer.appendChild(lineNumbers); // Creates the editor. var editor = document.createElement('pre'); editorContainer.appendChild(editor); // Place the line number bar to the left of the editor. lineNumbers.style.setProperty('mix-blend-mode', 'difference'); lineNumbers.style.float = 'left'; lineNumbers.style.width = '5%'; lineNumbers.style.outline = 'none'; lineNumbers.style.resize = 'none'; lineNumbers.style.textAlign = 'right'; // Sets the element's properties so that it can act as a code editor. if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) { var contentEditable = 'true'; } else { var contentEditable = 'plaintext-only'; } editor.setAttribute('contentEditable', contentEditable); editor.setAttribute('spellcheck', 'false'); editor.style.float = 'right'; editor.style.width = '95%'; editor.style.outline = 'none'; editor.style.resize = 'none'; editor.style.textAlign = 'left'; /** * Gets the editor's text. * @return {string} The text in the editor. */ this.getEditorHistoryLength = function() { return editor.editorHistoryLength; } /** * Sets the editor history length. * @param {number} length - Editor history length. * @return Sets the editor history length. */ this.setEditorHistoryLength = function(length) { editor.editorHistoryLength = length; } /** * Gets the editor's text. * @return {string} The text in the editor. */ this.getText = function() { return editor.textContent; } /** * Gets the editor's text. * @return {string} The text in the editor. */ this.getHtml = function() { return editor.innerHTML; } /** * Sets the editor inner HTML. * @param {string} html - Editor inner HTML. * @return Sets the editor inner HTML. */ this.setHtml = function(html) { editor.innerHTML = html; } /** * Sets the editor's text. * @param {string} text - Text to be set in the editor. * @return The text in the editor is set. */ this.setText = function(text) { editor.textContent = text; this.highlightCode(editor); } /** * Gets the text of the line before the cursor. * @return {string} The text of the line where the cursor is. */ this.getTextBeforeCursor = function() { // Gets the cursor position. var sel = window.getSelection(); var rangeAtCursor = sel.getRangeAt(0); // Gets the text to the left of the cursor. var rangeLeft = document.createRange(); rangeLeft.selectNodeContents(editor); rangeLeft.setEnd(rangeAtCursor.startContainer, rangeAtCursor.startOffset); var textBeforeCursor = rangeLeft.toString(); // Find the begin of previous line and get the text before for cursos in the current line. var textAtCursor = ''; if (textBeforeCursor.length > 1) { var i = textBeforeCursor.length - 1; while ((i >= 0) && !((textBeforeCursor[i] == '\r') || (textBeforeCursor[i] == '\n'))) { i--; } if ((i < 0) || (textBeforeCursor[i] == '\r') || (textBeforeCursor[i] == '\n')) { i++; } textAtCursor = textBeforeCursor.substr(i, textBeforeCursor.length - 1); } else if (textBeforeCursor.length == 1){ textAtCursor = textBeforeCursor; } return textAtCursor; } /** * Gets the text of the line after the cursor. * @return {string} The text of the line where the cursor is. */ this.getTextAfterCursor = function() { // Gets the cursor position. var sel = window.getSelection(); var rangeAtCursor = sel.getRangeAt(0); // Gets the text to the right of the cursor. var rangeRight = document.createRange(); rangeRight.selectNodeContents(editor); rangeRight.setStart(rangeAtCursor.endContainer, rangeAtCursor.endOffset); var textAfterCursor = rangeRight.toString(); // Find the begin of previous line and get the text before for cursos in the current line. var textAtCursor = ''; if (textAfterCursor.length > 0) { var i = 0; while ((i < textAfterCursor.length) && !((textAfterCursor[i] == '\r') || (textAfterCursor[i] == '\n'))) { i++; } var textAtCursor = textAfterCursor.substr(0, i); } return textAtCursor; } /** * Gets the text of the line where the cursor is. * @return {string} The text of the line where the cursor is. */ this.getTextAtCursor = function() { var textAtCursor = this.getTextBeforeCursor() + this.getTextAfterCursor(); return textAtCursor; } /** * Place the cursor after the selected text. * @param {object} editor - Editor object. * @return Place the cursor after the selected text. */ this.moveCursorToEndOfSelection = function(editor) { var pos = this.getCursorPosition(); pos.start = pos.end; this.setCursorPosition(pos); } /** * Appends text in terminal. * @param {string} text - Text to be set in the terminal. * @return The text is appended to terminal. */ this.appendText = function(text) { if (typeof text != 'undefined') { this.moveCursorToEnd(); maiaeditor.insertText(text); this.moveCursorToEnd(); } } /** * Inserts text in terminal at cursor position. * @param {string} text - Text to be inserted at cursor position * @return The text is inserted at cursor position. */ this.insertText = function(text) { if (typeof text != 'undefined') { document.execCommand('insertHTML', false, text); } } /** * Replaces text in the editor, based on a regular expression. * @param {string} pattern - Search pattern (regular expression). * @param {string} text - Substitute text. * @param {boolear} flags - Regular expression flags (g, i, m, u, y). * @return Pattern occurrences replaced. */ this.regSub = function(pattern, text, flags) { this.saveEditorContent(editor); if (typeof flags == 'undefined') { var flags = 'i'; } var oldText = this.getText(); var regex = core.regExp(pattern, flags); var newText = oldText.replace(regex, text); this.setText(newText); } /** * Gets the indexes of all occurrences of a pattern in text. * @param {string} pattern - Search pattern. * @param {string} text - Text where to look for the pattern. * @param {boolear} caseSensitive - True if case sensitive. False, otherwise. * @return {object} All occurrences of a pattern in text. */ this.getIndicesOf = function(pattern, text, caseSensitive) { if (typeof caseSensitive == 'undefined') { var caseSensitive = false; } var patternLen = pattern.length; if (patternLen == 0) { return []; } var startIndex = 0; var index; var indices = []; if (!caseSensitive) { text = text.toLowerCase(); pattern = pattern.toLowerCase(); } while ((index = text.indexOf(pattern, startIndex)) > -1) { indices.push(index); startIndex = index + patternLen; } return indices; } /** * Search and highlight text in the editor. * @param {string} text - Search pattern. * @param {boolear} caseSensitive - True if case sensitive. False, otherwise. * @return Pattern occurrence highlighted. */ this.search = function(text, caseSensitive) { if (typeof caseSensitive == 'undefined') { var caseSensitive = false; } var innerHTML = editor.innerHTML; var highlightedHTML = ''; var indices = this.getIndicesOf(text, innerHTML, caseSensitive); if (indices.length > 0) { var firstIndex = 0; for (var i = 0; i < indices.length; i++) { highlightedHTML += innerHTML.substring(firstIndex, indices[i]) + '<span style="background-color: yellow;">' + innerHTML.substring(indices[i], indices[i] + text.length) + '</span>'; firstIndex = indices[i] + text.length; } highlightedHTML += innerHTML.substring(indices[i] + text.length) editor.innerHTML = highlightedHTML; } } /** * Visits each of the text nodes in an object. * @param {object} editor - Editor object. * @param {object} visitor - Visiting object. * @return {number} The current position of the cursor. */ function visitElement(editor, visitor) { var queue = []; if (editor.firstChild) { queue.push(editor.firstChild); } var element = queue.pop(); while (element) { if (visitor(element) === 'stop') { break; } if (element.nextSibling) { queue.push(element.nextSibling); } if (element.firstChild) { queue.push(element.firstChild); } element = queue.pop(); } } /** * Gets the current position of the cursor. * @return {number} The current position of the cursor. */ this.getCursorPosition = function() { var sel = window.getSelection(); var position = {'start': 0, 'end': 0, 'dir': 'undefined'}; visitElement(editor, element => { if (element === sel.anchorNode && element === sel.focusNode) { position.start += sel.anchorOffset; position.end += sel.focusOffset; position.dir = sel.anchorOffset <= sel.focusOffset ? 'ltr' : 'rtl'; return 'stop'; } if (element === sel.anchorNode) { position.start += sel.anchorOffset; if (!position.dir) { position.dir = 'ltr'; } else { return 'stop'; } } else if (element === sel.focusNode) { position.end += sel.focusOffset; if (!position.dir) { position.dir = 'rtl'; } else { return 'stop'; } } if (element.nodeType === Node.TEXT_NODE) { if (position.dir != 'ltr') { position.start += element.nodeValue.length; } if (position.dir != 'rtl') { position.end += element.nodeValue.length; } } }); return position; } /** * Sets the cursor position. * @param {object} position - The cursor position. * @return The current position of the cursor is set. */ this.setCursorPosition = function(position) { var sel = window.getSelection(); var startNode, startOffset = 0; var endNode, endOffset = 0; if (!position.dir) { position.dir = 'ltr'; } if (position.start < 0) { position.start = 0; } if (position.end < 0) { position.end = 0; } // Flip start and end if the direction reversed. if (position.dir == 'rtl') { const { start, end } = position; position.start = end; position.end = start; } var current = 0; visitElement(editor, element => { if (element.nodeType !== Node.TEXT_NODE) { return; } var len = (element.nodeValue || '').length; if (current + len >= position.start) { if (!startNode) { startNode = element; startOffset = position.start - current; } if (current + len >= position.end) { endNode = element; endOffset = position.end - current; return 'stop'; } } current += len; }); // If everything deleted place cursor at editor. if (!startNode) startNode = editor; if (!endNode) endNode = editor; // Flip back the selection. if (position.dir == '<-') { [startNode, startOffset, endNode, endOffset] = [endNode, endOffset, startNode, startOffset]; } sel.setBaseAndExtent(startNode, startOffset, endNode, endOffset); } /** * Highlights the code syntax in the editor. * @param {object} element - Element to do code syntax highlighte. * @return The content of the editor is Highlighted. */ this.highlightCode = function(element) { if (typeof element == 'undefined') { var thisEditor = editor; } else { var thisEditor = element; } // Gets the code in the editor. var code = thisEditor.textContent || ''; // Saves the cursor position. var position = this.getCursorPosition(); // Highlights the code syntax in the editor. thisEditor.innerHTML = Prism.highlight(code, Prism.languages[language], language); // Restores the cursor position. this.setCursorPosition(position); // Displays line numbers. var numberOfLines = code.split(/\r\n|\r|\n/).length + (code.endsWith('\r') || code.endsWith('\n') ? 0 : 1); var text = ''; for (var i = 1; i < numberOfLines; i++) { text += `${i} \r\n`; } lineNumbers.innerText = text; } /** * Saves the current content of the editor. * @param {object} element - Element where to save content. * @return The current content of the editor is saved. */ this.saveEditorContent = function(element) { if (typeof element == 'undefined') { var element = editor; } // Place the previous contents on the stack. if (editorHistory.length >= editorHistoryLength) { editorHistory.shift(); editorHistoryBackup.shift(); } editorHistory.push(element.textContent); } /** * Restores the editor's previous content. * @param {object} element - Element where to restore content. * @return The editor's previous content is restored. */ this.restoreEditorContent = function(element) { if (typeof element == 'undefined') { var element = editor; } // Removes the previous contents from the stack. var lastContent = editorHistory.pop(); // Place the previous contents on the backup stack. editorHistoryBackup.push(lastContent); // Restores the content. editor.textContent = lastContent ? lastContent : editor.textContent; // Highlights the code syntax in the editor. this.highlightCode(element); } /** * Undo previous restores command. * @param {object} element - Element where to restore content. * @return The editor's previous content is restored. */ this.undoRestoreEditorContent = function(element) { if (typeof element == 'undefined') { var element = editor; } // Removes the previous contents from the backup stack. var lastContent = editorHistoryBackup.pop(); // Place the previous contents on the stack. editorHistory.push(lastContent); // Restores the content. editor.textContent = lastContent ? lastContent : editor.textContent; // Highlights the code syntax in the editor. this.highlightCode(element); } /** * Returns the selected text. * @return {string} The selected text. */ this.getSelectedText = function() { var sel = window.getSelection(); return sel.toString(); } /** * Replaces the selected text with one provided as a parameter. * @param {string} text - Text provided. * @return The selected text replaced. */ this.replaceSelectedText = function(text) { var sel = window.getSelection(); if (sel.rangeCount) { var range = sel.getRangeAt(0); range.deleteContents(); range.insertNode(document.createTextNode(text)); } } /** * Indents the selected text. * @param {object} element - Element where the selection is. * @return The selected text indented. */ this.indentSelection = function(element) { if (typeof element == 'undefined') { var element = editor; } var text = this.getSelectedText(); if (typeof text == 'string') { this.saveEditorContent(editor); var textLines = text.split(/\r\n|\r|\n/); var newText = ''; if (Array.isArray(textLines)) { for (var i = 0; i < textLines.length; i++) { newText += opts.indentChars + textLines[i] + (i < textLines.length - 1 ? (isFirefox ? '\n' : opts.lineBreak) : ''); } this.replaceSelectedText(newText); } } } /** * Unindents the selected text. * @param {object} element - Element where the selection is. * @return The selected text unindented. */ this.unindentSelection = function(element) { if (typeof element == 'undefined') { var element = editor; } var text = this.getSelectedText(); if (typeof text == 'string') { var textLines = text.split(/\r\n|\r|\n/); var newText = ''; if (Array.isArray(textLines)) { this.saveEditorContent(editor); for (var i = 0; i < textLines.length; i++) { newText += textLines[i].replace(opts.indentChars, '') + (i < textLines.length - 1 ? (isFirefox ? '\n' : opts.lineBreak) : ''); } this.replaceSelectedText(newText); } } } /** * Comments the selected text. * @param {object} element - Element where the selection is. * @return The selected text commented. */ this.commentSelection = function(element) { if (typeof element == 'undefined') { var element = editor; } // Get the selected text. var text = this.getSelectedText(); if (typeof text == 'string') { this.saveEditorContent(editor); var textLines = text.split(/\r\n|\r|\n/); var newText = ''; if (Array.isArray(textLines)) { for (var i = 0; i < textLines.length; i++) { newText += opts.commentChars + textLines[i] + (i < textLines.length - 1 ? (isFirefox ? '\n' : opts.lineBreak) : ''); } this.replaceSelectedText(newText); } } } /** * Uncomments the selected text. * @param {object} element - Element where the selection is. * @return The selected text uncommented. */ this.uncommentSelection = function(element) { if (typeof element == 'undefined') { var element = editor; } // Get the selected text. var text = this.getSelectedText(); if (typeof text == 'string') { this.saveEditorContent(editor); var textLines = text.split(/\r\n|\r|\n/); var newText = ''; if (Array.isArray(textLines)) { for (var i = 0; i < textLines.length; i++) { newText += textLines[i].replace(opts.commentChars, '') + (i < textLines.length - 1 ? (isFirefox ? '\n' : opts.lineBreak) : ''); } this.replaceSelectedText(newText); } } } /** * Copy the selected text to clipboard. * @return The selected text copied to clipboard. */ this.copySelection = function() { this.saveEditorContent(editor); try { document.execCommand('copy'); } catch (e) { alert('This browser does not support copy to clipboard from JavaScript code.'); } } /** * Cut the selected text from clipboard. * @return The selected text cuted from clipboard. */ this.cutSelection = function() { this.saveEditorContent(editor); try { document.execCommand('cut'); } catch (e) { alert('This browser does not support cut to clipboard from JavaScript code.'); } } /** * Paste the selected text to clipboard. * @return The selected text pasted to clipboard. */ this.pasteSelection = function() { this.saveEditorContent(editor); try { document.execCommand('paste'); } catch (e) { alert('This browser does not support paste from clipboard from JavaScript code.'); } } // It is necessary to update the HTML content of the element, whenever a key is pressed, // in order to keep the syntax coloring consistent. editor.addEventListener('keydown', function(event) { if (((!event.shiftKey && event.ctrlKey) || (!event.shiftKey && event.metaKey)) && ((event.key == 'Z') || (event.key == 'z'))) { maiaeditor.restoreEditorContent(maiaeditor.editor); } else if (((event.shiftKey && event.ctrlKey) || (event.shiftKey && event.metaKey)) && ((event.key == 'Z') || (event.key == 'z'))) { maiaeditor.undoRestoreEditorContent(maiaeditor.editor); } else if (((event.shiftKey && event.ctrlKey) || (event.shiftKey && event.metaKey)) && ((event.key == 'I') || (event.key == 'i'))) { maiaeditor.unindentSelection(maiaeditor.editor); } else if (((!event.shiftKey && event.ctrlKey) || (!event.shiftKey && event.metaKey)) && ((event.key == 'I') || (event.key == 'i'))) { maiaeditor.indentSelection(maiaeditor.editor); } else if (((event.shiftKey && event.ctrlKey) || (!event.shiftKey && event.metaKey)) && ((event.key == 'M') || (event.key == 'm'))) { maiaeditor.uncommentSelection(maiaeditor.editor); } else if (((!event.shiftKey && event.ctrlKey) || (!event.shiftKey && event.metaKey)) && ((event.key == 'M') || (event.key == 'm'))) { maiaeditor.commentSelection(maiaeditor.editor); } else { var openChars = {'{': '}', '[': ']', '(': ')'}; if (event.key == 'Enter') { var textBeforeCursor = maiaeditor.getTextBeforeCursor(); var textAfterCursor = maiaeditor.getTextAfterCursor(); // Calculates indentation. // Find beginning of previous line. var i = textBeforeCursor.length - 1; while ((i >= 0) && (textBeforeCursor[i] != '\n')) { i--; } i++; // Find padding of the line. var j = i; while ((j < textBeforeCursor.length) && (/[ \t]/.test(textBeforeCursor[j]))) { j++; } var padding = textBeforeCursor.substring(i, j) || ''; // Checks whether the line contains open braces. if (textBeforeCursor[textBeforeCursor.length - 1] == '{') { var indentation = padding + ' '; } else { var indentation = padding; } if (indentation.length > 0) { event.preventDefault(); maiaeditor.insertText(opts.lineBreak + indentation); } else { if (isFirefox) { event.preventDefault(); maiaeditor.insertText(opts.lineBreak); } } // Checks whether the line contains close braces. if ((indentation != padding) && (textAfterCursor[0] == '}')) { var pos = maiaeditor.getCursorPosition(); if (padding.length == 0) { maiaeditor.insertText(opts.lineBreak + opts.lineBreak); } else { maiaeditor.insertText(opts.lineBreak + padding); } maiaeditor.setCursorPosition(pos); } } else if (event.key in openChars) { var pos = maiaeditor.getCursorPosition(); event.preventDefault(); maiaeditor.insertText(event.key + openChars[event.key]); pos.start = ++pos.end; maiaeditor.setCursorPosition(pos); } maiaeditor.saveEditorContent(maiaeditor.editor); } }, false); // It is necessary to update the HTML content of the element, whenever a key is pressed, // in order to keep the syntax coloring consistent. editor.addEventListener('input', function(event) { if (event.defaultPrevented) { return; } if (event.isComposing) { return; } // Highlights the code syntax in the editor. maiaeditor.highlightCode(maiaeditor.editor); }, false); editor.addEventListener('click', function(event) { // Highlights the code syntax in the editor. if (window.getSelection) { var selectedText = window.getSelection().toString(); if (selectedText.length == 0) { maiaeditor.highlightCode(maiaeditor.editor); } } }, false); editor.addEventListener('keyup', function(event) { // Highlights the code syntax in the editor. if (window.getSelection) { var selectedText = window.getSelection().toString(); if (selectedText.length == 0) { maiaeditor.highlightCode(maiaeditor.editor); } } }, false); // Transfer the text from the container to the editor. this.setText(code); // Highlights the code syntax in the editor. this.highlightCode(editor); }