added two apps, and better implementation
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<div class="flex-vert">
|
||||
<div class="flex-shrink section top">
|
||||
code editor
|
||||
</div>
|
||||
<div class="flex-horiz">
|
||||
<code-editor style="min-width: 50%; max-width: 50%;"></code-editor>
|
||||
<output-frame></output-frame>
|
||||
</div>
|
||||
<div class="flex-shrink section bottom">methods: rect, box, center, width, height</div>
|
||||
</div>
|
||||
<notification-bubbles></notification-bubbles>
|
||||
@@ -0,0 +1,66 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.flex-horiz {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.flex-vert {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.flex-horiz > *, .flex-vert > * {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
flex-basis: 1px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flex-horiz .flex-shrink, .flex-vert .flex-shrink {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.flex-horiz .flex-grow, .flex-vert .flex-grow {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.flex-reset {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Segoe UI", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #e3e3e3;
|
||||
padding: 20px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.section.top {
|
||||
border-bottom: 1px solid #c2c2c2;
|
||||
}
|
||||
|
||||
.section.bottom {
|
||||
border-top: 1px solid #c2c2c2;
|
||||
font-size: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import './app.less';
|
||||
import bootHtml from './app.html';
|
||||
import { AppRoot } from './elements/app-root/app-root';
|
||||
import { CodeEditor } from './elements/code-editor/code-editor';
|
||||
import { OutputFrame } from './elements/output-frame/output-frame';
|
||||
import { NotificationBubbles } from './elements/notification-bubbles/notification-bubbles';
|
||||
import { NotificationBubble } from './elements/notification-bubbles/notification-bubble';
|
||||
|
||||
window.customElements.define('app-root', AppRoot);
|
||||
window.customElements.define('code-editor', CodeEditor);
|
||||
window.customElements.define('output-frame', OutputFrame);
|
||||
window.customElements.define('notification-bubbles', NotificationBubbles);
|
||||
window.customElements.define('notification-bubble', NotificationBubble);
|
||||
|
||||
|
||||
document.body.innerHTML += bootHtml;
|
||||
@@ -0,0 +1,103 @@
|
||||
export class BaseElement extends HTMLElement {
|
||||
private _hasConnectedCallback: boolean = false;
|
||||
connectedCallback() {
|
||||
|
||||
if (!this._hasConnectedCallback) {
|
||||
this.onInit();
|
||||
}
|
||||
else {
|
||||
this.onUpdate();
|
||||
}
|
||||
|
||||
this._hasConnectedCallback = true;
|
||||
|
||||
}
|
||||
|
||||
onInit() { }
|
||||
onUpdate() { }
|
||||
|
||||
find(tagName: string): HTMLElement | null {
|
||||
|
||||
var found = document.getElementsByTagName(tagName);
|
||||
if (found[0]) {
|
||||
return <HTMLElement>found[0];
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
findAncestor(tagName: string, startsWidth: boolean = false): Element | null {
|
||||
var element = <any>this;
|
||||
|
||||
while (element.parentElement !== null) {
|
||||
var parentEle = element.parentElement;
|
||||
|
||||
if (tagName.toUpperCase() === parentEle.tagName.toUpperCase()) {
|
||||
return parentEle;
|
||||
}
|
||||
else if (startsWidth && parentEle.tagName.toUpperCase().indexOf(tagName.toUpperCase()) === 0) {
|
||||
return parentEle;
|
||||
}
|
||||
|
||||
element = parentEle;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
findSibling(tagName: string, startsWidth: boolean = false): Element | null {
|
||||
var parentEle = this.parentElement;
|
||||
|
||||
if (parentEle != null) {
|
||||
|
||||
for (let i = 0; i < parentEle.children.length; i++) {
|
||||
const child = parentEle.children[i];
|
||||
|
||||
if (child != this) {
|
||||
if (tagName.toUpperCase() === child.tagName.toUpperCase()) {
|
||||
return child;
|
||||
}
|
||||
else if (startsWidth && child.tagName.toUpperCase().indexOf(tagName.toUpperCase()) === 0) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
hasChild(element: Element, startsWidth: boolean = false): boolean {
|
||||
var ele = this;
|
||||
|
||||
if (ele != null) {
|
||||
for (let i = 0; i < ele.children.length; i++) {
|
||||
const child = ele.children[i];
|
||||
|
||||
if (child == element) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
map(tagName: string): any {
|
||||
return this.getElementsByTagName(tagName)[0];
|
||||
}
|
||||
|
||||
mapGlobal(tagName: string): any {
|
||||
return document.getElementsByTagName(tagName)[0];
|
||||
}
|
||||
|
||||
waitFor(value: any, fn: Function) {
|
||||
var interv = setInterval(() => {
|
||||
if (value) {
|
||||
clearInterval(interv);
|
||||
fn();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
app-root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { BaseElement } from "../_base";
|
||||
import './app-root.less';
|
||||
|
||||
export class AppRoot extends BaseElement {
|
||||
onInit(): void {
|
||||
console.log("APP ROOT INIT");
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
console.log("APP ROOT UPDATE");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
monaco-editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { BaseElement } from "../_base";
|
||||
import './code-editor.less';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import { OutputFrame } from "../output-frame/output-frame";
|
||||
import initScript from './injects/editor-init.txt';
|
||||
|
||||
export class CodeEditor extends BaseElement {
|
||||
|
||||
public input: string = initScript;
|
||||
|
||||
onInit(): void {
|
||||
|
||||
|
||||
this.initWorkers();
|
||||
|
||||
|
||||
var editor = monaco.editor.create(this, {
|
||||
value: this.input,
|
||||
language: 'javascript',
|
||||
automaticLayout: true,
|
||||
contextmenu: false,
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
autoIndent: "full"
|
||||
});
|
||||
|
||||
monaco.languages.registerCompletionItemProvider("javascript", {
|
||||
triggerCharacters: ["."],
|
||||
provideCompletionItems: (model, position, context, token) => (
|
||||
{
|
||||
suggestions: [
|
||||
{
|
||||
label: 'for',
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: 'for (let i = 0; i < array.length; i++) {\n\n}',
|
||||
range: <any>null,
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
||||
},
|
||||
{
|
||||
label: 'forr',
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: 'for (var i = array.length - 1; i >= 0; i--) {\n\n}',
|
||||
range: <any>null,
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
editor.onDidChangeModelContent((e) => {
|
||||
this.input = editor.getValue();
|
||||
this.outputFrame.setScript(this.input);
|
||||
});
|
||||
|
||||
this.waitFor(this.outputFrame, () => {
|
||||
this.outputFrame.setScript(this.input);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
initWorkers() {
|
||||
// @ts-ignore
|
||||
self.MonacoEnvironment = {
|
||||
getWorkerUrl: function (moduleId: any, label: string) {
|
||||
if (label === 'json') {
|
||||
return './json.worker.bundle.js';
|
||||
}
|
||||
if (label === 'css' || label === 'scss' || label === 'less') {
|
||||
return './css.worker.bundle.js';
|
||||
}
|
||||
if (label === 'html' || label === 'handlebars' || label === 'razor') {
|
||||
return './html.worker.bundle.js';
|
||||
}
|
||||
if (label === 'typescript' || label === 'javascript') {
|
||||
return './ts.worker.bundle.js';
|
||||
}
|
||||
return './editor.worker.bundle.js';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
console.log("APP ROOT UPDATE");
|
||||
}
|
||||
|
||||
get outputFrame(): OutputFrame {
|
||||
return <OutputFrame>this.findSibling("output-frame");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
var myDiv = document.createElement("div");
|
||||
myDiv.innerText = "hello moto";
|
||||
|
||||
document.body.appendChild(myDiv);
|
||||
|
||||
|
||||
var i = 0;
|
||||
|
||||
while (i < 10) {
|
||||
console.log("hello " + i);
|
||||
i++;
|
||||
}
|
||||
|
||||
var obj = { hello: true, moto: "enabled" };
|
||||
|
||||
console.log(obj);
|
||||
@@ -0,0 +1,38 @@
|
||||
notification-bubble {
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
color: black;
|
||||
background-color: #a0ffa7;
|
||||
border: 1px solid #8ddf92;
|
||||
display: block;
|
||||
position: relative;
|
||||
box-shadow: 1px 1px 1px 1px rgba(0,0,0,0.19);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
notification-bubble.log {
|
||||
background-color: #ffffff;
|
||||
border-color: #dfdfdf;
|
||||
}
|
||||
|
||||
notification-bubble.info {
|
||||
background-color: #e7e7b3;
|
||||
border-color: #d4d4a9;
|
||||
}
|
||||
|
||||
notification-bubble.warn {
|
||||
background-color: #fff9a0;
|
||||
border-color: #e6e094;
|
||||
}
|
||||
|
||||
notification-bubble.error {
|
||||
background-color: #ffa0a0;
|
||||
border-color: #df8e8e;
|
||||
}
|
||||
|
||||
notification-bubble.syntax-error {
|
||||
background-color: #ff6262;
|
||||
background-color: #d15353;
|
||||
font-size: 22px;
|
||||
padding: 30px;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { BaseElement } from "../_base";
|
||||
import './notification-bubble.less';
|
||||
|
||||
export class NotificationBubble extends BaseElement {
|
||||
onInit(): void {
|
||||
|
||||
if (!this.classList.contains("syntax-error")) {
|
||||
setTimeout(() => {
|
||||
this.parentElement?.removeChild(this);
|
||||
}, 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
notification-bubbles {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
width: 380px;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { messageListener } from "../../../shared/message-listener";
|
||||
import { BaseElement } from "../_base";
|
||||
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') {
|
||||
|
||||
if (text && type) {
|
||||
var bubble = <NotificationBubble>document.createElement("notification-bubble");
|
||||
bubble.innerText = text;
|
||||
bubble.classList.add(type);
|
||||
this.appendChild(bubble);
|
||||
|
||||
if (this.children.length > 50) {
|
||||
this.removeChild(this.children[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.innerHTML = "";
|
||||
}
|
||||
|
||||
onInit(): void {
|
||||
messageListener((type, value) => {
|
||||
if (type != 'script' && type != 'response')
|
||||
this.add(value, type);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
output-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
output-frame > iframe {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { BaseElement } from "../_base";
|
||||
import './output-frame.less';
|
||||
import { NotificationBubbles } from "../notification-bubbles/notification-bubbles";
|
||||
import { GetOutputFrameUrl } from "../../../shared/url-helpers";
|
||||
import { GetNotificationBubbles } from "../../../shared/getdom";
|
||||
import { postMessage } from "../../../shared/post-message";
|
||||
|
||||
export class OutputFrame extends BaseElement {
|
||||
public Iframe: HTMLIFrameElement | null = null;
|
||||
public CurrentIframeScriptRemoveResponseListener: Function | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
onInit(): void {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
onUpdate(): void {
|
||||
}
|
||||
|
||||
reset(resetNotificationBubbles: boolean = true) {
|
||||
if (this.CurrentIframeScriptRemoveResponseListener != null) {
|
||||
this.CurrentIframeScriptRemoveResponseListener();
|
||||
this.CurrentIframeScriptRemoveResponseListener = null;
|
||||
}
|
||||
|
||||
if (this.Iframe && this.hasChild(this.Iframe)) {
|
||||
this.removeChild(this.Iframe);
|
||||
}
|
||||
|
||||
this.Iframe = document.createElement("iframe");
|
||||
this.Iframe.setAttribute("sandbox", "allow-pointer-lock allow-same-origin allow-scripts");
|
||||
this.Iframe.src = GetOutputFrameUrl();
|
||||
this.appendChild(this.Iframe);
|
||||
|
||||
if (resetNotificationBubbles) {
|
||||
var bubbles = GetNotificationBubbles();
|
||||
if (bubbles && bubbles.reset) {
|
||||
bubbles.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onIframeLoaded(fn: Function) {
|
||||
if (this.Iframe) {
|
||||
this.Iframe.onload = () => {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.waitFor(this.Iframe, () => {
|
||||
this.onIframeLoaded(fn);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<!--web.config url rewrite-->
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<rewrite>
|
||||
<rules>
|
||||
<rule name="Redirect to https" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions>
|
||||
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="https://{HTTP_HOST}{REQUEST_URI}" redirectType="Permanent" appendQueryString="false" />
|
||||
</rule>
|
||||
<rule name="Redirect To Index" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions>
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
|
||||
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
|
||||
</conditions>
|
||||
<action type="Rewrite" url="/index.html" />
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
@@ -0,0 +1,19 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" id="master-viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-title" content="Title">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<title>code editor</title>
|
||||
|
||||
<link rel="manifest" href="manifest.json?v=1">
|
||||
<base href="/" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script type="text/javascript" src="/main.bundle.js?v=1"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"short_name": "ShortName",
|
||||
"name": "AppName",
|
||||
"icons": [
|
||||
{
|
||||
"src": "logo.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
],
|
||||
"display": "fullscreen",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#252831",
|
||||
"theme_color": "#0094ff",
|
||||
"start_url": "/?webapp=true"
|
||||
}
|
||||
Reference in New Issue
Block a user