From 7db1445d09c5998e0fda450265a322fd43e76780 Mon Sep 17 00:00:00 2001 From: Martin Filser Date: Fri, 19 Nov 2021 00:29:56 +0100 Subject: [PATCH] Added copy button to all editor's --- client/components/main/editor.jade | 2 + client/components/main/editor.js | 552 +++++++++++++++-------------- client/components/main/editor.styl | 7 + 3 files changed, 296 insertions(+), 265 deletions(-) create mode 100644 client/components/main/editor.styl 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