Commit aebffaae authored by Sean McGivern's avatar Sean McGivern

Merge branch 'feature/service-desk-integration' into 'feature/service-desk-be'

Service desk integration

See merge request !1519
parents 5194f415 a2e2837a
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
import Clipboard from 'vendor/clipboard';
import Clipboard from 'clipboard';
var genericError, genericSuccess, showTooltip;
......
......@@ -49,6 +49,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
const ShortcutsBlob = require('./shortcuts_blob');
......@@ -228,6 +229,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:edit':
const el = document.querySelector('.js-service-desk-setting-root');
if (el) {
const serviceDeskRoot = new ServiceDeskRoot(el);
serviceDeskRoot.init();
}
break;
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
......
import eventHub from '../event_hub';
export default {
name: 'ServiceDeskSetting',
props: {
isEnabled: {
type: Boolean,
required: true,
},
incomingEmail: {
type: String,
required: false,
default: '',
},
fetchError: {
type: Error,
required: false,
default: null,
},
},
methods: {
onCheckboxToggle(e) {
const isChecked = e.target.checked;
eventHub.$emit('serviceDeskEnabledCheckboxToggled', isChecked);
},
},
template: `
<div>
<div class="checkbox">
<label for="service-desk-enabled-checkbox">
<input
type="checkbox"
id="service-desk-enabled-checkbox"
:checked="isEnabled"
@change="onCheckboxToggle($event)">
<span class="descr">
Activate service desk
</span>
</label>
</div>
<template v-if="isEnabled">
<div
class="panel-slim panel-default">
<div class="panel-heading">
<h3 class="panel-title">
Forward external support email address to:
</h3>
</div>
<div class="panel-body">
<template v-if="fetchError">
<i class="fa fa-exclamation-circle" aria-hidden="true" />
An error occurred while fetching the incoming email
</template>
<template v-else-if="incomingEmail">
<span ref="service-desk-incoming-email">
{{ incomingEmail }}
</span>
<button
class="btn btn-clipboard btn-transparent has-tooltip"
title="Copy incoming email address to clipboard"
:data-clipboard-text="incomingEmail"
@click.prevent>
<i class="fa fa-clipboard" aria-hidden="true" />
</button>
</template>
<template v-else>
<i class="fa fa-spinner fa-spin" aria-hidden="true" />
<span class="sr-only">
Fetching incoming email
</span>
</template>
</div>
</div>
<p class="settings-message">
We recommend you protect the external support email address.
Unblocked email spam would result in many spam issues being created,
and may disrupt your GitLab service.
</p>
</template>
</div>
`,
};
import Vue from 'vue';
export default new Vue();
/* eslint-disable no-new */
import Vue from 'vue';
import ServiceDeskSetting from './components/service_desk_setting';
import ServiceDeskStore from './stores/service_desk_store';
import ServiceDeskService from './services/service_desk_service';
import eventHub from './event_hub';
class ServiceDeskRoot {
constructor(wrapperElement) {
this.wrapperElement = wrapperElement;
const isEnabled = this.wrapperElement.dataset.enabled !== undefined &&
this.wrapperElement.dataset.enabled !== 'false';
const incomingEmail = this.wrapperElement.dataset.incomingEmail;
const endpoint = this.wrapperElement.dataset.endpoint;
this.store = new ServiceDeskStore({
isEnabled,
incomingEmail,
});
this.service = new ServiceDeskService(endpoint);
}
init() {
this.bindEvents();
if (this.store.state.isEnabled && !this.store.state.incomingEmail) {
this.fetchIncomingEmail();
}
this.render();
}
bindEvents() {
this.onEnableToggledWrapper = this.onEnableToggled.bind(this);
eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggledWrapper);
}
unbindEvents() {
eventHub.$on('serviceDeskEnabledCheckboxToggled', this.onEnableToggledWrapper);
}
render() {
this.vm = new Vue({
el: this.wrapperElement,
data: this.store.state,
template: `
<service-desk-setting
:isEnabled="isEnabled"
:incomingEmail="incomingEmail"
:fetchError="fetchError" />
`,
components: {
'service-desk-setting': ServiceDeskSetting,
},
});
}
fetchIncomingEmail() {
this.service.fetchIncomingEmail()
.then((incomingEmail) => {
this.store.setIncomingEmail(incomingEmail);
})
.catch((err) => {
this.store.setFetchError(err);
});
}
onEnableToggled(isChecked) {
this.store.setIsActivated(isChecked);
this.store.setIncomingEmail('');
this.store.setFetchError(null);
this.service.toggleServiceDesk(isChecked)
.then((incomingEmail) => {
this.store.setIncomingEmail(incomingEmail);
})
.catch((err) => {
this.store.setFetchError(err);
});
}
destroy() {
this.unbindEvents();
if (this.vm) {
this.vm.$destroy();
}
}
}
export default ServiceDeskRoot;
import Vue from 'vue';
import vueResource from 'vue-resource';
import '../../../vue_shared/vue_resource_interceptor';
Vue.use(vueResource);
class ServiceDeskService {
constructor(endpoint) {
this.serviceDeskResource = Vue.resource(`${endpoint}`);
}
fetchIncomingEmail() {
return this.serviceDeskResource.get()
.then((res) => {
const email = res.data.service_desk_address;
if (!email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
return email;
});
}
toggleServiceDesk(enable) {
return this.serviceDeskResource.update({
service_desk_enabled: enable,
})
.then((res) => {
const email = res.data.service_desk_address;
if (enable && !email) {
throw new Error('Response didn\'t include `service_desk_address`');
}
return email;
});
}
}
export default ServiceDeskService;
class ServiceDeskStore {
constructor(initialState = {}) {
this.state = Object.assign({
isEnabled: false,
incomingEmail: '',
fetchError: null,
}, initialState);
}
setIsActivated(value) {
this.state.isEnabled = value;
}
setIncomingEmail(value) {
this.state.incomingEmail = value;
}
setFetchError(value) {
this.state.fetchError = value;
}
}
export default ServiceDeskStore;
.panel {
margin-bottom: $gl-padding;
@mixin panel {
.panel-heading {
padding: $gl-vert-padding $gl-padding;
line-height: 36px;
......@@ -48,3 +46,14 @@
line-height: inherit;
}
}
.panel {
@include panel;
margin-bottom: $gl-padding;
}
.panel-slim {
@extend .panel;
@include panel;
margin-bottom: $gl-vert-padding;
}
= render "projects/settings/head"
.project-edit-container
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
......@@ -125,6 +126,16 @@
= render 'merge_request_settings', form: f
- if EE::Gitlab::ServiceDesk.enabled?
%hr
%fieldset.js-service-desk-setting-wrapper.features.append-bottom-default
%h5.prepend-top-0
Service Desk
= link_to icon('question-circle'), help_page_path("TODO")
.js-service-desk-setting-root{ data: { endpoint: namespace_project_service_desk_path(@project.namespace, @project),
enabled: @project.service_desk_enabled,
incoming_email: (@project.service_desk_address if @project.service_desk_enabled) } }
%hr
%fieldset.features.append-bottom-default
%h5.prepend-top-0
......
require 'spec_helper'
describe 'Service Desk Setting', js: true, feature: true do
include WaitForAjax
let(:project) { create(:project_empty_repo, :private) }
let(:user) { create(:user) }
before do
project.add_master(user)
login_as(user)
allow_any_instance_of(License).to receive(:add_on?).and_call_original
allow_any_instance_of(License).to receive(:add_on?).with('GitLab_ServiceDesk') { true }
visit edit_namespace_project_path(project.namespace, project)
end
it 'shows service desk activation checkbox' do
expect(page).to have_selector("#service-desk-enabled-checkbox")
end
it 'shows incoming email after activating' do
find("#service-desk-enabled-checkbox").click
wait_for_ajax
expect(find('.js-service-desk-setting-wrapper .panel-body')).to have_content(project.service_desk_address)
end
end
import Vue from 'vue';
import eventHub from '~/projects/settings_service_desk/event_hub';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting';
const createComponent = (propsData) => {
const Component = Vue.extend(ServiceDeskSetting);
return new Component({
el: document.createElement('div'),
propsData,
});
};
describe('ServiceDeskSetting', () => {
let vm;
afterEach(() => {
if (vm) {
vm.$destroy();
}
});
describe('when isEnabled=true', () => {
let el;
describe('only isEnabled', () => {
beforeEach(() => {
vm = createComponent({
isEnabled: true,
});
el = vm.$el;
});
it('see main panel with the email info', () => {
expect(el.querySelector('.panel')).toBeDefined();
});
it('see loading spinner', () => {
expect(el.querySelector('.fa-spinner')).toBeDefined();
expect(el.querySelector('.fa-exclamation-circle')).toBeNull();
expect(vm.$refs['service-desk-incoming-email']).toBeUndefined();
});
it('see warning message', () => {
expect(el.querySelector('.settings-message')).toBeDefined();
});
});
describe('with incomingEmail', () => {
beforeEach(() => {
vm = createComponent({
isEnabled: true,
incomingEmail: 'foo@bar.com',
});
el = vm.$el;
});
it('see email', () => {
expect(vm.$refs['service-desk-incoming-email'].textContent.trim()).toEqual('foo@bar.com');
expect(el.querySelector('.fa-spinner')).toBeNull();
expect(el.querySelector('.fa-exclamation-circle')).toBeNull();
});
});
describe('with fetchError', () => {
beforeEach(() => {
vm = createComponent({
isEnabled: true,
fetchError: new Error('some-fake-failure'),
});
el = vm.$el;
});
it('see error message', () => {
expect(el.querySelector('.fa-exclamation-circle')).toBeDefined();
expect(el.querySelector('.panel-body').textContent.trim()).toEqual('An error occurred while fetching the incoming email');
expect(el.querySelector('.fa-spinner')).toBeNull();
expect(vm.$refs['service-desk-incoming-email']).toBeUndefined();
});
});
});
describe('when isEnabled=false', () => {
let el;
beforeEach(() => {
vm = createComponent({
isEnabled: false,
});
el = vm.$el;
});
it('should not see panel', () => {
expect(el.querySelector('.panel')).toBeNull();
});
it('should not see warning message', () => {
expect(el.querySelector('.settings-message')).toBeNull();
});
});
describe('methods', () => {
describe('onCheckboxToggle', () => {
let onCheckboxToggleSpy;
beforeEach(() => {
onCheckboxToggleSpy = jasmine.createSpy('spy');
eventHub.$on('serviceDeskEnabledCheckboxToggled', onCheckboxToggleSpy);
vm = createComponent({
isEnabled: false,
});
});
afterEach(() => {
eventHub.$off('serviceDeskEnabledCheckboxToggled', onCheckboxToggleSpy);
});
it('when getting checked', () => {
expect(onCheckboxToggleSpy).not.toHaveBeenCalled();
vm.onCheckboxToggle({
target: {
checked: true,
},
});
expect(onCheckboxToggleSpy).toHaveBeenCalledWith(true);
});
it('when getting unchecked', () => {
expect(onCheckboxToggleSpy).not.toHaveBeenCalled();
vm.onCheckboxToggle({
target: {
checked: false,
},
});
expect(onCheckboxToggleSpy).toHaveBeenCalledWith(false);
});
});
});
});
import ServiceDeskService from '~/projects/settings_service_desk/services/service_desk_service';
describe('ServiceDeskService', () => {
let service;
beforeEach(() => {
service = new ServiceDeskService('');
});
it('fetchIncomingEmail', (done) => {
spyOn(service.serviceDeskResource, 'get').and.returnValue(Promise.resolve({
data: {
service_desk_enabled: true,
service_desk_address: 'foo@bar.com',
},
}));
service.fetchIncomingEmail()
.then((incomingEmail) => {
expect(incomingEmail).toEqual('foo@bar.com');
done();
})
.catch((err) => {
done.fail(`Failed to fetch incoming email:\n${err}`);
});
});
describe('toggleServiceDesk', () => {
it('enable service desk', (done) => {
spyOn(service.serviceDeskResource, 'update').and.returnValue(Promise.resolve({
data: {
service_desk_enabled: true,
service_desk_address: 'foo@bar.com',
},
}));
service.toggleServiceDesk(true)
.then((incomingEmail) => {
expect(incomingEmail).toEqual('foo@bar.com');
done();
})
.catch((err) => {
done.fail(`Failed to enable service desk and fetch incoming email:\n${err}`);
});
});
it('disable service desk', (done) => {
spyOn(service.serviceDeskResource, 'update').and.returnValue(Promise.resolve({
data: {
service_desk_enabled: false,
service_desk_address: null,
},
}));
service.toggleServiceDesk(false)
.then((incomingEmail) => {
expect(incomingEmail).toEqual(null);
done();
})
.catch((err) => {
done.fail(`Failed to disable service desk and reset incoming email:\n${err}`);
});
});
});
});
import ServiceDeskStore from '~/projects/settings_service_desk/stores/service_desk_store';
describe('ServiceDeskStore', () => {
let store;
beforeEach(() => {
store = new ServiceDeskStore();
});
describe('setIsActivated', () => {
it('defaults to false', () => {
expect(store.state.isEnabled).toEqual(false);
});
it('set true', () => {
store.setIsActivated(true);
expect(store.state.isEnabled).toEqual(true);
});
it('set false', () => {
store.setIsActivated(false);
expect(store.state.isEnabled).toEqual(false);
});
});
describe('setIncomingEmail', () => {
it('defaults to an empty string', () => {
expect(store.state.incomingEmail).toEqual('');
});
it('set true', () => {
const email = 'foo@bar.com';
store.setIncomingEmail(email);
expect(store.state.incomingEmail).toEqual(email);
});
});
describe('setFetchError', () => {
it('defaults to null', () => {
expect(store.state.fetchError).toEqual(null);
});
it('set true', () => {
const err = new Error('some-fake-failure');
store.setFetchError(err);
expect(store.state.fetchError).toEqual(err);
});
});
});
/*!
* clipboard.js v1.4.2
* https://zenorocha.github.io/clipboard.js
*
* Licensed MIT © Zeno Rocha
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/**
* Module dependencies.
*/
var closest = require('closest')
, event = require('component-event');
/**
* Delegate event `type` to `selector`
* and invoke `fn(e)`. A callback function
* is returned which may be passed to `.unbind()`.
*
* @param {Element} el
* @param {String} selector
* @param {String} type
* @param {Function} fn
* @param {Boolean} capture
* @return {Function}
* @api public
*/
// Some events don't bubble, so we want to bind to the capture phase instead
// when delegating.
var forceCaptureEvents = ['focus', 'blur'];
exports.bind = function(el, selector, type, fn, capture){
if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
return event.bind(el, type, function(e){
var target = e.target || e.srcElement;
e.delegateTarget = closest(target, selector, true, el);
if (e.delegateTarget) fn.call(el, e);
}, capture);
};
/**
* Unbind event `type`'s callback `fn`.
*
* @param {Element} el
* @param {String} type
* @param {Function} fn
* @param {Boolean} capture
* @api public
*/
exports.unbind = function(el, type, fn, capture){
if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
event.unbind(el, type, fn, capture);
};
},{"closest":2,"component-event":4}],2:[function(require,module,exports){
var matches = require('matches-selector')
module.exports = function (element, selector, checkYoSelf) {
var parent = checkYoSelf ? element : element.parentNode
while (parent && parent !== document) {
if (matches(parent, selector)) return parent;
parent = parent.parentNode
}
}
},{"matches-selector":3}],3:[function(require,module,exports){
/**
* Element prototype.
*/
var proto = Element.prototype;
/**
* Vendor function.
*/
var vendor = proto.matchesSelector
|| proto.webkitMatchesSelector
|| proto.mozMatchesSelector
|| proto.msMatchesSelector
|| proto.oMatchesSelector;
/**
* Expose `match()`.
*/
module.exports = match;
/**
* Match `el` to `selector`.
*
* @param {Element} el
* @param {String} selector
* @return {Boolean}
* @api public
*/
function match(el, selector) {
if (vendor) return vendor.call(el, selector);
var nodes = el.parentNode.querySelectorAll(selector);
for (var i = 0; i < nodes.length; ++i) {
if (nodes[i] == el) return true;
}
return false;
}
},{}],4:[function(require,module,exports){
var bind = window.addEventListener ? 'addEventListener' : 'attachEvent',
unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent',
prefix = bind !== 'addEventListener' ? 'on' : '';
/**
* Bind `el` event `type` to `fn`.
*
* @param {Element} el
* @param {String} type
* @param {Function} fn
* @param {Boolean} capture
* @return {Function}
* @api public
*/
exports.bind = function(el, type, fn, capture){
el[bind](prefix + type, fn, capture || false);
return fn;
};
/**
* Unbind `el` event `type`'s callback `fn`.
*
* @param {Element} el
* @param {String} type
* @param {Function} fn
* @param {Boolean} capture
* @return {Function}
* @api public
*/
exports.unbind = function(el, type, fn, capture){
el[unbind](prefix + type, fn, capture || false);
return fn;
};
},{}],5:[function(require,module,exports){
function E () {
// Keep this empty so it's easier to inherit from
// (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
},
once: function (name, callback, ctx) {
var self = this;
var fn = function () {
self.off(name, fn);
callback.apply(ctx, arguments);
};
return this.on(name, fn, ctx);
},
emit: function (name) {
var data = [].slice.call(arguments, 1);
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
},
off: function (name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback) liveEvents.push(evts[i]);
}
}
// Remove event from queue to prevent memory leak
// Suggested by https://github.com/lazd
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
};
module.exports = E;
},{}],6:[function(require,module,exports){
/**
* Inner class which performs selection from either `text` or `target`
* properties and then executes copy or cut operations.
*/
'use strict';
exports.__esModule = true;
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
var ClipboardAction = (function () {
/**
* @param {Object} options
*/
function ClipboardAction(options) {
_classCallCheck(this, ClipboardAction);
this.resolveOptions(options);
this.initSelection();
}
/**
* Defines base properties passed from constructor.
* @param {Object} options
*/
ClipboardAction.prototype.resolveOptions = function resolveOptions() {
var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
this.action = options.action;
this.emitter = options.emitter;
this.target = options.target;
this.text = options.text;
this.trigger = options.trigger;
this.selectedText = '';
};
/**
* Decides which selection strategy is going to be applied based
* on the existence of `text` and `target` properties.
*/
ClipboardAction.prototype.initSelection = function initSelection() {
if (this.text && this.target) {
throw new Error('Multiple attributes declared, use either "target" or "text"');
} else if (this.text) {
this.selectFake();
} else if (this.target) {
this.selectTarget();
} else {
throw new Error('Missing required attributes, use either "target" or "text"');
}
};
/**
* Creates a fake textarea element, sets its value from `text` property,
* and makes a selection on it.
*/
ClipboardAction.prototype.selectFake = function selectFake() {
var _this = this;
this.removeFake();
this.fakeHandler = document.body.addEventListener('click', function () {
return _this.removeFake();
});
this.fakeElem = document.createElement('textarea');
this.fakeElem.style.position = 'absolute';
this.fakeElem.style.left = '-9999px';
this.fakeElem.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
this.fakeElem.setAttribute('readonly', '');
this.fakeElem.value = this.text;
this.selectedText = this.text;
document.body.appendChild(this.fakeElem);
this.fakeElem.select();
this.copyText();
};
/**
* Only removes the fake element after another click event, that way
* a user can hit `Ctrl+C` to copy because selection still exists.
*/
ClipboardAction.prototype.removeFake = function removeFake() {
if (this.fakeHandler) {
document.body.removeEventListener('click');
this.fakeHandler = null;
}
if (this.fakeElem) {
document.body.removeChild(this.fakeElem);
this.fakeElem = null;
}
};
/**
* Selects the content from element passed on `target` property.
*/
ClipboardAction.prototype.selectTarget = function selectTarget() {
if (this.target.nodeName === 'INPUT' || this.target.nodeName === 'TEXTAREA') {
this.target.select();
this.selectedText = this.target.value;
} else {
var range = document.createRange();
var selection = window.getSelection();
selection.removeAllRanges();
range.selectNodeContents(this.target);
selection.addRange(range);
this.selectedText = selection.toString();
}
this.copyText();
};
/**
* Executes the copy operation based on the current selection.
*/
ClipboardAction.prototype.copyText = function copyText() {
var succeeded = undefined;
try {
succeeded = document.execCommand(this.action);
} catch (err) {
succeeded = false;
}
this.handleResult(succeeded);
};
/**
* Fires an event based on the copy operation result.
* @param {Boolean} succeeded
*/
ClipboardAction.prototype.handleResult = function handleResult(succeeded) {
if (succeeded) {
this.emitter.emit('success', {
action: this.action,
text: this.selectedText,
trigger: this.trigger,
clearSelection: this.clearSelection.bind(this)
});
} else {
this.emitter.emit('error', {
action: this.action,
trigger: this.trigger,
clearSelection: this.clearSelection.bind(this)
});
}
};
/**
* Removes current selection and focus from `target` element.
*/
ClipboardAction.prototype.clearSelection = function clearSelection() {
if (this.target) {
this.target.blur();
}
window.getSelection().removeAllRanges();
};
/**
* Sets the `action` to be performed which can be either 'copy' or 'cut'.
* @param {String} action
*/
/**
* Destroy lifecycle.
*/
ClipboardAction.prototype.destroy = function destroy() {
this.removeFake();
};
_createClass(ClipboardAction, [{
key: 'action',
set: function set() {
var action = arguments.length <= 0 || arguments[0] === undefined ? 'copy' : arguments[0];
this._action = action;
if (this._action !== 'copy' && this._action !== 'cut') {
throw new Error('Invalid "action" value, use either "copy" or "cut"');
}
},
/**
* Gets the `action` property.
* @return {String}
*/
get: function get() {
return this._action;
}
/**
* Sets the `target` property using an element
* that will be have its content copied.
* @param {Element} target
*/
}, {
key: 'target',
set: function set(target) {
if (target !== undefined) {
if (target && typeof target === 'object' && target.nodeType === 1) {
this._target = target;
} else {
throw new Error('Invalid "target" value, use a valid Element');
}
}
},
/**
* Gets the `target` property.
* @return {String|HTMLElement}
*/
get: function get() {
return this._target;
}
}]);
return ClipboardAction;
})();
exports['default'] = ClipboardAction;
module.exports = exports['default'];
},{}],7:[function(require,module,exports){
'use strict';
exports.__esModule = true;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var _clipboardAction = require('./clipboard-action');
var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
var _delegateEvents = require('delegate-events');
var _delegateEvents2 = _interopRequireDefault(_delegateEvents);
var _tinyEmitter = require('tiny-emitter');
var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
/**
* Base class which takes a selector, delegates a click event to it,
* and instantiates a new `ClipboardAction` on each click.
*/
var Clipboard = (function (_Emitter) {
_inherits(Clipboard, _Emitter);
/**
* @param {String} selector
* @param {Object} options
*/
function Clipboard(selector, options) {
_classCallCheck(this, Clipboard);
_Emitter.call(this);
this.resolveOptions(options);
this.delegateClick(selector);
}
/**
* Helper function to retrieve attribute value.
* @param {String} suffix
* @param {Element} element
*/
/**
* Defines if attributes would be resolved using internal setter functions
* or custom functions that were passed in the constructor.
* @param {Object} options
*/
Clipboard.prototype.resolveOptions = function resolveOptions() {
var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
this.text = typeof options.text === 'function' ? options.text : this.defaultText;
};
/**
* Delegates a click event on the passed selector.
* @param {String} selector
*/
Clipboard.prototype.delegateClick = function delegateClick(selector) {
var _this = this;
this.binding = _delegateEvents2['default'].bind(document.body, selector, 'click', function (e) {
return _this.onClick(e);
});
};
/**
* Undelegates a click event on body.
* @param {String} selector
*/
Clipboard.prototype.undelegateClick = function undelegateClick() {
_delegateEvents2['default'].unbind(document.body, 'click', this.binding);
};
/**
* Defines a new `ClipboardAction` on each click event.
* @param {Event} e
*/
Clipboard.prototype.onClick = function onClick(e) {
if (this.clipboardAction) {
this.clipboardAction = null;
}
this.clipboardAction = new _clipboardAction2['default']({
action: this.action(e.delegateTarget),
target: this.target(e.delegateTarget),
text: this.text(e.delegateTarget),
trigger: e.delegateTarget,
emitter: this
});
};
/**
* Default `action` lookup function.
* @param {Element} trigger
*/
Clipboard.prototype.defaultAction = function defaultAction(trigger) {
return getAttributeValue('action', trigger);
};
/**
* Default `target` lookup function.
* @param {Element} trigger
*/
Clipboard.prototype.defaultTarget = function defaultTarget(trigger) {
var selector = getAttributeValue('target', trigger);
if (selector) {
return document.querySelector(selector);
}
};
/**
* Default `text` lookup function.
* @param {Element} trigger
*/
Clipboard.prototype.defaultText = function defaultText(trigger) {
return getAttributeValue('text', trigger);
};
/**
* Destroy lifecycle.
*/
Clipboard.prototype.destroy = function destroy() {
this.undelegateClick();
if (this.clipboardAction) {
this.clipboardAction.destroy();
this.clipboardAction = null;
}
};
return Clipboard;
})(_tinyEmitter2['default']);
function getAttributeValue(suffix, element) {
var attribute = 'data-clipboard-' + suffix;
if (!element.hasAttribute(attribute)) {
return;
}
return element.getAttribute(attribute);
}
exports['default'] = Clipboard;
module.exports = exports['default'];
},{"./clipboard-action":6,"delegate-events":1,"tiny-emitter":5}]},{},[7])(7)
});
......@@ -1074,6 +1074,14 @@ cli-width@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a"
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"
......@@ -1383,6 +1391,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"
......@@ -2180,6 +2192,12 @@ globby@^5.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
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"
graceful-fs@^4.1.2, 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"
......@@ -3923,6 +3941,10 @@ select2@3.5.2-browserify:
version "3.5.2-browserify"
resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.2-browserify.tgz#dc4dafda38d67a734e8a97a46f0d3529ae05391d"
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.3.0, semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
......@@ -4329,6 +4351,10 @@ timers-browserify@^2.0.2:
dependencies:
setimmediate "^1.0.4"
tiny-emitter@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb"
tmp@0.0.28, tmp@0.0.x:
version "0.0.28"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment