diff options
Diffstat (limited to 'client/components')
-rw-r--r-- | client/components/activities/comments.js | 3 | ||||
-rw-r--r-- | client/components/boards/boardsList.jade | 28 | ||||
-rw-r--r-- | client/components/boards/boardsList.js | 4 | ||||
-rw-r--r-- | client/components/cards/attachments.js | 82 | ||||
-rw-r--r-- | client/components/cards/minicard.jade | 9 | ||||
-rw-r--r-- | client/components/cards/minicard.js | 11 | ||||
-rwxr-xr-x | client/components/main/editor.js | 218 | ||||
-rw-r--r-- | client/components/main/layouts.styl | 1 | ||||
-rw-r--r-- | client/components/settings/peopleBody.jade | 4 | ||||
-rw-r--r-- | client/components/sidebar/sidebar.jade | 5 | ||||
-rw-r--r-- | client/components/sidebar/sidebar.js | 9 |
11 files changed, 232 insertions, 142 deletions
diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js index 8289b628..95084646 100644 --- a/client/components/activities/comments.js +++ b/client/components/activities/comments.js @@ -38,6 +38,7 @@ BlazeComponent.extendComponent({ resetCommentInput(input); Tracker.flush(); autosize.update(input); + input.trigger('submitted'); } evt.preventDefault(); }, @@ -54,7 +55,7 @@ BlazeComponent.extendComponent({ // XXX This should be a static method of the `commentForm` component function resetCommentInput(input) { - input.val('').trigger('input'); // without manually trigger, input event won't be fired + input.val(''); // without manually trigger, input event won't be fired input.blur(); commentFormIsOpen.set(false); } diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade index 85f47963..79bae502 100644 --- a/client/components/boards/boardsList.jade +++ b/client/components/boards/boardsList.jade @@ -31,12 +31,28 @@ template(name="boardList") i.fa.js-has-spenttime-cards( class="fa-circle{{#if hasOvertimeCards}} has-overtime-card-active{{else}} no-overtime-card-active{{/if}}" title="{{#if hasOvertimeCards}}{{_ 'has-overtime-cards'}}{{else}}{{_ 'has-spenttime-cards'}}{{/if}}") - i.fa.js-clone-board( - class="fa-clone" - title="{{_ 'duplicate-board'}}") - i.fa.js-archive-board( - class="fa-archive" - title="{{_ 'archive-board'}}") + unless isMiniScreen + if isSandstorm + i.fa.js-clone-board( + class="fa-clone" + title="{{_ 'duplicate-board'}}") + i.fa.js-archive-board( + class="fa-archive" + title="{{_ 'archive-board'}}") + else if currentUser.isBoardAdmin + i.fa.js-clone-board( + class="fa-clone" + title="{{_ 'duplicate-board'}}") + i.fa.js-archive-board( + class="fa-archive" + title="{{_ 'archive-board'}}") + else if currentUser.isAdmin + i.fa.js-clone-board( + class="fa-clone" + title="{{_ 'duplicate-board'}}") + i.fa.js-archive-board( + class="fa-archive" + title="{{_ 'archive-board'}}") template(name="boardListHeaderBar") h1 {{_ 'my-boards'}} diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js index b1371747..3918af82 100644 --- a/client/components/boards/boardsList.js +++ b/client/components/boards/boardsList.js @@ -8,10 +8,10 @@ Template.boardListHeaderBar.events({ Template.boardListHeaderBar.helpers({ templatesBoardId() { - return Meteor.user().getTemplatesBoardId(); + return Meteor.user() && Meteor.user().getTemplatesBoardId(); }, templatesBoardSlug() { - return Meteor.user().getTemplatesBoardSlug(); + return Meteor.user() && Meteor.user().getTemplatesBoardSlug(); }, }); diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js index f536a655..843f1eb7 100644 --- a/client/components/cards/attachments.js +++ b/client/components/cards/attachments.js @@ -55,24 +55,12 @@ Template.cardAttachmentsPopup.events({ 'change .js-attach-file'(event) { const card = this; const processFile = f => { - const file = new FS.File(f); - if (card.isLinkedCard()) { - file.boardId = Cards.findOne(card.linkedId).boardId; - file.cardId = card.linkedId; - } else { - file.boardId = card.boardId; - file.swimlaneId = card.swimlaneId; - file.listId = card.listId; - file.cardId = card._id; - } - file.userId = Meteor.userId(); - const attachment = Attachments.insert(file); - - if (attachment && attachment._id && attachment.isImage()) { - card.setCover(attachment._id); - } - - Popup.close(); + Utils.processUploadedAttachment(card, f, attachment => { + if (attachment && attachment._id && attachment.isImage()) { + card.setCover(attachment._id); + } + Popup.close(); + }); }; FS.Utility.eachFile(event, f => { @@ -86,7 +74,7 @@ Template.cardAttachmentsPopup.events({ reader.onload = function(e) { const dataurl = e && e.target && e.target.result; if (dataurl !== undefined) { - shrinkImage({ + Utils.shrinkImage({ dataurl, maxSize: MAX_IMAGE_PIXEL, ratio: COMPRESS_RATIO, @@ -118,59 +106,9 @@ Template.cardAttachmentsPopup.events({ 'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'), }); -const MAX_IMAGE_PIXEL = Meteor.settings.public.MAX_IMAGE_PIXEL; -const COMPRESS_RATIO = Meteor.settings.public.IMAGE_COMPRESS_RATIO; +const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL; +const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO; let pastedResults = null; -const shrinkImage = function(options) { - // shrink image to certain size - const dataurl = options.dataurl, - callback = options.callback, - toBlob = options.toBlob; - let canvas = document.createElement('canvas'), - image = document.createElement('img'); - const maxSize = options.maxSize || 1024; - const ratio = options.ratio || 1.0; - const next = function(result) { - image = null; - canvas = null; - if (typeof callback === 'function') { - callback(result); - } - }; - image.onload = function() { - let width = this.width, - height = this.height; - let changed = false; - if (width > height) { - if (width > maxSize) { - height *= maxSize / width; - width = maxSize; - changed = true; - } - } else if (height > maxSize) { - width *= maxSize / height; - height = maxSize; - changed = true; - } - canvas.width = width; - canvas.height = height; - canvas.getContext('2d').drawImage(this, 0, 0, width, height); - if (changed === true) { - const type = 'image/jpeg'; - if (toBlob) { - canvas.toBlob(next, type, ratio); - } else { - next(canvas.toDataURL(type, ratio)); - } - } else { - next(changed); - } - }; - image.onerror = function() { - next(false); - }; - image.src = dataurl; -}; Template.previewClipboardImagePopup.onRendered(() => { // we can paste image from clipboard @@ -182,7 +120,7 @@ Template.previewClipboardImagePopup.onRendered(() => { }; if (MAX_IMAGE_PIXEL) { // if has size limitation on image we shrink it before uploading - shrinkImage({ + Utils.shrinkImage({ dataurl: results.dataURL, maxSize: MAX_IMAGE_PIXEL, ratio: COMPRESS_RATIO, diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade index f714baae..3806ce41 100644 --- a/client/components/cards/minicard.jade +++ b/client/components/cards/minicard.jade @@ -8,9 +8,12 @@ template(name="minicard") if labels .minicard-labels each labels - span.card-label(class="card-label-{{color}}" title=name) - +viewer - = name + unless hiddenMinicardLabelText + span.card-label(class="card-label-{{color}}" title=name) + +viewer + = name + if hiddenMinicardLabelText + .minicard-label(class="card-label-{{color}}" title="{{name}}") .minicard-title .handle .fa.fa-arrows diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js index 0718c629..4c25c11d 100644 --- a/client/components/cards/minicard.js +++ b/client/components/cards/minicard.js @@ -16,6 +16,17 @@ BlazeComponent.extendComponent({ Utils.goBoardId(this.data().linkedId); }, }, + { + 'click .js-toggle-minicard-label-text'() { + Meteor.call('toggleMinicardLabelText'); + }, + }, ]; }, }).register('minicard'); + +Template.minicard.helpers({ + hiddenMinicardLabelText() { + return Meteor.user().hasHiddenMinicardLabelText(); + }, +}); diff --git a/client/components/main/editor.js b/client/components/main/editor.js index 98461c4f..248f4588 100755 --- a/client/components/main/editor.js +++ b/client/components/main/editor.js @@ -1,7 +1,77 @@ +import _sanitizeXss from 'xss'; +const sanitizeXss = (input, options) => { + const defaultAllowedIframeSrc = /^(https:){0,1}\/\/.*?(youtube|vimeo|dailymotion|youku)/i; + const allowedIframeSrcRegex = (function() { + let reg = defaultAllowedIframeSrc; + const SAFE_IFRAME_SRC_PATTERN = + Meteor.settings.public.SAFE_IFRAME_SRC_PATTERN; + try { + if (SAFE_IFRAME_SRC_PATTERN !== undefined) { + reg = new RegExp(SAFE_IFRAME_SRC_PATTERN, 'i'); + } + } catch (e) { + /*eslint no-console: ["error", { allow: ["warn", "error"] }] */ + + console.error('Wrong pattern specified', SAFE_IFRAM_SRC_PATTERN, e); + } + return reg; + })(); + const targetWindow = '_blank'; + options = { + onTag(tag, html, options) { + if (tag === 'iframe') { + const clipCls = 'note-vide-clip'; + if (!options.isClosing) { + const srcp = /src=(['"]{0,1})(\S*)(\1)/; + let safe = html.indexOf(`class="${clipCls}"`) > -1; + if (srcp.exec(html)) { + const src = RegExp.$2; + if (allowedIframeSrcRegex.exec(src)) { + safe = true; + } + if (safe) + return `<iframe src='${src}' class="${clipCls}" width=100% height=auto allowfullscreen></iframe>`; + } + } else { + return ''; + } + } else if (tag === 'a') { + if (!options.isClosing) { + if (/href=(['"]{0,1})(\S*)(\1)/.exec(html)) { + const href = RegExp.$2; + if (href.match(/^((http(s){0,1}:){0,1}\/\/|\/)/)) { + // a valid url + return `<a href=${href} target=${targetWindow}>`; + } + } + } + } else if (tag === 'img') { + if (!options.isClosing) { + if (new RegExp('src=([\'"]{0,1})(\\S*)(\\1)').exec(html)) { + const src = RegExp.$2; + return `<a href='${src}' class='swipebox'><img src='${src}' class="attachment-image-preview mCS_img_loaded"></a>`; + } + } + } + return undefined; + }, + onTagAttr(tag, name, value) { + if (tag === 'img' && name === 'src') { + if (value && value.substr(0, 5) === 'data:') { + // allow image with dataURI src + return `${name}='${value}'`; + } + } else if (tag === 'a' && name === 'target') { + return `${name}='${targetWindow}'`; // always change a href target to a new window + } + return undefined; + }, + ...options, + }; + return _sanitizeXss(input, options); +}; Template.editor.onRendered(() => { const textareaSelector = 'textarea'; - const enableRicherEditor = - Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR || true; const mentions = [ // User mentions { @@ -32,7 +102,7 @@ Template.editor.onRendered(() => { autosize($textarea); $textarea.escapeableTextComplete(mentions); }; - if (enableRicherEditor) { + if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR !== false) { const isSmall = Utils.isMiniScreen(); const toolbar = isSmall ? [ @@ -50,47 +120,11 @@ Template.editor.onRendered(() => { ['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', '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', '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 cleanPastedHTML = sanitizeXss; const editor = '.editor'; const selectors = [ `.js-new-comment-form ${editor}`, @@ -116,8 +150,8 @@ Template.editor.onRendered(() => { callbacks: { onInit(object) { const originalInput = this; - $(originalInput).on('input', function() { - // when comment is submitted, the original textarea will be set to '', so shall we + $(originalInput).on('submitted', function() { + // resetCommentInput has been called if (!this.value) { const sn = getSummernote(this); sn && sn.summernote('reset'); @@ -138,6 +172,77 @@ Template.editor.onRendered(() => { }); } }, + onImageUpload(files) { + const $summernote = getSummernote(this); + if (files && files.length > 0) { + const image = files[0]; + const currentCard = Cards.findOne(Session.get('currentCard')); + const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL; + const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO; + const insertImage = src => { + 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(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); + } + }, + }); + } + }; + reader.readAsDataURL(image); + } else { + processData(image); + } + } + }, onPaste() { // clear up unwanted tag info when user pasted in text const thisNote = this; @@ -185,8 +290,6 @@ Template.editor.onRendered(() => { } }); -import sanitizeXss from 'xss'; - // XXX I believe we should compute a HTML rendered field on the server that // would handle markdown and user mentions. We can simply have two // fields, one source, and one compiled version (in HTML) and send only the @@ -237,32 +340,35 @@ Blaze.Template.registerHelper( content = content.replace(fullMention, Blaze.toHTML(link)); } - return HTML.Raw(sanitizeXss(content)); }), ); - Template.viewer.events({ // Viewer sometimes have click-able wrapper around them (for instance to edit // the corresponding text). Clicking a link shouldn't fire these actions, stop // we stop these event at the viewer component level. 'click a'(event, templateInstance) { - event.stopPropagation(); - - // XXX We hijack the build-in browser action because we currently don't have - // `_blank` attributes in viewer links, and the transformer function is - // handled by a third party package that we can't configure easily. Fix that - // by using directly `_blank` attribute in the rendered HTML. - event.preventDefault(); - + let prevent = true; const userId = event.currentTarget.dataset.userid; if (userId) { Popup.open('member').call({ userId }, event, templateInstance); } else { const href = event.currentTarget.href; - if (href) { + const child = event.currentTarget.firstElementChild; + if (child && child.tagName === 'IMG') { + prevent = false; + } else if (href) { window.open(href, '_blank'); } } + if (prevent) { + event.stopPropagation(); + + // XXX We hijack the build-in browser action because we currently don't have + // `_blank` attributes in viewer links, and the transformer function is + // handled by a third party package that we can't configure easily. Fix that + // by using directly `_blank` attribute in the rendered HTML. + event.preventDefault(); + } }, }); diff --git a/client/components/main/layouts.styl b/client/components/main/layouts.styl index 672e4520..06538554 100644 --- a/client/components/main/layouts.styl +++ b/client/components/main/layouts.styl @@ -232,6 +232,7 @@ kbd background: darken(white, 2%) border-radius: 3px border: 1px solid darken(white, 10%) + color: unset box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.15) .clear diff --git a/client/components/settings/peopleBody.jade b/client/components/settings/peopleBody.jade index 5db7470a..ff343e37 100644 --- a/client/components/settings/peopleBody.jade +++ b/client/components/settings/peopleBody.jade @@ -107,8 +107,8 @@ template(name="editUserPopup") label | {{_ 'password'}} input.js-profile-password(type="password") - //div.buttonsContainer - // input.primary.wide(type="submit" value="{{_ 'save'}}") + div.buttonsContainer + input.primary.wide(type="submit" value="{{_ 'save'}}") // div // input#deleteButton.primary.wide(type="button" value="{{_ 'delete'}}") diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index 2b869314..2dfe41b3 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -22,6 +22,11 @@ template(name='homeSidebar') +membersWidget hr +labelsWidget + ul#cards.label-text-hidden + a.flex.js-toggle-minicard-label-text + span {{_ 'hide-minicard-label-text'}} + b + .materialCheckBox(class="{{#if hiddenMinicardLabelText}}is-checked{{/if}}") hr unless currentUser.isNoComments h3 diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 8468595a..8b98fd7e 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -101,6 +101,9 @@ BlazeComponent.extendComponent({ 'click .js-hide-sidebar': this.hide, 'click .js-toggle-sidebar': this.toggle, 'click .js-back-home': this.setView, + 'click .js-toggle-minicard-label-text'() { + Meteor.call('toggleMinicardLabelText'); + }, 'click .js-shortcuts'() { FlowRouter.go('shortcuts'); }, @@ -111,6 +114,12 @@ BlazeComponent.extendComponent({ Blaze.registerHelper('Sidebar', () => Sidebar); +Template.homeSidebar.helpers({ + hiddenMinicardLabelText() { + return Meteor.user().hasHiddenMinicardLabelText(); + }, +}); + EscapeActions.register( 'sidebarView', () => { |