diff --git a/package.json b/package.json index 28ea5aff..3d597fba 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,10 @@ } }, "dependencies": { + "clipboard": "2.0.1", "escape-string-regexp": "^1.0.5", - "popper.js": "^1.12.9", - "flatpickr": "4.5.0" + "flatpickr": "4.5.0", + "popper.js": "^1.12.9" }, "devDependencies": { "angular": "~1.6.1", diff --git a/packages/oui-angular/src/index.js b/packages/oui-angular/src/index.js index 61ca5c65..cfbd0b70 100644 --- a/packages/oui-angular/src/index.js +++ b/packages/oui-angular/src/index.js @@ -28,6 +28,7 @@ import "@oui-angular/oui-chips/src"; import "@oui-angular/oui-popover/src"; import "@oui-angular/oui-stepper/src"; import "@oui-angular/oui-skeleton/src"; +import "@oui-angular/oui-clipboard/src"; angular.module("oui", [ "oui.button", @@ -59,5 +60,6 @@ angular.module("oui", [ "oui.chips", "oui.popover", "oui.stepper", - "oui.skeleton" + "oui.skeleton", + "oui.clipboard" ]); diff --git a/packages/oui-angular/src/index.spec.js b/packages/oui-angular/src/index.spec.js index e0a634ce..df59e3c1 100644 --- a/packages/oui-angular/src/index.spec.js +++ b/packages/oui-angular/src/index.spec.js @@ -29,6 +29,7 @@ loadTests(require.context("../../oui-chips/src/", true, /.*((\.spec)|(index))$/) loadTests(require.context("../../oui-popover/src/", true, /.*((\.spec)|(index))$/)); loadTests(require.context("../../oui-stepper/src/", true, /.*((\.spec)|(index))$/)); loadTests(require.context("../../oui-skeleton/src/", true, /.*((\.spec)|(index))$/)); +loadTests(require.context("../../oui-clipboard/src/", true, /.*((\.spec)|(index))$/)); function loadTests (context) { context.keys().forEach(context); diff --git a/packages/oui-clipboard/README.md b/packages/oui-clipboard/README.md new file mode 100644 index 00000000..aa04d74f --- /dev/null +++ b/packages/oui-clipboard/README.md @@ -0,0 +1,36 @@ +# Clipboard + + + +## Usage + +### Default + +```html:preview +
+ +
+
+

Model value: {{$ctrl.simpleModel | json}}

+
+``` + +### Formatted text + +```html:preview +
+ +
+
+

Model value: {{$ctrl.formattedModel}}

+
+``` + +## API + +| Attribute | Type | Binding | One-time Binding | Values | Default | Description +| ---- | ---- | ---- | ---- | ---- | ---- | ---- +| id | string | @? | true | | | id attribute of the input +| name | string | @? | true | | | name attribute of the input +| model | object | = | true | | | model bound to component diff --git a/packages/oui-clipboard/src/clipboard.component.js b/packages/oui-clipboard/src/clipboard.component.js new file mode 100644 index 00000000..d4b479f0 --- /dev/null +++ b/packages/oui-clipboard/src/clipboard.component.js @@ -0,0 +1,12 @@ +import controller from "./clipboard.controller"; +import template from "./clipboard.html"; + +export default { + template, + controller, + bindings: { + name: "@?", + id: "@?", + model: "=" + } +}; diff --git a/packages/oui-clipboard/src/clipboard.controller.js b/packages/oui-clipboard/src/clipboard.controller.js new file mode 100644 index 00000000..7256100d --- /dev/null +++ b/packages/oui-clipboard/src/clipboard.controller.js @@ -0,0 +1,76 @@ +import Clipboard from "clipboard"; +export default class { + constructor ($attrs, $element, $timeout, ouiClipboardConfiguration) { + "ngInject"; + this.$attrs = $attrs; + this.$element = $element; + this.$timeout = $timeout; + this.translations = angular.copy(ouiClipboardConfiguration.translations); + } + + $onInit () { + this.tooltipText = this.translations.copyToClipboardLabel; + this.trigger = this.$element[0].querySelector(".oui-clipboard__button"); + this.target = this.$element[0].querySelector(".oui-clipboard__control"); + } + + $onDestroy () { + this.clipboard.destroy(); + } + + $postLink () { + this.$timeout(() => { + this.$element + .addClass("oui-input-group oui-input-group_clipboard") + .removeAttr("id") + .removeAttr("name"); + }); + + // Init the clipboard instance + this.clipboard = new Clipboard(this.trigger, { + target: () => this.target, + text: () => this.model + }); + + // Events for updating the tooltip + this.clipboard + .on("success", () => this.selectInputText(this.translations.copiedLabel)) + .on("error", () => this.selectInputText(this.translations.notSupported)); + } + + selectInputText (tooltipText) { + const selectionEnd = this.model.length || 0; + + this.$timeout(() => { + // Need to focus before selecting + this.target.focus(); + + // Select text on the target + this.target.selectionStart = 0; + this.target.selectionEnd = selectionEnd; + this.target.setSelectionRange(0, selectionEnd); + this.target.select(); + + // Update tooltip text + this.tooltipText = tooltipText; + + // Need to bind the reset like this because + // ClipboardJS triggered the "blur" event + // By copying in a fake textarea + angular.element(this.target).one("blur", () => this.reset()); + }); + } + + onInputClick () { + this.trigger.click(); + } + + reset () { + const resetDelay = 500; + + // Add delay for resetting after tooltip animation + this.$timeout(() => { + this.tooltipText = this.translations.copyToClipboardLabel; + }, resetDelay); + } +} diff --git a/packages/oui-clipboard/src/clipboard.html b/packages/oui-clipboard/src/clipboard.html new file mode 100644 index 00000000..3171c5c9 --- /dev/null +++ b/packages/oui-clipboard/src/clipboard.html @@ -0,0 +1,12 @@ + + diff --git a/packages/oui-clipboard/src/clipboard.provider.js b/packages/oui-clipboard/src/clipboard.provider.js new file mode 100644 index 00000000..5564aa7a --- /dev/null +++ b/packages/oui-clipboard/src/clipboard.provider.js @@ -0,0 +1,25 @@ +import { merge } from "lodash"; +export default class { + constructor () { + this.translations = { + copyToClipboardLabel: "Copy to clipboard", + copiedLabel: "Copied", + notSupported: "Copy to clipboard not supported. Please copy the text manually" + }; + } + + /** + * Set the translations + * @param {Object} translations a map of translations + */ + setTranslations (translations) { + this.translations = merge(this.translations, translations); + return this; + } + + $get () { + return { + translations: this.translations + }; + } +} diff --git a/packages/oui-clipboard/src/index.js b/packages/oui-clipboard/src/index.js new file mode 100644 index 00000000..7173ae55 --- /dev/null +++ b/packages/oui-clipboard/src/index.js @@ -0,0 +1,6 @@ +import Clipboard from "./clipboard.component.js"; +import ClipboardProvider from "./clipboard.provider.js"; + +angular + .module("oui.clipboard", []).component("ouiClipboard", Clipboard) + .provider("ouiClipboardConfiguration", ClipboardProvider); diff --git a/packages/oui-clipboard/src/index.spec.js b/packages/oui-clipboard/src/index.spec.js new file mode 100644 index 00000000..8b80a439 --- /dev/null +++ b/packages/oui-clipboard/src/index.spec.js @@ -0,0 +1,110 @@ +describe("ouiClipboard", () => { + let $timeout; + let testUtils; + let configuration; + + beforeEach(angular.mock.module("oui.clipboard")); + beforeEach(angular.mock.module("oui.clipboard.configuration")); + beforeEach(angular.mock.module("oui.test-utils")); + + beforeEach(inject((_$timeout_, _TestUtils_) => { + $timeout = _$timeout_; + testUtils = _TestUtils_; + })); + + describe("Provider", () => { + + angular.module("oui.clipboard.configuration", [ + "oui.clipboard" + ]).config(ouiClipboardConfigurationProvider => { + ouiClipboardConfigurationProvider.setTranslations({ + foo: "bar" + }); + }); + + beforeEach(inject(_ouiClipboardConfiguration_ => { + configuration = _ouiClipboardConfiguration_; + })); + + it("should have custom options", () => { + expect(configuration.translations.foo).toEqual("bar"); + }); + }); + + describe("Component", () => { + it("should generate an input with the given text", () => { + const model = "foo"; + const element = testUtils.compileTemplate("", { + model + }); + + const inputElement = element[0].querySelector("input[type=text]"); + expect(angular.element(inputElement).val()).toMatch(model); + }); + + it("should generate an input with name and id attribute", () => { + const element = testUtils.compileTemplate(""); + const inputElement = element[0].querySelector("input[type=text]"); + + $timeout.flush(); + + expect(angular.element(inputElement).attr("id")).toBe("id"); + expect(angular.element(inputElement).attr("name")).toBe("name"); + }); + + it("should have an instance of clipboardjs", () => { + const model = "foo"; + const element = testUtils.compileTemplate("", { + model + }); + const $ctrl = element.controller("ouiClipboard"); + + expect($ctrl.clipboard).toBeDefined(); + + const target = angular.element($ctrl.clipboard.target()); + expect(target.hasClass("oui-clipboard__control")).toBeTruthy(); + expect($ctrl.clipboard.text()).toBe(model); + }); + + it("should update tooltip text when copied on click", (done) => { + const model = "bar"; + const element = testUtils.compileTemplate("", { + model + }); + const btnElement = element[0].querySelector(".oui-clipboard__button"); + const $ctrl = element.controller("ouiClipboard"); + + $ctrl.clipboard + .on("success", () => { + $timeout.flush(); + expect($ctrl.tooltipText).toEqual(configuration.translations.copiedLabel); + done(); + }) + .on("error", () => { + $timeout.flush(); + expect($ctrl.tooltipText).toEqual(configuration.translations.notSupported); + done(); + }); + + btnElement.click(); + }); + + it("should reset tooltip text", () => { + const element = testUtils.compileTemplate("", { + model: "foo" + }); + const btnElement = element[0].querySelector(".oui-clipboard__button"); + const $ctrl = element.controller("ouiClipboard"); + + // Simulate click + btnElement.click(); + $timeout.flush(); + + // Then reset + $ctrl.reset(); + $timeout.flush(); + + expect($ctrl.tooltipText).toEqual(configuration.translations.copyToClipboardLabel); + }); + }); +}); diff --git a/packages/oui-stepper/src/stepper.provider.js b/packages/oui-stepper/src/stepper.provider.js index 8d376281..74be80e6 100644 --- a/packages/oui-stepper/src/stepper.provider.js +++ b/packages/oui-stepper/src/stepper.provider.js @@ -12,6 +12,10 @@ export default class { }; } + /** + * Set the translations + * @param {Object} translations a map of translations + */ setTranslations (translations) { this.translations = merge(this.translations, translations); return this; diff --git a/packages/oui-tooltip/README.md b/packages/oui-tooltip/README.md index bfa0a2d8..264ae69b 100644 --- a/packages/oui-tooltip/README.md +++ b/packages/oui-tooltip/README.md @@ -46,7 +46,7 @@ If there is no `aria-label` attribute, the directive create one based on `oui-to | Attribute | Type | Binding | One-time Binding | Values | Default | Description | | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -| oui-tooltip | string | @ | true | | | tooltip text | +| oui-tooltip | string | @ | | | | tooltip text | | oui-tooltip-placement | string | @? | true | top,top-start,top-end,bottom,bottom-start,bottom-end | top | tooltip placement | diff --git a/packages/oui-tooltip/src/tooltip.html b/packages/oui-tooltip/src/tooltip.html index 996838b3..ce3eb6c7 100644 --- a/packages/oui-tooltip/src/tooltip.html +++ b/packages/oui-tooltip/src/tooltip.html @@ -1 +1 @@ - + diff --git a/yarn.lock b/yarn.lock index fcf52c9d..dfceec7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1851,6 +1851,14 @@ cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" +clipboard@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.1.tgz#a12481e1c13d8a50f5f036b0560fe5d16d74e46a" + dependencies: + good-listener "^1.2.2" + select "^1.1.2" + tiny-emitter "^2.0.0" + cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -2595,6 +2603,10 @@ delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" +delegate@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" + delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -3767,6 +3779,12 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +good-listener@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" + dependencies: + delegate "^3.1.2" + graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -7146,6 +7164,10 @@ schema-utils@^0.4.0, schema-utils@^0.4.5: ajv "^6.1.0" ajv-keywords "^3.1.0" +select@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" + "semver@2 || 3 || 4 || 5", semver@5.5.0, semver@^5.3.0, semver@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" @@ -7830,6 +7852,10 @@ timespan@2.3.x: version "2.3.0" resolved "https://registry.yarnpkg.com/timespan/-/timespan-2.3.0.tgz#4902ce040bd13d845c8f59b27e9d59bad6f39929" +tiny-emitter@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c" + tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"