diff --git a/client/components/activities/comments.styl b/client/components/activities/comments.styl
index ccf24b726..d7492b767 100644
--- a/client/components/activities/comments.styl
+++ b/client/components/activities/comments.styl
@@ -31,7 +31,6 @@
background-color: #fff
border: 0
box-shadow: 0 1px 2px rgba(0, 0, 0, .23)
- color: #8c8c8c
height: 36px
margin: 4px 4px 6px 0
padding: 9px 11px
diff --git a/client/components/cards/cardDetails.jade b/client/components/cards/cardDetails.jade
index 659f70918..cf049625c 100644
--- a/client/components/cards/cardDetails.jade
+++ b/client/components/cards/cardDetails.jade
@@ -22,6 +22,7 @@ template(name="cardDetails")
title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}"
)
+ span.copied-tooltip {{_ 'copied'}}
else
unless isPopup
a.fa.fa-times-thin.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
@@ -33,6 +34,7 @@ template(name="cardDetails")
title="{{_ 'copy-card-link-to-clipboard'}}"
href="{{ originRelativeUrl }}"
)
+ span.copied-tooltip {{_ 'copied'}}
h2.card-details-title.js-card-title(
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
+viewer
@@ -798,6 +800,7 @@ template(name="cardMorePopup")
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
input.inline-input(type="text" id="cardURL" readonly value="{{ originRelativeUrl }}" autofocus="autofocus")
button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
+ .copied-tooltip {{_ 'copied'}}
span.clearfix
br
h2 {{_ 'change-card-parent'}}
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index b967ca087..adf5a9a5f 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -325,7 +325,10 @@ BlazeComponent.extendComponent({
},
'click .js-copy-link'(event) {
event.preventDefault();
- Utils.copyTextToClipboard(event.target.href);
+ const promise = Utils.copyTextToClipboard(event.target.href);
+
+ const $tooltip = this.$('.card-details-header .copied-tooltip');
+ Utils.showCopied(promise, $tooltip);
},
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
'submit .js-card-description'(event) {
@@ -1068,7 +1071,10 @@ BlazeComponent.extendComponent({
return [
{
'click .js-copy-card-link-to-clipboard'(event) {
- Utils.copyTextToClipboard(location.origin + document.getElementById('cardURL').value);
+ const promise = Utils.copyTextToClipboard(location.origin + document.getElementById('cardURL').value);
+
+ const $tooltip = this.$('.copied-tooltip');
+ Utils.showCopied(promise, $tooltip);
},
'click .js-delete': Popup.afterConfirm('cardDelete', function () {
Popup.close();
diff --git a/client/components/cards/cardDetails.styl b/client/components/cards/cardDetails.styl
index 202600e2e..ac007b169 100644
--- a/client/components/cards/cardDetails.styl
+++ b/client/components/cards/cardDetails.styl
@@ -76,6 +76,12 @@ avatar-radius = 50%
box-shadow: 0 0 0 2px darken(white, 60%) inset
// Other card details
+.copied-tooltip
+ display: none
+ padding: 0px 10px;
+ background-color: #000000df;
+ color: #fff;
+ border-radius: 5px;
.card-details
padding: 0
@@ -118,7 +124,8 @@ avatar-radius = 50%
.card-copy-button,
.card-copy-mobile-button,
.close-card-details-mobile-web,
- .card-details-menu-mobile-web
+ .card-details-menu-mobile-web,
+ .copied-tooltip
float: right
.close-card-details,
@@ -187,6 +194,14 @@ avatar-radius = 50%
border-radius: 3px
padding: 0px 5px
+ .copied-tooltip
+ display: none
+ margin-right: 10px
+ padding: 10px;
+ background-color: #000000df;
+ color: #fff;
+ border-radius: 5px;
+
.card-description textarea
min-height: 100px
diff --git a/client/components/cards/checklists.jade b/client/components/cards/checklists.jade
index 06f419282..b27a771db 100644
--- a/client/components/cards/checklists.jade
+++ b/client/components/cards/checklists.jade
@@ -63,12 +63,16 @@ template(name="checklistDeleteDialog")
button.toggle-delete-checklist-dialog(type="button") {{_ 'cancel'}}
template(name="addChecklistItemForm")
+ a.fa.fa-copy(title="copy text to clipboard")
+ span.copied-tooltip {{_ 'copied'}}
textarea.js-add-checklist-item(rows='1' autofocus)
.edit-controls.clearfix
button.primary.confirm.js-submit-add-checklist-item-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
template(name="editChecklistItemForm")
+ a.fa.fa-copy(title="copy text to clipboard")
+ span.copied-tooltip {{_ 'copied'}}
textarea.js-edit-checklist-item(rows='1' autofocus dir="auto")
if $eq type 'item'
= item.title
diff --git a/client/components/cards/checklists.js b/client/components/cards/checklists.js
index feec56d37..0d9521635 100644
--- a/client/components/cards/checklists.js
+++ b/client/components/cards/checklists.js
@@ -279,13 +279,59 @@ Template.checklists.helpers({
},
});
-Template.addChecklistItemForm.onRendered(() => {
- autosize($('textarea.js-add-checklist-item'));
-});
+BlazeComponent.extendComponent({
+ onRendered() {
+ autosize(this.$('textarea.js-add-checklist-item'));
+ },
+ canModifyCard() {
+ return (
+ Meteor.user() &&
+ Meteor.user().isBoardMember() &&
+ !Meteor.user().isCommentOnly() &&
+ !Meteor.user().isWorker()
+ );
+ },
+ events() {
+ return [
+ {
+ 'click a.fa.fa-copy'(event) {
+ const $editor = this.$('textarea');
+ const promise = Utils.copyTextToClipboard($editor[0].value);
-Template.editChecklistItemForm.onRendered(() => {
- autosize($('textarea.js-edit-checklist-item'));
-});
+ const $tooltip = this.$('.copied-tooltip');
+ Utils.showCopied(promise, $tooltip);
+ },
+ }
+ ];
+ }
+}).register('addChecklistItemForm');
+
+BlazeComponent.extendComponent({
+ onRendered() {
+ autosize(this.$('textarea.js-edit-checklist-item'));
+ },
+ canModifyCard() {
+ return (
+ Meteor.user() &&
+ Meteor.user().isBoardMember() &&
+ !Meteor.user().isCommentOnly() &&
+ !Meteor.user().isWorker()
+ );
+ },
+ events() {
+ return [
+ {
+ 'click a.fa.fa-copy'(event) {
+ const $editor = this.$('textarea');
+ const promise = Utils.copyTextToClipboard($editor[0].value);
+
+ const $tooltip = this.$('.copied-tooltip');
+ Utils.showCopied(promise, $tooltip);
+ },
+ }
+ ];
+ }
+}).register('editChecklistItemForm');
Template.checklistDeleteDialog.onCreated(() => {
const $cardDetails = this.$('.card-details');
diff --git a/client/components/main/editor.jade b/client/components/main/editor.jade
index dbd617154..6e7702ff3 100644
--- a/client/components/main/editor.jade
+++ b/client/components/main/editor.jade
@@ -1,4 +1,6 @@
template(name="editor")
+ a.fa.fa-copy(title="copy text to clipboard")
+ span.copied-tooltip {{_ 'copied'}}
textarea.editor(
dir="auto"
class="{{class}}"
diff --git a/client/components/main/editor.js b/client/components/main/editor.js
index 34ada0e24..fec040a23 100644
--- a/client/components/main/editor.js
+++ b/client/components/main/editor.js
@@ -4,283 +4,299 @@ const specialHandles = [
];
const specialHandleNames = specialHandles.map(m => m.username);
-Template.editor.onRendered(() => {
- const textareaSelector = 'textarea';
- const mentions = [
- // User mentions
- {
- match: /\B@([\w.]*)$/,
- search(term, callback) {
- const currentBoard = Boards.findOne(Session.get('currentBoard'));
- callback(
- _.union(
- currentBoard
- .activeMembers()
- .map(member => {
- const user = Users.findOne(member.userId);
- const username = user.username;
- const fullName = user.profile && user.profile !== undefined ? user.profile.fullname : "";
- return username.includes(term) || fullName.includes(term) ? fullName + "(" + username + ")" : null;
- })
- .filter(Boolean), [...specialHandleNames])
- );
- },
- template(value) {
- return value;
- },
- replace(username) {
- return `@${username} `;
- },
- index: 1,
- },
- ];
- const enableTextarea = function() {
- const $textarea = this.$(textareaSelector);
- autosize($textarea);
- $textarea.escapeableTextComplete(mentions);
- };
- if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR !== false) {
- const isSmall = Utils.isMiniScreen();
- const toolbar = isSmall
- ? [
- ['view', ['fullscreen']],
- ['table', ['table']],
- ['font', ['bold', 'underline']],
- //['fontsize', ['fontsize']],
- ['color', ['color']],
- ]
- : [
- ['style', ['style']],
- ['font', ['bold', 'underline', 'clear']],
- ['fontsize', ['fontsize']],
- ['fontname', ['fontname']],
- ['color', ['color']],
- ['para', ['ul', 'ol', 'paragraph']],
- ['table', ['table']],
- //['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
- ['insert', ['link']], //, 'picture']], // modal popup has issue somehow :(
- ['view', ['fullscreen', 'codeview', 'help']],
- ];
- const cleanPastedHTML = function(input) {
- const badTags = [
- 'style',
- 'script',
- 'applet',
- 'embed',
- 'noframes',
- 'noscript',
- 'meta',
- 'link',
- 'button',
- 'form',
- ].join('|');
- const badPatterns = new RegExp(
- `(?:${[
- `<(${badTags})s*[^>][\\s\\S]*?<\\/\\1>`,
- `<(${badTags})[^>]*?\\/>`,
- ].join('|')})`,
- 'gi',
- );
- let output = input;
- // remove bad Tags
- output = output.replace(badPatterns, '');
- // remove attributes ' style="..."'
- const badAttributes = new RegExp(
- `(?:${[
- 'on\\S+=([\'"]?).*?\\1',
- 'href=([\'"]?)javascript:.*?\\2',
- 'style=([\'"]?).*?\\3',
- 'target=\\S+',
- ].join('|')})`,
- 'gi',
- );
- output = output.replace(badAttributes, '');
- output = output.replace(/( -1) {
- return mSummernotes[idx];
- }
- return undefined;
- };
- inputs.each(function(idx, input) {
- mSummernotes[idx] = $(input).summernote({
- placeholder,
- callbacks: {
- onInit(object) {
- const originalInput = this;
- $(originalInput).on('submitted', function() {
- // when comment is submitted, the original textarea will be set to '', so shall we
- if (!this.value) {
- const sn = getSummernote(this);
- sn && sn.summernote('code', '');
- }
- });
- const jEditor = object && object.editable;
- const toolbar = object && object.toolbar;
- if (jEditor !== undefined) {
- jEditor.escapeableTextComplete(mentions);
- }
- if (toolbar !== undefined) {
- const fBtn = toolbar.find('.btn-fullscreen');
- fBtn.on('click', function() {
- const $this = $(this),
- isActive = $this.hasClass('active');
- $('.minicards,#header-quick-access').toggle(!isActive); // mini card is still showing when editor is in fullscreen mode, we hide here manually
- });
- }
- },
- onImageUpload(files) {
- const $summernote = getSummernote(this);
- if (files && files.length > 0) {
- const image = files[0];
- const currentCard = Utils.getCurrentCard();
- const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
- const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
- const insertImage = src => {
- // process all image upload types to the description/comment window
- const img = document.createElement('img');
- img.src = src;
- img.setAttribute('width', '100%');
- $summernote.summernote('insertNode', img);
- };
- const processData = function(fileObj) {
- Utils.processUploadedAttachment(
- currentCard,
- fileObj,
- attachment => {
- if (
- attachment &&
- attachment._id &&
- attachment.isImage()
- ) {
- attachment.one('uploaded', function() {
- const maxTry = 3;
- const checkItvl = 500;
- let retry = 0;
- const checkUrl = function() {
- // even though uploaded event fired, attachment.url() is still null somehow //TODO
- const url = attachment.url();
- if (url) {
- insertImage(
- `${location.protocol}//${location.host}${url}`,
- );
- } else {
- retry++;
- if (retry < maxTry) {
- setTimeout(checkUrl, checkItvl);
+BlazeComponent.extendComponent({
+ onRendered() {
+ const textareaSelector = 'textarea';
+ const mentions = [
+ // User mentions
+ {
+ match: /\B@([\w.]*)$/,
+ search(term, callback) {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ callback(
+ _.union(
+ currentBoard
+ .activeMembers()
+ .map(member => {
+ const user = Users.findOne(member.userId);
+ const username = user.username;
+ const fullName = user.profile && user.profile !== undefined ? user.profile.fullname : "";
+ return username.includes(term) || fullName.includes(term) ? fullName + "(" + username + ")" : null;
+ })
+ .filter(Boolean), [...specialHandleNames])
+ );
+ },
+ template(value) {
+ return value;
+ },
+ replace(username) {
+ return `@${username} `;
+ },
+ index: 1,
+ },
+ ];
+ const enableTextarea = function() {
+ const $textarea = this.$(textareaSelector);
+ autosize($textarea);
+ $textarea.escapeableTextComplete(mentions);
+ };
+ if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR !== false) {
+ const isSmall = Utils.isMiniScreen();
+ const toolbar = isSmall
+ ? [
+ ['view', ['fullscreen']],
+ ['table', ['table']],
+ ['font', ['bold', 'underline']],
+ //['fontsize', ['fontsize']],
+ ['color', ['color']],
+ ]
+ : [
+ ['style', ['style']],
+ ['font', ['bold', 'underline', 'clear']],
+ ['fontsize', ['fontsize']],
+ ['fontname', ['fontname']],
+ ['color', ['color']],
+ ['para', ['ul', 'ol', 'paragraph']],
+ ['table', ['table']],
+ //['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
+ ['insert', ['link']], //, 'picture']], // modal popup has issue somehow :(
+ ['view', ['fullscreen', 'codeview', 'help']],
+ ];
+ const cleanPastedHTML = function(input) {
+ const badTags = [
+ 'style',
+ 'script',
+ 'applet',
+ 'embed',
+ 'noframes',
+ 'noscript',
+ 'meta',
+ 'link',
+ 'button',
+ 'form',
+ ].join('|');
+ const badPatterns = new RegExp(
+ `(?:${[
+ `<(${badTags})s*[^>][\\s\\S]*?<\\/\\1>`,
+ `<(${badTags})[^>]*?\\/>`,
+ ].join('|')})`,
+ 'gi',
+ );
+ let output = input;
+ // remove bad Tags
+ output = output.replace(badPatterns, '');
+ // remove attributes ' style="..."'
+ const badAttributes = new RegExp(
+ `(?:${[
+ 'on\\S+=([\'"]?).*?\\1',
+ 'href=([\'"]?)javascript:.*?\\2',
+ 'style=([\'"]?).*?\\3',
+ 'target=\\S+',
+ ].join('|')})`,
+ 'gi',
+ );
+ output = output.replace(badAttributes, '');
+ output = output.replace(/( -1) {
+ return mSummernotes[idx];
+ }
+ return undefined;
+ };
+ inputs.each(function(idx, input) {
+ mSummernotes[idx] = $(input).summernote({
+ placeholder,
+ callbacks: {
+ onInit(object) {
+ const originalInput = this;
+ $(originalInput).on('submitted', function() {
+ // when comment is submitted, the original textarea will be set to '', so shall we
+ if (!this.value) {
+ const sn = getSummernote(this);
+ sn && sn.summernote('code', '');
+ }
+ });
+ const jEditor = object && object.editable;
+ const toolbar = object && object.toolbar;
+ if (jEditor !== undefined) {
+ jEditor.escapeableTextComplete(mentions);
+ }
+ if (toolbar !== undefined) {
+ const fBtn = toolbar.find('.btn-fullscreen');
+ fBtn.on('click', function() {
+ const $this = $(this),
+ isActive = $this.hasClass('active');
+ $('.minicards,#header-quick-access').toggle(!isActive); // mini card is still showing when editor is in fullscreen mode, we hide here manually
+ });
+ }
+ },
+
+ onImageUpload(files) {
+ const $summernote = getSummernote(this);
+ if (files && files.length > 0) {
+ const image = files[0];
+ const currentCard = Utils.getCurrentCard();
+ const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
+ const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
+ const insertImage = src => {
+ // process all image upload types to the description/comment window
+ const img = document.createElement('img');
+ img.src = src;
+ img.setAttribute('width', '100%');
+ $summernote.summernote('insertNode', img);
+ };
+ const processData = function(fileObj) {
+ Utils.processUploadedAttachment(
+ currentCard,
+ fileObj,
+ attachment => {
+ if (
+ attachment &&
+ attachment._id &&
+ attachment.isImage()
+ ) {
+ attachment.one('uploaded', function() {
+ const maxTry = 3;
+ const checkItvl = 500;
+ let retry = 0;
+ const checkUrl = function() {
+ // even though uploaded event fired, attachment.url() is still null somehow //TODO
+ const url = attachment.url();
+ if (url) {
+ insertImage(
+ `${location.protocol}//${location.host}${url}`,
+ );
+ } else {
+ retry++;
+ if (retry < maxTry) {
+ setTimeout(checkUrl, checkItvl);
+ }
}
+ };
+ checkUrl();
+ });
+ }
+ },
+ );
+ };
+ if (MAX_IMAGE_PIXEL) {
+ const reader = new FileReader();
+ reader.onload = function(e) {
+ const dataurl = e && e.target && e.target.result;
+ if (dataurl !== undefined) {
+ // need to shrink image
+ Utils.shrinkImage({
+ dataurl,
+ maxSize: MAX_IMAGE_PIXEL,
+ ratio: COMPRESS_RATIO,
+ toBlob: true,
+ callback(blob) {
+ if (blob !== false) {
+ blob.name = image.name;
+ processData(blob);
}
- };
- checkUrl();
+ },
});
}
- },
- );
- };
- if (MAX_IMAGE_PIXEL) {
- const reader = new FileReader();
- reader.onload = function(e) {
- const dataurl = e && e.target && e.target.result;
- if (dataurl !== undefined) {
- // need to shrink image
- Utils.shrinkImage({
- dataurl,
- maxSize: MAX_IMAGE_PIXEL,
- ratio: COMPRESS_RATIO,
- toBlob: true,
- callback(blob) {
- if (blob !== false) {
- blob.name = image.name;
- processData(blob);
- }
- },
- });
- }
- };
- reader.readAsDataURL(image);
- } else {
- processData(image);
+ };
+ reader.readAsDataURL(image);
+ } else {
+ processData(image);
+ }
}
- }
- },
- onPaste(e) {
- var clipboardData = e.clipboardData;
- var pastedData = clipboardData.getData('Text');
+ },
+ onPaste(e) {
+ var clipboardData = e.clipboardData;
+ var pastedData = clipboardData.getData('Text');
- //if pasted data is an image, exit
- if (!pastedData.length) {
- e.preventDefault();
- return;
- }
+ //if pasted data is an image, exit
+ if (!pastedData.length) {
+ e.preventDefault();
+ return;
+ }
- // clear up unwanted tag info when user pasted in text
- const thisNote = this;
- const updatePastedText = function(object) {
- const someNote = getSummernote(object);
- // Fix Pasting text into a card is adding a line before and after
- // (and multiplies by pasting more) by changing paste "p" to "br".
- // Fixes https://github.com/wekan/wekan/2890 .
- // == Fix Start ==
- someNote.execCommand('defaultParagraphSeparator', false, 'br');
- // == Fix End ==
- const original = someNote.summernote('code');
- const cleaned = cleanPastedHTML(original); //this is where to call whatever clean function you want. I have mine in a different file, called CleanPastedHTML.
- someNote.summernote('code', ''); //clear original
- someNote.summernote('pasteHTML', cleaned); //this sets the displayed content editor to the cleaned pasted code.
- };
- setTimeout(function() {
- //this kinda sucks, but if you don't do a setTimeout,
- //the function is called before the text is really pasted.
- updatePastedText(thisNote);
- }, 10);
+ // clear up unwanted tag info when user pasted in text
+ const thisNote = this;
+ const updatePastedText = function(object) {
+ const someNote = getSummernote(object);
+ // Fix Pasting text into a card is adding a line before and after
+ // (and multiplies by pasting more) by changing paste "p" to "br".
+ // Fixes https://github.com/wekan/wekan/2890 .
+ // == Fix Start ==
+ someNote.execCommand('defaultParagraphSeparator', false, 'br');
+ // == Fix End ==
+ const original = someNote.summernote('code');
+ const cleaned = cleanPastedHTML(original); //this is where to call whatever clean function you want. I have mine in a different file, called CleanPastedHTML.
+ someNote.summernote('code', ''); //clear original
+ someNote.summernote('pasteHTML', cleaned); //this sets the displayed content editor to the cleaned pasted code.
+ };
+ setTimeout(function() {
+ //this kinda sucks, but if you don't do a setTimeout,
+ //the function is called before the text is really pasted.
+ updatePastedText(thisNote);
+ }, 10);
+ },
},
- },
- dialogsInBody: true,
- spellCheck: true,
- disableGrammar: false,
- disableDragAndDrop: false,
- toolbar,
- popover: {
- image: [
- ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
- ['float', ['floatLeft', 'floatRight', 'floatNone']],
- ['remove', ['removeMedia']],
- ],
- link: [['link', ['linkDialogShow', 'unlink']]],
- table: [
- ['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
- ['delete', ['deleteRow', 'deleteCol', 'deleteTable']],
- ],
- air: [
- ['color', ['color']],
- ['font', ['bold', 'underline', 'clear']],
- ],
- },
- height: 200,
+ dialogsInBody: true,
+ spellCheck: true,
+ disableGrammar: false,
+ disableDragAndDrop: false,
+ toolbar,
+ popover: {
+ image: [
+ ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
+ ['float', ['floatLeft', 'floatRight', 'floatNone']],
+ ['remove', ['removeMedia']],
+ ],
+ link: [['link', ['linkDialogShow', 'unlink']]],
+ table: [
+ ['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
+ ['delete', ['deleteRow', 'deleteCol', 'deleteTable']],
+ ],
+ air: [
+ ['color', ['color']],
+ ['font', ['bold', 'underline', 'clear']],
+ ],
+ },
+ height: 200,
+ });
});
- });
+ }
+ } else {
+ enableTextarea();
}
- } else {
- enableTextarea();
+ },
+ events() {
+ return [
+ {
+ 'click a.fa.fa-copy'(event) {
+ const $editor = this.$('textarea.editor');
+ const promise = Utils.copyTextToClipboard($editor[0].value);
+
+ const $tooltip = this.$('.copied-tooltip');
+ Utils.showCopied(promise, $tooltip);
+ },
+ }
+ ]
}
-});
+}).register('editor');
import DOMPurify from 'dompurify';
diff --git a/client/components/main/editor.styl b/client/components/main/editor.styl
new file mode 100644
index 000000000..07e1c627f
--- /dev/null
+++ b/client/components/main/editor.styl
@@ -0,0 +1,7 @@
+.new-comment,
+.inlined-form
+ a.fa.fa-copy
+ float: right
+ position: relative
+ top: 20px
+ right: 6px
diff --git a/client/lib/utils.js b/client/lib/utils.js
index ebc76f22f..eb53a6a4c 100644
--- a/client/lib/utils.js
+++ b/client/lib/utils.js
@@ -481,6 +481,9 @@ Utils = {
try {
document.execCommand('copy');
+ return Promise.resolve(true);
+ } catch (e) {
+ return Promise.reject(false);
} finally {
document.body.removeChild(textArea);
}
@@ -489,15 +492,33 @@ Utils = {
/** copy the text to the clipboard
* @see https://stackoverflow.com/questions/400212/how-do-i-copy-to-the-clipboard-in-javascript/30810322#30810322
* @param string copy this text to the clipboard
+ * @return Promise
*/
copyTextToClipboard(text) {
+ let ret;
if (navigator.clipboard) {
- navigator.clipboard.writeText(text).then(function() {
+ ret = navigator.clipboard.writeText(text).then(function() {
}, function(err) {
console.error('Async: Could not copy text: ', err);
});
} else {
- fallbackCopyTextToClipboard(text);
+ ret = Utils.fallbackCopyTextToClipboard(text);
+ }
+ return ret;
+ },
+
+ /** show the "copied!" message
+ * @param promise the promise of Utils.copyTextToClipboard
+ * @param $tooltip jQuery tooltip element
+ */
+ showCopied(promise, $tooltip) {
+ if (promise) {
+ promise.then(() => {
+ $tooltip.show(100);
+ setTimeout(() => $tooltip.hide(100), 1000);
+ }, (err) => {
+ console.error("error: ", err);
+ });
}
},
};
diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json
index 8e6170397..24a6a1b1f 100644
--- a/i18n/en.i18n.json
+++ b/i18n/en.i18n.json
@@ -1122,5 +1122,6 @@
"to-create-organizations-contact-admin": "To create organizations, please contact administrator.",
"custom-legal-notice-link-url": "Custom legal notice page URL",
"acceptance_of_our_legalNotice": "By continuing, you accept our",
- "legalNotice": "legal notice"
+ "legalNotice": "legal notice",
+ "copied": "copied!"
}