diff --git a/app/assets/javascripts/components/copy_button.ts b/app/assets/javascripts/components/copy_button.ts index da4e3b75e5..8af56b11db 100644 --- a/app/assets/javascripts/components/copy_button.ts +++ b/app/assets/javascripts/components/copy_button.ts @@ -1,25 +1,45 @@ import { html, PropertyValues, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { initTooltips, ready } from "utilities"; +import { initTooltips } from "utilities"; import { i18n } from "i18n/i18n"; import { DodonaElement } from "components/meta/dodona_element"; /** * A button that copies the text content of a given element to the clipboard. + * Alternatively, the text can be set directly. * 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. + * @property {HTMLElement} target - The element whose text content is copied to the clipboard. + * @property {string} targetId - The id of the element whose text content is copied to the clipboard. + * @property {string} text - The text that is copied to the clipboard. */ @customElement("d-copy-button") export class CopyButton extends DodonaElement { + @property({ type: String, attribute: "target-id" }) + targetId: string; + + _target: HTMLElement; @property({ type: Object }) - codeElement: HTMLElement; + get target(): HTMLElement { + return this._target ?? document.getElementById(this.targetId); + } + + set target(value: HTMLElement) { + this._target = value; + } + + _text: string; + + @property({ type: String }) + get text(): string { + return this._text ?? this.target?.textContent; + } - get code(): string { - return this.codeElement.textContent; + set text(value: string) { + this._text = value; } @property({ state: true }) @@ -27,11 +47,17 @@ export class CopyButton extends DodonaElement { async copyCode(): Promise { try { - await navigator.clipboard.writeText(this.code); + await navigator.clipboard.writeText(this.text); this.status = "success"; } catch (err) { - window.getSelection().selectAllChildren(this.codeElement); - this.status = "error"; + if (this.target) { + // Select the text in the code element so the user can copy it manually. + window.getSelection().selectAllChildren(this.target); + this.status = "error"; + } else { + // rethrow the error if there is no target + throw err; + } } } diff --git a/app/assets/javascripts/components/copy_container.ts b/app/assets/javascripts/components/copy_container.ts new file mode 100644 index 0000000000..254b0279ce --- /dev/null +++ b/app/assets/javascripts/components/copy_container.ts @@ -0,0 +1,29 @@ +import { html, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import "components/copy_button"; +import { DodonaElement } from "components/meta/dodona_element"; + +/** + * A container that holds code that can be copied to the clipboard. + * + * @element d-copy-container + * + * @property {string} content - The content that is copied to the clipboard. + */ +@customElement("d-copy-container") +export class CopyContainer extends DodonaElement { + @property({ type: String }) + content: string; + + @property({ state: true }) + containerId: string; + + constructor() { + super(); + this.containerId = "copy-container-" + Math.random().toString(36).substring(7); + } + + protected render(): TemplateResult { + return html`
${this.content}
`; + } +} diff --git a/app/assets/javascripts/copy.ts b/app/assets/javascripts/copy.ts deleted file mode 100644 index 1da55809cb..0000000000 --- a/app/assets/javascripts/copy.ts +++ /dev/null @@ -1,32 +0,0 @@ -import ClipboardJS from "clipboard"; -import { ready, tooltip } from "utilities"; -import { i18n } from "i18n/i18n"; - -export async function initClipboard(): Promise { - await ready; - const selector = ".btn"; - const clip = new ClipboardJS(selector); - const targetOf = (e): Element => document.querySelector(e.trigger.dataset["clipboard-target"]); - clip.on("success", e => tooltip(targetOf(e), i18n.t("js.copy-success"))); - clip.on("error", e => tooltip(targetOf(e), i18n.t("js.copy-fail"))); -} - -/** - * Small wrapper around ClipboardJS to copy some content the clipboard when the - * element with the given identifier is pressed. If the content you need to copy - * is in some HTML element, you probably don't need this and can use ClipboardJS - * directly. - * - * @param {string} identifier The identifying query for the button to attach the listener to. - * @param {string} code The code to put on the clipboard. - */ -export function attachClipboard(identifier: string, code: string): void { - const clipboardBtn = document.querySelector(identifier); - const clipboard = new ClipboardJS(clipboardBtn, { text: () => code }); - clipboard.on("success", () => { - tooltip(clipboardBtn, i18n.t("js.copy-success")); - }); - clipboard.on("error", () => { - tooltip(clipboardBtn, i18n.t("js.copy-fail")); - }); -} diff --git a/app/assets/javascripts/exercise.ts b/app/assets/javascripts/exercise.ts index d02b41e643..0c750c9907 100644 --- a/app/assets/javascripts/exercise.ts +++ b/app/assets/javascripts/exercise.ts @@ -127,7 +127,7 @@ function initCodeFragments(): void { const wrapper = codeElement.parentElement; wrapper.classList.add("code-wrapper"); const copyButton = new CopyButton(); - copyButton.codeElement = codeElement; + copyButton.target = codeElement; render(copyButton, wrapper, { renderBefore: codeElement }); }); diff --git a/app/assets/stylesheets/models/activities.css.scss b/app/assets/stylesheets/models/activities.css.scss index 37d47c35e6..1bd9a7cc05 100644 --- a/app/assets/stylesheets/models/activities.css.scss +++ b/app/assets/stylesheets/models/activities.css.scss @@ -192,7 +192,7 @@ center img { code { overflow: auto; - width: 100%; + width: calc(100%); display: block; } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index df91b7ba0e..2adc4ffdd1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -121,17 +121,6 @@ def activatable_link_to(url, options = nil, &) link_to(url, options, &) end - def clipboard_button_for(selector) - selector = selector.to_s - selector.prepend('#') unless selector.starts_with?('#') - button_tag class: 'btn btn-icon', - type: 'button', - title: t('js.copy-to-clipboard'), - data: { clipboard_target: selector } do - tag.i(class: 'mdi mdi-clipboard-outline') - end - end - def markdown_unsafe(source) source ||= '' Kramdown::Document.new(source, diff --git a/app/helpers/renderers/feedback_code_renderer.rb b/app/helpers/renderers/feedback_code_renderer.rb index e9737ca18b..cc3ebd4f3b 100644 --- a/app/helpers/renderers/feedback_code_renderer.rb +++ b/app/helpers/renderers/feedback_code_renderer.rb @@ -1,38 +1,17 @@ class FeedbackCodeRenderer require 'json' include Rails.application.routes.url_helpers - - @instances = 0 - - class << self - attr_accessor :instances - end - def initialize(code, programming_language) @code = code @programming_language = programming_language @builder = Builder::XmlMarkup.new - self.class.instances += 1 - @instance = self.class.instances end def add_code @builder.div(class: 'code-listing-container') do parse # Only display copy button when the submission is not empty - if @code.present? - # Not possible to use clipboard_button_for here since the behaviour is different. - @builder.button(class: 'btn btn-icon copy-btn', id: "copy-to-clipboard-#{@instance}", title: I18n.t('js.code.copy-to-clipboard'), 'data-bs-toggle': 'tooltip', 'data-bs-placement': 'top') do - @builder.i(class: 'mdi mdi-clipboard-outline') {} - end - end - @builder.script(type: 'application/javascript') do - @builder << <<~HEREDOC - window.dodona.ready.then(() => { - window.dodona.attachClipboard("#copy-to-clipboard-#{@instance}", #{@code.to_json}); - }); - HEREDOC - end + @builder.tag!('d-copy-button', text: @code) {} if @code.present? end self end diff --git a/app/javascript/packs/application_pack.js b/app/javascript/packs/application_pack.js index d9750e5f31..5a007f5dbe 100644 --- a/app/javascript/packs/application_pack.js +++ b/app/javascript/packs/application_pack.js @@ -28,7 +28,6 @@ import { Drawer } from "drawer"; import { Toast } from "toast"; import { Notification } from "notification"; import { checkTimeZone, checkIframe, initTooltips, ready, setHTMLExecuteScripts, replaceHTMLExecuteScripts } from "utilities.ts"; -import { initClipboard } from "copy"; import { FaviconManager } from "favicon"; import { themeState } from "state/Theme"; import "components/saved_annotations/saved_annotation_list"; @@ -36,9 +35,7 @@ import "components/progress_bar"; import "components/theme_picker"; import { userState } from "state/Users"; import "components/series_icon.ts"; - -// Initialize clipboard.js -initClipboard(); +import "components/copy_container.ts"; // Init drawer ready.then(() => new Drawer()); diff --git a/app/javascript/packs/frame.js b/app/javascript/packs/frame.js index 36022c05cd..524d86e793 100644 --- a/app/javascript/packs/frame.js +++ b/app/javascript/packs/frame.js @@ -9,7 +9,6 @@ const bootstrap = { Alert, Button, Collapse, Dropdown, Modal, Popover, Tab, Tool window.bootstrap = bootstrap; import { initTooltips, ready, setHTMLExecuteScripts } from "utilities.ts"; -import { initClipboard } from "copy"; import { themeState } from "state/Theme"; // Use a global dodona object to prevent polluting the global na @@ -20,7 +19,4 @@ dodona.setTheme = theme => themeState.selectedTheme = theme; dodona.setHTMLExecuteScripts = setHTMLExecuteScripts; window.dodona = dodona; -// Initialize clipboard.js -initClipboard(); - ready.then(initTooltips); diff --git a/app/javascript/packs/submission.js b/app/javascript/packs/submission.js index c45ab5b66a..09646aa1fd 100644 --- a/app/javascript/packs/submission.js +++ b/app/javascript/packs/submission.js @@ -1,6 +1,5 @@ import { initSubmissionShow, initCorrectSubmissionToNextLink, initSubmissionHistory, showLastTab } from "submission.ts"; import { initMathJax, onFrameMessage, onFrameScroll } from "exercise.ts"; -import { attachClipboard } from "copy"; import { evaluationState } from "state/Evaluations"; import codeListing from "code_listing"; import { annotationState } from "state/Annotations"; @@ -9,7 +8,6 @@ import { initFileViewers } from "file_viewer"; window.dodona.initSubmissionShow = initSubmissionShow; window.dodona.codeListing = codeListing; -window.dodona.attachClipboard = attachClipboard; window.dodona.initMathJax = initMathJax; window.dodona.initCorrectSubmissionToNextLink = initCorrectSubmissionToNextLink; window.dodona.initSubmissionHistory = initSubmissionHistory; diff --git a/app/views/api_tokens/_show.html.erb b/app/views/api_tokens/_show.html.erb index c47c40b5d2..c517c18f9e 100644 --- a/app/views/api_tokens/_show.html.erb +++ b/app/views/api_tokens/_show.html.erb @@ -3,11 +3,7 @@
-
- <% name = "fresh-token-value" %> - <%= text_field_tag name, token.token, readonly: true, class: 'form-control' %> - <%= clipboard_button_for name %> -
+
diff --git a/app/views/application/_token_field.html.erb b/app/views/application/_token_field.html.erb index 8c2adf3d56..d809127c67 100644 --- a/app/views/application/_token_field.html.erb +++ b/app/views/application/_token_field.html.erb @@ -1,12 +1,10 @@ -<%= text_field_tag name, value, readonly: true, class: 'form-control' %> -<%= clipboard_button_for name %> + <% if reset_url.present? %> <%= link_to reset_url, - title: t('general.generate_token'), method: :post, data: {confirm: t('general.confirm_reset_token')}, remote: true, - class: 'btn btn-icon' do %> - + class: 'btn btn-outline btn-icon ms-1 flex-shrink-0' do %> + <% end %> <% end %> diff --git a/app/views/courses/_form.html.erb b/app/views/courses/_form.html.erb index a86f991c03..36144c0512 100644 --- a/app/views/courses/_form.html.erb +++ b/app/views/courses/_form.html.erb @@ -116,7 +116,7 @@
- diff --git a/app/views/repositories/show.html.erb b/app/views/repositories/show.html.erb index 60c6970b22..73163af962 100644 --- a/app/views/repositories/show.html.erb +++ b/app/views/repositories/show.html.erb @@ -52,10 +52,7 @@

Webhook

<%= t ".webhook_html" %>

-
- <%= text_field_tag :webhook_link, webhook_repository_url(@repository), readonly: true, class: 'form-control' %> - <%= clipboard_button_for :webhook_link %> -
+
<% end %> diff --git a/app/views/series/edit.html.erb b/app/views/series/edit.html.erb index bc7c9db62a..bd7055eb99 100644 --- a/app/views/series/edit.html.erb +++ b/app/views/series/edit.html.erb @@ -20,7 +20,7 @@
<%= label_tag :access_token, t('.access-link'), class: 'col-sm-3 col-form-label' %>
-
+
<%= render partial: 'token_field', locals: { name: :access_token, diff --git a/package.json b/package.json index b8e322c448..e7760b1696 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "babel-loader": "^9.1.3", "babel-plugin-macros": "^3.1.0", "bootstrap": "5.3.3", - "clipboard": "^2.0.11", "codemirror-lang-prolog": "^0.1.0", "codemirror-lang-r": "^0.1.0-2", "core-js": "^3.37.1", diff --git a/test/renderers/feedback_code_renderer_test.rb b/test/renderers/feedback_code_renderer_test.rb index 1e198076c6..5ca7ea09d0 100644 --- a/test/renderers/feedback_code_renderer_test.rb +++ b/test/renderers/feedback_code_renderer_test.rb @@ -27,11 +27,4 @@ class FeedbackCodeRendererTest < ActiveSupport::TestCase assert_equal gen_html_orig, gen_html_cr end end - - test 'Multiple instances result in unique html' do - programming_language = 'python' - tables = 5.times.collect { FeedbackCodeRenderer.new('print(5)', programming_language).add_code.html } - - assert_equal tables.uniq, tables - end end diff --git a/yarn.lock b/yarn.lock index 2aebb34a8c..bf916bac26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3374,15 +3374,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -clipboard@^2.0.11: - version "2.0.11" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" - integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - cliui@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" @@ -4045,11 +4036,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -4913,13 +4899,6 @@ globjoin@^0.1.4: resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg== -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== - dependencies: - delegate "^3.1.2" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -7498,11 +7477,6 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== - "semver@2 || 3 || 4 || 5", semver@^5.5.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -8076,11 +8050,6 @@ ticky@1.0.1: resolved "https://registry.yarnpkg.com/ticky/-/ticky-1.0.1.tgz#b7cfa71e768f1c9000c497b9151b30947c50e46d" integrity sha512-RX35iq/D+lrsqhcPWIazM9ELkjOe30MSeoBHQHSsRwd1YuhJO5ui1K1/R0r7N3mFvbLBs33idw+eR6j+w6i/DA== -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tippy.js@^6.3.7: version "6.3.7" resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"