diff --git a/package.json b/package.json
index 334d22356..471269a4f 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"bootstrap": "^3.3.2",
"bootstrap-datepicker": "^1.6.4",
"cal-heatmap": "^3.3.10",
+ "clipboard": "^1.6.1",
"core-js": "^2.4.1",
"d3": "^3.3.3",
"eonasdan-bootstrap-datetimepicker": "^4.17.43",
diff --git a/static/css/quay.css b/static/css/quay.css
index bf3f07000..c7ffa24f2 100644
--- a/static/css/quay.css
+++ b/static/css/quay.css
@@ -1569,7 +1569,7 @@ p.editable:hover i {
transition: color 0.5s ease-in-out;
}
-.copy-box-element .copy-container .copy-icon.zeroclipboard-is-hover {
+.copy-box-element .copy-container .copy-icon:hover {
color: #444;
}
diff --git a/static/directives/build-logs-view.html b/static/directives/build-logs-view.html
index f1574a602..f939caa95 100644
--- a/static/directives/build-logs-view.html
+++ b/static/directives/build-logs-view.html
@@ -1,6 +1,7 @@
-
diff --git a/static/directives/copy-box.html b/static/directives/copy-box.html
index 7532a6d68..79b033059 100644
--- a/static/directives/copy-box.html
+++ b/static/directives/copy-box.html
@@ -1,11 +1,12 @@
diff --git a/static/js/directives/ui/build-logs-view.js b/static/js/directives/ui/build-logs-view.js
index 714a42bc3..15bad6680 100644
--- a/static/js/directives/ui/build-logs-view.js
+++ b/static/js/directives/ui/build-logs-view.js
@@ -23,10 +23,6 @@ angular.module('quay').directive('buildLogsView', function () {
repoStatusApiCall = ApiService.getRepoBuildStatusSuperUser;
repoLogApiCall = ApiService.getRepoBuildLogsSuperUserAsResource;
}
- var result = $element.find('#copyButton').clipboardCopy();
- if (!result) {
- $element.find('#copyButton').hide();
- }
$scope.logEntries = null;
$scope.currentParentEntry = null;
diff --git a/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.spec.ts b/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.spec.ts
new file mode 100644
index 000000000..7080dc6b2
--- /dev/null
+++ b/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.spec.ts
@@ -0,0 +1,77 @@
+import { ClipboardCopyDirective } from './clipboard-copy.directive';
+import * as Clipboard from 'clipboard';
+import { Mock } from 'ts-mocks';
+import Spy = jasmine.Spy;
+
+
+describe("ClipboardCopyDirective", () => {
+ var directive: ClipboardCopyDirective;
+ var $elementMock: any;
+ var $timeoutMock: any;
+ var $documentMock: any;
+ var clipboardFactory: any;
+ var clipboardMock: Mock
;
+
+ beforeEach(() => {
+ $elementMock = new Mock();
+ $timeoutMock = jasmine.createSpy('$timeoutSpy').and.callFake((fn: () => void, delay) => fn());
+ $documentMock = new Mock();
+ clipboardMock = new Mock();
+ clipboardMock.setup(mock => mock.on).is((eventName: string, callback: (event) => void) => {});
+ clipboardFactory = jasmine.createSpy('clipboardFactory').and.returnValue(clipboardMock.Object);
+ directive = new ClipboardCopyDirective([$elementMock.Object],
+ $timeoutMock,
+ [$documentMock.Object],
+ clipboardFactory);
+ directive.copyTargetSelector = "#copy-input-box-0";
+ });
+
+ describe("ngAfterContentInit", () => {
+
+ it("initializes new Clipboard instance", () => {
+ const target = new Mock();
+ $documentMock.setup(mock => mock.querySelector).is(selector => target.Object);
+ directive.ngAfterContentInit();
+
+ expect(clipboardFactory).toHaveBeenCalled();
+ expect((clipboardFactory.calls.argsFor(0)[0])).toEqual($elementMock.Object);
+ expect((clipboardFactory.calls.argsFor(0)[1]['target']())).toEqual(target.Object);
+ });
+
+ it("sets error callback for Clipboard instance", () => {
+ directive.ngAfterContentInit();
+
+ expect((clipboardMock.Object.on.calls.argsFor(0)[0])).toEqual('error');
+ expect((clipboardMock.Object.on.calls.argsFor(0)[1])).toBeDefined();
+ });
+
+ it("sets success callback for Clipboard instance", (done) => {
+ directive.ngAfterContentInit();
+
+ expect((clipboardMock.Object.on.calls.argsFor(1)[0])).toEqual('success');
+ expect((clipboardMock.Object.on.calls.argsFor(1)[1])).toBeDefined();
+ done();
+ });
+ });
+
+ describe("ngOnDestroy", () => {
+
+ beforeEach(() => {
+ clipboardMock.setup(mock => mock.destroy).is(() => null);
+ });
+
+ it("calls method to destroy Clipboard instance if set", (done) => {
+ directive.ngAfterContentInit();
+ directive.ngOnDestroy();
+
+ expect((clipboardMock.Object.destroy)).toHaveBeenCalled();
+ done();
+ });
+
+ it("does not call method to destroy Clipboard instance if not set", () => {
+ directive.ngOnDestroy();
+
+ expect((clipboardMock.Object.destroy)).not.toHaveBeenCalled();
+ });
+ });
+});
\ No newline at end of file
diff --git a/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.ts b/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.ts
new file mode 100644
index 000000000..0be17118b
--- /dev/null
+++ b/static/js/directives/ui/clipboard-copy/clipboard-copy.directive.ts
@@ -0,0 +1,63 @@
+import { Directive, Inject, Input, AfterContentInit, OnDestroy } from 'ng-metadata/core';
+import * as Clipboard from 'clipboard';
+
+
+@Directive({
+ selector: '[clipboardCopy]'
+})
+export class ClipboardCopyDirective implements AfterContentInit, OnDestroy {
+
+ @Input('@clipboardCopy') public copyTargetSelector: string;
+
+ private clipboard: Clipboard;
+
+ constructor(@Inject('$element') private $element: ng.IAugmentedJQuery,
+ @Inject('$timeout') private $timeout: ng.ITimeoutService,
+ @Inject('$document') private $document: ng.IDocumentService,
+ @Inject('clipboardFactory') private clipboardFactory: (elem, options) => Clipboard) {
+
+ }
+
+ public ngAfterContentInit(): void {
+ // FIXME: Need to wait for DOM to render to find target element
+ this.$timeout(() => {
+ this.clipboard = this.clipboardFactory(this.$element[0], {target: (trigger) => {
+ return this.$document[0].querySelector(this.copyTargetSelector);
+ }});
+
+ this.clipboard.on("error", (e) => {
+ console.error(e);
+ });
+
+ this.clipboard.on('success', (e) => {
+ const container = e.trigger.parentNode.parentNode.parentNode;
+ const messageElem = container.querySelector('.clipboard-copied-message');
+ if (!messageElem) {
+ return;
+ }
+
+ // Resets the animation.
+ var elem = messageElem;
+ elem.style.display = 'none';
+ elem.classList.remove('animated');
+
+ // Show the notification.
+ setTimeout(() => {
+ elem.style.display = 'inline-block';
+ elem.classList.add('animated');
+ }, 10);
+
+ // Reset the notification.
+ setTimeout(() => {
+ elem.style.display = 'none';
+ }, 5000);
+ });
+ }, 100);
+ }
+
+ public ngOnDestroy(): void {
+ if (this.clipboard) {
+ this.clipboard.destroy();
+ }
+ }
+}
\ No newline at end of file
diff --git a/static/js/directives/ui/copy-box.js b/static/js/directives/ui/copy-box.js
index 0bb61f5fa..63cb69cb9 100644
--- a/static/js/directives/ui/copy-box.js
+++ b/static/js/directives/ui/copy-box.js
@@ -1,49 +1,5 @@
-$.fn.clipboardCopy = function() {
- if (__zeroClipboardSupported) {
- (new ZeroClipboard($(this)));
- return true;
- }
-
- this.hide();
- return false;
-};
-
-// Initialize the clipboard system.
-(function () {
- __zeroClipboardSupported = true;
-
- ZeroClipboard.on("error", function(e) {
- __zeroClipboardSupported = false;
- });
-
- ZeroClipboard.on('aftercopy', function(e) {
- var container = e.target.parentNode.parentNode.parentNode;
- var message = $(container).find('.clipboard-copied-message')[0];
- if (!message) {
- return;
- }
-
- // Resets the animation.
- var elem = message;
- elem.style.display = 'none';
- elem.classList.remove('animated');
-
- // Show the notification.
- setTimeout(function() {
- elem.style.display = 'inline-block';
- elem.classList.add('animated');
- }, 10);
-
- // Reset the notification.
- setTimeout(function() {
- elem.style.display = 'none';
- }, 5000);
- });
-})();
-
/**
- * An element which displays a textfield with a "Copy to Clipboard" icon next to it. Note
- * that this method depends on the clipboard copying library in the lib/ folder.
+ * An element which displays a textfield with a "Copy to Clipboard" icon next to it.
*/
angular.module('quay').directive('copyBox', function () {
var directiveDefinitionObject = {
@@ -62,13 +18,6 @@ angular.module('quay').directive('copyBox', function () {
var number = $rootScope.__copyBoxIdCounter || 0;
$rootScope.__copyBoxIdCounter = number + 1;
$scope.inputId = "copy-box-input-" + number;
-
- var button = $($element).find('.copy-icon');
- var input = $($element).find('input');
-
- input.attr('id', $scope.inputId);
- button.attr('data-clipboard-target', $scope.inputId);
- $scope.disabled = !button.clipboardCopy();
}
};
return directiveDefinitionObject;
diff --git a/static/js/directives/ui/fetch-tag-dialog.js b/static/js/directives/ui/fetch-tag-dialog.js
index 0ae6505ef..ae84b0f6d 100644
--- a/static/js/directives/ui/fetch-tag-dialog.js
+++ b/static/js/directives/ui/fetch-tag-dialog.js
@@ -125,7 +125,6 @@ angular.module('quay').directive('fetchTagDialog', function () {
updateFormats();
- $element.find('#copyClipboard').clipboardCopy();
$element.find('#fetchTagDialog').modal({});
}
};
diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts
index b90dffc49..cce2b51da 100644
--- a/static/js/quay.module.ts
+++ b/static/js/quay.module.ts
@@ -36,7 +36,9 @@ import { MarkdownToolbarComponent } from './directives/ui/markdown/markdown-tool
import { MarkdownEditorComponent } from './directives/ui/markdown/markdown-editor.component';
import { BrowserPlatform, browserPlatform } from './constants/platform.constant';
import { ManageTriggerComponent } from './directives/ui/manage-trigger/manage-trigger.component';
+import { ClipboardCopyDirective } from './directives/ui/clipboard-copy/clipboard-copy.directive';
import { Converter, ConverterOptions } from 'showdown';
+import * as Clipboard from 'clipboard';
/**
@@ -75,6 +77,7 @@ import { Converter, ConverterOptions } from 'showdown';
CorTabComponent,
CorTabPaneComponent,
ManageTriggerComponent,
+ ClipboardCopyDirective,
],
providers: [
ViewArrayImpl,
@@ -86,6 +89,7 @@ import { Converter, ConverterOptions } from 'showdown';
{provide: 'markdownConverterFactory', useValue: (options?: ConverterOptions) => new Converter(options)},
{provide: 'BrowserPlatform', useValue: browserPlatform},
{provide: 'CorTabCurrentHandlerFactory', useValue: CorTabCurrentHandlerFactory},
+ {provide: 'clipboardFactory', useValue: (trigger, options) => new Clipboard(trigger, options)},
],
})
export class QuayModule {
diff --git a/webpack.config.js b/webpack.config.js
index bf45ba6f5..e823b1e29 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -22,9 +22,7 @@ var config = {
rules: [
{
test: /\.tsx?$/,
- use: [
- "ts-loader",
- ],
+ use: ["ts-loader"],
exclude: /node_modules/
},
{
@@ -41,13 +39,12 @@ var config = {
'ngtemplate-loader?relativeTo=' + (path.resolve(__dirname)),
'html-loader',
]
- }
+ },
]
},
plugins: [
// Replace references to global variables with associated modules
new webpack.ProvidePlugin({
- ZeroClipboard: 'zeroclipboard',
FileSaver: 'file-saver',
angular: "angular",
$: "jquery",
diff --git a/yarn.lock b/yarn.lock
index 5f1c16a57..fd154515a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -651,6 +651,14 @@ cli-boxes@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143"
+clipboard@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53"
+ dependencies:
+ good-listener "^1.2.0"
+ select "^1.1.2"
+ tiny-emitter "^1.0.0"
+
cliui@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
@@ -1038,6 +1046,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.1.2"
+ resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.2.tgz#1e1bc6f5cadda6cb6cbf7e6d05d0bcdd5712aebe"
+
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -1567,6 +1579,12 @@ globule@^1.0.0:
lodash "~4.16.4"
minimatch "~3.0.2"
+good-listener@^1.2.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
+ dependencies:
+ delegate "^3.1.2"
+
got@^5.0.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35"
@@ -3645,6 +3663,10 @@ script-loader@^0.7.0:
dependencies:
raw-loader "~0.5.1"
+select@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
+
semver-diff@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
@@ -3996,6 +4018,10 @@ timers-browserify@^2.0.2:
dependencies:
setimmediate "^1.0.4"
+tiny-emitter@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.2.0.tgz#6dc845052cb08ebefc1874723b58f24a648c3b6f"
+
tmp@0.0.x:
version "0.0.31"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"