Merge remote-tracking branch 'upstream/devel' into devel

This commit is contained in:
Xavier Priour 2015-12-02 09:54:21 +01:00
commit 569f8d50ba
13 changed files with 270 additions and 19 deletions

View file

@ -63,7 +63,7 @@ id-map@1.0.4
idmontie:migrations@1.0.1
jquery@1.11.4
kadira:blaze-layout@2.2.0
kadira:dochead@1.3.2
kadira:dochead@1.4.0
kadira:flow-router@2.9.0
kenton:accounts-sandstorm@0.1.8
launch-screen@1.0.4
@ -95,7 +95,7 @@ mquandalle:jade@0.4.5
mquandalle:jade-compiler@0.4.5
mquandalle:jquery-textcomplete@0.8.0_1
mquandalle:jquery-ui-drag-drop-sort@0.1.0
mquandalle:moment@1.0.0
mquandalle:moment@1.0.1
mquandalle:mousetrap-bindglobal@0.0.1
mquandalle:perfect-scrollbar@0.6.5_2
mquandalle:stylus@1.1.1

View file

@ -2,14 +2,16 @@
This release features:
* Card import from Trello
* Trello boards and cards importation, including card history, assigned members,
labels, comments, and attachments;
* Autocompletion in the minicard editor. Start with <kbd>@</kbd> to start the
a board member autocompletion, or <kbd>#</kbd> for a label.
a board member autocompletion, or <kbd>#</kbd> for a label;
* Accelerate the initial page rendering by sending the data on the intial HTTP
response instead of waiting for the DDP connection to open.
response instead of waiting for the DDP connection to open;
* Support images attachments copy pasting.
Thanks to GitHub users AlexanderS, fisle, FuzzyWuzzie, ndarilek, and
xavierpriour for their contributions.
Thanks to GitHub users AlexanderS, fisle, floatinghotpot, FuzzyWuzzie, mnutt,
ndarilek, SirCmpwn, and xavierpriour for their contributions.
# v0.9

View file

@ -134,7 +134,7 @@ Template.boardBody.onRendered(function() {
if (!Meteor.user() || !Meteor.user().isBoardMember())
return;
self.$(self.listsDom).sortable({
$(self.listsDom).sortable({
tolerance: 'pointer',
helper: 'clone',
handle: '.js-list-header',
@ -146,7 +146,7 @@ Template.boardBody.onRendered(function() {
Popup.close();
},
stop() {
self.$('.js-lists').find('.js-list:not(.js-list-composer)').each(
$(self.listsDom).find('.js-list:not(.js-list-composer)').each(
(i, list) => {
const data = Blaze.getData(list);
Lists.update(data._id, {
@ -161,7 +161,7 @@ Template.boardBody.onRendered(function() {
// Disable drag-dropping while in multi-selection mode
self.autorun(() => {
self.$(self.listsDom).sortable('option', 'disabled',
$(self.listsDom).sortable('option', 'disabled',
MultiSelection.isActive());
});

View file

@ -3,6 +3,16 @@ template(name="cardAttachmentsPopup")
li
input.js-attach-file.hide(type="file" name="file" multiple)
a.js-computer-upload {{_ 'computer'}}
li
a.js-upload-clipboard-image {{_ 'clipboard'}}
template(name="previewClipboardImagePopup")
p <kbd>Ctrl</kbd>+<kbd>V</kbd> {{_ "paste-or-dragdrop"}}
img.preview-clipboard-image()
button.primary.js-upload-pasted-image {{_ 'upload'}}
template(name="previewAttachedImagePopup")
img.preview-large-image.js-large-image-clicked(src="{{pathFor url}}")
template(name="attachmentDeletePopup")
p {{_ "attachment-delete-pop"}}
@ -15,7 +25,7 @@ template(name="attachmentsGalery")
.attachment-thumbnail
if isUploaded
if isImage
img.attachment-thumbnail-img(src="{{pathFor url}}")
img.attachment-thumbnail-img.js-preview-image(src="{{pathFor url}}")
else
span.attachment-thumbnail-ext= extension
else

View file

@ -20,6 +20,39 @@ Template.attachmentsGalery.events({
'click .js-remove-cover'() {
Cards.findOne(this.cardId).unsetCover();
},
'click .js-preview-image'(evt) {
Popup.open('previewAttachedImage').call(this, evt);
// when multiple thumbnails, if click one then another very fast,
// we might get a wrong width from previous img.
// when popup reused, onRendered() won't be called, so we cannot get there.
// here make sure to get correct size when this img fully loaded.
const img = $('img.preview-large-image')[0];
if (!img) return;
const rePosPopup = () => {
const w = img.width;
const h = img.height;
// if the image is too large, we resize & center the popup.
if (w > 300) {
$('div.pop-over').css({
width: (w + 20),
position: 'absolute',
left: (window.innerWidth - w)/2,
top: (window.innerHeight - h)/2,
});
}
};
const url = $(evt.currentTarget).attr('src');
if (img.src === url && img.complete)
rePosPopup();
else
img.onload = rePosPopup;
},
});
Template.previewAttachedImagePopup.events({
'click .js-large-image-clicked'(){
Popup.close();
},
});
Template.cardAttachmentsPopup.events({
@ -28,7 +61,7 @@ Template.cardAttachmentsPopup.events({
FS.Utility.eachFile(evt, (f) => {
const file = new FS.File(f);
file.boardId = card.boardId;
file.cardId = card._id;
file.cardId = card._id;
Attachments.insert(file);
Popup.close();
@ -38,4 +71,48 @@ Template.cardAttachmentsPopup.events({
tpl.find('.js-attach-file').click();
evt.preventDefault();
},
'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'),
});
let pastedResults = null;
Template.previewClipboardImagePopup.onRendered(() => {
// we can paste image from clipboard
$(document.body).pasteImageReader((results) => {
if (results.dataURL.startsWith('data:image/')) {
$('img.preview-clipboard-image').attr('src', results.dataURL);
pastedResults = results;
}
});
// we can also drag & drop image file to it
$(document.body).dropImageReader((results) => {
if (results.dataURL.startsWith('data:image/')) {
$('img.preview-clipboard-image').attr('src', results.dataURL);
pastedResults = results;
}
});
});
Template.previewClipboardImagePopup.events({
'click .js-upload-pasted-image'() {
const results = pastedResults;
if (results && results.file) {
const card = this;
const file = new FS.File(results.file);
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.'));
}
}
file.updatedAt(new Date());
file.boardId = card.boardId;
file.cardId = card._id;
Attachments.insert(file);
pastedResults = null;
$(document.body).pasteImageReader(() => {});
Popup.close();
}
},
});

View file

@ -45,3 +45,14 @@
display: block
box-shadow: 0 1px 2px rgba(0,0,0,.2)
.preview-large-image
max-width: 1000px
display: block
box-shadow: 0 1px 2px rgba(0,0,0,.2)
.preview-clipboard-image
width: 280px
height: 200px
display: block
border: 1px solid black
box-shadow: 0 1px 2px rgba(0,0,0,.2)

View file

@ -14,11 +14,6 @@
padding: 5px 8px
margin: 5px
font-size: 18px
font-weight: bold
background: white
border-radius: 3px
border: 1px solid darken(white, 10%)
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.15)
.shortcuts-list-item-action
font-size: 1.4em

View file

@ -172,6 +172,15 @@ dl, dt
dd
margin: 0 0 16px 24px
kbd
padding: 1px 3px
margin: 3px
font-weight: bold
background: darken(white, 2%)
border-radius: 3px
border: 1px solid darken(white, 10%)
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.15)
.clear
clear: both

62
client/lib/dropImage.js Normal file
View file

@ -0,0 +1,62 @@
/* eslint-disable */
// ------------------------------------------------------------------------
// Created by STRd6
// MIT License
// https://github.com/distri/jquery-image_reader/blob/master/drop.coffee.md
//
// Raymond re-write it to javascript
(function($) {
$.event.fix = (function(originalFix) {
return function(event) {
event = originalFix.apply(this, arguments);
if (event.type.indexOf('drag') === 0 || event.type.indexOf('drop') === 0) {
event.dataTransfer = event.originalEvent.dataTransfer;
}
return event;
};
})($.event.fix);
const defaults = {
callback: $.noop,
matchType: /image.*/,
};
return $.fn.dropImageReader = function(options) {
if (typeof options === 'function') {
options = {
callback: options,
};
}
options = $.extend({}, defaults, options);
const stopFn = function(event) {
event.stopPropagation();
return event.preventDefault();
};
return this.each(function() {
const element = this;
$(element).bind('dragenter dragover dragleave', stopFn);
return $(element).bind('drop', function(event) {
stopFn(event);
const files = event.dataTransfer.files;
for(let i=0; i<files.length; i++) {
const f = files[i];
if(f.type.match(options.matchType)) {
const reader = new FileReader();
reader.onload = function(evt) {
return options.callback.call(element, {
dataURL: evt.target.result,
event: evt,
file: f,
name: f.name,
});
};
reader.readAsDataURL(f);
return;
}
}
});
});
};
})(jQuery);

57
client/lib/pasteImage.js Normal file
View file

@ -0,0 +1,57 @@
/* eslint-disable */
// ------------------------------------------------------------------------
// Created by STRd6
// MIT License
// https://github.com/distri/jquery-image_reader/blob/master/paste.coffee.md
//
// Raymond re-write it to javascript
(function($) {
$.event.fix = (function(originalFix) {
return function(event) {
event = originalFix.apply(this, arguments);
if (event.type.indexOf('copy') === 0 || event.type.indexOf('paste') === 0) {
event.clipboardData = event.originalEvent.clipboardData;
}
return event;
};
})($.event.fix);
const defaults = {
callback: $.noop,
matchType: /image.*/,
};
return $.fn.pasteImageReader = function(options) {
if (typeof options === 'function') {
options = {
callback: options,
};
}
options = $.extend({}, defaults, options);
return this.each(function() {
const element = this;
return $(element).bind('paste', function(event) {
const types = event.clipboardData.types;
const items = event.clipboardData.items;
for(let i=0; i<types.length; i++) {
if(types[i].match(options.matchType) || items[i].type.match(options.matchType)) {
const f = items[i].getAsFile();
const reader = new FileReader();
reader.onload = function(evt) {
return options.callback.call(element, {
dataURL: evt.target.result,
event: evt,
file: f,
name: f.name,
});
};
reader.readAsDataURL(f);
return;
}
}
});
});
};
})(jQuery);

View file

@ -46,3 +46,9 @@ AccountsTemplates.configureRoute('changePwd', {
Popup.back();
},
});
if (Meteor.isServer) {
if (process.env.MAIL_FROM) {
Accounts.emailTemplates.from = process.env.MAIL_FROM;
}
}

View file

@ -87,6 +87,7 @@
"changePermissionsPopup-title": "Change Permissions",
"click-to-star": "Click to star this board.",
"click-to-unstar": "Click to unstar this board.",
"clipboard" : "Clipboard or drag & drop",
"close": "Close",
"close-board": "Close Board",
"close-board-pop": "You can re-open the board by clicking the “Boards” menu from the header, selecting “View Closed Boards”, finding the board and clicking “Re-open”.",
@ -189,6 +190,10 @@
"page-maybe-private": "This page may be private. You may be able to view it by <a href='%s'>logging in</a>.",
"page-not-found": "Page not found.",
"password": "Password",
"paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)",
"preview": "Preview",
"previewClipboardImagePopup-title": "Preview",
"previewAttachedImagePopup-title": "Preview",
"private": "Private",
"private-desc": "This board is private. Only people added to the board can view and edit it.",
"profile": "Profile",
@ -228,6 +233,7 @@
"title": "Title",
"unassign-member": "Unassign member",
"unsaved-description": "You have an unsaved description.",
"upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"username": "Username",

View file

@ -54,10 +54,10 @@ if (isSandstorm && Meteor.isServer) {
// XXX If this routing scheme changes, this will break. We should generate
// the location URL using the router, but at the time of writing, the
// it is only accessible on the client.
const path = `/boards/${sandstormBoard._id}/${sandstormBoard.slug}`;
const boardPath = `/b/${sandstormBoard._id}/${sandstormBoard.slug}`;
res.writeHead(301, {
Location: base + path,
Location: base + boardPath,
});
res.end();
@ -126,6 +126,22 @@ if (isSandstorm && Meteor.isServer) {
}
if (isSandstorm && Meteor.isClient) {
// Since the Sandstorm grain is displayed in an iframe of the Sandstorm shell,
// we need to explicitly expose meta data like the page title or the URL path
// so that they could appear in the browser window.
// See https://docs.sandstorm.io/en/latest/developing/path/
function updateSandstormMetaData(msg) {
return window.parent.postMessage(msg, '*');
}
FlowRouter.triggers.enter([({ path }) => {
updateSandstormMetaData({ setPath: path });
}]);
Tracker.autorun(() => {
updateSandstormMetaData({ setTitle: DocHead.getTitle() });
});
// XXX Hack. `Meteor.absoluteUrl` doesn't work in Sandstorm, since every
// session has a different URL whereas Meteor computes absoluteUrl based on
// the ROOT_URL environment variable. So we overwrite this function on a