diff --git a/client/components/main/editor.jade b/client/components/main/editor.jade
index dbd617154..bbf65e45e 100644
--- a/client/components/main/editor.jade
+++ b/client/components/main/editor.jade
@@ -1,4 +1,6 @@
template(name="editor")
+ span.fa.fa-copy
+ 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..2992c2938 100644
--- a/client/components/main/editor.js
+++ b/client/components/main/editor.js
@@ -4,283 +4,305 @@ 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 span.fa.fa-copy'(event) {
+ const $editor = this.$('textarea.editor');
+ const promise = Utils.copyTextToClipboard($editor[0].value);
+ if (promise) {
+ promise.then(() => {
+ const $tooltip = this.$('.copied-tooltip');
+ $tooltip.show(100);
+ setTimeout(() => $tooltip.hide(100), 1000);
+ }, (err) => {
+ console.error("error: ", err);
+ });
+ }
+ },
+ }
+ ]
}
-});
+}).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..c5e225dda
--- /dev/null
+++ b/client/components/main/editor.styl
@@ -0,0 +1,7 @@
+.new-comment,
+.inlined-form
+ span.fa.fa-copy
+ float: right
+ position: relative
+ top: 20px
+ right: 6px