summaryrefslogtreecommitdiffstats
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/components/activities/activities.js12
-rw-r--r--client/components/boards/boardBody.js8
-rw-r--r--client/components/boards/boardColors.styl165
-rw-r--r--client/components/cards/attachments.jade13
-rw-r--r--client/components/cards/attachments.js103
-rw-r--r--client/components/cards/attachments.styl11
-rw-r--r--client/components/cards/cardDetails.js13
-rw-r--r--client/components/cards/minicard.jade2
-rw-r--r--client/components/cards/minicard.js3
-rw-r--r--client/components/lists/list.js24
-rwxr-xr-xclient/components/main/editor.js50
-rw-r--r--client/components/main/fonts.styl24
-rw-r--r--client/components/main/layouts.styl2
-rw-r--r--client/components/rules/actions/boardActions.js4
-rw-r--r--client/components/rules/actions/cardActions.js1
-rw-r--r--client/components/sidebar/sidebar.jade6
-rw-r--r--client/components/sidebar/sidebar.js9
-rw-r--r--client/lib/dropImage.js4
-rw-r--r--client/lib/exportHTML.js206
-rw-r--r--client/lib/pasteImage.js2
-rw-r--r--client/lib/popup.js2
-rw-r--r--client/lib/utils.js44
22 files changed, 611 insertions, 97 deletions
diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js
index 5d356f6e..4f15d421 100644
--- a/client/components/activities/activities.js
+++ b/client/components/activities/activities.js
@@ -151,21 +151,23 @@ BlazeComponent.extendComponent({
},
attachmentLink() {
- const attachment = this.currentData().activity.attachment();
+ const activity = this.currentData().activity;
+ const attachment = activity.attachment();
+ const link = attachment ? attachment.link('original', '/') : null;
// trying to display url before file is stored generates js errors
return (
(attachment &&
- attachment.url({ download: true }) &&
+ link &&
Blaze.toHTML(
HTML.A(
{
- href: attachment.url({ download: true }),
+ href: link,
target: '_blank',
},
- attachment.name(),
+ attachment.name,
),
)) ||
- this.currentData().activity.attachmentName
+ activity.attachmentName
);
},
diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js
index 4e473f18..e70faa49 100644
--- a/client/components/boards/boardBody.js
+++ b/client/components/boards/boardBody.js
@@ -365,12 +365,12 @@ BlazeComponent.extendComponent({
};
currentBoard
.cardsInInterval(start.toDate(), end.toDate())
- .forEach(function(card) {
+ .forEach(card => {
pushEvent(card);
});
currentBoard
.cardsDueInBetween(start.toDate(), end.toDate())
- .forEach(function(card) {
+ .forEach(card => {
pushEvent(
card,
`${card.title} ${TAPi18n.__('card-due')}`,
@@ -378,8 +378,8 @@ BlazeComponent.extendComponent({
new Date(card.dueAt.getTime() + 36e5),
);
});
- events.sort(function(first, second) {
- return first.id > second.id ? 1 : -1;
+ events.sort((first, second) => {
+ return first.id === second.id ? 0 : first.id > second.id ? 1 : -1;
});
callback(events);
},
diff --git a/client/components/boards/boardColors.styl b/client/components/boards/boardColors.styl
index 6b3994ff..0081143f 100644
--- a/client/components/boards/boardColors.styl
+++ b/client/components/boards/boardColors.styl
@@ -542,6 +542,169 @@ setBoardClear(color1,color2)
.list-header
background-color: #c9cfc3
border-bottom: 6px solid #c9cfc3
-
+
.swimlane .swimlane-header-wrap
background-color: #c2c0ab
+
+/*
+ Alternate "Modern" Styling
+*/
+.board-color-modern
+ setBoardColor(#2A80B8)
+
+ /* General */
+ body
+ background: #f5f5f5
+
+ &#header-quick-access
+ padding: 10px
+ font-size: 14px
+ background: #333 !important
+
+ &#header-quick-access ul
+ overflow: visible
+
+ &#header-quick-access ul li.current
+ border: 0 !important
+ font-weight: bold
+
+ &#header-quick-access ul li.separator
+ display: none
+
+ &#header-quick-access ul li:nth-child(3)
+ margin-right: 10px
+
+ &#header-quick-access ul li a
+ padding: 5px 10px
+ border-radius: 2px
+
+ &#header-quick-access ul li.current a
+ border-radius: 2px
+ background: rgba(255,255,255,.2)
+
+ &#header #header-main-bar h1
+ font-family: Poppins
+ font-weight: bold
+ &#header-quick-access #header-user-bar
+ position relative
+
+ &#header-quick-access #header-user-bar .header-user-bar-name
+ margin: 5px 3px 0 0;
+
+ section#notifications-drawer
+ top: 46px;
+ box-shadow: 0 4px 20px rgba(0,0,0,.1)
+ max-width: 100%
+
+ section#notifications-drawer .header
+ top: 46px;
+ border-radius: 0 3px
+ height: 21px
+ background: #f7f7f7
+
+ /* Swimlane */
+ .swimlane
+ background: #f5f5f5
+
+ .swimlane .swimlane-header-wrap .swimlane-header
+ font-family: Poppins
+
+ /* All board views */
+ .board-list .board-list-item
+ padding: 20px
+
+ .board-list-item-name
+ font-family: Poppins
+
+ /* Board */
+ .list
+ background: transparent
+ border-left: 0
+ margin: 10px 0
+ padding: 0px
+ border-radius: 5px
+ min-width: 300px
+
+ .list-body .open-minicard-composer:hover /*me*/
+ background: none
+ box-shadow: none
+
+ .list:first-child
+ margin-left: 5px
+
+ .list.list-composer.js-list-composer
+ transition: all .3s ease
+ min-width: 80px
+
+ .open-list-composer.js-open-inlined-form:hover
+ color: #222
+
+ .list-header
+ background: none
+ border-bottom-width: 0px
+
+ .list-header .list-header-name
+ font-family: Poppins
+ color: #000
+ font-weight: 500
+
+ /* Card changes */
+ .minicard
+ background: #FFF
+ padding: 15px 15px 10px
+ box-shadow: 0 3px 8px rgba(0,0,0,.05)
+
+ .minicard-plum:hover:not(.minicard-composer), .is-selected .minicard-plum, .draggable-hover-card .minicard-plum
+ background: none
+
+ .minicard-title
+ line-height: 1.5em
+
+ .minicard .minicard-cover
+ background-size: cover
+ margin: -15px -15px 10px
+ height: 100px
+
+ .card-label-orange
+ color: #fff
+
+ .card-date
+ font-size: 12px
+ padding: 3px 5px
+
+ /* Pop over */
+ .header-title
+ font-family: Poppins
+ font-size: 16px
+ color: #333
+
+ .pop-over
+ box-shadow: 0 4px 20px rgba(0,0,0,.1)
+ border: 0
+ border-radius: 5px
+
+ .pop-over .header
+ padding: 10px
+ border-bottom: 0
+ border-radius: 5px 5px 0 0
+
+ .pop-over .content-container .content
+ padding: 5px 20px 20px
+ width: 260px
+
+ .pop-over-list li > a
+ border-radius: 5px
+
+ .pop-over-list li > a > i
+ margin-right: 5px
+
+ .pop-over-list li>a .sub-name
+ margin-bottom: 8px
+
+ /* Sidebar */
+ .sidebar .sidebar-shadow
+ box-shadow: 0 0 60px rgba(0,0,0,.2)
+
+ .sidebar .sidebar-content
+ padding: 30px
+
diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade
index 61454fa7..eda6d118 100644
--- a/client/components/cards/attachments.jade
+++ b/client/components/cards/attachments.jade
@@ -18,12 +18,19 @@ template(name="attachmentDeletePopup")
p {{_ "attachment-delete-pop"}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+template(name="uploadingPopup")
+ .uploading-info
+ span.upload-percentage {{progress}}%
+ .upload-progress-frame
+ .upload-progress-bar(style="width: {{progress}}%;")
+ span.upload-size {{fileSize}}
+
template(name="attachmentsGalery")
.attachments-galery
each attachments
.attachment-item
- a.attachment-thumbnail.swipebox(href="{{url}}" title="{{name}}")
- if isUploaded
+ a.attachment-thumbnail.swipebox(href="{{url}}" download="{{name}}" title="{{name}}")
+ if isUploaded
if isImage
img.attachment-thumbnail-img(src="{{url}}")
else
@@ -33,7 +40,7 @@ template(name="attachmentsGalery")
p.attachment-details
= name
span.attachment-details-actions
- a.js-download(href="{{url download=true}}")
+ a.js-download(href="{{url download=true}}" download="{{name}}")
i.fa.fa-download
| {{_ 'download'}}
if currentUser.isBoardMember
diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js
index e4439155..81f6c6e1 100644
--- a/client/components/cards/attachments.js
+++ b/client/components/cards/attachments.js
@@ -13,10 +13,10 @@ Template.attachmentsGalery.events({
event.stopPropagation();
},
'click .js-add-cover'() {
- Cards.findOne(this.cardId).setCover(this._id);
+ Cards.findOne(this.meta.cardId).setCover(this._id);
},
'click .js-remove-cover'() {
- Cards.findOne(this.cardId).unsetCover();
+ Cards.findOne(this.meta.cardId).unsetCover();
},
'click .js-preview-image'(event) {
Popup.open('previewAttachedImage').call(this, event);
@@ -45,22 +45,63 @@ Template.attachmentsGalery.events({
},
});
+Template.attachmentsGalery.helpers({
+ url() {
+ return Attachments.link(this, 'original', '/');
+ },
+ isUploaded() {
+ return !this.meta.uploading;
+ },
+ isImage() {
+ return !!this.isImage;
+ },
+});
+
Template.previewAttachedImagePopup.events({
'click .js-large-image-clicked'() {
Popup.close();
},
});
+Template.previewAttachedImagePopup.helpers({
+ url() {
+ return Attachments.link(this, 'original', '/');
+ }
+});
+
+// For uploading popup
+
+let uploadFileSize = new ReactiveVar('');
+let uploadProgress = new ReactiveVar(0);
+
Template.cardAttachmentsPopup.events({
- 'change .js-attach-file'(event) {
+ 'change .js-attach-file'(event, instance) {
const card = this;
- const processFile = f => {
- Utils.processUploadedAttachment(card, f, attachment => {
- if (attachment && attachment._id && attachment.isImage()) {
- card.setCover(attachment._id);
+ const callbacks = {
+ onBeforeUpload: (err, fileData) => {
+ Popup.open('uploading')(this.clickEvent);
+ uploadFileSize.set('...');
+ uploadProgress.set(0);
+ return true;
+ },
+ onUploaded: (err, attachment) => {
+ if (attachment && attachment._id && attachment.isImage) {
+ card.setCover(attachment._id);
+ }
+ Popup.close();
+ },
+ onStart: (error, fileData) => {
+ uploadFileSize.set(formatBytes(fileData.size));
+ },
+ onError: (err, fileObj) => {
+ console.log('Error!', err);
+ },
+ onProgress: (progress, fileData) => {
+ uploadProgress.set(progress);
}
- Popup.close();
- });
+ };
+ const processFile = f => {
+ Utils.processUploadedAttachment(card, f, callbacks);
};
FS.Utility.eachFile(event, f => {
@@ -100,12 +141,22 @@ Template.cardAttachmentsPopup.events({
});
},
'click .js-computer-upload'(event, templateInstance) {
+ this.clickEvent = event;
templateInstance.find('.js-attach-file').click();
event.preventDefault();
},
'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'),
});
+Template.uploadingPopup.helpers({
+ fileSize: () => {
+ return uploadFileSize.get();
+ },
+ progress: () => {
+ return uploadProgress.get();
+ }
+});
+
const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
let pastedResults = null;
@@ -149,20 +200,26 @@ Template.previewClipboardImagePopup.events({
if (results && results.file) {
window.oPasted = pastedResults;
const card = this;
- const file = new FS.File(results.file);
+ const settings = {
+ file: results.file,
+ streams: 'dynamic',
+ chunkSize: 'dynamic',
+ };
if (!results.name) {
// if no filename, it's from clipboard. then we give it a name, with ext name from MIME type
if (typeof results.file.type === 'string') {
- file.name(results.file.type.replace('image/', 'clipboard.'));
+ settings.fileName =
+ new Date().getTime() + results.file.type.replace('.+/', '');
}
}
- file.updatedAt(new Date());
- file.boardId = card.boardId;
- file.cardId = card._id;
- file.userId = Meteor.userId();
- const attachment = Attachments.insert(file);
+ settings.meta = {};
+ settings.meta.updatedAt = new Date().getTime();
+ settings.meta.boardId = card.boardId;
+ settings.meta.cardId = card._id;
+ settings.meta.userId = Meteor.userId();
+ const attachment = Attachments.insert(settings);
- if (attachment && attachment._id && attachment.isImage()) {
+ if (attachment && attachment._id && attachment.isImage) {
card.setCover(attachment._id);
}
@@ -172,3 +229,15 @@ Template.previewClipboardImagePopup.events({
}
},
});
+
+function formatBytes(bytes, decimals = 2) {
+ if (bytes === 0) return '0 Bytes';
+
+ const k = 1024;
+ const dm = decimals < 0 ? 0 : decimals;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
+}
diff --git a/client/components/cards/attachments.styl b/client/components/cards/attachments.styl
index 4a22fd8a..61ea8232 100644
--- a/client/components/cards/attachments.styl
+++ b/client/components/cards/attachments.styl
@@ -64,6 +64,17 @@
border: 1px solid black
box-shadow: 0 1px 2px rgba(0,0,0,.2)
+.uploading-info
+ .upload-progress-frame
+ background-color: grey;
+ border: 1px solid;
+ height: 22px;
+
+ .upload-progress-bar
+ background-color: blue;
+ height: 20px;
+ padding: 1px;
+
@media screen and (max-width: 800px)
.attachments-galery
flex-direction
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index 441068b0..d877421c 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -74,11 +74,18 @@ BlazeComponent.extendComponent({
scrollParentContainer() {
const cardPanelWidth = 510;
- const bodyBoardComponent = this.parentComponent().parentComponent();
+ const parentComponent = this.parentComponent();
+ // TODO sometimes parentComponent is not available, maybe because it's not
+ // yet created?!
+ if (!parentComponent) return;
+ const bodyBoardComponent = parentComponent.parentComponent();
//On Mobile View Parent is Board, Not Board Body. I cant see how this funciton should work then.
if (bodyBoardComponent === null) return;
const $cardView = this.$(this.firstNode());
const $cardContainer = bodyBoardComponent.$('.js-swimlanes');
+ // TODO sometimes cardContainer is not available, maybe because it's not yet
+ // created?!
+ if (!$cardContainer) return;
const cardContainerScroll = $cardContainer.scrollLeft();
const cardContainerWidth = $cardContainer.width();
@@ -805,9 +812,9 @@ Template.copyChecklistToManyCardsPopup.events({
// copy subtasks
cursor = Cards.find({ parentId: oldId });
- cursor.forEach(function() {
+ cursor.forEach(cur => {
'use strict';
- const subtask = arguments[0];
+ const subtask = cur;
subtask.parentId = _id;
subtask._id = null;
/* const newSubtaskId = */ Cards.insert(subtask);
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade
index 79dd9127..e9de2fea 100644
--- a/client/components/cards/minicard.jade
+++ b/client/components/cards/minicard.jade
@@ -11,7 +11,7 @@ template(name="minicard")
.handle
.fa.fa-arrows
if cover
- .minicard-cover(style="background-image: url('{{cover.url}}');")
+ .minicard-cover(style="background-image: url('{{coverUrl}}');")
if labels
.minicard-labels
each labels
diff --git a/client/components/cards/minicard.js b/client/components/cards/minicard.js
index da36b87f..200e019d 100644
--- a/client/components/cards/minicard.js
+++ b/client/components/cards/minicard.js
@@ -52,4 +52,7 @@ Template.minicard.helpers({
return false;
}
},
+ coverUrl() {
+ return Attachments.findOne(this.coverId).link('original', '/');
+ },
});
diff --git a/client/components/lists/list.js b/client/components/lists/list.js
index 839304f8..5c315588 100644
--- a/client/components/lists/list.js
+++ b/client/components/lists/list.js
@@ -74,18 +74,16 @@ BlazeComponent.extendComponent({
const sortIndex = calculateIndex(prevCardDom, nextCardDom, nCards);
const listId = Blaze.getData(ui.item.parents('.list').get(0))._id;
const currentBoard = Boards.findOne(Session.get('currentBoard'));
- let swimlaneId = '';
+ const defaultSwimlaneId = currentBoard.getDefaultSwimline()._id;
+ let targetSwimlaneId = null;
+
+ // only set a new swimelane ID if the swimlanes view is active
if (
Utils.boardView() === 'board-view-swimlanes' ||
currentBoard.isTemplatesBoard()
)
- swimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))._id;
- else if (
- Utils.boardView() === 'board-view-lists' ||
- Utils.boardView() === 'board-view-cal' ||
- !Utils.boardView
- )
- swimlaneId = currentBoard.getDefaultSwimline()._id;
+ targetSwimlaneId = Blaze.getData(ui.item.parents('.swimlane').get(0))
+ ._id;
// Normally the jquery-ui sortable library moves the dragged DOM element
// to its new position, which disrupts Blaze reactive updates mechanism
@@ -98,9 +96,12 @@ BlazeComponent.extendComponent({
if (MultiSelection.isActive()) {
Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
+ const newSwimlaneId = targetSwimlaneId
+ ? targetSwimlaneId
+ : card.swimlaneId || defaultSwimlaneId;
card.move(
currentBoard._id,
- swimlaneId,
+ newSwimlaneId,
listId,
sortIndex.base + i * sortIndex.increment,
);
@@ -108,7 +109,10 @@ BlazeComponent.extendComponent({
} else {
const cardDomElement = ui.item.get(0);
const card = Blaze.getData(cardDomElement);
- card.move(currentBoard._id, swimlaneId, listId, sortIndex.base);
+ const newSwimlaneId = targetSwimlaneId
+ ? targetSwimlaneId
+ : card.swimlaneId || defaultSwimlaneId;
+ card.move(currentBoard._id, newSwimlaneId, listId, sortIndex.base);
}
boardComponent.setIsDragging(false);
},
diff --git a/client/components/main/editor.js b/client/components/main/editor.js
index 0c2e3186..abe4160f 100755
--- a/client/components/main/editor.js
+++ b/client/components/main/editor.js
@@ -152,33 +152,31 @@ Template.editor.onRendered(() => {
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);
+ fileObj,
+ { onUploaded:
+ 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 = Attachments.link(attachment, 'original', '/');
+ if (url) {
+ insertImage(
+ `${location.protocol}//${location.host}${url}`,
+ );
+ } else {
+ retry++;
+ if (retry < maxTry) {
+ setTimeout(checkUrl, checkItvl);
+ }
}
- }
- };
- checkUrl();
- });
+ };
+ checkUrl();
+ });
+ }
}
},
);
diff --git a/client/components/main/fonts.styl b/client/components/main/fonts.styl
index fc8c8f00..5d6fb558 100644
--- a/client/components/main/fonts.styl
+++ b/client/components/main/fonts.styl
@@ -15,3 +15,27 @@
local('Roboto-Bold'),
url('/fonts/roboto-bold.woff2') format('woff2'),
url('/fonts/roboto-bold.woff') format('woff')
+
+@font-face
+ font-family: 'Poppins'
+ font-style: normal
+ font-weight: 400
+ src: local('Poppins'),
+ local('Poppins-Regular'),
+ url('/fonts/poppins-regular.woff') format('woff')
+
+@font-face
+ font-family: 'Poppins'
+ font-style: normal
+ font-weight: 500
+ src: local('Poppins Medium'),
+ local('Poppins-Medium'),
+ url('/fonts/poppins-medium.woff') format('woff')
+
+@font-face
+ font-family: 'Poppins'
+ font-style: normal
+ font-weight: 700
+ src: local('Poppins Bold'),
+ local('Poppins-Bold'),
+ url('/fonts/poppins-bold.woff') format('woff')
diff --git a/client/components/main/layouts.styl b/client/components/main/layouts.styl
index 01ce2f16..85a5f1b2 100644
--- a/client/components/main/layouts.styl
+++ b/client/components/main/layouts.styl
@@ -32,7 +32,7 @@ a:hover,a:focus
border-radius: unset
html, body, input, select, textarea, button
- font: 14px Roboto, "Helvetica Neue", Arial, Helvetica, sans-serif
+ font: 14px Roboto, Poppins, "Helvetica Neue", Arial, Helvetica, sans-serif
line-height: 18px
color: #4d4d4d
diff --git a/client/components/rules/actions/boardActions.js b/client/components/rules/actions/boardActions.js
index 02910cc1..5675873f 100644
--- a/client/components/rules/actions/boardActions.js
+++ b/client/components/rules/actions/boardActions.js
@@ -68,8 +68,8 @@ BlazeComponent.extendComponent({
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const actionSelected = this.find('#move-spec-action').value;
- const swimlaneName = this.find('#swimlaneName').value;
- const listName = this.find('#listName').value;
+ const swimlaneName = this.find('#swimlaneName').value || '*';
+ const listName = this.find('#listName').value || '*';
const boardId = Session.get('currentBoard');
const destBoardId = this.find('#board-id').value;
const desc = Utils.getTriggerActionDesc(event, this);
diff --git a/client/components/rules/actions/cardActions.js b/client/components/rules/actions/cardActions.js
index 7dc6c2b5..2290249c 100644
--- a/client/components/rules/actions/cardActions.js
+++ b/client/components/rules/actions/cardActions.js
@@ -164,6 +164,7 @@ BlazeComponent.extendComponent({
const boardId = Session.get('currentBoard');
const actionId = Actions.insert({
actionType: 'removeMember',
+ // deepcode ignore NoHardcodedCredentials: it's no credential
username: '*',
boardId,
desc,
diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade
index 7d637142..04f2a8c2 100644
--- a/client/components/sidebar/sidebar.jade
+++ b/client/components/sidebar/sidebar.jade
@@ -363,7 +363,7 @@ template(name="boardMenuPopup")
template(name="exportBoard")
ul.pop-over-list
li
- a(href="{{exportUrl}}", download="{{exportJsonFilename}}")
+ a.download-json-link(href="{{exportUrl}}", download="{{exportJsonFilename}}")
i.fa.fa-share-alt
| {{_ 'export-board-json'}}
li
@@ -374,6 +374,10 @@ template(name="exportBoard")
a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}")
i.fa.fa-share-alt
| {{_ 'export-board-tsv'}}
+ li
+ a.html-export-board
+ i.fa.fa-archive
+ | {{_ 'export-board-html'}}
template(name="labelsWidget")
.board-widget.board-widget-labels
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
index 2c1cfd75..7b14c1e0 100644
--- a/client/components/sidebar/sidebar.js
+++ b/client/components/sidebar/sidebar.js
@@ -463,6 +463,13 @@ BlazeComponent.extendComponent({
},
}).register('exportBoardPopup');
+Template.exportBoard.events({
+ 'click .html-export-board': async event => {
+ event.preventDefault();
+ await ExportHtml(Popup)();
+ }
+});
+
Template.labelsWidget.events({
'click .js-label': Popup.open('editLabel'),
'click .js-add-label': Popup.open('createLabel'),
@@ -646,7 +653,7 @@ BlazeComponent.extendComponent({
'subtext-with-parent',
'no-parent',
];
- options.forEach(function(element) {
+ options.forEach(element => {
if (element !== value) {
$(`#${element} ${MCB}`).toggleClass(CKCLS, false);
$(`#${element}`).toggleClass(CKCLS, false);
diff --git a/client/lib/dropImage.js b/client/lib/dropImage.js
index f4e1d8ab..559754f4 100644
--- a/client/lib/dropImage.js
+++ b/client/lib/dropImage.js
@@ -39,8 +39,8 @@
};
return this.each(function() {
const element = this;
- $(element).bind('dragenter dragover dragleave', stopFn);
- return $(element).bind('drop', function(event) {
+ $(element).on('dragenter dragover dragleave', stopFn);
+ return $(element).on('drop', function(event) {
stopFn(event);
const files = event.dataTransfer.files;
for (let i = 0; i < files.length; i++) {
diff --git a/client/lib/exportHTML.js b/client/lib/exportHTML.js
new file mode 100644
index 00000000..fe15b6aa
--- /dev/null
+++ b/client/lib/exportHTML.js
@@ -0,0 +1,206 @@
+const JSZip = require('jszip');
+
+window.ExportHtml = (Popup) => {
+ const saveAs = function(blob, filename) {
+ let dl = document.createElement('a');
+ dl.href = window.URL.createObjectURL(blob);
+ dl.onclick = event => document.body.removeChild(event.target);
+ dl.style.display = 'none';
+ dl.target = '_blank';
+ dl.download = filename;
+ document.body.appendChild(dl);
+ dl.click();
+ };
+
+ const asyncForEach = async function (array, callback) {
+ for (let index = 0; index < array.length; index++) {
+ await callback(array[index], index, array);
+ }
+ };
+
+ const getPageHtmlString = () => {
+ return `<!doctype html>${
+ window.document.querySelector('html').outerHTML
+ }`;
+ };
+
+ const removeAnchors = htmlString => {
+ const replaceOpenAnchor = htmlString.replace(new RegExp('<a ', 'gim'), '<span ');
+ return replaceOpenAnchor.replace(new RegExp('<\/a', 'gim'), '</span');
+ };
+
+ const ensureSidebarRemoved = () => {
+ document.querySelector('.board-sidebar.sidebar').remove();
+ };
+
+ const addJsonExportToZip = async (zip, boardSlug) => {
+ const downloadJSONLink = document.querySelector('.download-json-link');
+ const downloadJSONURL = downloadJSONLink.href;
+ const response = await fetch(downloadJSONURL);
+ const responseBody = await response.text();
+ zip.file(`data/${boardSlug}.json`, responseBody);
+ };
+
+ const closeSidebar = () => {
+ document.querySelector('.board-header-btn.js-toggle-sidebar').click();
+ };
+
+ const cleanBoardHtml = () => {
+ Array.from(document.querySelectorAll('script')).forEach(elem =>
+ elem.remove(),
+ );
+ Array.from(
+ document.querySelectorAll('link:not([rel="stylesheet"])'),
+ ).forEach(elem => elem.remove());
+ document.querySelector('#header-quick-access').remove();
+ Array.from(
+ document.querySelectorAll('#header-main-bar .board-header-btns'),
+ ).forEach(elem => elem.remove());
+ Array.from(document.querySelectorAll('.list-composer')).forEach(elem =>
+ elem.remove(),
+ );
+ Array.from(
+ document.querySelectorAll(
+ '.list-composer,.js-card-composer, .js-add-card',
+ ),
+ ).forEach(elem => elem.remove());
+ Array.from(
+ document.querySelectorAll('.js-perfect-scrollbar > div:nth-of-type(n+2)'),
+ ).forEach(elem => elem.remove());
+ Array.from(document.querySelectorAll('.js-perfect-scrollbar')).forEach(
+ elem => {
+ elem.style = 'overflow-y: auto !important;';
+ elem.classList.remove('js-perfect-scrollbar');
+ },
+ );
+ Array.from(document.querySelectorAll('[href]:not(link)')).forEach(elem =>
+ elem.attributes.removeNamedItem('href'),
+ );
+ Array.from(document.querySelectorAll('[href]')).forEach(elem => {
+ // eslint-disable-next-line no-self-assign
+ elem.href = elem.href;
+ // eslint-disable-next-line no-self-assign
+ elem.src = elem.src;
+ });
+ Array.from(document.querySelectorAll('.is-editable')).forEach(elem => {
+ elem.classList.remove('is-editable')
+ })
+
+ };
+
+ const getBoardSlug = () => {
+ return window.location.href.split('/').pop();
+ };
+
+ const getStylesheetList = () => {
+ return Array.from(
+ document.querySelectorAll('link[href][rel="stylesheet"]'),
+ );
+ };
+
+ const downloadStylesheets = async (stylesheets, zip) => {
+ await asyncForEach(stylesheets, async elem => {
+ const response = await fetch(elem.href);
+ const responseBody = await response.text();
+
+ const finalResponse = responseBody.replace(
+ new RegExp('packages\/[^\/]+\/upstream\/', 'gim'), '../'
+ );
+
+ const filename = elem.href
+ .split('/')
+ .pop()
+ .split('?')
+ .shift();
+ const fileFullPath = `style/${filename}`;
+ zip.file(fileFullPath, finalResponse);
+ elem.href = `../${fileFullPath}`;
+ });
+ };
+
+ const getSrcAttached = () => {
+ return Array.from(document.querySelectorAll('[src]'));
+ };
+
+ const downloadSrcAttached = async (elements, zip, boardSlug) => {
+ await asyncForEach(elements, async elem => {
+ const response = await fetch(elem.src);
+ const responseBody = await response.blob();
+ const filename = elem.src
+ .split('/')
+ .pop()
+ .split('?')
+ .shift();
+ const fileFullPath = `${boardSlug}/${elem.tagName.toLowerCase()}/${filename}`;
+ zip.file(fileFullPath, responseBody);
+ elem.src = `./${elem.tagName.toLowerCase()}/${filename}`;
+ });
+ };
+
+ const removeCssUrlSurround = url => {
+ const working = url || "";
+ return working
+ .split("url(")
+ .join("")
+ .split("\")")
+ .join("")
+ .split("\"")
+ .join("")
+ .split("')")
+ .join("")
+ .split("'")
+ .join("")
+ .split(")")
+ .join("");
+ };
+
+ const getCardCovers = () => {
+ return Array.from(document.querySelectorAll('.minicard-cover'))
+ .filter(elem => elem.style['background-image'])
+ }
+
+ const downloadCardCovers = async (elements, zip, boardSlug) => {
+ await asyncForEach(elements, async elem => {
+ const response = await fetch(removeCssUrlSurround(elem.style['background-image']));
+ const responseBody = await response.blob();
+ const filename = removeCssUrlSurround(elem.style['background-image'])
+ .split('/')
+ .pop()
+ .split('?')
+ .shift()
+ .split('#')
+ .shift();
+ const fileFullPath = `${boardSlug}/covers/${filename}`;
+ zip.file(fileFullPath, responseBody);
+ elem.style = "background-image: url('" + `covers/${filename}` + "')";
+ });
+ };
+
+ const addBoardHTMLToZip = (boardSlug, zip) => {
+ ensureSidebarRemoved();
+ const htmlOutputPath = `${boardSlug}/index.html`;
+ zip.file(htmlOutputPath, new Blob([
+ removeAnchors(getPageHtmlString())
+ ], { type: 'application/html' }));
+ };
+
+ return async () => {
+ const zip = new JSZip();
+ const boardSlug = getBoardSlug();
+
+ await addJsonExportToZip(zip, boardSlug);
+ Popup.close();
+ closeSidebar();
+ cleanBoardHtml();
+
+ await downloadStylesheets(getStylesheetList(), zip);
+ await downloadSrcAttached(getSrcAttached(), zip, boardSlug);
+ await downloadCardCovers(getCardCovers(), zip, boardSlug);
+
+ addBoardHTMLToZip(boardSlug, zip);
+
+ const content = await zip.generateAsync({ type: 'blob' });
+ saveAs(content, `${boardSlug}.zip`);
+ window.location.reload();
+ }
+};
diff --git a/client/lib/pasteImage.js b/client/lib/pasteImage.js
index e6f8c6c2..195a781d 100644
--- a/client/lib/pasteImage.js
+++ b/client/lib/pasteImage.js
@@ -35,7 +35,7 @@
options = $.extend({}, defaults, options);
return this.each(function() {
const element = this;
- return $(element).bind('paste', function(event) {
+ return $(element).on('paste', function(event) {
const types = event.clipboardData.types;
const items = event.clipboardData.items;
for (let i = 0; i < types.length; i++) {
diff --git a/client/lib/popup.js b/client/lib/popup.js
index 8095fbd2..cae22659 100644
--- a/client/lib/popup.js
+++ b/client/lib/popup.js
@@ -49,7 +49,7 @@ window.Popup = new (class {
// has one. This allows us to position a sub-popup exactly at the same
// position than its parent.
let openerElement;
- if (clickFromPopup(evt)) {
+ if (clickFromPopup(evt) && self._getTopStack()) {
openerElement = self._getTopStack().openerElement;
} else {
self._stack = [];
diff --git a/client/lib/utils.js b/client/lib/utils.js
index c921fddc..e72f177e 100644
--- a/client/lib/utils.js
+++ b/client/lib/utils.js
@@ -61,30 +61,38 @@ Utils = {
},
MAX_IMAGE_PIXEL: Meteor.settings.public.MAX_IMAGE_PIXEL,
COMPRESS_RATIO: Meteor.settings.public.IMAGE_COMPRESS_RATIO,
- processUploadedAttachment(card, fileObj, callback) {
- const next = attachment => {
- if (typeof callback === 'function') {
- callback(attachment);
- }
- };
+ processUploadedAttachment(card, fileObj, callbacks) {
if (!card) {
- return next();
+ return onUploaded();
}
- const file = new FS.File(fileObj);
+ let settings = {
+ file: fileObj,
+ streams: 'dynamic',
+ chunkSize: 'dynamic',
+ };
+ settings.meta = {
+ uploading: true
+ };
if (card.isLinkedCard()) {
- file.boardId = Cards.findOne(card.linkedId).boardId;
- file.cardId = card.linkedId;
+ settings.meta.boardId = Cards.findOne(card.linkedId).boardId;
+ settings.meta.cardId = card.linkedId;
} else {
- file.boardId = card.boardId;
- file.swimlaneId = card.swimlaneId;
- file.listId = card.listId;
- file.cardId = card._id;
+ settings.meta.boardId = card.boardId;
+ settings.meta.swimlaneId = card.swimlaneId;
+ settings.meta.listId = card.listId;
+ settings.meta.cardId = card._id;
}
- file.userId = Meteor.userId();
- if (file.original) {
- file.original.name = fileObj.name;
+ settings.meta.userId = Meteor.userId();
+ if (typeof callbacks === 'function') {
+ settings.onEnd = callbacks;
+ } else {
+ for (const key in callbacks) {
+ if (key.substring(0, 2) === 'on') {
+ settings[key] = callbacks[key];
+ }
+ }
}
- return next(Attachments.insert(file));
+ Attachments.insert(settings);
},
shrinkImage(options) {
// shrink image to certain size