diff --git a/ui/core/app/components/copyable/index.hbs b/ui/core/app/components/copyable/index.hbs new file mode 100644 index 000000000..8d0a3df75 --- /dev/null +++ b/ui/core/app/components/copyable/index.hbs @@ -0,0 +1,22 @@ +{{#if this.isClipboardLibrarySupported}} + + + {{yield}} + + + {{@text}} + +{{else}} + {{yield}} +{{/if}} diff --git a/ui/core/app/components/copyable/index.js b/ui/core/app/components/copyable/index.js new file mode 100644 index 000000000..01dcad838 --- /dev/null +++ b/ui/core/app/components/copyable/index.js @@ -0,0 +1,79 @@ +import Component from '@glimmer/component'; +import ClipboardJS from 'clipboard'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { generateComponentID } from 'rose/utilities/component-auto-id'; +import { task, timeout } from 'ember-concurrency'; +import { set } from '@ember/object'; +import { bind } from '@ember/runloop'; + +export default class CopyableComponent extends Component { + + // =attributes + + id = generateComponentID(); + copyableButtonId = `copyable-button-${this.id}`; + icon = 'copy-action'; + + @tracked text; + @tracked clipboard; + + /** + * A Ember Concurrency-based task that updates copy icon to a success state + * and back after 1 second. This icon flash indicates success on + * copy to clipboard action. + * + * NOTE: tasks are sort of attributes and sort of methods, but they are not + * language-level constructs. Thus we annotate this task as if it + * is an attribute. + * @type {Task} + */ + @task(function * () { + set(this, 'icon', 'copy-success'); + yield timeout(1000); + set(this, 'icon', 'copy-action'); + }).drop() successIconTimer; + + // =methods + + /** + * Checks for ClipboardJS support. + * @return {boolean} + */ + get isClipboardLibrarySupported() { + return ClipboardJS.isSupported(); + } + + /** + * When copy to clipboard is successful, begin timer to show success icon. + */ + onCopySuccess(clipboardEvent) { + clipboardEvent.clearSelection(); + this.successIconTimer.perform(); + } + + // =actions + + /** + * Initialize ClipboardJS library and register for it's events. + */ + @action + register() { + if(!this.clipboard) { + this.clipboard = new ClipboardJS(`#${this.copyableButtonId}`, { + text: () => this.args.text, + }); + this.clipboard.on('success', bind(this, this.onCopySuccess)); + } + } + + /** + * Destroy ClipboardJS library instance and cancel icon timer. + */ + @action + destroy() { + this.clipboard?.destroy(); + this.successIconTimer.cancelAll(); + } + +} diff --git a/ui/core/app/styles/app.scss b/ui/core/app/styles/app.scss index b1a6af3c1..9e72b055f 100644 --- a/ui/core/app/styles/app.scss +++ b/ui/core/app/styles/app.scss @@ -80,3 +80,7 @@ } } } + +.copyable .copyable-button { + padding: 0rem; +} diff --git a/ui/core/ember-cli-build.js b/ui/core/ember-cli-build.js index cc31a1b6c..25d88f3cc 100644 --- a/ui/core/ember-cli-build.js +++ b/ui/core/ember-cli-build.js @@ -20,6 +20,14 @@ module.exports = function (defaults) { }, }); + // Only import when in development or test mode + if (app.env.match(/(development)|(test)/i)) { + app.import('node_modules/clipboard/dist/clipboard.js'); + } + else { + app.import('node_modules/clipboard/dist/clipboard.min.js'); + } + // Use `app.import` to add additional libraries to the generated // output files. // diff --git a/ui/core/package.json b/ui/core/package.json index ee54d5c47..6b6547fbd 100644 --- a/ui/core/package.json +++ b/ui/core/package.json @@ -29,6 +29,7 @@ "devDependencies": { "@babel/core": "^7.9.6", "@ember/optional-features": "^1.3.0", + "@ember/render-modifiers": "^1.0.2", "@glimmer/component": "^1.0.0", "@glimmer/tracking": "^1.0.0", "api": "*", @@ -36,6 +37,7 @@ "babel-eslint": "^10.0.3", "babel-loader": "^8.1.0", "broccoli-asset-rev": "^3.0.0", + "clipboard": "^2.0.6", "doctoc": "^1.4.0", "ember-a11y-testing": "^3.0.0", "ember-auto-import": "^1.5.3", diff --git a/ui/core/tests/integration/components/copyable-test.js b/ui/core/tests/integration/components/copyable-test.js new file mode 100644 index 000000000..07be2788f --- /dev/null +++ b/ui/core/tests/integration/components/copyable-test.js @@ -0,0 +1,15 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, find } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | copyable', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function(assert) { + await render(hbs`text`); + assert.ok(find('.copyable')); + assert.equal(find('.copyable-content').textContent.trim(), 'text'); + assert.equal(find('.copyable-button').textContent.trim(), 'Copy me'); + }); +}); diff --git a/ui/core/translations/actions/en-us.yaml b/ui/core/translations/actions/en-us.yaml index 40a38941d..7e7a9002a 100644 --- a/ui/core/translations/actions/en-us.yaml +++ b/ui/core/translations/actions/en-us.yaml @@ -27,4 +27,4 @@ add-members: Add Members add-hosts: Add Hosts set-password: Set Password change-password: Change Password -edit-form: Edit Form +edit-form: Edit Form \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index a205584da..f13b0ae16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2543,6 +2543,14 @@ ember-cli-babel "^6.16.0" ember-compatibility-helpers "^1.1.1" +"@ember/render-modifiers@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@ember/render-modifiers/-/render-modifiers-1.0.2.tgz#2e87c48db49d922ce4850d707215caaac60d8444" + integrity sha512-6tEnHl5+62NTSAG2mwhGMFPhUrJQjoVqV+slsn+rlTknm2Zik+iwxBQEbwaiQOU1FUYxkS8RWcieovRNMR8inQ== + dependencies: + ember-cli-babel "^7.10.0" + ember-modifier-manager-polyfill "^1.1.0" + "@ember/test-helpers@^1.7.0", "@ember/test-helpers@^1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@ember/test-helpers/-/test-helpers-1.7.1.tgz#cc22a954b3b46856518f034bd492a74e0482389f" @@ -7307,7 +7315,7 @@ cli-width@^2.0.0: resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw== -clipboard@^2.0.0: +clipboard@^2.0.0, clipboard@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376" integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg== @@ -9601,7 +9609,7 @@ ember-maybe-import-regenerator@^0.1.5, ember-maybe-import-regenerator@^0.1.6: ember-cli-babel "^6.0.0-beta.4" regenerator-runtime "^0.9.5" -ember-modifier-manager-polyfill@^1.2.0: +ember-modifier-manager-polyfill@^1.1.0, ember-modifier-manager-polyfill@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/ember-modifier-manager-polyfill/-/ember-modifier-manager-polyfill-1.2.0.tgz#cf4444e11a42ac84f5c8badd85e635df57565dda" integrity sha512-bnaKF1LLKMkBNeDoetvIJ4vhwRPKIIumWr6dbVuW6W6p4QV8ZiO+GdF8J7mxDNlog9CeL9Z/7wam4YS86G8BYA==