attachments image slider

stable
Juanfran 2016-05-20 15:01:03 +02:00 committed by Alejandro Alonso
parent a3345de76d
commit 389582bd1f
18 changed files with 638 additions and 32 deletions

View File

@ -7,6 +7,7 @@
- Show a confirmation notice when you exit edit mode by pressing ESC in the markdown inputs. - Show a confirmation notice when you exit edit mode by pressing ESC in the markdown inputs.
- Add the tribe button to link stories from tree.taiga.io with gigs in tribe.taiga.io. - Add the tribe button to link stories from tree.taiga.io with gigs in tribe.taiga.io.
- Errors (not found, server error, permissions and blocked project) don't change the current url. - Errors (not found, server error, permissions and blocked project) don't change the current url.
- Attachments image slider
### Misc ### Misc
- Lots of small and not so small bugfixes. - Lots of small and not so small bugfixes.

View File

@ -395,3 +395,50 @@ Autofocus = ($timeout) ->
} }
module.directive('tgAutofocus', ['$timeout', Autofocus]) module.directive('tgAutofocus', ['$timeout', Autofocus])
module.directive 'tgPreloadImage', () ->
spinner = "<img class='loading-spinner' src='/" + window._version + "/svg/spinner-circle.svg' alt='loading...' />"
template = """
<div>
<ng-transclude></ng-transclude>
</div>
"""
preload = (src, onLoad) ->
image = new Image()
image.onload = onLoad
image.src = src
return image
return {
template: template,
transclude: true,
replace: true,
link: (scope, el, attrs) ->
image = el.find('img:last')
timeout = null
onLoad = () ->
el.find('.loading-spinner').remove()
image.show()
if timeout
clearTimeout(timeout)
timeout = null
attrs.$observe 'preloadSrc', (src) ->
if timeout
clearTimeout(timeout)
el.find('.loading-spinner').remove()
timeout = setTimeout () ->
el.prepend(spinner)
, 200
image.hide()
preload(src, onLoad)
}

View File

@ -686,22 +686,6 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS
module.directive("tgLbWatchers", ["$tgRepo", "lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", "$compile", WatchersLightboxDirective]) module.directive("tgLbWatchers", ["$tgRepo", "lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", "$compile", WatchersLightboxDirective])
#############################################################################
## Attachment Preview Lighbox
#############################################################################
AttachmentPreviewLightboxDirective = (lightboxService, $template, $compile) ->
link = ($scope, $el, attrs) ->
lightboxService.open($el)
return {
templateUrl: 'common/lightbox/lightbox-attachment-preview.html',
link: link,
scope: true
}
module.directive("tgLbAttachmentPreview", ["lightboxService", "$tgTemplate", "$compile", AttachmentPreviewLightboxDirective])
LightboxLeaveProjectWarningDirective = (lightboxService, $template, $compile) -> LightboxLeaveProjectWarningDirective = (lightboxService, $template, $compile) ->
link = ($scope, $el, attrs) -> link = ($scope, $el, attrs) ->
lightboxService.open($el) lightboxService.open($el)

View File

@ -127,6 +127,26 @@
} }
.attachment-preview { .attachment-preview {
.attachment-preview-container {
svg {
@include svg-size(3rem);
fill: $gray-light;
&:hover {
fill: $primary-light;
transition: fill .3s linear;
}
}
}
.previous {
left: 3rem;
position: absolute;
top: calc(50% - 3rem);
}
.next {
position: absolute;
right: 3rem;
top: calc(50% - 3rem);
}
img { img {
max-height: 80vh; max-height: 80vh;
max-width: 80vw; max-width: 80vw;

View File

@ -17,7 +17,7 @@
# File: attachment-link.directive.coffee # File: attachment-link.directive.coffee
### ###
AttachmentLinkDirective = ($parse, lightboxFactory) -> AttachmentLinkDirective = ($parse, attachmentsPreviewService, lightboxService) ->
link = (scope, el, attrs) -> link = (scope, el, attrs) ->
attachment = $parse(attrs.tgAttachmentLink)(scope) attachment = $parse(attrs.tgAttachmentLink)(scope)
@ -26,11 +26,8 @@ AttachmentLinkDirective = ($parse, lightboxFactory) ->
event.preventDefault() event.preventDefault()
scope.$apply -> scope.$apply ->
lightboxFactory.create('tg-lb-attachment-preview', { lightboxService.open($('tg-attachments-preview'))
class: 'lightbox lightbox-block' attachmentsPreviewService.fileId = attachment.getIn(['file', 'id'])
}, {
file: attachment.get('file')
})
scope.$on "$destroy", -> el.off() scope.$on "$destroy", -> el.off()
return { return {
@ -39,7 +36,8 @@ AttachmentLinkDirective = ($parse, lightboxFactory) ->
AttachmentLinkDirective.$inject = [ AttachmentLinkDirective.$inject = [
"$parse", "$parse",
"tgLightboxFactory" "tgAttachmentsPreviewService",
"lightboxService"
] ]
angular.module("taigaComponents").directive("tgAttachmentLink", AttachmentLinkDirective) angular.module("taigaComponents").directive("tgAttachmentLink", AttachmentLinkDirective)

View File

@ -2,7 +2,7 @@
ng-class="{deprecated: vm.attachment.getIn(['file', 'is_deprecated'])}", ng-class="{deprecated: vm.attachment.getIn(['file', 'is_deprecated'])}",
ng-if="vm.attachment.getIn(['file', 'id'])", ng-if="vm.attachment.getIn(['file', 'id'])",
) )
a.attachment-image( a.attachment-image.e2e-attachment-link(
tg-attachment-link="vm.attachment" tg-attachment-link="vm.attachment"
href="{{::vm.attachment.getIn(['file', 'url'])}}" href="{{::vm.attachment.getIn(['file', 'url'])}}"
title="{{::vm.attachment.getIn(['file', 'name'])}}" title="{{::vm.attachment.getIn(['file', 'name'])}}"

View File

@ -5,7 +5,7 @@ form.single-attachment(
) )
.attachment-name .attachment-name
a( a.e2e-attachment-link(
tg-attachment-link="vm.attachment" tg-attachment-link="vm.attachment"
href="{{::vm.attachment.getIn(['file', 'url'])}}" href="{{::vm.attachment.getIn(['file', 'url'])}}"
title="{{::vm.attachment.get(['file', 'name'])}}" title="{{::vm.attachment.get(['file', 'name'])}}"

View File

@ -26,10 +26,11 @@ class AttachmentsFullController
"$tgConfig", "$tgConfig",
"$tgStorage", "$tgStorage",
"tgAttachmentsFullService", "tgAttachmentsFullService",
"tgProjectService" "tgProjectService",
"tgAttachmentsPreviewService"
] ]
constructor: (@translate, @confirm, @config, @storage, @attachmentsFullService, @projectService) -> constructor: (@translate, @confirm, @config, @storage, @attachmentsFullService, @projectService, @attachmentsPreviewService) ->
@.mode = @storage.get('attachment-mode', 'list') @.mode = @storage.get('attachment-mode', 'list')
@.maxFileSize = @config.get("maxUploadFileSize", null) @.maxFileSize = @config.get("maxUploadFileSize", null)
@ -64,6 +65,8 @@ class AttachmentsFullController
@attachmentsFullService.loadAttachments(@.type, @.objId, @.projectId) @attachmentsFullService.loadAttachments(@.type, @.objId, @.projectId)
deleteAttachment: (toDeleteAttachment) -> deleteAttachment: (toDeleteAttachment) ->
@attachmentsPreviewService.fileId = null
title = @translate.instant("ATTACHMENT.TITLE_LIGHTBOX_DELETE_ATTACHMENT") title = @translate.instant("ATTACHMENT.TITLE_LIGHTBOX_DELETE_ATTACHMENT")
message = @translate.instant("ATTACHMENT.MSG_LIGHTBOX_DELETE_ATTACHMENT", { message = @translate.instant("ATTACHMENT.MSG_LIGHTBOX_DELETE_ATTACHMENT", {
fileName: toDeleteAttachment.getIn(['file', 'name']) fileName: toDeleteAttachment.getIn(['file', 'name'])

View File

@ -94,3 +94,8 @@ section.attachments(
alt="{{'COMMON.LOADING' | translate}}" alt="{{'COMMON.LOADING' | translate}}"
) )
.attachment-data {{file.progressMessage}} .attachment-data {{file.progressMessage}}
tg-attachments-preview.lightbox.lightbox-block(
ng-show="vm.showAttachments()",
attachments="vm.attachments"
)

View File

@ -0,0 +1,75 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attchments-preview.controller.coffee
###
class AttachmentsPreviewController
@.$inject = [
"tgAttachmentsPreviewService"
]
constructor: (@attachmentsPreviewService) ->
taiga.defineImmutableProperty @, "current", () =>
if !@attachmentsPreviewService.fileId
return null
return @.getCurrent()
hasPagination: () ->
images = @.attachments.filter (attachment) =>
return taiga.isImage(attachment.getIn(['file', 'name']))
return images.size > 1
getCurrent: () ->
attachment = @.attachments.find (attachment) =>
@attachmentsPreviewService.fileId == attachment.getIn(['file', 'id'])
file = attachment.get('file')
return file
getIndex: () ->
return @.attachments.findIndex (attachment) =>
@attachmentsPreviewService.fileId == attachment.getIn(['file', 'id'])
next: () ->
attachmentIndex = @.getIndex()
image = @.attachments.slice(attachmentIndex + 1).find (attachment) ->
return taiga.isImage(attachment.getIn(['file', 'name']))
if !image
image = @.attachments.find (attachment) ->
return taiga.isImage(attachment.getIn(['file', 'name']))
@attachmentsPreviewService.fileId = image.getIn(['file', 'id'])
previous: () ->
attachmentIndex = @.getIndex()
image = @.attachments.slice(0, attachmentIndex).findLast (attachment) ->
return taiga.isImage(attachment.getIn(['file', 'name']))
if !image
image = @.attachments.findLast (attachment) ->
return taiga.isImage(attachment.getIn(['file', 'name']))
@attachmentsPreviewService.fileId = image.getIn(['file', 'id'])
angular.module('taigaComponents').controller('AttachmentsPreview', AttachmentsPreviewController)

View File

@ -0,0 +1,346 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attachments-preview.controller.spec.coffee
###
describe "AttachmentsPreviewController", ->
$provide = null
$controller = null
scope = null
mocks = {}
_mockAttachmentsPreviewService = ->
mocks.attachmentsPreviewService = {}
$provide.value("tgAttachmentsPreviewService", mocks.attachmentsPreviewService)
_mocks = ->
module (_$provide_) ->
$provide = _$provide_
_mockAttachmentsPreviewService()
return null
_inject = ->
inject (_$controller_, $rootScope) ->
$controller = _$controller_
scope = $rootScope.$new()
_setup = ->
_mocks()
_inject()
beforeEach ->
module "taigaComponents"
_setup()
it "get current file", () ->
attachment = Immutable.fromJS({
file: {
description: 'desc',
is_deprecated: false
}
})
ctrl = $controller("AttachmentsPreview", {
$scope: scope
})
ctrl.attachments = Immutable.fromJS([
{
file: {
id: 1
}
},
{
file: {
id: 2
}
},
{
file: {
id: 3
}
}
])
mocks.attachmentsPreviewService.fileId = 2;
current = ctrl.getCurrent()
expect(current.get('id')).to.be.equal(2)
expect(ctrl.current.get('id')).to.be.equal(2)
it "has pagination", () ->
attachment = Immutable.fromJS({
file: {
description: 'desc',
is_deprecated: false
}
})
ctrl = $controller("AttachmentsPreview", {
$scope: scope
})
ctrl.getIndex = sinon.stub().returns(0)
ctrl.attachments = Immutable.fromJS([
{
file: {
id: 1,
name: "xx"
}
},
{
file: {
id: 2,
name: "xx"
}
},
{
file: {
id: 3,
name: "xx.jpg"
}
}
])
mocks.attachmentsPreviewService.fileId = 1;
pagination = ctrl.hasPagination()
expect(pagination).to.be.false
ctrl.attachments = ctrl.attachments.push(Immutable.fromJS({
file: {
id: 4,
name: "xx.jpg"
}
}))
pagination = ctrl.hasPagination()
expect(pagination).to.be.true
it "get index", () ->
attachment = Immutable.fromJS({
file: {
description: 'desc',
is_deprecated: false
}
})
ctrl = $controller("AttachmentsPreview", {
$scope: scope
})
ctrl.attachments = Immutable.fromJS([
{
file: {
id: 1
}
},
{
file: {
id: 2
}
},
{
file: {
id: 3
}
}
])
mocks.attachmentsPreviewService.fileId = 2;
currentIndex = ctrl.getIndex()
expect(currentIndex).to.be.equal(1)
it "next", () ->
attachment = Immutable.fromJS({
file: {
description: 'desc',
is_deprecated: false
}
})
ctrl = $controller("AttachmentsPreview", {
$scope: scope
})
ctrl.getIndex = sinon.stub().returns(0)
ctrl.attachments = Immutable.fromJS([
{
file: {
id: 1,
name: "xx"
}
},
{
file: {
id: 2,
name: "xx"
}
},
{
file: {
id: 3,
name: "xx.jpg"
}
}
])
mocks.attachmentsPreviewService.fileId = 1;
currentIndex = ctrl.next()
expect(mocks.attachmentsPreviewService.fileId).to.be.equal(3)
it "next infinite", () ->
attachment = Immutable.fromJS({
file: {
description: 'desc',
is_deprecated: false
}
})
ctrl = $controller("AttachmentsPreview", {
$scope: scope
})
ctrl.getIndex = sinon.stub().returns(2)
ctrl.attachments = Immutable.fromJS([
{
file: {
id: 1,
name: "xx.jpg"
}
},
{
file: {
id: 2,
name: "xx"
}
},
{
file: {
id: 3,
name: "xx.jpg"
}
}
])
mocks.attachmentsPreviewService.fileId = 3;
currentIndex = ctrl.next()
expect(mocks.attachmentsPreviewService.fileId).to.be.equal(1)
it "previous", () ->
attachment = Immutable.fromJS({
file: {
description: 'desc',
is_deprecated: false
}
})
ctrl = $controller("AttachmentsPreview", {
$scope: scope
})
ctrl.getIndex = sinon.stub().returns(2)
ctrl.attachments = Immutable.fromJS([
{
file: {
id: 1,
name: "xx.jpg"
}
},
{
file: {
id: 2,
name: "xx"
}
},
{
file: {
id: 3,
name: "xx.jpg"
}
}
])
mocks.attachmentsPreviewService.fileId = 3;
currentIndex = ctrl.previous()
expect(mocks.attachmentsPreviewService.fileId).to.be.equal(1)
it "previous infinite", () ->
attachment = Immutable.fromJS({
file: {
description: 'desc',
is_deprecated: false
}
})
ctrl = $controller("AttachmentsPreview", {
$scope: scope
})
ctrl.getIndex = sinon.stub().returns(0)
ctrl.attachments = Immutable.fromJS([
{
file: {
id: 1,
name: "xx.jpg"
}
},
{
file: {
id: 2,
name: "xx"
}
},
{
file: {
id: 3,
name: "xx.jpg"
}
}
])
mocks.attachmentsPreviewService.fileId = 1;
currentIndex = ctrl.previous()
expect(mocks.attachmentsPreviewService.fileId).to.be.equal(3)

View File

@ -0,0 +1,48 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attachments-preview.directive.coffee
###
AttachmentPreviewLightboxDirective = (lightboxService, attachmentsPreviewService) ->
link = ($scope, el, attrs, ctrl) ->
$(document.body).on "keydown.image-preview", (e) ->
if attachmentsPreviewService.fileId
if e.keyCode == 39
ctrl.next()
else if e.keyCode == 37
ctrl.previous()
$scope.$digest()
$scope.$on '$destroy', () ->
$(document.body).off('.image-preview')
return {
scope: {},
controller: 'AttachmentsPreview',
templateUrl: 'components/attachments-preview/attachments-preview.html',
link: link,
controllerAs: "vm",
bindToController: {
attachments: "="
}
}
angular.module('taigaComponents').directive("tgAttachmentsPreview", [
"lightboxService",
"tgAttachmentsPreviewService",
AttachmentPreviewLightboxDirective])

View File

@ -0,0 +1,21 @@
.attachment-preview(ng-if="vm.attachments.size && vm.current")
tg-lightbox-close
.attachment-preview-container
a.previous(
href="#",
ng-click="vm.previous()",
ng-if="vm.hasPagination()"
)
tg-svg(svg-icon="icon-arrow-left")
a(href="{{vm.current.get('url')}}", title="{{vm.current.get('description')}}", target="_blank", download="{{vm.current.get('name')}}")
tg-preload-image(preload-src="{{vm.getCurrent().get('url')}}")
img(ng-src="{{vm.getCurrent().get('url')}}")
a.next(
href="#",
ng-click="vm.next()",
ng-if="vm.hasPagination()"
)
tg-svg(svg-icon="icon-arrow-right")

View File

@ -0,0 +1,25 @@
###
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: attachments-preview.service.coffee
###
class AttachmentsPreviewService extends taiga.Service
@.$inject = []
constructor: () ->
angular.module("taigaComponents").service("tgAttachmentsPreviewService", AttachmentsPreviewService)

View File

@ -1,5 +0,0 @@
.attachment-preview
tg-lightbox-close
a(href="{{::file.get('url')}}", title="{{::file.get('description')}}", target="_blank", download="{{::file.get('name')}}")
img(src="{{::file.get('url')}}")

View File

@ -417,6 +417,18 @@ helper.attachment = function() {
list: function() { list: function() {
$('.view-list').click(); $('.view-list').click();
}, },
previewLightbox: function() {
return utils.lightbox.open($('tg-attachments-preview'));
},
getPreviewSrc: function() {
return $('tg-attachments-preview img').getAttribute('src');
},
nextPreview: function() {
return $('tg-attachments-preview .next').click();
},
attachmentLinks: function() {
return $$('.e2e-attachment-link');
}
}; };
return obj; return obj;

View File

@ -315,6 +315,28 @@ shared.attachmentTesting = async function() {
attachmentHelper.list(); attachmentHelper.list();
// Gallery images
var fileToUploadImage = commonUtil.uploadImagePath();
await attachmentHelper.upload(fileToUploadImage, 'testing image ' + date);
await attachmentHelper.upload(fileToUpload, 'testing image ' + date);
await attachmentHelper.upload(fileToUploadImage, 'testing image ' + date);
await browser.sleep(5000);
attachmentHelper.attachmentLinks().last().click();
await attachmentHelper.previewLightbox();
let previewSrc = await attachmentHelper.getPreviewSrc();
await attachmentHelper.nextPreview();
let previewSrc2 = await attachmentHelper.getPreviewSrc();
expect(previewSrc).not.to.be.equal(previewSrc2);
await lightbox.exit();
// Deleting // Deleting
attachmentsLength = await attachmentHelper.countAttachments(); attachmentsLength = await attachmentHelper.countAttachments();
await attachmentHelper.deleteLastAttachment(); await attachmentHelper.deleteLastAttachment();

View File

@ -4,6 +4,10 @@ var lightbox = module.exports;
var transition = 300; var transition = 300;
lightbox.exit = function(el) { lightbox.exit = function(el) {
if (!el) {
el = $('.lightbox.open');
}
if (typeof el === 'string' || el instanceof String) { if (typeof el === 'string' || el instanceof String) {
el = $(el); el = $(el);
} }