code-editor

added html and css editors

3/26/2022 8:44:52 PM

Details

diff --git a/codeeditor-app/app.html b/codeeditor-app/app.html
index e022ccc..0febe63 100644
--- a/codeeditor-app/app.html
+++ b/codeeditor-app/app.html
@@ -1,11 +1,25 @@
 <div class="flex-vert">
     <div class="flex-shrink section top">
-        code editor
+        <h1>code editor</h1>
+        <window-control>
+            <window-control-connector target="code-editor-container[language='Javascript']" class="active">Javascript</window-control-connector>
+            <window-control-connector target="code-editor-container[language='HTML']">HTML</window-control-connector>
+            <window-control-connector target="code-editor-container[language='CSS']">CSS</window-control-connector>
+        </window-control>
     </div>
     <div class="flex-horiz">
-        <code-editor style="min-width: 50%; max-width: 50%;"></code-editor>
+        <div class="code-editor-containers resize-target flex-vert" style="min-width: 50%; max-width: 50%;">
+            <code-editor-container language="Javascript" dir="vertical">
+            </code-editor-container>
+            
+            <code-editor-container language="HTML" dir="vertical" style="display: none;">
+            </code-editor-container>
+            
+            <code-editor-container language="CSS" dir="vertical" style="display: none;">
+            </code-editor-container>
+        </div>
         <div>
-            <resize-handle></resize-handle>
+            <resize-handle target=".resize-target" dir="horizontal"></resize-handle>
             <output-frame></output-frame>
         </div>
     </div>
diff --git a/codeeditor-app/app.less b/codeeditor-app/app.less
index af2d476..99cc228 100644
--- a/codeeditor-app/app.less
+++ b/codeeditor-app/app.less
@@ -8,10 +8,12 @@ html, body {
     background: @median-bg-white;
 }
 
+
 .flex-horiz {
     display: flex;
     flex-direction: row;
     height: 100%;
+    overflow: hidden;
 }
 
 .flex-vert {
@@ -28,6 +30,10 @@ html, body {
     position: relative;
 }
 
+.animate .flex-horiz > *, .animate .flex-vert > * {
+    transition: all 280ms ease-in-out;
+}
+
 .flex-horiz .flex-shrink, .flex-vert .flex-shrink {
     flex-grow: 0;
     flex-shrink: 1;
@@ -60,6 +66,16 @@ body {
 
 .section.top {
     border-bottom: 1px solid @primary-border-color-white;
+    padding: 0;
+}
+
+.section.top h1 {
+    margin: 0;
+    padding: 10px;
+    font-family: @primary-font-family;
+    font-weight: 400;
+    font-size: 18px;
+    display: inline-block;
 }
 
 .section.bottom {
@@ -69,6 +85,15 @@ body {
     text-align: right;
 }
 
+.code-editor-containers {
+    background: @editor-bg-white;
+}
+
+.code-editor-containers.force-close {
+    min-width: 0 !important;
+    max-width: 0 !important;
+}
+
 @media (prefers-color-scheme: dark) {
     body {
         background: @median-bg-dark;
@@ -86,4 +111,8 @@ body {
     .section.bottom {
         border-color: @primary-bg-color-dark;
     }
-}
\ No newline at end of file
+
+    .code-editor-containers {
+        background: @editor-bg-dark;
+    }
+}
diff --git a/codeeditor-app/boot.ts b/codeeditor-app/boot.ts
index 2d4578c..0e4eb2b 100644
--- a/codeeditor-app/boot.ts
+++ b/codeeditor-app/boot.ts
@@ -6,6 +6,9 @@ import { OutputFrame } from './elements/output-frame/output-frame';
 import { NotificationBubbles } from './elements/notification-bubbles/notification-bubbles';
 import { NotificationBubble } from './elements/notification-bubbles/notification-bubble';
 import { ResizeHandle } from './elements/resize-handle/resize-handle';
+import { CodeEditorContainer } from './elements/code-editor-container/code-editor-container';
+import { WindowControl } from './elements/window-control/window-control';
+import { WindowControlConnector } from './elements/window-control/window-control connector';
 
 window.customElements.define('app-root', AppRoot);
 window.customElements.define('code-editor', CodeEditor);
@@ -13,6 +16,9 @@ window.customElements.define('output-frame', OutputFrame);
 window.customElements.define('notification-bubbles', NotificationBubbles);
 window.customElements.define('notification-bubble', NotificationBubble);
 window.customElements.define('resize-handle', ResizeHandle);
+window.customElements.define('code-editor-container', CodeEditorContainer);
+window.customElements.define('window-control', WindowControl);
+window.customElements.define('window-control-connector', WindowControlConnector);
 
 
 document.body.innerHTML += bootHtml;
\ No newline at end of file
diff --git a/codeeditor-app/elements/code-editor/code-editor.ts b/codeeditor-app/elements/code-editor/code-editor.ts
index 0dbffc3..25f231b 100644
--- a/codeeditor-app/elements/code-editor/code-editor.ts
+++ b/codeeditor-app/elements/code-editor/code-editor.ts
@@ -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 = {
diff --git a/codeeditor-app/elements/code-editor/injects/editor-init.css b/codeeditor-app/elements/code-editor/injects/editor-init.css
new file mode 100644
index 0000000..47e1dc8
--- /dev/null
+++ b/codeeditor-app/elements/code-editor/injects/editor-init.css
@@ -0,0 +1,2 @@
+body {
+}
\ No newline at end of file
diff --git a/codeeditor-app/elements/code-editor/injects/editor-init.html b/codeeditor-app/elements/code-editor/injects/editor-init.html
new file mode 100644
index 0000000..094418b
--- /dev/null
+++ b/codeeditor-app/elements/code-editor/injects/editor-init.html
@@ -0,0 +1,2 @@
+<div>
+</div>
\ No newline at end of file
diff --git a/codeeditor-app/elements/code-editor/injects/editor-init.js b/codeeditor-app/elements/code-editor/injects/editor-init.js
index 2e07dda..a74da39 100644
--- a/codeeditor-app/elements/code-editor/injects/editor-init.js
+++ b/codeeditor-app/elements/code-editor/injects/editor-init.js
@@ -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);
\ No newline at end of file
+
+//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");
\ No newline at end of file
diff --git a/codeeditor-app/elements/code-editor-container/code-editor-container.less b/codeeditor-app/elements/code-editor-container/code-editor-container.less
new file mode 100644
index 0000000..0b1e01b
--- /dev/null
+++ b/codeeditor-app/elements/code-editor-container/code-editor-container.less
@@ -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;
+    }
+}
\ No newline at end of file
diff --git a/codeeditor-app/elements/code-editor-container/code-editor-container.ts b/codeeditor-app/elements/code-editor-container/code-editor-container.ts
new file mode 100644
index 0000000..2796983
--- /dev/null
+++ b/codeeditor-app/elements/code-editor-container/code-editor-container.ts
@@ -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);
+    }
+}
\ No newline at end of file
diff --git a/codeeditor-app/elements/notification-bubbles/notification-bubbles.ts b/codeeditor-app/elements/notification-bubbles/notification-bubbles.ts
index dc6a479..d886910 100644
--- a/codeeditor-app/elements/notification-bubbles/notification-bubbles.ts
+++ b/codeeditor-app/elements/notification-bubbles/notification-bubbles.ts
@@ -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");
diff --git a/codeeditor-app/elements/output-frame/output-frame.ts b/codeeditor-app/elements/output-frame/output-frame.ts
index 6580019..ff2215e 100644
--- a/codeeditor-app/elements/output-frame/output-frame.ts
+++ b/codeeditor-app/elements/output-frame/output-frame.ts
@@ -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";
-        }
+        this.reset(() => {
+            if (this.Iframe)
+                this.Iframe.src = GetOutputFrameUrl() + "execution-time-error.html";
+        });
     }
 
-    preventInfiniteLoop(value: string) {
-        return value.replace("while (true)", "while (false)");
-    }
+    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]);
+            }
 
-    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");
-                }
-            });
+            if (this.scripts["javascript"])
+                this.doPostMessage("javascript", this.scripts["javascript"]);
         });
     }
 
-    onIframeLoaded(fn: Function) {
-        if (this.Iframe) {
-            this.Iframe.onload = () => {
-                fn();
+    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 {
-            this.waitFor(this.Iframe, () => {
-                this.onIframeLoaded(fn);
-            });
-        }
+            else {
+                var bubbles = GetNotificationBubbles();
+                bubbles.add(language + " executed in: " + executeTimeInMs + "ms", "info");
+            }
+        } : undefined);
     }
 }
\ No newline at end of file
diff --git a/codeeditor-app/elements/resize-handle/resize-handle.less b/codeeditor-app/elements/resize-handle/resize-handle.less
index 650c2ed..361f49f 100644
--- a/codeeditor-app/elements/resize-handle/resize-handle.less
+++ b/codeeditor-app/elements/resize-handle/resize-handle.less
@@ -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[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: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 {
diff --git a/codeeditor-app/elements/resize-handle/resize-handle.ts b/codeeditor-app/elements/resize-handle/resize-handle.ts
index 65ddf91..89f7a3b 100644
--- a/codeeditor-app/elements/resize-handle/resize-handle.ts
+++ b/codeeditor-app/elements/resize-handle/resize-handle.ts
@@ -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";
+            evt.preventDefault();
+            evt.stopPropagation();
+            this.setOverlay({
+                x: evt.touches[0].pageX,
+                y: evt.touches[0].pageY
+            });
         });
+    }
 
-        window.addEventListener("touchmove", (evt: TouchEvent) => {
-            if (this.isDowned) {
-                this.setPosition(evt.touches[0].pageX);
-            }
-        });
+    setSize(x: number, y: number) {
+        var t = this.getTarget();
 
-        window.addEventListener("touchend", (evt: TouchEvent) => {
-            if (this.isDowned) {
-                this.getIframe().style.display = "block";
-                this.isDowned = false;
+        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";
+            }
+        }
     }
 
-    setPosition(x: number) {
-        var t = this.getTarget();
+    getTarget() {
+        var targetAttr = this.getAttribute("target");
+        if (targetAttr) {
+            var target = <HTMLElement>document.querySelector(targetAttr);
 
-        t.style.minWidth = x + "px";
-        t.style.maxWidth = x + "px";
+            if (target) {
+                return target;
+            }
+        }
+
+        return this.parentElement;
     }
 
-    getTarget() {
-        return <CodeEditor>this.find("code-editor");
+    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");
+    }
 }
\ No newline at end of file
diff --git a/codeeditor-app/elements/window-control/window-control connector.ts b/codeeditor-app/elements/window-control/window-control connector.ts
new file mode 100644
index 0000000..e846390
--- /dev/null
+++ b/codeeditor-app/elements/window-control/window-control connector.ts
@@ -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;
+    }
+
+}
\ No newline at end of file
diff --git a/codeeditor-app/elements/window-control/window-control.ts b/codeeditor-app/elements/window-control/window-control.ts
new file mode 100644
index 0000000..f1474d9
--- /dev/null
+++ b/codeeditor-app/elements/window-control/window-control.ts
@@ -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();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/codeeditor-app/elements/window-control/window-control-connector.less b/codeeditor-app/elements/window-control/window-control-connector.less
new file mode 100644
index 0000000..bde357a
--- /dev/null
+++ b/codeeditor-app/elements/window-control/window-control-connector.less
@@ -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;
+    }
+}
\ No newline at end of file
diff --git a/codeeditor-app/static/index.html b/codeeditor-app/static/index.html
index 19e3113..722a5dd 100644
--- a/codeeditor-app/static/index.html
+++ b/codeeditor-app/static/index.html
@@ -12,7 +12,7 @@
     <base href="/" />
 </head>
 
-<body>
+<body class="animate">
     <script type="text/javascript" src="/main.bundle.js?v=1"></script>
 </body>
 
diff --git a/outputframe-app/boot.ts b/outputframe-app/boot.ts
index d5fc47a..d5cb429 100644
--- a/outputframe-app/boot.ts
+++ b/outputframe-app/boot.ts
@@ -2,7 +2,7 @@ import './app.less';
 import bootHtml from './app.html';
 import { watchConsole } from './injects/watch-console';
 import { setOnErrorListener } from './injects/on-error';
-import { createScript } from '../shared/create-script';
+import { createCss, createHtml, createScript } from '../shared/create-script';
 import { messageListener } from '../shared/message-listener';
 import { postMessage, postResponseMessage } from '../shared/post-message';
 import { RenderCanvas } from './elements/render-canvas/render-canvas';
@@ -17,10 +17,16 @@ watchConsole((type, value) => {
     postMessage(parent, type, value);
 });
 messageListener((type, value, id) => {
-    if (type == "script") {
+    if (type == "javascript") {
         createScript(value);
         postResponseMessage(parent, id, value);
     }
+    else if (type == "html") {
+        createHtml(value);
+    }
+    else if (type == "css") {
+        createCss(value);
+    }
 });
 setOnErrorListener((type, value) => {
     postMessage(parent, type, value);
diff --git a/outputframe-app/elements/render-canvas/render-canvas.ts b/outputframe-app/elements/render-canvas/render-canvas.ts
index ea062bf..77e94a3 100644
--- a/outputframe-app/elements/render-canvas/render-canvas.ts
+++ b/outputframe-app/elements/render-canvas/render-canvas.ts
@@ -11,10 +11,10 @@ export class RenderCanvas extends BaseElement {
         width: 400,
         height: 400
     }
-    public drawCount: number = 0;
-    public drawQueue: { points: { x: number, y: number }[], color?: string }[] = [];
+    private drawCount: number = 0;
+    private drawQueue: { points: { x: number, y: number }[], color?: string, connected?: boolean }[] = [];
 
-    public prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
+    private prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
 
     private theme: any;
 
@@ -44,10 +44,12 @@ export class RenderCanvas extends BaseElement {
             this.canvas.style.height = smallest + "px";
             this.canvas.setAttribute("width", smallest.toString());
             this.canvas.setAttribute("height", smallest.toString());
+
+            this.draw();
         }
     }
 
-    drawPoly(points: { x: number, y: number }[], color?: string | CanvasGradient) {
+    private drawPoly(points: { x: number, y: number }[], color?: string | CanvasGradient) {
         if (this.ctx && this.canvas) {
             this.canvas.style.display = "block";
             if (color instanceof CanvasGradient) {
@@ -77,7 +79,7 @@ export class RenderCanvas extends BaseElement {
         }
     }
 
-    centerOf(points: { x: number, y: number }[]) {
+    private centerOf(points: { x: number, y: number }[]) {
         var x = 0,
             y = 0,
             i,
@@ -99,7 +101,7 @@ export class RenderCanvas extends BaseElement {
         return { x: x / f, y: y / f };
     };
 
-    areaOf(points: { x: number, y: number }[]) {
+    private areaOf(points: { x: number, y: number }[]) {
         var area = 0,
             i,
             j,
@@ -117,50 +119,42 @@ export class RenderCanvas extends BaseElement {
         return area;
     };
 
-    angleBetweenPoints(point1: { x: number, y: number }, point2: { x: number, y: number }) {
-        // angle in radians
-        var angleRadians = Math.atan2(point2.y - point1.y, point2.x - point1.x);
-
-        // angle in degrees
+    private angleBetweenPoints(point1: { x: number, y: number }, point2: { x: number, y: number }) {
         return Math.atan2(point2.y - point1.y, point2.x - point1.x) * 180 / Math.PI;
     }
 
-    executeDrawQueue() {
-        let prevShape: any | null = null;
+    draw() {
+        let prevConnectedShape: any | null = null;
         for (let i = 0; i < this.drawQueue.length; i++) {
             const shape = this.drawQueue[i];
 
-            if (prevShape != null && this.ctx) {
-                var prevCenter = this.centerOf(prevShape.points);
+            if (shape.connected && prevConnectedShape != null && this.ctx) {
+                var prevCenter = this.centerOf(prevConnectedShape.points);
                 var thisCenter = this.centerOf(shape.points);
 
-                var middle = {
-                    x: (prevCenter.x + thisCenter.x) / 2,
-                    y: (prevCenter.y + thisCenter.y) / 2
-                };
-
                 var grd = this.ctx.createLinearGradient(this.unitsToPx(prevCenter.x), this.unitsToPx(prevCenter.y), this.unitsToPx(thisCenter.x), this.unitsToPx(thisCenter.y));
-                grd.addColorStop(0, prevShape?.color ?? this.getColor());
+                grd.addColorStop(0, prevConnectedShape?.color ?? this.getColor());
                 grd.addColorStop(1, shape?.color ?? this.getColor());
 
                 this.drawPoly([prevCenter, thisCenter], grd);
-
-                //this.drawPoly([prevCenter, middle], prevShape.color);
-                //this.drawPoly([middle, thisCenter], shape.color);
             }
 
-            prevShape = shape;
+            if (shape.connected) {
+                prevConnectedShape = shape;
+            }
         }
 
         for (let i = 0; i < this.drawQueue.length; i++) {
             const shape = this.drawQueue[i];
             this.drawPoly(shape.points, shape.color);
             var thisCenter = this.centerOf(shape.points);
-            this.drawText(thisCenter.x, thisCenter.y, (i + 1).toString(), this.unitsToPx((shape.points[1].x - shape.points[0].x) * 0.6), shape.color);
+            if (shape.connected) {
+                this.drawText(thisCenter.x, thisCenter.y, (i + 1).toString(), this.unitsToPx((shape.points[1].x - shape.points[0].x) * 0.6), shape.color);
+            }
         }
     }
 
-    drawText(x: number, y: number, text: string, size: number = 20, color?: string) {
+    private drawText(x: number, y: number, text: string, size: number = 20, color?: string) {
         if (this.ctx && this.canvas) {
             this.ctx.moveTo(x, y);
             this.ctx.fillStyle = this.getColor(color);
@@ -171,27 +165,23 @@ export class RenderCanvas extends BaseElement {
         }
     }
 
-    getColor(color?: string) {
+    private getColor(color?: string) {
         return color ? color : (this.prefersDarkScheme.matches ? this.theme["@draw-color-dark"] : this.theme["@draw-color-white"]);
     }
 
-    getBackgroundColor() {
+    private getBackgroundColor() {
         return this.prefersDarkScheme.matches ? this.theme["@median-bg-dark"] : this.theme["@median-bg-white"];
     }
 
-    drawRect(x: number, y: number, width: number, height: number, color?: string) {
-        this.drawPoly([{ x: x, y: y }, { x: x + width, y: y }, { x: x + width, y: y + height }, { x: x, y: y + height }], color);
-    }
-
-    queuePoly(points: { x: number, y: number }[], color?: string) {
+    poly(points: { x: number, y: number }[], color?: string) {
         this.drawQueue.push({ points: points, color: color });
     }
 
-    queueRect(x: number, y: number, width: number, height: number, color?: string) {
-        this.drawQueue.push({ points: [{ x: x, y: y }, { x: x + width, y: y }, { x: x + width, y: y + height }, { x: x, y: y + height }], color: color });
+    rect(x: number, y: number, width: number, height: number, color?: string, connected?: boolean) {
+        this.drawQueue.push({ points: [{ x: x, y: y }, { x: x + width, y: y }, { x: x + width, y: y + height }, { x: x, y: y + height }], color: color, connected: connected });
     }
 
-    unitsToPx(unitNumber: number) {
+    private unitsToPx(unitNumber: number) {
         var newPPU = Math.min(this.width, this.height) / this.units.width;
         return Math.floor(unitNumber * newPPU);
     }
diff --git a/outputframe-app/injects/dom-helpers.ts b/outputframe-app/injects/dom-helpers.ts
index b94bf72..f1b1b07 100644
--- a/outputframe-app/injects/dom-helpers.ts
+++ b/outputframe-app/injects/dom-helpers.ts
@@ -6,12 +6,12 @@ export function clear() {
 
 export function rect(x: number, y: number, width: number, height: number, color?: string): void {
     var renderCanvas = <RenderCanvas>document.getElementsByTagName("render-canvas")[0];
-    renderCanvas.drawRect(x, y, width, height, color);
+    renderCanvas.rect(x, y, width, height, color, false);
 }
 
 export function box(x: number, y: number, width: number, height: number, color?: string): void {
     var renderCanvas = <RenderCanvas>document.getElementsByTagName("render-canvas")[0];
-    renderCanvas.queueRect(x, y, width, height, color);
+    renderCanvas.rect(x, y, width, height, color, true);
 }
 
 export function getWidth(): number {
@@ -35,7 +35,7 @@ export function getRandom(min: number, max: number): number {
 
 export function executeDrawQueue(): void {
     var renderCanvas = <RenderCanvas>document.getElementsByTagName("render-canvas")[0];
-    renderCanvas.executeDrawQueue();
+    renderCanvas.draw();
 }
 
 export function getRandomColor(): string {

shared/_base.ts 16(+16 -0)

diff --git a/shared/_base.ts b/shared/_base.ts
index c90c3e5..82bb140 100644
--- a/shared/_base.ts
+++ b/shared/_base.ts
@@ -67,6 +67,22 @@ export class BaseElement extends HTMLElement {
 
         return null;
     }
+    
+    findVisible(tagName: string) {
+        var result = [];
+        
+        var elements = document.getElementsByTagName(tagName);
+
+        for (let i = 0; i < elements.length; i++) {
+            const element = <HTMLElement>elements[i];
+            
+            if (element.style.display !== "none") {
+                result.push(element);
+            }
+        }
+
+        return result;
+    }
 
     hasChild(element: Element, startsWidth: boolean = false): boolean {
         var ele = this;
diff --git a/shared/create-script.ts b/shared/create-script.ts
index a75b403..2119c3f 100644
--- a/shared/create-script.ts
+++ b/shared/create-script.ts
@@ -8,4 +8,18 @@ export function createScript(value: string) {
     document.body.appendChild(script);
 
     executeDrawQueue();
+}
+
+export function createHtml(value: string) {
+    var div: HTMLDivElement = document.createElement("div");
+    div.innerHTML = value;
+
+    document.body.appendChild(div);
+}
+
+export function createCss(value: string) {
+    var style: HTMLStyleElement = document.createElement("style");
+    style.innerHTML = value;
+
+    document.body.appendChild(style);
 }
\ No newline at end of file
diff --git a/shared/message-listener.ts b/shared/message-listener.ts
index 75c3a29..58c0f72 100644
--- a/shared/message-listener.ts
+++ b/shared/message-listener.ts
@@ -1,4 +1,4 @@
-export function messageListener(fn: (type: 'script' | 'log' | 'info' | 'warn' | 'error' | 'syntax-error' | 'time' | 'response', value: string, id: number) => void): Function {
+export function messageListener(fn: (type: 'script' | 'log' | 'info' | 'warn' | 'error' | 'syntax-error' | 'time' | 'response' | string, value: string, id: number) => void): Function {
     
     var evt = function(e: any) {
         if (!e.data || e.data === '' || e.data.type === 'webpackOk') {