"use strict"; const CE_FLAG_PREVENT_DEFAULT = 1;const CE_FLAG_REFRESH = 2;const CE_FLAG_LISTENER = 4; class CodeEditor { constructor(canv) { this.text = ""; this.cur1 = 0; this.cur2 = 0; this.cursor_col = 0; this.cursor_row = 0; this.target_vis_col = -1; this.view_x = 0; this.view_y = 0; this.view_w = 0; this.view_h = 0; this.prev_action = ""; this.blink_timer = null; this.blink_state = true; this.canv_elem = canv; this.plane_elem = canv.parentElement; this.outer_elem = this.plane_elem.parentElement; var outer_style = getComputedStyle(this.outer_elem); this.tab_w = 4; this.fore = outer_style.getPropertyValue("color"); this.back = outer_style.getPropertyValue("background-color"); this.hl = "#48f"; this.canv_pad_x = 400; this.canv_pad_y = 400; this.canvas = canv.getContext("2d"); var px_height = 12; this.font_string = "" + px_height + "px monospace"; this.char_base = px_height / 5; this.canvas.fillStyle = this.fore; this.canvas.font = this.font_string; this.canvas.imageSmoothingEnabled = false this.resize_canvas(); var metrics = this.canvas.measureText("W"); this.char_w = metrics.width; this.char_h = px_height + 2 * this.char_base; } resize_canvas() { var styles = getComputedStyle(this.outer_elem); var w = Math.floor(parseFloat(styles.getPropertyValue("width")) + this.canv_pad_x); var h = Math.floor(parseFloat(styles.getPropertyValue("height")) + this.canv_pad_y); if (w == this.view_w && h == this.view_h) return; this.view_w = w; this.view_h = h; var scale = window.devicePixelRatio || 1; this.canv_elem.width = Math.floor(this.view_w * scale); this.canv_elem.height = Math.floor(this.view_h * scale); this.canvas.scale(scale, scale); this.canv_elem.style.width = "" + this.view_w + "px"; this.canv_elem.style.height = "" + this.view_h + "px"; this.canvas.font = this.font_string; } blink() { if (this.blink_timer) clearTimeout(this.blink_timer); if (this.cur1 != this.cur2) return; this.blink_timer = setTimeout(function(editor){editor.blink();}, 1000, this); var x = Math.floor(this.cursor_col * this.char_w - this.view_x + 0.5); var y = Math.floor(this.cursor_row * this.char_h - this.view_y + 0.5); var w = 1; var h = Math.floor(this.char_h + 0.5); this.canvas.fillStyle = this.blink_state ? this.fore : this.back; this.canvas.fillRect(x, y, w, h); this.blink_state = !this.blink_state; } find_target_column(start, end) { var vis_cols = 0; for (var i = start; i < end && i < this.text.length; i++) { var code = this.text.charCodeAt(i); if (code == 10) break; if (code == 9) { vis_cols += this.tab_w - (vis_cols % this.tab_w); } else { vis_cols++; } } this.target_vis_cols = vis_cols; } get_delim_kind(c) { if (c == 0x20 || c == 9) return 0; else if ((c >= 0x30 && c <= 0x39) || (c >= 0x41 && c <= 0x5a) || c == 0x5f || (c >= 0x61 && c <= 0x7a)) return 1; return 2; } move_prev_word(selecting) { var idx = this.cur2; for (var i = 0; i < 2; i++) { if (idx <= 1) { this.cur2 = 0; if (!selecting) this.cur1 = this.cur2; return; } idx--; var start_kind = this.get_delim_kind(this.text.charCodeAt(idx)); var kind = 0; while (idx > 0) { idx--; kind = this.get_delim_kind(this.text.charCodeAt(idx)); if (kind != start_kind) { idx++; break; } } if (start_kind != 0 && kind != 0) break; } this.cur2 = idx; if (!selecting) this.cur1 = this.cur2; } move_next_word(selecting) { var idx = this.cur2; var len = this.text.length; for (var i = 0; i < 2; i++) { if (idx >= len-1) { this.cur2 = len; if (!selecting) this.cur1 = this.cur2; return; } var start_kind = this.get_delim_kind(this.text.charCodeAt(idx)); var kind = 0; while (idx < len) { idx++; kind = this.get_delim_kind(this.text.charCodeAt(idx)); if (kind != start_kind) { break; } } if (start_kind != 0 && kind != 0) break; } this.cur2 = idx; if (!selecting) this.cur1 = this.cur2; } move_distance(distance, selecting) { var idx = this.cur2 + distance; if (idx < 0 || idx > this.text.length) return; this.cur2 = idx; if (!selecting) this.cur1 = this.cur2; } move_up(selecting) { if (this.prev_action != "arrowup" && this.prev_action != "arrowdown") this.target_vis_cols = -1; var idx = this.cur2; if (idx <= 0) { this.target_vis_cols = -1; this.cur2 = 0; if (!selecting) this.cur1 = this.cur2; return; } while (idx > 0) { idx--; if (this.text.charCodeAt(idx) == 10) { idx++; break; } } if (idx <= 0) { this.cur2 = 0; if (!selecting) this.cur1 = this.cur2; return; } var vis_cols = this.target_vis_cols; if (vis_cols < 0) { this.find_target_column(idx, this.cur2); vis_cols = this.target_vis_cols; } idx--; while (idx > 0) { idx--; if (this.text.charCodeAt(idx) == 10) { idx++; break; } } var vis = 0; while (vis < vis_cols) { var code = this.text.charCodeAt(idx); if (code == 10) { break; } if (code == 9) { vis += this.tab_w - (vis % this.tab_w); } else { vis++; } idx++; } this.cur2 = idx; if (!selecting) this.cur1 = this.cur2; return; } move_down(selecting) { if (this.prev_action != "arrowup" && this.prev_action != "arrowdown") this.target_vis_cols = -1; var len = this.text.length; var idx = this.cur2; if (idx >= len) { this.target_vis_cols = -1; this.cur2 = len; if (!selecting) this.cur1 = this.cur2; return; } var end = idx; while (end < len && this.text.charCodeAt(end) != 10) { end++; } if (end >= len) { this.cur2 = len; if (!selecting) this.cur1 = this.cur2; return; } while (idx > 0) { idx--; if (this.text.charCodeAt(idx) == 10) { idx++; break; } } var vis_cols = this.target_vis_cols; if (vis_cols < 0) { this.find_target_column(idx, this.cur2); vis_cols = this.target_vis_cols; } idx = end + 1; var vis = 0; while (idx < len && vis < vis_cols) { var code = this.text.charCodeAt(idx); if (code == 10) { break; } if (code == 9) { vis += this.tab_w - (vis % this.tab_w); } else { vis++; } idx++; } this.cur2 = idx; if (!selecting) this.cur1 = this.cur2; return; } move_home(selecting) { var len = this.text.length; var idx = this.cur2; var was_home = true; while (idx > 0) { idx--; if (this.text.charCodeAt(idx) == 10) { idx++; break; } was_home = false; } if (was_home) { while (idx < len && this.text.charCodeAt(idx) == 9) { idx++; } } this.cur2 = idx; if (!selecting) this.cur1 = this.cur2; return; } move_end(selecting) { var len = this.text.length; var idx = this.cur2; while (idx < len && this.text.charCodeAt(idx) != 10) { idx++; } this.cur2 = idx; if (!selecting) this.cur1 = this.cur2; return; } get_selected(should_delete) { if (this.cur1 == this.cur2) return ""; var start = this.cur1 < this.cur2 ? this.cur1 : this.cur2; var end = this.cur1 >= this.cur2 ? this.cur1 : this.cur2; var selected = this.text.substring(start, end); if (should_delete) { this.text = this.text.substring(0, start) + this.text.substring(end); this.cur1 = start; this.cur2 = start; } return selected; } clear_selection() { if (this.cur1 == this.cur2) return false; var start = this.cur1 < this.cur2 ? this.cur1 : this.cur2; var end = this.cur1 >= this.cur2 ? this.cur1 : this.cur2; var len = this.text.length; this.text = this.text.substring(0, start) + this.text.substring(end, len); this.cur1 = start; this.cur2 = this.cur1; return true; } insert(str) { this.clear_selection(); var len = this.text.length; this.text = this.text.substring(0, this.cur1) + str + this.text.substring(this.cur1, len); this.cur1 += str.length; this.cur2 = this.cur1; } delete(distance) { if (this.clear_selection()) return; var idx = this.cur1 + distance; var len = this.text.length; if (idx < 0 || idx >= len) return; this.text = this.text.substring(0, idx) + this.text.substring(idx + 1, len); this.cur1 = idx; this.cur2 = this.cur1; } indent(forwards) { if (this.cur1 == this.cur2) { this.insert("\t"); return; } var start = this.cur1 < this.cur2 ? this.cur1 : this.cur2; var end = this.cur1 >= this.cur2 ? this.cur1 : this.cur2; var lines = 0; for (var i = start; i < end + lines && i < this.text.length; i++) { var c = this.text.charCodeAt(i); if (c == 10) { lines++; var next = 0; if (i < this.text.length-1) next = this.text.charCodeAt(i+1); if (next != 10) { if (forwards) { this.text = this.text.substring(0, i+1) + "\t" + this.text.substring(i+1); } else if (next == 9) { this.text = this.text.substring(0, i+1) + this.text.substring(i+2); } } } } if (lines == 0) { this.insert("\t"); return; } while (start > 0 && this.text.charCodeAt(start) != 10) { start--; } if (forwards) { this.text = this.text.substring(0, start) + "\t" + this.text.substring(start); } else if (this.text.charCodeAt(start) == 9) { this.text = this.text.substring(0, start) + this.text.substring(start+1); } var start_diff = forwards ? 1 : -1; var end_diff = lines + 1; end_diff *= start_diff; if (this.cur1 < this.cur2) { this.cur1 += start_diff; this.cur2 += end_diff; } else { this.cur1 += end_diff; this.cur2 += start_diff; } } find_offset_from_xy(x, y) { var target_col = Math.floor((x + this.view_x) / this.char_w + 0.25); var target_row = Math.floor((y + this.view_y) / this.char_h); var col = 0; var row = 0; var len = this.text.length; for (var i = 0; i < len; i++) { var c = this.text.charCodeAt(i); if (c == 10) { col = 0; row++; } else if (c == 9) { col += this.tab_w - (col % this.tab_w); } else { col++; } if (row > target_row || (row == target_row && col > target_col)) return i; } return len; } handle_keypress(action, ctrl, shift) { var res = CE_FLAG_PREVENT_DEFAULT | CE_FLAG_REFRESH; if (action.length == 1) { if (ctrl) { if (action == "r") { res &= ~CE_FLAG_PREVENT_DEFAULT; } else if (action == "a") { this.cur1 = 0; this.cur2 = this.text.length; } else if (action == "c" || action == "x" || action == "v") { res &= ~CE_FLAG_PREVENT_DEFAULT; } } else { this.insert(action); res |= CE_FLAG_LISTENER; } } else { action = action.toLowerCase(); console.log(action); if (action == "control" || action == "shift" || action == "capslock") { action = null; res &= ~CE_FLAG_REFRESH; } else if (action == "enter") { this.insert("\n"); res |= CE_FLAG_LISTENER; } else if (action == "tab") { this.indent(!shift); res |= CE_FLAG_LISTENER; } else if (action == "backspace") { this.delete(-1); res |= CE_FLAG_LISTENER; } else if (action == "delete") { this.delete(0); res |= CE_FLAG_LISTENER; } else if (action == "home") { this.move_home(shift); } else if (action == "end") { this.move_end(shift); } else if (action == "arrowleft") { if (ctrl) { this.move_prev_word(shift); } else { this.move_distance(-1, shift); } } else if (action == "arrowright") { if (ctrl) { this.move_next_word(shift); } else { this.move_distance(1, shift); } } else if (action == "arrowup") { this.move_up(shift); } else if (action == "arrowdown") { this.move_down(shift); } } if (action) this.prev_action = action; return res; } maybe_scroll(pos_x, pos_y) { var x = Math.floor(pos_x - (pos_x % this.canv_pad_x)); var y = Math.floor(pos_y - (pos_y % this.canv_pad_y)); if (x == this.view_x && y == this.view_y) return; this.view_x = x; this.view_y = y; requestAnimationFrame(() => {this.refresh();}); } refresh() { this.resize_canvas(); this.canvas.fillStyle = this.back; this.canvas.fillRect(0, 0, this.canv_elem.width, this.canv_elem.height); this.canvas.fillStyle = this.fore; var start_sel = this.cur1 < this.cur2 ? this.cur1 : this.cur2; var end_sel = this.cur1 >= this.cur2 ? this.cur1 : this.cur2; var total_cols = 0; var total_rows = 0; var col = 0; var row = 0; var len = this.text.length; for (var i = 0; i < len; i++) { var code = 0; var test_col = col; var x = col * this.char_w - this.view_x; var y = row * this.char_h - this.view_y; for (var j = i; j < len; j++) { if (j == this.cur1) { this.cursor_col = test_col; this.cursor_row = row; } code = this.text.charCodeAt(j); if (code < 0x20) break; if (j >= start_sel && j < end_sel && x >= -this.char_w && x <= this.view_w && y >= -this.char_h && y <= this.view_h ) { this.canvas.fillStyle = this.hl; this.canvas.fillRect(x, y, this.char_w + 1, this.char_h + 1); } x += this.char_w; test_col++; } x = col * this.char_w - this.view_x; col = test_col; if (code == 9) { var x_hl = test_col * this.char_w - this.view_x; var gap = this.tab_w - (col % this.tab_w); col += gap; if (j >= start_sel && j < end_sel && x_hl >= -this.char_w && x_hl <= this.view_w && y >= -this.char_h && y <= this.view_h ) { this.canvas.fillStyle = this.hl; this.canvas.fillRect(x_hl, y, gap * this.char_w + 1, this.char_h + 1); } } else if (code == 10) { var x_hl = test_col * this.char_w - this.view_x; if (col > total_cols) total_cols = col + 1; col = 0; row++; if (j >= start_sel && j < end_sel && x_hl >= -this.char_w && x_hl <= this.view_w && y >= -this.char_h && y <= this.view_h ) { this.canvas.fillStyle = this.hl; this.canvas.fillRect(x_hl, y, this.char_w + 1, this.char_h + 1); } } else if (j < len-1) { col++; } if (j > i && x <= this.view_w && y >= -this.char_h && y <= this.view_h) { this.canvas.fillStyle = this.fore; this.canvas.fillText(this.text.substring(i, j), x + 1, y + this.char_h - this.char_base); } i = j; } total_rows = row + 1; if (col > total_cols) total_cols = col + 1; if (this.cur1 == this.text.length) { this.cursor_col = col; this.cursor_row = row; } this.canv_elem.style.left = "" + this.view_x + "px"; this.canv_elem.style.top = "" + this.view_y + "px"; this.plane_elem.style.width = "" + (total_cols * this.char_w) + "px"; this.plane_elem.style.height = "" + (total_rows * this.char_h) + "px"; this.blink_state = true; this.blink(); }} function code_editor_keydown_handler(event) { var editor = event.target.code_editor; var res = editor.handle_keypress(event.key, event.ctrlKey, event.shiftKey); if (res & 1) event.preventDefault(); if (res & 2) editor.refresh(); if ((res & 4) && editor.listener) editor.listener(editor.text);} function code_editor_keyup_handler(event) { //alert("key up");} function code_editor_mousedown_handler(event) { event.target.tabIndex = 1; var editor = event.target.code_editor; var idx = editor.find_offset_from_xy(event.offsetX, event.offsetY); editor.cur1 = idx; editor.cur2 = idx; editor.refresh();} function code_editor_mousemove_handler(event) { if ((event.buttons & 1) == 0) return; var editor = event.target.code_editor; var idx = editor.find_offset_from_xy(event.offsetX, event.offsetY); var should_refresh = editor.cur2 != idx; editor.cur2 = idx; if (should_refresh) editor.refresh();} function code_editor_mouseup_handler(event) { //alert("mouse up");} function code_editor_cut_copy_handler(event, elem, is_cut) { event.preventDefault(); var selected = elem.code_editor.get_selected(is_cut); if (selected.length > 0) event.clipboardData.setData("text/plain", selected); elem.code_editor.refresh(); if (elem.code_editor.listener) elem.code_editor.listener(elem.code_editor.text);} function code_editor_paste_handler(event, elem) { event.preventDefault(); var text = (event.clipboardData || window.clipboardData).getData("text"); if (text) elem.code_editor.insert(text); elem.code_editor.refresh(); if (elem.code_editor.listener) elem.code_editor.listener(elem.code_editor.text);} function code_editor_scroll_handler(event) { var x = event.target.scrollLeft; var y = event.target.scrollTop; event.target.firstChild.firstChild.code_editor.maybe_scroll(x, y);} function init_code_editor(canv_elem, input_listener) { var editor = new CodeEditor(canv_elem); editor.listener = input_listener; canv_elem.code_editor = editor; canv_elem.addEventListener("keydown", code_editor_keydown_handler); canv_elem.addEventListener("keyup", code_editor_keyup_handler); canv_elem.addEventListener("mousedown", code_editor_mousedown_handler); canv_elem.addEventListener("mousemove", code_editor_mousemove_handler); canv_elem.addEventListener("mouseup", code_editor_mouseup_handler); canv_elem.parentElement.parentElement.addEventListener("scroll", code_editor_scroll_handler); canv_elem.addEventListener("blur", (event) => { console.log("BLUR!"); });} function load_code_editors() { var editor_elems = document.getElementsByClassName("code-editor"); if (editor_elems.length == 0) return; for (var e of editor_elems) { e.style.overflow = "auto"; var plane_elem = document.createElement("div"); plane_elem.style.position = "relative"; plane_elem.style.overflow = "hidden"; plane_elem.style.minWidth = "100%"; plane_elem.style.minHeight = "100%"; var canv_elem = document.createElement("canvas"); canv_elem.style.position = "absolute"; canv_elem.style.top = "0"; canv_elem.style.left = "0"; canv_elem.style.outline = "0"; plane_elem.appendChild(canv_elem); e.appendChild(plane_elem); init_code_editor(canv_elem, window[e.dataset.listener]); canv_elem.code_editor.refresh(); } document.addEventListener("copy", (event) => { if (document.activeElement.code_editor) code_editor_cut_copy_handler(event, document.activeElement, false); }); document.addEventListener("cut", (event) => { if (document.activeElement.code_editor) code_editor_cut_copy_handler(event, document.activeElement, true); }); document.addEventListener("paste", (event) => { if (document.activeElement.code_editor) code_editor_paste_handler(event, document.activeElement); });}