added html and css editors

This commit is contained in:
David Meincke
2022-03-26 20:44:52 +01:00
parent cda1b5c972
commit 6ed4b80c64
23 changed files with 548 additions and 144 deletions
@@ -0,0 +1,26 @@
@import url('../../../shared/theme.less');
div > code-editor-container.active:last-child {
min-height: auto !important;
max-height: none !important;
min-width: auto !important;
max-width: none !important;
}
.code-editor-header {
padding: 10px 18px;
background: lighten(@editor-bg-white, 1%);
color: @primary-text-color-white;
font-size: 13px;
border-style: solid none;
border-color: @primary-bg-color-white;
border-width: 1px;
}
@media (prefers-color-scheme: dark) {
.code-editor-header {
background: lighten(@editor-bg-dark, 1%);
color: @primary-text-color-dark;
border-color: @primary-bg-color-dark;
}
}
@@ -0,0 +1,34 @@
import { BaseElement } from "../../../shared/_base";
import { CodeEditor } from "../code-editor/code-editor";
import { ResizeHandle } from "../resize-handle/resize-handle";
import './code-editor-container.less';
export class CodeEditorContainer extends BaseElement {
onInit(): void {
var language = this.getAttribute("language");
this.classList.add("flex-vert");
if (language) {
this.setHeader(language);
this.createCodeEditor(language, this.getAttribute("dir")?.toLowerCase().indexOf("v") === 0 ? 'vertical' : 'horizontal');
}
}
setHeader(title: string) {
var div = document.createElement("div");
div.classList.add("code-editor-header");
div.classList.add("flex-shrink");
div.innerText = title;
this.appendChild(div);
}
createCodeEditor(language: string, expandDir: 'vertical' | 'horizontal') {
var codeEditor = <CodeEditor>document.createElement("code-editor");
codeEditor.setAttribute("language", language);
var resizeHandle = <ResizeHandle>document.createElement("resize-handle");
resizeHandle.setAttribute("dir", expandDir);
this.appendChild(codeEditor);
this.appendChild(resizeHandle);
}
}
@@ -2,24 +2,29 @@ import { BaseElement } from "../../../shared/_base";
import './code-editor.less';
import * as monaco from 'monaco-editor';
import { OutputFrame } from "../output-frame/output-frame";
import initScript from '!!raw-loader!./injects/editor-init.js';
import initJs from '!!raw-loader!./injects/editor-init.js';
import initHtml from '!!raw-loader!./injects/editor-init.html';
import initCss from '!!raw-loader!./injects/editor-init.css';
import spiralBoxesSolutionScript from '!!raw-loader!./injects/spiral-boxes-solution.js';
import { debounceManager } from "../../../shared/ensure-debounce";
import declarations from '!!raw-loader!./injects/declarations.d.ts';
export class CodeEditor extends BaseElement {
public input: string = initScript;
public input: string = "";
public language: string = "";
onInit(): void {
this.initWorkers();
this.language = this.getAttribute("language")?.toLocaleLowerCase() ?? "javascript";
this.input = this.setInitInput(this.language);
const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
var editor = monaco.editor.create(this, {
value: this.input,
language: 'javascript',
language: this.language,
automaticLayout: true,
contextmenu: false,
minimap: {
@@ -29,12 +34,45 @@ export class CodeEditor extends BaseElement {
theme: prefersDarkScheme.matches ? 'vs-dark' : 'vs-light'
});
var debounce = debounceManager(1000);
editor.onDidChangeModelContent((e) => {
this.input = editor.getValue();
debounce.ensureDebounce(() => {
this.outputFrame.setScript(this.language, this.input);
});
});
this.waitFor(this.outputFrame, () => {
this.outputFrame.setScript(this.language, this.input);
});
}
setInitInput(language: string | null) {
if (language) {
if (language.toLowerCase() === "javascript") {
this.initJs();
return initJs;
}
else if (language.toLowerCase() === 'html') {
return initHtml;
}
else if (language.toLowerCase() === 'css') {
return initCss;
}
}
return "";
}
initJs() {
monaco.languages.typescript.javascriptDefaults.addExtraLib(declarations, 'ts:filename/declarations.d.ts');
monaco.languages.registerCompletionItemProvider("javascript", {
triggerCharacters: ["."],
provideCompletionItems: (model, position, context, token) => (
{
provideCompletionItems: (model, position, context, token) => {
return {
suggestions: [
{
label: 'for',
@@ -60,24 +98,11 @@ export class CodeEditor extends BaseElement {
}
]
}
)
}
});
var debounce = debounceManager(1000);
editor.onDidChangeModelContent((e) => {
this.input = editor.getValue();
debounce.ensureDebounce(() => {
this.outputFrame.setScript(this.input);
});
});
this.waitFor(this.outputFrame, () => {
this.outputFrame.setScript(this.input);
});
}
initWorkers() {
// @ts-ignore
self.MonacoEnvironment = {
@@ -0,0 +1,2 @@
body {
}
@@ -0,0 +1,2 @@
<div>
</div>
@@ -1,4 +1,17 @@
//create a box
box(10, 10, 20, 20);
//use random:
box(50, rand(100, 200), 20, 20);
//use random color:
box(160, 50, 20, 20, randomColor());
box(center(), center(), 20, 20);
//center method for center of canvas:
box(center() - 10, center() - 10, 20, 20);
//width and height gives canvas size:
box(width() - 20, height() - 20, 20, 20);
//rect will not be connected to boxes:
rect(300, 200, 40, 40, "green");
@@ -4,7 +4,7 @@ import { NotificationBubble } from "./notification-bubble";
import './notification-bubbles.less';
export class NotificationBubbles extends BaseElement {
add(text: string, type: 'log' | 'info' | 'warn' | 'error' | 'syntax-error' | 'script' | 'time' | 'response') {
add(text: string, type: 'log' | 'info' | 'warn' | 'error' | 'syntax-error' | 'script' | 'time' | 'response' | string) {
if (text && type) {
var bubble = <NotificationBubble>document.createElement("notification-bubble");
@@ -6,12 +6,14 @@ import { postMessage } from "../../../shared/post-message";
export class OutputFrame extends BaseElement {
public Iframe: HTMLIFrameElement | null = null;
public CurrentIframeScriptRemoveResponseListener: Function | null = null;
private CurrentIframeScriptRemoveResponseListener: Function | null = null;
constructor() {
super();
}
private scripts: any = {};
onInit(): void {
this.reset();
}
@@ -19,7 +21,7 @@ export class OutputFrame extends BaseElement {
onUpdate(): void {
}
reset(resetNotificationBubbles: boolean = true) {
reset(onload: ((ev: Event) => void) | null = null, resetNotificationBubbles: boolean = true) {
if (this.CurrentIframeScriptRemoveResponseListener != null) {
this.CurrentIframeScriptRemoveResponseListener();
this.CurrentIframeScriptRemoveResponseListener = null;
@@ -32,6 +34,13 @@ export class OutputFrame extends BaseElement {
this.Iframe = document.createElement("iframe");
this.Iframe.setAttribute("sandbox", "allow-pointer-lock allow-same-origin allow-scripts");
this.Iframe.src = GetOutputFrameUrl();
if (onload) {
this.Iframe.onload = ((ev) => {
onload(ev);
});
}
this.appendChild(this.Iframe);
if (resetNotificationBubbles) {
@@ -43,41 +52,34 @@ export class OutputFrame extends BaseElement {
}
setError() {
this.reset();
if (this.Iframe) {
this.Iframe.src = GetOutputFrameUrl() + "execution-time-error.html";
}
}
preventInfiniteLoop(value: string) {
return value.replace("while (true)", "while (false)");
}
setScript(value: string) {
this.reset();
this.onIframeLoaded(() => {
this.CurrentIframeScriptRemoveResponseListener = postMessage(this.Iframe?.contentWindow, "script", value, (executeTimeInMs, exceedTimeInMs) => {
if (executeTimeInMs === -1) {
this.setError();
}
else {
var bubbles = GetNotificationBubbles();
bubbles.add("code executed in: " + executeTimeInMs + "ms", "info");
}
});
this.reset(() => {
if (this.Iframe)
this.Iframe.src = GetOutputFrameUrl() + "execution-time-error.html";
});
}
onIframeLoaded(fn: Function) {
if (this.Iframe) {
this.Iframe.onload = () => {
fn();
setScript(language: string, value: string) {
this.scripts[language] = value;
this.reset(() => {
for (const lang in this.scripts) {
if (lang != "javascript")
this.doPostMessage(lang, this.scripts[lang]);
}
}
else {
this.waitFor(this.Iframe, () => {
this.onIframeLoaded(fn);
});
}
if (this.scripts["javascript"])
this.doPostMessage("javascript", this.scripts["javascript"]);
});
}
doPostMessage(language: string, value: string) {
this.CurrentIframeScriptRemoveResponseListener = postMessage(this.Iframe?.contentWindow, language, this.scripts[language], language == 'javascript' ? (executeTimeInMs, exceedTimeInMs) => {
if (executeTimeInMs === -1) {
this.setError();
}
else {
var bubbles = GetNotificationBubbles();
bubbles.add(language + " executed in: " + executeTimeInMs + "ms", "info");
}
} : undefined);
}
}
@@ -2,20 +2,42 @@
resize-handle {
background: transparent;
position: absolute;
top: 0;
bottom: 0;
width: 8px;
position: absolute !important;
z-index: 9;
cursor: ew-resize;
transition: 1s ease-in;
flex: none !important;
}
resize-handle:hover {
resize-handle[dir="horizontal"] {
cursor: ew-resize;
width: 8px;
top: 0;
bottom: 0;
}
resize-handle[dir="vertical"] {
cursor: ns-resize;
height: 8px;
left: 0;
right: 0;
bottom: 0;
}
resize-handle:hover, resize-handle.active {
background: @primary-interaction-highlight-white;
transition: none;
}
#fixed-resize-overlay {
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
}
@media (prefers-color-scheme: dark) {
resize-handle:hover {
@@ -4,7 +4,6 @@ import './resize-handle.less';
export class ResizeHandle extends BaseElement {
public isDowned = false;
onInit(): void {
this.setDragEvents();
@@ -12,54 +11,120 @@ export class ResizeHandle extends BaseElement {
setDragEvents() {
this.addEventListener("mousedown", (evt: MouseEvent) => {
this.isDowned = true;
this.getIframe().style.display = "none";
});
evt.preventDefault();
evt.stopPropagation();
this.setOverlay({
x: evt.pageX,
y: evt.pageY
});
window.addEventListener("mousemove", (evt: MouseEvent) => {
if (this.isDowned) {
this.setPosition(evt.pageX);
}
});
window.addEventListener("mouseup", (evt: MouseEvent) => {
if (this.isDowned) {
this.getIframe().style.display = "block";
this.isDowned = false;
}
});
this.addEventListener("touchstart", (evt: TouchEvent) => {
this.isDowned = true;
this.getIframe().style.display = "none";
});
window.addEventListener("touchmove", (evt: TouchEvent) => {
if (this.isDowned) {
this.setPosition(evt.touches[0].pageX);
}
});
window.addEventListener("touchend", (evt: TouchEvent) => {
if (this.isDowned) {
this.getIframe().style.display = "block";
this.isDowned = false;
}
evt.preventDefault();
evt.stopPropagation();
this.setOverlay({
x: evt.touches[0].pageX,
y: evt.touches[0].pageY
});
});
}
setPosition(x: number) {
setSize(x: number, y: number) {
var t = this.getTarget();
t.style.minWidth = x + "px";
t.style.maxWidth = x + "px";
if (t) {
if (this.directionIsHorizontal()) {
t.style.minWidth = x + "px";
t.style.maxWidth = x + "px";
}
else {
t.style.minHeight = y + "px";
t.style.maxHeight = y + "px";
}
}
}
getTarget() {
return <CodeEditor>this.find("code-editor");
var targetAttr = this.getAttribute("target");
if (targetAttr) {
var target = <HTMLElement>document.querySelector(targetAttr);
if (target) {
return target;
}
}
return this.parentElement;
}
directionIsHorizontal() {
var attr = this.getAttribute("dir");
if (attr) {
if (attr.toLowerCase().indexOf('v') === 0) {
return false;
}
}
return true;
}
getIframe() {
return <HTMLIFrameElement>this.find("iframe");
}
setOverlay(start: { x: number, y: number }) {
var overlay = document.createElement("div");
overlay.setAttribute("id", "fixed-resize-overlay");
var targetStartSize = { width: 0, height: 0 };
var target = this.getTarget();
if (target) {
var bounds = target.getBoundingClientRect();
targetStartSize = {
width: bounds.width,
height: bounds.height
};
}
overlay.addEventListener("mousemove", (evt: MouseEvent) => {
this.getIframe().style.pointerEvents = "none";
this.classList.add("active");
this.setSize(targetStartSize.width + (evt.pageX - start.x), targetStartSize.height + (evt.pageY - start.y));
});
overlay.addEventListener("mouseup", (evt: MouseEvent) => {
this.getIframe().style.pointerEvents = "all";
this.classList.remove("active");
this.removeOverlay();
});
overlay.addEventListener("touchmove", (evt: TouchEvent) => {
this.getIframe().style.pointerEvents = "none";
this.classList.add("active");
this.setSize(targetStartSize.width + (start.x - evt.touches[0].pageX), targetStartSize.height + (start.y - evt.touches[0].pageY));
});
overlay.addEventListener("touchend", (evt: TouchEvent) => {
this.getIframe().style.pointerEvents = "all";
this.classList.remove("active");
this.removeOverlay();
});
document.body.classList.remove("animate");
document.body.appendChild(overlay);
}
removeOverlay() {
var overlay = document.getElementById("fixed-resize-overlay");
if (overlay) {
overlay.parentElement?.removeChild(overlay);
}
document.body.classList.add("animate");
}
}
@@ -0,0 +1,74 @@
import { BaseElement } from "../../../shared/_base";
import { CodeEditorContainer } from "../code-editor-container/code-editor-container";
import './window-control-connector.less';
export class WindowControlConnector extends BaseElement {
onInit(): void {
this.addEventListener("click", () => {
this.toggle();
})
}
toggle() {
if (this.classList.contains("active")) {
this.deactivate();
}
else {
this.activate();
}
}
activate() {
this.classList.add("active");
var target = this.getTarget();
if (target) {
target.classList.add("active");
target.style.display = "block";
}
this.distributeEvenly();
}
deactivate() {
this.classList.remove("active");
var target = this.getTarget();
if (target) {
target.classList.remove("active");
target.style.display = "none";
}
this.distributeEvenly();
}
private distributeEvenly() {
var elements = <CodeEditorContainer[]>this.findVisible("code-editor-container");
var containers = document.getElementsByClassName("code-editor-containers")[0];
if (elements.length === 0) {
containers.classList.add("force-close");
}
else {
containers.classList.remove("force-close");
var size = (100 / elements.length) + "%";
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
element.style.minHeight = size;
element.style.maxHeight = size;
}
}
}
private getTarget() {
var attr = this.getAttribute("target");
if (attr) {
return <HTMLElement>document.querySelector(attr);
}
return null;
}
}
@@ -0,0 +1,46 @@
@import url('../../../shared/theme.less');
window-control {
display: inline-block;
margin-left: 20px;
}
window-control-connector {
position: relative;
font-size: 12px;
display: inline-block;
background: lighten(@editor-bg-white, 1%);
padding: 6px;
padding-right: 16px;
transition: all 200ms;
cursor: pointer;
}
window-control-connector::after {
position: absolute;
content: "❌";
font-size: 8px;
transform: rotate(45deg);
filter: grayscale();
top: 4px;
right: 4px;
transition: all 200ms;
pointer-events: none;
}
window-control-connector.active {
transform: translateY(7px);
}
window-control-connector.active::after {
transform: rotate(0);
transform-origin: center;
}
@media (prefers-color-scheme: dark) {
window-control-connector {
background: lighten(@editor-bg-dark, 1%);
color: @primary-text-color-dark;
}
}
@@ -0,0 +1,18 @@
import { BaseElement } from "../../../shared/_base";
import { WindowControlConnector } from "./window-control connector";
export class WindowControl extends BaseElement {
setActive(element: WindowControlConnector) {
for (let i = 0; i < this.children.length; i++) {
const child = <WindowControlConnector>this.children[i];
if (element == child) {
child.activate();
}
else {
child.deactivate();
}
}
}
}