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
JavaScript
/**
* @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);
}