diff --git a/codeeditor-app/app.html b/codeeditor-app/app.html index d803575..2a4fe52 100644 --- a/codeeditor-app/app.html +++ b/codeeditor-app/app.html @@ -2,12 +2,17 @@

code editor

+ Javascript HTML CSS
+
diff --git a/codeeditor-app/boot.ts b/codeeditor-app/boot.ts index b990417..8fd4372 100644 --- a/codeeditor-app/boot.ts +++ b/codeeditor-app/boot.ts @@ -12,6 +12,10 @@ import { JavascriptCodeEditor } from './elements/code-editor/javascript-code-edi import { HtmlCodeEditor } from './elements/code-editor/html-code-editor'; import { CssCodeEditor } from './elements/code-editor/css-code-editor'; import { setDynamicFlexListener } from '../shared/set-dynamic-flex-listener'; +import { SessionList } from './elements/session-list/session-list'; +import { SessionListItem } from './elements/session-list-item/session-list-item'; +import { LoadingSpinner } from './elements/loading-spinner/loading-spinner'; +import { isSessionPage } from '../shared/page'; window.customElements.define('app-root', AppRoot); window.customElements.define('code-editor-container', CodeEditorContainer); @@ -24,8 +28,23 @@ window.customElements.define('notification-bubble', NotificationBubble); window.customElements.define('resize-handle', ResizeHandle); window.customElements.define('window-control', WindowControl); window.customElements.define('window-control-connector', WindowControlConnector); - +window.customElements.define('session-list', SessionList); +window.customElements.define('session-list-item', SessionListItem); +window.customElements.define('loading-spinner', LoadingSpinner); document.body.innerHTML += bootHtml; +if (isSessionPage()) { + var menuEle = document.getElementById("session-menu"); + var containers = document.getElementsByClassName("code-editor-containers")[0]; + var outputframe = document.getElementsByClassName("output-frame-container")[0]; + var sessionMenuItem = document.querySelector("window-control-connector[target='.menu-container']"); + if (menuEle) { + menuEle.style.display = "block"; + sessionMenuItem.style.display = "inline"; + containers.style.display = "none"; + outputframe.style.display = "none"; + } +} + setDynamicFlexListener(); diff --git a/codeeditor-app/elements/code-editor-container/code-editor-container.ts b/codeeditor-app/elements/code-editor-container/code-editor-container.ts index 665fbd4..ceee3e1 100644 --- a/codeeditor-app/elements/code-editor-container/code-editor-container.ts +++ b/codeeditor-app/elements/code-editor-container/code-editor-container.ts @@ -4,6 +4,7 @@ import { ResizeHandle } from "../resize-handle/resize-handle"; import './code-editor-container.less'; export class CodeEditorContainer extends BaseElement { + private codeEditor: CodeEditor | null = null; onInit(): void { var language = this.getAttribute("language"); this.classList.add("flex-vert"); @@ -22,11 +23,15 @@ export class CodeEditorContainer extends BaseElement { this.appendChild(div); } + setInput(input: any) { + this.codeEditor?.setInput(input); + } + createCodeEditor(language: string, expandDir: 'vertical' | 'horizontal') { var codeEditorContent = document.createElement("div"); codeEditorContent.classList.add("code-editor-content"); - var codeEditor = document.createElement(language + "-code-editor"); - codeEditorContent.appendChild(codeEditor); + this.codeEditor = document.createElement(language + "-code-editor"); + codeEditorContent.appendChild(this.codeEditor); var resizeHandle = document.createElement("resize-handle"); resizeHandle.setAttribute("dir", expandDir); diff --git a/codeeditor-app/elements/code-editor/code-editor.ts b/codeeditor-app/elements/code-editor/code-editor.ts index 74ca11c..b7390c6 100644 --- a/codeeditor-app/elements/code-editor/code-editor.ts +++ b/codeeditor-app/elements/code-editor/code-editor.ts @@ -3,11 +3,14 @@ import './code-editor.less'; import * as monaco from 'monaco-editor'; import { OutputFrame } from "../output-frame/output-frame"; import { debounceManager } from "../../../shared/ensure-debounce"; +import * as sessionApi from '../../../shared/session-api'; +import { isSessionPage } from "../../../shared/page"; export class CodeEditor extends BaseElement { public input: string = ""; public language: string = ""; + public editor!: monaco.editor.IStandaloneCodeEditor; onInit(): void { @@ -18,11 +21,12 @@ export class CodeEditor extends BaseElement { const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)"); - var editor = monaco.editor.create(this, { - value: this.input, + this.editor = monaco.editor.create(this, { + value: isSessionPage() ? "" : this.input, language: this.language, automaticLayout: true, contextmenu: false, + readOnly: isSessionPage(), minimap: { enabled: false }, @@ -31,19 +35,30 @@ export class CodeEditor extends BaseElement { }); var debounce = debounceManager(1000); - editor.onDidChangeModelContent((e) => { - this.input = editor.getValue(); - debounce.ensureDebounce(() => { - this.outputFrame.setContent(this.language, this.input); - }); + this.editor.onDidChangeModelContent((e) => { + if (!isSessionPage()) { + this.input = this.editor.getValue(); + debounce.ensureDebounce(() => { + sessionApi.setSessionData(this.language, this.input); + this.outputFrame.setContent(this.language, this.input); + }); + } }); this.waitFor(this.outputFrame, () => { - this.outputFrame.setContent(this.language, this.input); + this.outputFrame.setContent(this.language, isSessionPage() ? "" : this.input); }); } + setInput(input: any) { + if (this.input !== input) { + this.input = input; + this.editor.setValue(input); + this.outputFrame.setContent(this.language, this.input); + } + } + setInitInput(): string { return ""; } diff --git a/codeeditor-app/elements/loading-spinner/loading-spinner.less b/codeeditor-app/elements/loading-spinner/loading-spinner.less new file mode 100644 index 0000000..715e310 --- /dev/null +++ b/codeeditor-app/elements/loading-spinner/loading-spinner.less @@ -0,0 +1,176 @@ +loading-spinner { + display: block; + position: relative; + margin: 0 auto; + top: 50%; + transform: translateY(-50%); + border-radius: 50%; + opacity: 0.3; +} + +.infinity-1 { + width:90px; + height:14px; + background: + radial-gradient(circle 7px at bottom, #fff 92%,#0000 ) 0 0, + radial-gradient(circle 7px at top , #fff 92%,#0000 ) 0 100%; + background-size:calc(100%/4) 50%; + background-repeat:repeat-x; + animation:i1 1s infinite; + } + @keyframes i1 { + 80%,100% {background-position: calc(100%/3) 0,calc(100%/-3) 100%} + } + + .infinity-2 { + width:90px; + height:14px; + background: + conic-gradient(from 135deg at top ,#fff 90deg,#0000 0) 0 0, + conic-gradient(from -45deg at bottom,#fff 90deg,#0000 0) 0 100%; + background-size:calc(100%/4) 50%; + background-repeat:repeat-x; + animation:i2 1s infinite; + } + @keyframes i2 { + 80%,100% {background-position: calc(100%/3) 0,calc(100%/-3) 100%} + } + + .infinity-3 { + width:90px; + height:14px; + background: + radial-gradient(circle 7px at bottom, #fff 92%,#0000 ) calc(100%/2) 0, + radial-gradient(circle 7px at top , #fff 92%,#0000 ) calc(100%/2) 100%, + conic-gradient(from 135deg at top ,#fff 90deg,#0000 0) 0 0, + conic-gradient(from -45deg at bottom,#fff 90deg,#0000 0) 0 100%; + background-size:calc(100%/2) 50%; + background-repeat:repeat-x; + animation:i3 3s infinite; + } + @keyframes i3 { + 0% {background-position: calc(100%/2) 0,calc(100%/2) 100%,0 0, 0 100%} + 20%, + 30% {background-position: calc(3*100%/4) 0,calc(100%/4) 100%,calc(100%/4) 0, calc(100%/-4) 100%} + 45%, + 55% {background-position: 100% 0,0 100%,calc(100%/2) 0, calc(100%/-2) 100%} + 70%, + 80% {background-position: calc(5*100%/4) 0,calc(100%/-4) 100%,calc(3*100%/4) 0, calc(3*100%/-4) 100%} + 100%{background-position: calc(3*100%/2) 0,calc(100%/-2) 100%,100% 0, -100% 100%} + } + + .infinity-4 { + width:90px; + height:14px; + background: + radial-gradient(circle closest-side, #fff 92%,#0000 ) calc(100%/-4) 0, + radial-gradient(circle closest-side, #fff 92%,#0000 ) calc(100%/4) 0; + background-size:calc(100%/2) 100%; + animation:i4 1.5s infinite; + } + @keyframes i4 { + 0% {background-position: calc(100%/-4) 0 ,calc(100%/4) 0} + 50% {background-position: calc(100%/-4) -14px,calc(100%/4) 14px} + 100% {background-position: calc(100%/4) -14px,calc(3*100%/4) 14px} + } + + .infinity-5 { + width:60px; + aspect-ratio:1; + --g: conic-gradient(from -90deg at 10px 10px,#fff 90deg,#0000 0); + background: + var(--g), + var(--g) 10px 10px, + var(--g) 20px 20px; + background-size: 50% 50%; + animation:i5 1s infinite; + } + @keyframes i5 { + 90%,100% {background-position: -30px 30px,-20px 40px,-10px 50px} + } + + .infinity-6 { + width:60px; + aspect-ratio:1; + --g: conic-gradient(from -90deg at 10px 10px,#fff 90deg,#0000 0); + background: var(--g), var(--g), var(--g); + background-size: 50% 50%; + animation:i6 1s infinite; + } + @keyframes i6 { + 0% {background-position:0 0 ,10px 10px,20px 20px} + 50% {background-position:0 20px,10px 10px,20px 0 } + 100% {background-position:20px 20px,10px 10px,0 0 } + } + + .infinity-7 { + width:60px; + aspect-ratio:1; + --g: conic-gradient(from -90deg at 10px 10px,#fff 90deg,#0000 0); + background: var(--g), var(--g), var(--g); + background-size: 50% 50%; + animation:i7 1s infinite; + } + @keyframes i7 { + 0% {background-position:0 0, 10px 10px, 20px 20px} + 33% {background-position:-30px 0, 10px 10px, 20px 20px} + 66% {background-position:-30px 0,-20px 10px, 20px 20px} + 100% {background-position:-30px 0,-20px 10px,-10px 20px} + } + + .infinity-8 { + width:60px; + aspect-ratio:1; + --g: conic-gradient(from -90deg at 10px 10px,#fff 90deg,#0000 0); + background: var(--g), var(--g), var(--g); + background-size: 50% 50%; + animation:i8 1s infinite; + } + @keyframes i8 { + 0% {background-position:0 0, 10px 10px, 20px 20px} + 33% {background-position:-30px 0, 10px 10px, 20px 20px} + 66% {background-position:-30px 0, 10px 40px, 20px 20px} + 100% {background-position:-30px 0, 10px 40px, 50px 20px} + } + + .infinity-9 { + width:60px; + aspect-ratio:1; + --g: conic-gradient(from -90deg at 10px 10px,#fff 90deg,#0000 0); + background: var(--g), var(--g), var(--g); + background-size: 50% 50%; + animation:i9 1s infinite; + } + @keyframes i9 { + 0% {background-position:0 0, 10px 10px, 20px 20px} + 33% {background-position:10px 10px} + 66% {background-position:0 20px,10px 10px,20px 0 } + 100% {background-position:0 0, 10px 10px, 20px 20px} + } + + .infinity-10 { + width:60px; + display:flex; + align-items:flex-start; + aspect-ratio:1; + } + .infinity-10:before, + .infinity-10:after { + content:""; + flex:1; + aspect-ratio:1; + --g: conic-gradient(from -90deg at 10px 10px,#fff 90deg,#0000 0); + background: var(--g), var(--g), var(--g); + filter:drop-shadow(30px 30px 0 #fff); + animation:i10 1s infinite; + } + .infinity-10:after { + transform:scaleX(-1); + } + @keyframes i10 { + 0% {background-position:0 0, 10px 10px, 20px 20px} + 33% {background-position:10px 10px} + 66% {background-position:0 20px,10px 10px,20px 0 } + 100% {background-position:0 0, 10px 10px, 20px 20px} + } + \ No newline at end of file diff --git a/codeeditor-app/elements/loading-spinner/loading-spinner.ts b/codeeditor-app/elements/loading-spinner/loading-spinner.ts new file mode 100644 index 0000000..bd59f31 --- /dev/null +++ b/codeeditor-app/elements/loading-spinner/loading-spinner.ts @@ -0,0 +1,8 @@ +import { BaseElement } from '../../../shared/_base'; +import './loading-spinner.less'; + +export class LoadingSpinner extends BaseElement { + onInit(): void { + this.classList.add("infinity-5"); + } +} \ 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 78b4719..daae0bc 100644 --- a/codeeditor-app/elements/resize-handle/resize-handle.less +++ b/codeeditor-app/elements/resize-handle/resize-handle.less @@ -34,6 +34,13 @@ resize-handle[dir="vertical"][place='top'] { bottom: auto; } +resize-handle[dir="horizontal"][place='right'] { + top: 0; + bottom: 0; + right: 0; + left: auto; +} + #fixed-resize-overlay { position: fixed; left: 0; diff --git a/codeeditor-app/elements/session-list-item/session-list-item.less b/codeeditor-app/elements/session-list-item/session-list-item.less new file mode 100644 index 0000000..9995337 --- /dev/null +++ b/codeeditor-app/elements/session-list-item/session-list-item.less @@ -0,0 +1,33 @@ +session-list-item { + color: white; + padding: 10px; + display: block; + position: relative; + border-bottom: 1px solid grey; + cursor: pointer; +} + +session-list-item.selected { + background-color: #1e1e1e; +} + +.session-list-item-session-id::before { + content: "Session: "; +} + +.session-list-item-session-id { + display: block; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.session-list-item-number-of-changes { + font-size: 12px; +} + +.session-list-item-number-of-changes::before { + content: "Number of changes: "; + position: relative; + z-index: 0; +} \ No newline at end of file diff --git a/codeeditor-app/elements/session-list-item/session-list-item.ts b/codeeditor-app/elements/session-list-item/session-list-item.ts new file mode 100644 index 0000000..3549be3 --- /dev/null +++ b/codeeditor-app/elements/session-list-item/session-list-item.ts @@ -0,0 +1,108 @@ +import { BaseElement } from '../../../shared/_base'; +import { CodeEditorContainer } from '../code-editor-container/code-editor-container'; +import './session-list-item.less'; +import * as sessionApi from '../../../shared/session-api'; + +export class SessionListItem extends BaseElement { + onInit(): void { + if (this.hasAttribute("sessionId")) { + var sessionIdEle = document.createElement("div"); + sessionIdEle.classList.add("session-list-item-session-id"); + sessionIdEle.innerHTML = this.getAttribute("sessionId")?.toString() ?? ""; + this.appendChild(sessionIdEle); + } + + if (this.hasAttribute("numberOfChanges")) { + var numberOfChangesEle = document.createElement("div"); + numberOfChangesEle.classList.add("session-list-item-number-of-changes"); + numberOfChangesEle.innerHTML = this.getAttribute("numberOfChanges")?.toString() ?? ""; + this.appendChild(numberOfChangesEle); + } + + this.addEventListener("click", (evt) => { + this.itemClicked(); + }); + } + + private intervalHandle: number | null = null; + stopUpdate() { + this.classList.remove("selected"); + + if (this.intervalHandle) { + window.clearInterval(this.intervalHandle); + this.intervalHandle = null; + } + } + + startUpdate() { + this.stopUpdate(); + + this.classList.add("selected"); + + this.intervalHandle = window.setInterval(this.updateInputs.bind(this), 1000); + } + + updateInputs() { + var codeEditorHTMLContainer = document.querySelector("code-editor-container[language='HTML']"); + var codeEditorCSSContainer = document.querySelector("code-editor-container[language='CSS']"); + var codeEditorJavascriptContainer = document.querySelector("code-editor-container[language='Javascript']"); + + this.getSessionData((js, html, css) => { + codeEditorJavascriptContainer.setInput(js?.data ?? ""); + codeEditorHTMLContainer.setInput(html?.data ?? ""); + codeEditorCSSContainer.setInput(css?.data ?? ""); + }); + + } + + getSessionData(onGet: (javascript: any, html: any, css: any) => void) { + var lastJavascript: any | null = null; + var lastHtml: any | null = null; + var lastCSS: any | null = null; + + sessionApi.getSessionData(this.getAttribute("sessionId")?.toString()?? "", (data) => { + for (var i = data.length - 1; i >= 0; i--) { + var dataPoint = data[i]; + if (!lastJavascript && dataPoint.type == 0) { + lastJavascript = dataPoint; + } + else if (!lastHtml && dataPoint.type == 1) { + lastHtml = dataPoint; + } + else if (!lastCSS && dataPoint.type == 2) { + lastCSS = dataPoint; + } + } + + onGet(lastJavascript, lastHtml, lastCSS); + }); + } + + itemClicked() { + if (!this.intervalHandle) { + var containers = document.getElementsByClassName("code-editor-containers")[0]; + var outputframe = document.getElementsByClassName("output-frame-container")[0]; + + containers.style.display = "block"; + outputframe.style.display = "block"; + + var children = this.parentElement?.children; + + if (children) { + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (child != this) { + child.stopUpdate(); + } + else { + child.startUpdate(); + } + } + } + } + else { + console.log("WOULD STOP!"); + this.stopUpdate(); + } + } +} \ No newline at end of file diff --git a/codeeditor-app/elements/session-list/session-list.less b/codeeditor-app/elements/session-list/session-list.less new file mode 100644 index 0000000..e69de29 diff --git a/codeeditor-app/elements/session-list/session-list.ts b/codeeditor-app/elements/session-list/session-list.ts new file mode 100644 index 0000000..090c041 --- /dev/null +++ b/codeeditor-app/elements/session-list/session-list.ts @@ -0,0 +1,49 @@ +import { BaseElement } from '../../../shared/_base'; +import './session-list.less'; +import * as sessionApi from '../../../shared/session-api'; +import { isSessionPage } from '../../../shared/page'; + +export class SessionList extends BaseElement { + onInit(): void { + + if (isSessionPage()) { + var loadingSpinner = document.createElement("loading-spinner"); + this.appendChild(loadingSpinner); + + this.setUpdate(); + + setInterval(() => { + this.setUpdate(); + }, 5000); + } + } + + setUpdate() { + + sessionApi.getSessions((sessions) => { + var loadingSpinner = this.getElementsByTagName("loading-spinner")[0]; + + console.log(loadingSpinner); + + if (loadingSpinner) { + this.removeChild(loadingSpinner); + } + + for (let session of sessions) { + var exists = document.querySelector("session-list-item[sessionId='" + session.id + "']"); + + if (!exists) { + var ele = document.createElement("session-list-item"); + ele.setAttribute("sessionId", session.id); + ele.setAttribute("numberOfChanges", session.numberOfChanges); + + this.appendChild(ele); + } + else { + exists.setAttribute("numberOfChanges", session.numberOfChanges); + exists.getElementsByClassName("session-list-item-number-of-changes")[0].innerHTML = session.numberOfChanges; + } + } + }); + } +} \ No newline at end of file diff --git a/shared/page.ts b/shared/page.ts new file mode 100644 index 0000000..cc222c0 --- /dev/null +++ b/shared/page.ts @@ -0,0 +1,7 @@ +export function isSessionPage() { + return document.URL.indexOf("/sessions") != -1; +} + +export function isRootPage() { + return document.URL.indexOf("/sessions") == -1; +} \ No newline at end of file diff --git a/shared/session-api.ts b/shared/session-api.ts new file mode 100644 index 0000000..580e158 --- /dev/null +++ b/shared/session-api.ts @@ -0,0 +1,90 @@ +var apiUrl = "https://codeeditor-api.davidssoft.com/"; + +function getSessionId() { + var localstorageSessionId = localStorage.getItem("sessionId"); + + if (!localstorageSessionId) { + return setNewSession(); + } + else { + var expiration = localStorage.getItem("sessionIdExpiration"); + + if (expiration) { + var expirationDate = new Date(expiration); + + if (dateDiff(expirationDate, new Date()) > 120) { + return setNewSession(); + } + else { + localStorage.setItem("sessionIdExpiration", (new Date()).toString()); + return localstorageSessionId; + } + + } + else { + return setNewSession(); + } + } +} + +function setNewSession() { + var newSessionId = uuidv4(); + localStorage.setItem("sessionIdExpiration", (new Date()).toString()); + localStorage.setItem("sessionId", newSessionId); + + return newSessionId; +} + +function dateDiff(startTime: Date, endTime: Date) { + var difference = endTime.getTime() - startTime.getTime(); // This will give difference in milliseconds + var resultInMinutes = Math.round(difference / 60000); + return resultInMinutes; +} + +function uuidv4() { + //@ts-ignore + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); + } + +export function getSessions(onGet: (sessions: any[]) => void) { + fetch(apiUrl + "session/all").then((res) => { + res.json().then((jsonRes) => { + onGet(jsonRes as any); + }); + }); +} + +export function getSessionData(sessionId: string, onGet: (sessions: any[]) => void) { + fetch(apiUrl + "session/data/" + sessionId, { method: "GET" }).then((res) => { + res.json().then((jsonRes) => { + onGet(jsonRes as any); + }); + }); +} + +export function setSessionData(language: string, input: any, onSet?: () => void) { + var type = 0; + + if (language.toLowerCase() == "javascript") { + type = 0; + } + else if (language.toLowerCase() == "html") { + type = 1; + } + else if (language.toLowerCase() == "css") { + type = 2; + } + + fetch(apiUrl + "session/set", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ sessionId: getSessionId(), timestamp: new Date(), type: type, data: input }) }).then((res) => { + if (onSet) { + onSet(); + } + }); +} \ No newline at end of file