diff --git a/js/editor-types.ts b/js/editor-types.ts index c02e6a2..be103bf 100644 --- a/js/editor-types.ts +++ b/js/editor-types.ts @@ -2,6 +2,7 @@ export interface EditorType { setLanguage(identifier: string): void getValue(): string setValue(text: string): void + update(): void unload(): void } diff --git a/js/views/editor-types/codemirror/codemirror.css b/js/views/editor-types/codemirror/codemirror.css index 22c0cab..9b12254 100644 --- a/js/views/editor-types/codemirror/codemirror.css +++ b/js/views/editor-types/codemirror/codemirror.css @@ -1,5 +1,5 @@ .CodeMirror { border: 1px solid #bbbdbe; - height: 372px; + height: 100%; font-size: 14.4px; } diff --git a/js/views/editor-types/codemirror/codemirror.ts b/js/views/editor-types/codemirror/codemirror.ts index 635ad2d..3a54f9d 100644 --- a/js/views/editor-types/codemirror/codemirror.ts +++ b/js/views/editor-types/codemirror/codemirror.ts @@ -55,6 +55,8 @@ class CodeMirrorEditor { this.editor.setValue(value) } + update() {} + unload() { this.editor.toTextArea() } diff --git a/js/views/editor-types/monaco/monaco.css b/js/views/editor-types/monaco/monaco.css index 01bed4f..dede17c 100644 --- a/js/views/editor-types/monaco/monaco.css +++ b/js/views/editor-types/monaco/monaco.css @@ -1,6 +1,5 @@ .monaco { border: 1px solid #bbbdbe; - height: auto; + height: 100%; font-size: 14.4px; - height: 372px; } diff --git a/js/views/editor-types/monaco/monaco.ts b/js/views/editor-types/monaco/monaco.ts index 4a9a241..ef6c617 100644 --- a/js/views/editor-types/monaco/monaco.ts +++ b/js/views/editor-types/monaco/monaco.ts @@ -75,6 +75,14 @@ class MonacoEditor { this.editor.setValue(value) } + update() { + // Monaco has no idea how to reflow, so let's force it to reflow twice + this.container.style.width = '0' + this.editor.layout() + this.container.style.width = '' + this.editor.layout() + } + unload() { this.textarea.value = this.getValue() this.editor.dispose() diff --git a/js/views/editor-types/textarea.ts b/js/views/editor-types/textarea.ts index 833e472..c907f9f 100644 --- a/js/views/editor-types/textarea.ts +++ b/js/views/editor-types/textarea.ts @@ -17,6 +17,8 @@ class TextAreaEditor { this.textarea.value = value } + update() {} + unload() { this.textarea.removeEventListener('input', this.onChange) } diff --git a/js/views/editor/editor.ts b/js/views/editor/editor.ts index 1e7b360..298bb4d 100644 --- a/js/views/editor/editor.ts +++ b/js/views/editor/editor.ts @@ -10,14 +10,13 @@ class Editor { codeElement: HTMLTextAreaElement output: Output autodeleteText: HTMLSpanElement - autodeleteCheckbox: HTMLLabelElement - helloWorldLink: HTMLSpanElement - submit: HTMLInputElement + submitButtons: HTMLInputElement[] detailsElement: HTMLDetailsElement stdinElement: HTMLTextAreaElement editor: EditorType currentLanguage: string | null = null abortEval: AbortController | null = null + isHelloWorld: boolean = false initialize(form) { this.languageSelector = form.querySelector('#language') @@ -26,20 +25,20 @@ class Editor { this.initializeEditor(createTextareaEditor) onChange(editor => this.changeEditor(editor)) this.initConfiguredEditor() - this.output = new Output(form.querySelector('#output')) + this.output = Output.addTo(form.querySelector('#split')) const stdout = document.querySelector('#dbstdout') if (stdout) { - this.output.display({}, { + this.displayOutput({}, { stdout: stdout.value, stderr: document.querySelector('#dbstderr').value, status: +document.querySelector('#dbstatus') ?.value, }) } this.autodeleteText = form.querySelector('#autodelete-text') - this.autodeleteCheckbox = form.querySelector('#automatically-hidden-label') - this.helloWorldLink = form.querySelector('#hello-world') - this.submit = form.querySelector('[type=submit]') - this.submit.disabled = true + this.submitButtons = form.querySelectorAll('[type=submit]') + for (const submit of this.submitButtons) { + submit.disabled = true + } form.addEventListener('submit', () => { if (this.output.json && !this.output.wrapper.isFormatter) { for (const name of ['stdout', 'stderr', 'status']) { @@ -58,7 +57,10 @@ class Editor { summary.textContent = 'Standard input' this.stdinElement = document.createElement('textarea') this.stdinElement.name = 'stdin' - this.stdinElement.addEventListener('change', () => this.changeToLookLikeNewPaste()) + this.stdinElement.addEventListener('change', () => { + this.isHelloWorld = false + this.changeToLookLikeNewPaste() + }) this.detailsElement.append(summary, this.stdinElement) const dbStdin = document.querySelector('#dbstdin') ?.value if (dbStdin) { @@ -67,12 +69,10 @@ class Editor { } else { this.detailsElement.style.display = 'none' } - form.querySelector('#buttons').append(this.detailsElement) - if (this.autodeleteText) { - this.autodeleteCheckbox.style.display = 'none' - } + form.querySelector('#extrafields').append(this.detailsElement) this.assignEvents() this.updateLanguage() + addEventListener('resize', () => this.editor.update()) } async initConfiguredEditor() { @@ -85,7 +85,10 @@ class Editor { } initializeEditor(createEditor) { - this.editor = createEditor(this.codeElement, () => this.changeToLookLikeNewPaste()) + this.editor = createEditor(this.codeElement, () => { + this.changeToLookLikeNewPaste() + this.isHelloWorld = false + }) if (this.currentLanguage) { this.editor.setLanguage(this.currentLanguage) } @@ -99,10 +102,10 @@ class Editor { changeToLookLikeNewPaste() { if (this.autodeleteText) { this.autodeleteText.style.display = 'none' - this.autodeleteCheckbox.style.display = '' } - this.submit.disabled = false - this.output.clear() + for (const submit of this.submitButtons) { + submit.disabled = false + } } assignEvents() { @@ -114,21 +117,22 @@ class Editor { async updateLanguage() { this.wrapperButtons.clear() - this.helloWorldLink.textContent = '' const identifier = this.getLanguageIdentifier() this.setLanguage(identifier) const isStillValid = () => identifier === this.getLanguageIdentifier() const language = await getLanguage(identifier, isStillValid) // This deals with user changing the language after asynchronous event if (isStillValid()) { - if (language.helloWorldPaste) { - const anchor = document.createElement('a') - anchor.href = '/' + language.helloWorldPaste - anchor.textContent = 'Hello world program' - this.helloWorldLink.append(' | ', anchor) - } this.detailsElement.style.display = language.implementations.length ? 'block' : 'none' this.wrapperButtons.update(language.implementations) + const isStillHelloWorld = () => this.isHelloWorld || this.editor.getValue() === '' + if (isStillHelloWorld()) { + const helloWorldText = language.helloWorldPaste ? await (await fetch(`/${language.helloWorldPaste}.txt`)).text() : "" + if (isStillHelloWorld()) { + this.editor.setValue(helloWorldText) + this.isHelloWorld = true + } + } } } @@ -138,6 +142,7 @@ class Editor { async run(wrapper, compilerOptions) { this.output.clear() + this.editor.update() if (this.abortEval) { this.abortEval.abort() } @@ -168,7 +173,12 @@ class Editor { if (wrapper.isFormatter) { this.editor.setValue(response.stdout) } + this.displayOutput(wrapper, response) + } + + displayOutput(wrapper, response) { this.output.display(wrapper, response) + this.editor.update() } } diff --git a/js/views/editor/output.ts b/js/views/editor/output.ts index f350f35..da50498 100644 --- a/js/views/editor/output.ts +++ b/js/views/editor/output.ts @@ -1,15 +1,29 @@ import { Wrapper } from './types' +import { SplitChunksPlugin } from 'webpack' const filterRegex = /(?:\t\.(?:text|file|section|globl|p2align|type|cfi_.*|size|section)\b|.Lfunc_end).*\n?/g export default class Output { + split: HTMLDivElement + outputContainer: HTMLDivElement output: HTMLDivElement filterAsm = document.createElement('label') filterAsmCheckbox = document.createElement('input') wrapper: Wrapper | null = null json: { stdout: string, stderr: string, status: number | null } | null = null - constructor(output) { + static addTo(split: HTMLDivElement) { + const outputContainer = document.createElement('div') + outputContainer.id = 'outputcontainer' + const output = document.createElement('div') + output.id = 'output' + outputContainer.append(output) + return new Output(split, outputContainer, output) + } + + private constructor(split: HTMLDivElement, outputContainer: HTMLDivElement, output: HTMLDivElement) { + this.split = split + this.outputContainer = outputContainer this.output = output this.filterAsmCheckbox.type = 'checkbox' this.filterAsmCheckbox.checked = true @@ -18,8 +32,8 @@ export default class Output { } clear() { - this.json = null this.output.textContent = '' + this.outputContainer.remove() } error() { @@ -34,7 +48,8 @@ export default class Output { update() { const { stdout, stderr, status } = this.json - this.output.textContent = '' + this.clear() + this.split.append(this.outputContainer) if (stderr) { const stderrHeader = document.createElement('h2') stderrHeader.textContent = 'Standard error' diff --git a/js/views/editor/wrapper-buttons.ts b/js/views/editor/wrapper-buttons.ts index 232b8a6..be498d6 100644 --- a/js/views/editor/wrapper-buttons.ts +++ b/js/views/editor/wrapper-buttons.ts @@ -29,12 +29,13 @@ export default class WrapperButtons { this.select.append(option) } this.buttonsContainer.textContent = '' - if (implementations.length > 1) { - this.buttonsContainer.append(this.select) - this.select.addEventListener('change', () => this.updateButtons()) - } if (implementations.length !== 0) { - this.buttonsContainer.append(this.compilerOptions, this.buttons) + this.buttonsContainer.append(this.buttons) + if (implementations.length > 1) { + this.buttonsContainer.append(this.select) + this.select.addEventListener('change', () => this.updateButtons()) + } + this.buttonsContainer.append(this.compilerOptions) } this.updateButtons() } @@ -71,7 +72,7 @@ export default class WrapperButtons { first = false } button.addEventListener('click', event) - this.buttons.append(button) + this.buttons.append(button, ' ') } } } diff --git a/src/models/paste.rs b/src/models/paste.rs index 850a178..fe914e5 100644 --- a/src/models/paste.rs +++ b/src/models/paste.rs @@ -112,6 +112,7 @@ pub fn insert( Ok(insert_paste.identifier) } +#[derive(Default)] pub struct ExternPaste { pub paste: String, pub language_id: i32, diff --git a/src/routes/insert_paste.rs b/src/routes/insert_paste.rs index 932e5d3..f2f7991 100644 --- a/src/routes/insert_paste.rs +++ b/src/routes/insert_paste.rs @@ -4,7 +4,6 @@ use crate::Connection; use chrono::{Duration, Utc}; use futures::Future; use futures03::TryFutureExt; -use serde::de::IgnoredAny; use serde::Deserialize; use tokio_executor::blocking; use warp::http::header::LOCATION; @@ -15,7 +14,7 @@ use warp::{reply, Rejection, Reply}; pub struct PasteForm { language: String, code: String, - autodelete: Option, + share: Share, #[serde(default)] stdin: String, stdout: Option, @@ -23,11 +22,18 @@ pub struct PasteForm { status: Option, } +#[derive(Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Share { + Share, + Share24, +} + pub fn insert_paste( PasteForm { language, code, - autodelete, + share, stdin, stdout, stderr, @@ -36,7 +42,10 @@ pub fn insert_paste( connection: Connection, ) -> impl Future { blocking::run(move || { - let delete_at = autodelete.map(|_| Utc::now() + Duration::hours(24)); + let delete_at = match share { + Share::Share => None, + Share::Share24 => Some(Utc::now() + Duration::hours(24)), + }; let identifier = paste::insert( &connection, delete_at, diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 2fad898..f7bd4c3 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -203,8 +203,8 @@ fn not_found(pool: PgPool) -> impl Clone + Fn(Rejection) -> NotFoundFuture { Err(rejection) } } - .boxed() - .compat() + .boxed() + .compat() } } @@ -310,7 +310,7 @@ mod test { #[test] #[cfg_attr(not(feature = "database_tests"), ignore)] fn test_raw_pastes() { - let body = format!("language={}&code=abc", get_sh_id()); + let body = format!("language={}&code=abc&share=share24", get_sh_id()); let reply = warp::test::request() .method("POST") .header(CONTENT_LENGTH, body.len()) diff --git a/static/style.css b/static/style-v2.css similarity index 60% rename from static/style.css rename to static/style-v2.css index 13108d9..527ea73 100644 --- a/static/style.css +++ b/static/style-v2.css @@ -1,5 +1,11 @@ +* { + box-sizing: border-box; +} body { margin: 0; + height: 100vh; + display: flex; + flex-direction: column; line-height: 1.6; color: black; text-align: justify; @@ -13,9 +19,7 @@ body { box-shadow: 0 -5px 2px rgba(0, 0, 0, 0.1) inset; } #header div { - margin: 0 auto; padding: 0 1em; - max-width: 900px; display: flex; align-items: center; justify-content: space-between; @@ -40,17 +44,41 @@ body { border: none; } #article { - margin: 0 auto; - padding: 0 1em; - max-width: 980px; + display: flex; + flex: 1; + flex-direction: column; + padding: 0 1em 1em; + position: relative; +} +form { + display: flex; + flex: 1; + flex-direction: column; } h1, h2, h3, h4, h5, h6 { line-height: 1.2; } +#toolbar { + padding: 6px 0; +} +#split { + display: flex; + flex: 1; + position: relative; +} +#extrafieldsplit { + display: flex; + flex: 1; + flex-direction: column; +} +#textarea { + flex: 1; + position: relative; +} textarea { width: 100%; - height: 372px; - resize: vertical; + height: 100%; + resize: none; } [name=stdin] { height: 124px; @@ -58,6 +86,28 @@ textarea { #right-buttons { float: right; } +#outputcontainer { + flex: 1; + position: relative; +} +#output { + padding: 0 1em; + position: absolute; + width: 100%; + height: 100%; + overflow: auto; +} +@media (max-width: 1200px) { + #split { + flex-direction: column; + flex: auto; + } + #output { + padding: 0; + max-height: 40vh; + max-width: unset; + } +} table { overflow: auto; border-collapse: collapse; diff --git a/templates/buttons.rs.html b/templates/buttons.rs.html index e4a5c59..ddb301c 100644 --- a/templates/buttons.rs.html +++ b/templates/buttons.rs.html @@ -1,8 +1,6 @@ @() -
-
- - -
-
-
+ + + + + diff --git a/templates/display_paste.rs.html b/templates/display_paste.rs.html index f0e7cc7..60e100d 100644 --- a/templates/display_paste.rs.html +++ b/templates/display_paste.rs.html @@ -8,20 +8,21 @@ @Html(paste.markdown)
-

+ @if let Some(delete_at) = paste.delete_at { +

+ This paste will be automatically deleted on @delete_at.format("%Y-%m-%d %H:%M") UTC. +
+ } +
@:language_selection(selection) - @if let Some(delete_at) = paste.delete_at { - - This paste will be automatically deleted on @delete_at.format("%Y-%m-%d %H:%M") UTC. - - } - - - - -

-

- @:buttons() + @:buttons() +
+
+
+
+
+
+
@if let Some(exit_code) = paste.exit_code { diff --git a/templates/header.rs.html b/templates/header.rs.html index 593f33e..21f929f 100644 --- a/templates/header.rs.html +++ b/templates/header.rs.html @@ -6,7 +6,7 @@ pastebin.run - +