From cc37c523a11d432264ede624b434e01d863bf185 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Thu, 16 Mar 2023 16:12:03 +0100 Subject: [PATCH 01/11] Add copy code button to code fragments in descriptions --- .../javascripts/components/copy_button.ts | 56 +++++++++++++++++++ app/assets/javascripts/exercise.ts | 16 ++++++ .../stylesheets/components/btn.css.scss | 2 +- .../stylesheets/models/activities.css.scss | 12 ++++ app/javascript/packs/description.js | 3 + app/views/activities/description.html.erb | 2 + 6 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/components/copy_button.ts diff --git a/app/assets/javascripts/components/copy_button.ts b/app/assets/javascripts/components/copy_button.ts new file mode 100644 index 0000000000..e864736116 --- /dev/null +++ b/app/assets/javascripts/components/copy_button.ts @@ -0,0 +1,56 @@ +import { ShadowlessLitElement } from "components/shadowless_lit_element"; +import { html, PropertyValues, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { initTooltips, ready } from "util.js"; + +@customElement("d-copy-button") +export class CopyButton extends ShadowlessLitElement { + @property({ type: String }) + code: string; + + @property({ state: true }) + status: "idle" | "success" | "error" = "idle"; + + async copyCode(): Promise { + try { + await navigator.clipboard.writeText(this.code); + this.status = "success"; + } catch (err) { + this.status = "error"; + } + } + + get tooltip(): string { + switch (this.status) { + case "success": + return I18n.t("js.copy-success"); + case "error": + return I18n.t("js.copy-fail"); + default: + return I18n.t("js.code.copy-to-clipboard"); + } + } + + protected updated(_changedProperties: PropertyValues): void { + super.updated(_changedProperties); + initTooltips(this); + } + + constructor() { + super(); + + // Reload when I18n is loaded + ready.then(() => this.requestUpdate()); + } + + protected render(): TemplateResult { + return html``; + } +} diff --git a/app/assets/javascripts/exercise.ts b/app/assets/javascripts/exercise.ts index 0dc1dc2916..c7bd5437ef 100644 --- a/app/assets/javascripts/exercise.ts +++ b/app/assets/javascripts/exercise.ts @@ -3,6 +3,8 @@ import { initTooltips, updateURLParameter, fetch } from "util.js"; import { Toast } from "./toast"; import GLightbox from "glightbox"; import { IFrameMessageData } from "iframe-resizer"; +import { render } from "lit"; +import { CopyButton } from "components/copy_button"; function showLightbox(content): void { const lightbox = new GLightbox(content); @@ -110,9 +112,23 @@ function initMathJax(): void { }; } +function initCodeFragments(): void { + const codeElements = document.querySelectorAll("code"); + codeElements.forEach(codeElement => { + const code = codeElement.textContent; + + const copyButton = new CopyButton(); + copyButton.code = code; + + render(copyButton, codeElement, { renderBefore: codeElement.firstChild }); + initTooltips(codeElement); + }); +} + function initExerciseDescription(): void { initLightboxes(); centerImagesAndTables(); + initCodeFragments(); } function initExerciseShow(exerciseId: number, programmingLanguage: string, loggedIn: boolean, editorShown: boolean, courseId: number, _deadline: string, baseSubmissionsUrl: string): void { diff --git a/app/assets/stylesheets/components/btn.css.scss b/app/assets/stylesheets/components/btn.css.scss index 132f58e027..821b0aa102 100644 --- a/app/assets/stylesheets/components/btn.css.scss +++ b/app/assets/stylesheets/components/btn.css.scss @@ -138,7 +138,7 @@ cursor: default; } - &:not(.btn-icon-filled) &:not(.disabled-with-tooltip) { + &:not(.btn-icon-filled, .disabled-with-tooltip) { &:hover, &:focus, &:active { diff --git a/app/assets/stylesheets/models/activities.css.scss b/app/assets/stylesheets/models/activities.css.scss index 30e2118a2c..95ac91b373 100644 --- a/app/assets/stylesheets/models/activities.css.scss +++ b/app/assets/stylesheets/models/activities.css.scss @@ -185,3 +185,15 @@ center img { padding-top: 0; } } + +code { + display: inline-block; + position: relative; + width: 100%; + + d-copy-button { + position: absolute; + right: 0; + top: -8px; + } +} diff --git a/app/javascript/packs/description.js b/app/javascript/packs/description.js index b2aac00cdc..f4e3c3326d 100644 --- a/app/javascript/packs/description.js +++ b/app/javascript/packs/description.js @@ -1,6 +1,9 @@ import { iframeResizerContentWindow } from "iframe-resizer"; import { initExerciseDescription, initMathJax } from "exercise.ts"; +import { I18n } from "i18n/i18n"; +window.I18n = new I18n(); + window.iframeResizerContentWindow = iframeResizerContentWindow; window.dodona.initMathJax = initMathJax; window.dodona.initDescription = initExerciseDescription; diff --git a/app/views/activities/description.html.erb b/app/views/activities/description.html.erb index 0718150975..08e4171050 100644 --- a/app/views/activities/description.html.erb +++ b/app/views/activities/description.html.erb @@ -31,5 +31,7 @@ From 40333721884f51e5b153154baa10e03acdb51e91 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Fri, 17 Mar 2023 13:36:14 +0100 Subject: [PATCH 02/11] Fix copying in chrome --- app/helpers/activity_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/activity_helper.rb b/app/helpers/activity_helper.rb index b008c4f605..acd9a84e18 100644 --- a/app/helpers/activity_helper.rb +++ b/app/helpers/activity_helper.rb @@ -80,7 +80,7 @@ def description_iframe(activity) class: 'dodona-iframe', scrolling: 'no', onload: resizeframe, - allow: 'fullscreen https://www.youtube.com https://www.youtube-nocookie.com https://player.vimeo.com/ ', + allow: 'clipboard-write; fullscreen https://www.youtube.com https://www.youtube-nocookie.com https://player.vimeo.com/ ', src: url, height: '500px' end From 814d803b04b80df855917696759d8774513e3663 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Fri, 17 Mar 2023 14:05:52 +0100 Subject: [PATCH 03/11] Fix layout single line code previews --- app/assets/stylesheets/models/activities.css.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/models/activities.css.scss b/app/assets/stylesheets/models/activities.css.scss index 95ac91b373..115fd8cbba 100644 --- a/app/assets/stylesheets/models/activities.css.scss +++ b/app/assets/stylesheets/models/activities.css.scss @@ -186,10 +186,11 @@ center img { } } -code { +code:has(d-copy-button) { display: inline-block; position: relative; width: 100%; + min-height: 23px; d-copy-button { position: absolute; @@ -197,3 +198,6 @@ code { top: -8px; } } + +pre:has(d-copy-button) { +} From 0a786e971557f054c084c4a36c71abc3f0464eed Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Fri, 17 Mar 2023 14:12:04 +0100 Subject: [PATCH 04/11] Remove empty css --- app/assets/stylesheets/models/activities.css.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/assets/stylesheets/models/activities.css.scss b/app/assets/stylesheets/models/activities.css.scss index 115fd8cbba..3a32b57c57 100644 --- a/app/assets/stylesheets/models/activities.css.scss +++ b/app/assets/stylesheets/models/activities.css.scss @@ -198,6 +198,3 @@ code:has(d-copy-button) { top: -8px; } } - -pre:has(d-copy-button) { -} From c80a6c441e903e06fb0c9ecc9109f901f1fcb633 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Fri, 17 Mar 2023 14:27:02 +0100 Subject: [PATCH 05/11] Select text when copy fails --- app/assets/javascripts/components/copy_button.ts | 9 +++++++-- app/assets/javascripts/exercise.ts | 4 +--- app/assets/stylesheets/models/activities.css.scss | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/components/copy_button.ts b/app/assets/javascripts/components/copy_button.ts index e864736116..db3861dd01 100644 --- a/app/assets/javascripts/components/copy_button.ts +++ b/app/assets/javascripts/components/copy_button.ts @@ -5,8 +5,12 @@ import { initTooltips, ready } from "util.js"; @customElement("d-copy-button") export class CopyButton extends ShadowlessLitElement { - @property({ type: String }) - code: string; + @property({ type: Object }) + codeElement: HTMLElement; + + get code(): string { + return this.codeElement.textContent; + } @property({ state: true }) status: "idle" | "success" | "error" = "idle"; @@ -16,6 +20,7 @@ export class CopyButton extends ShadowlessLitElement { await navigator.clipboard.writeText(this.code); this.status = "success"; } catch (err) { + window.getSelection().selectAllChildren(this.codeElement); this.status = "error"; } } diff --git a/app/assets/javascripts/exercise.ts b/app/assets/javascripts/exercise.ts index c7bd5437ef..5b2af9d441 100644 --- a/app/assets/javascripts/exercise.ts +++ b/app/assets/javascripts/exercise.ts @@ -115,10 +115,8 @@ function initMathJax(): void { function initCodeFragments(): void { const codeElements = document.querySelectorAll("code"); codeElements.forEach(codeElement => { - const code = codeElement.textContent; - const copyButton = new CopyButton(); - copyButton.code = code; + copyButton.codeElement = codeElement; render(copyButton, codeElement, { renderBefore: codeElement.firstChild }); initTooltips(codeElement); diff --git a/app/assets/stylesheets/models/activities.css.scss b/app/assets/stylesheets/models/activities.css.scss index 3a32b57c57..9fcc919e7e 100644 --- a/app/assets/stylesheets/models/activities.css.scss +++ b/app/assets/stylesheets/models/activities.css.scss @@ -186,7 +186,7 @@ center img { } } -code:has(d-copy-button) { +code { display: inline-block; position: relative; width: 100%; From f1b1351f00166a37844fe973bc8d818d8a7fc487 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Fri, 17 Mar 2023 14:37:10 +0100 Subject: [PATCH 06/11] Only add copy button to codeblocks --- app/assets/javascripts/exercise.ts | 4 ++-- app/assets/stylesheets/models/activities.css.scss | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/exercise.ts b/app/assets/javascripts/exercise.ts index 5b2af9d441..c530199b9f 100644 --- a/app/assets/javascripts/exercise.ts +++ b/app/assets/javascripts/exercise.ts @@ -113,8 +113,8 @@ function initMathJax(): void { } function initCodeFragments(): void { - const codeElements = document.querySelectorAll("code"); - codeElements.forEach(codeElement => { + const codeElements = document.querySelectorAll("pre code"); + codeElements.forEach((codeElement: HTMLElement) => { const copyButton = new CopyButton(); copyButton.codeElement = codeElement; diff --git a/app/assets/stylesheets/models/activities.css.scss b/app/assets/stylesheets/models/activities.css.scss index 9fcc919e7e..45593858fd 100644 --- a/app/assets/stylesheets/models/activities.css.scss +++ b/app/assets/stylesheets/models/activities.css.scss @@ -186,7 +186,7 @@ center img { } } -code { +pre code { display: inline-block; position: relative; width: 100%; From ede86d20a0c5fb87d90098d436e152e02071e52a Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Mon, 20 Mar 2023 10:20:11 +0100 Subject: [PATCH 07/11] Trim textcontent --- app/assets/javascripts/components/copy_button.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/components/copy_button.ts b/app/assets/javascripts/components/copy_button.ts index db3861dd01..ffa7e5c8eb 100644 --- a/app/assets/javascripts/components/copy_button.ts +++ b/app/assets/javascripts/components/copy_button.ts @@ -9,7 +9,7 @@ export class CopyButton extends ShadowlessLitElement { codeElement: HTMLElement; get code(): string { - return this.codeElement.textContent; + return this.codeElement.textContent.trim(); } @property({ state: true }) From 268fa3791cd9723b058c2371c9ba17d63bb16202 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Mon, 20 Mar 2023 10:31:36 +0100 Subject: [PATCH 08/11] Fix background on long lines --- app/assets/stylesheets/models/activities.css.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/models/activities.css.scss b/app/assets/stylesheets/models/activities.css.scss index 45593858fd..b25af8671b 100644 --- a/app/assets/stylesheets/models/activities.css.scss +++ b/app/assets/stylesheets/models/activities.css.scss @@ -196,5 +196,6 @@ pre code { position: absolute; right: 0; top: -8px; + background-color: $code-bg; } } From f660ffb8681bd2598372b66bf7370bfae5c801b8 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Mon, 20 Mar 2023 10:41:38 +0100 Subject: [PATCH 09/11] Clean up css --- app/assets/javascripts/components/copy_button.ts | 2 +- app/assets/javascripts/exercise.ts | 2 +- app/assets/stylesheets/models/activities.css.scss | 7 ++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/components/copy_button.ts b/app/assets/javascripts/components/copy_button.ts index ffa7e5c8eb..db3861dd01 100644 --- a/app/assets/javascripts/components/copy_button.ts +++ b/app/assets/javascripts/components/copy_button.ts @@ -9,7 +9,7 @@ export class CopyButton extends ShadowlessLitElement { codeElement: HTMLElement; get code(): string { - return this.codeElement.textContent.trim(); + return this.codeElement.textContent; } @property({ state: true }) diff --git a/app/assets/javascripts/exercise.ts b/app/assets/javascripts/exercise.ts index c530199b9f..daf10d4be8 100644 --- a/app/assets/javascripts/exercise.ts +++ b/app/assets/javascripts/exercise.ts @@ -118,7 +118,7 @@ function initCodeFragments(): void { const copyButton = new CopyButton(); copyButton.codeElement = codeElement; - render(copyButton, codeElement, { renderBefore: codeElement.firstChild }); + render(copyButton, codeElement.parentElement, { renderBefore: codeElement }); initTooltips(codeElement); }); } diff --git a/app/assets/stylesheets/models/activities.css.scss b/app/assets/stylesheets/models/activities.css.scss index b25af8671b..f0c7fd07f4 100644 --- a/app/assets/stylesheets/models/activities.css.scss +++ b/app/assets/stylesheets/models/activities.css.scss @@ -186,16 +186,13 @@ center img { } } -pre code { - display: inline-block; +pre { position: relative; - width: 100%; - min-height: 23px; d-copy-button { position: absolute; right: 0; - top: -8px; + top: 0; background-color: $code-bg; } } From 246740fd97a2fb29e2925d98463d099ed908d082 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 21 Mar 2023 10:59:39 +0100 Subject: [PATCH 10/11] Add docs --- app/assets/javascripts/components/copy_button.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/javascripts/components/copy_button.ts b/app/assets/javascripts/components/copy_button.ts index db3861dd01..558abfdec1 100644 --- a/app/assets/javascripts/components/copy_button.ts +++ b/app/assets/javascripts/components/copy_button.ts @@ -3,6 +3,13 @@ import { html, PropertyValues, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import { initTooltips, ready } from "util.js"; +/** + * A button that copies the text content of a given element to the clipboard. + * The button is styled as a small icon button. + * The button is a tooltip that shows the current status of the copy operation. + * + * @property {HTMLElement} codeElement - The element whose text content is copied to the clipboard. + */ @customElement("d-copy-button") export class CopyButton extends ShadowlessLitElement { @property({ type: Object }) From dc2daef22e7f2980819e55262a4161e1a1406c57 Mon Sep 17 00:00:00 2001 From: jorg-vr Date: Tue, 21 Mar 2023 11:00:09 +0100 Subject: [PATCH 11/11] Add docs --- app/assets/javascripts/components/copy_button.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/components/copy_button.ts b/app/assets/javascripts/components/copy_button.ts index 558abfdec1..8d271909d9 100644 --- a/app/assets/javascripts/components/copy_button.ts +++ b/app/assets/javascripts/components/copy_button.ts @@ -8,6 +8,8 @@ import { initTooltips, ready } from "util.js"; * The button is styled as a small icon button. * The button is a tooltip that shows the current status of the copy operation. * + * @element d-copy-button + * * @property {HTMLElement} codeElement - The element whose text content is copied to the clipboard. */ @customElement("d-copy-button")