Browse Source

Added copy button to all editor's

Martin Filser 3 years ago
parent
commit
7db1445d09
3 changed files with 292 additions and 261 deletions
  1. 2 0
      client/components/main/editor.jade
  2. 283 261
      client/components/main/editor.js
  3. 7 0
      client/components/main/editor.styl

+ 2 - 0
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}}"

+ 283 - 261
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} `;
+
+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,
       },
-      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(/(<a )/gi, '$1target=_ '); // always to new target
-      return output;
+    ];
+    const enableTextarea = function() {
+      const $textarea = this.$(textareaSelector);
+      autosize($textarea);
+      $textarea.escapeableTextComplete(mentions);
     };
-    const editor = '.editor';
-    const selectors = [
-      `.js-new-description-form ${editor}`,
-      `.js-new-comment-form ${editor}`,
-      `.js-edit-comment ${editor}`,
-    ].join(','); // only new comment and edit comment
-    const inputs = $(selectors);
-    if (inputs.length === 0) {
-      // only enable richereditor to new comment or edit comment no others
-      enableTextarea();
-    } else {
-      const placeholder = inputs.attr('placeholder') || '';
-      const mSummernotes = [];
-      const getSummernote = function(input) {
-        const idx = inputs.index(input);
-        if (idx > -1) {
-          return mSummernotes[idx];
-        }
-        return undefined;
+    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(/(<a )/gi, '$1target=_ '); // always to new target
+        return output;
       };
-      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
+      const editor = '.editor';
+      const selectors = [
+        `.js-new-description-form ${editor}`,
+        `.js-new-comment-form ${editor}`,
+        `.js-edit-comment ${editor}`,
+      ].join(','); // only new comment and edit comment
+      const inputs = $(selectors);
+      if (inputs.length === 0) {
+        // only enable richereditor to new comment or edit comment no others
+        enableTextarea();
+      } else {
+        const placeholder = inputs.attr('placeholder') || '';
+        const mSummernotes = [];
+        const getSummernote = function(input) {
+          const idx = inputs.index(input);
+          if (idx > -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);
+              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']],
+              ],
             },
-          },
-          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,
+            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';
 

+ 7 - 0
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