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"