Reduce taiga weight

- improve uglify
- remove jquery ui
- add dragula
- new autocomplete
stable
Juanfran 2016-02-11 10:48:05 +01:00 committed by Alejandro Alonso
parent 0e3e30d48e
commit dd0db6cd78
39 changed files with 1303 additions and 8985 deletions

View File

@ -601,6 +601,9 @@ i18nInit = (lang, $translate) ->
# i18n - moment.js # i18n - moment.js
moment.locale(lang) moment.locale(lang)
if (lang != 'en') # en is the default, the file doesn't exist
ljs.load "/#{window._version}/locales/moment-locales/" + lang + ".js"
# i18n - checksley.js # i18n - checksley.js
messages = { messages = {
defaultMessage: $translate.instant("COMMON.FORM_ERRORS.DEFAULT_MESSAGE") defaultMessage: $translate.instant("COMMON.FORM_ERRORS.DEFAULT_MESSAGE")

View File

@ -137,22 +137,31 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame, $tra
itemEl = null itemEl = null
tdom = $el.find(".sortable") tdom = $el.find(".sortable")
tdom.sortable({ drake = dragula([tdom[0]], {
handle: ".row.table-main.visualization", direction: 'vertical',
dropOnEmpty: true copySortSource: false,
connectWith: ".project-values-body" copy: false,
revert: 400 mirrorContainer: tdom[0],
axis: "y" moves: (item) -> return $(item).is('div[tg-bind-scope]')
}) })
tdom.on "sortstop", (event, ui) -> drake.on 'dragend', (item) ->
itemEl = ui.item itemEl = $(item)
itemValue = itemEl.scope().value itemValue = itemEl.scope().value
itemIndex = itemEl.index() itemIndex = itemEl.index()
$scope.$broadcast("admin:project-values:move", itemValue, itemIndex) $scope.$broadcast("admin:project-values:move", itemValue, itemIndex)
scroll = autoScroll(window, {
margin: 20,
pixels: 30,
scrollWhenOutside: true,
autoScroll: () ->
return this.down && drake.dragging;
})
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
drake.destroy()
## Value Link ## Value Link

View File

@ -139,8 +139,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@rootscope.$broadcast("filters:update") @rootscope.$broadcast("filters:update")
@scope.$on("sprint:us:move", @.moveUs) @scope.$on("sprint:us:move", @.moveUs)
@scope.$on("sprint:us:moved", @.loadSprints) @scope.$on "sprint:us:moved", () =>
@scope.$on("sprint:us:moved", @.loadProjectStats) @.loadSprints()
@.loadProjectStats()
@scope.$on("backlog:load-closed-sprints", @.loadClosedSprints) @scope.$on("backlog:load-closed-sprints", @.loadClosedSprints)
@scope.$on("backlog:unload-closed-sprints", @.unloadClosedSprints) @scope.$on("backlog:unload-closed-sprints", @.unloadClosedSprints)
@ -390,8 +391,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
# Persist in bulk all affected # Persist in bulk all affected
# userstories with order change # userstories with order change
@rs.userstories.bulkUpdateBacklogOrder(project, data).then => @rs.userstories.bulkUpdateBacklogOrder(project, data).then =>
for us in usList @rootscope.$broadcast("sprint:us:moved")
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId)
# For sprint # For sprint
else else
@ -402,8 +402,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
# Persist in bulk all affected # Persist in bulk all affected
# userstories with order change # userstories with order change
@rs.userstories.bulkUpdateSprintOrder(project, data).then => @rs.userstories.bulkUpdateSprintOrder(project, data).then =>
for us in usList @rootscope.$broadcast("sprint:us:moved")
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId)
return promise return promise
@ -430,7 +429,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
items = @.resortUserStories(@scope.userstories, "backlog_order") items = @.resortUserStories(@scope.userstories, "backlog_order")
data = @.prepareBulkUpdateData(items, "backlog_order") data = @.prepareBulkUpdateData(items, "backlog_order")
return @rs.userstories.bulkUpdateBacklogOrder(us.project, data).then => return @rs.userstories.bulkUpdateBacklogOrder(us.project, data).then =>
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) @rootscope.$broadcast("sprint:us:moved")
if movedFromClosedSprint if movedFromClosedSprint
@rootscope.$broadcast("backlog:load-closed-sprints") @rootscope.$broadcast("backlog:load-closed-sprints")
@ -443,17 +442,15 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
# From backlog to sprint # From backlog to sprint
if oldSprintId == null if oldSprintId == null
us.milestone = newSprintId for us in usList us.milestone = newSprintId for us in usList
args = [newUsIndex, 0].concat(usList)
@scope.$apply => # Add moving us to sprint user stories list
args = [newUsIndex, 0].concat(usList) Array.prototype.splice.apply(newSprint.user_stories, args)
# Add moving us to sprint user stories list # Remove moving us from backlog userstories lists.
Array.prototype.splice.apply(newSprint.user_stories, args) for us, key in usList
r = @scope.userstories.indexOf(us)
# Remove moving us from backlog userstories lists. @scope.userstories.splice(r, 1)
for us, key in usList
r = @scope.userstories.indexOf(us)
@scope.userstories.splice(r, 1)
# From sprint to sprint # From sprint to sprint
else else
@ -470,21 +467,20 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
r = sprint.user_stories.indexOf(us) r = sprint.user_stories.indexOf(us)
sprint.user_stories.splice(r, 1) sprint.user_stories.splice(r, 1)
# Persist the milestone change of userstory #Persist the milestone change of userstory
promises = _.map usList, (us) => @repo.save(us) promises = _.map usList, (us) => @repo.save(us)
# Rehash userstories order field #Rehash userstories order field
# and persist in bulk all changes. #and persist in bulk all changes.
promise = @q.all(promises).then => promise = @q.all(promises).then =>
items = @.resortUserStories(newSprint.user_stories, "sprint_order") items = @.resortUserStories(newSprint.user_stories, "sprint_order")
data = @.prepareBulkUpdateData(items, "sprint_order") data = @.prepareBulkUpdateData(items, "sprint_order")
@rs.userstories.bulkUpdateSprintOrder(project, data).then (result) => @rs.userstories.bulkUpdateSprintOrder(project, data).then (result) =>
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) @rootscope.$broadcast("sprint:us:moved")
@rs.userstories.bulkUpdateBacklogOrder(project, data).then => @rs.userstories.bulkUpdateBacklogOrder(project, data).then =>
for us in usList @rootscope.$broadcast("sprint:us:moved")
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId)
if movedToClosedSprint || movedFromClosedSprint if movedToClosedSprint || movedFromClosedSprint
@scope.$broadcast("backlog:load-closed-sprints") @scope.$broadcast("backlog:load-closed-sprints")
@ -680,7 +676,7 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
return _.map(rowElements, (x) -> angular.element(x)) return _.map(rowElements, (x) -> angular.element(x))
$scope.$on("userstories:loaded", reloadDoomLine) $scope.$on("userstories:loaded", reloadDoomLine)
$scope.$watch "stats", reloadDoomLine $scope.$watch("stats", reloadDoomLine)
## Move to current sprint link ## Move to current sprint link
@ -835,8 +831,6 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
linkFilters($scope, $el, $attrs, $ctrl) linkFilters($scope, $el, $attrs, $ctrl)
linkDoomLine($scope, $el, $attrs, $ctrl) linkDoomLine($scope, $el, $attrs, $ctrl)
$el.find(".backlog-table-body").disableSelection()
filters = $ctrl.getUrlFilters() filters = $ctrl.getUrlFilters()
if filters.status || if filters.status ||
filters.tags || filters.tags ||

View File

@ -38,187 +38,117 @@ module = angular.module("taigaBacklog")
############################################################################# #############################################################################
deleteElement = (el) -> deleteElement = (el) ->
el.scope().$destroy() $(el).scope().$destroy()
el.off() $(el).off()
el.remove() $(el).remove()
BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) -> BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) ->
# Notes about jquery bug:
# http://stackoverflow.com/questions/5791886/jquery-draggable-shows-
# helper-in-wrong-place-when-scrolled-down-page
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
getUsIndex = (us) =>
return $(us).index(".backlog-table-body .row")
bindOnce $scope, "project", (project) -> bindOnce $scope, "project", (project) ->
# If the user has not enough permissions we don't enable the sortable # If the user has not enough permissions we don't enable the sortable
if not (project.my_permissions.indexOf("modify_us") > -1) if not (project.my_permissions.indexOf("modify_us") > -1)
return return
initIsBacklog = false
filterError = -> filterError = ->
text = $translate.instant("BACKLOG.SORTABLE_FILTER_ERROR") text = $translate.instant("BACKLOG.SORTABLE_FILTER_ERROR")
$tgConfirm.notify("error", text) $tgConfirm.notify("error", text)
$el.sortable({ drake = dragula([$el[0], $('.empty-backlog')[0]], {
items: ".us-item-row", copySortSource: false,
cancel: ".popover" copy: false,
connectWith: ".sprint" isContainer: (el) -> return el.classList.contains('sprint-table'),
dropOnEmpty: true moves: (item) ->
placeholder: "row us-item-row us-item-drag sortable-placeholder" if !$(item).hasClass('row')
scroll: true return false
disableHorizontalScroll: true
# A consequence of length of backlog user story item
# the default tolerance ("intersection") not works properly.
tolerance: "pointer"
# Revert on backlog is disabled bacause it works bad. Something
# on the current taiga backlog structure or style makes jquery ui
# works unexpectly (in some circumstances calculates wrong
# position for revert).
revert: false
start: () ->
$(document.body).addClass("drag-active")
stop: () ->
$(document.body).removeClass("drag-active")
if $el.hasClass("active-filters") # it doesn't move is the filter is open
$el.sortable("cancel") parent = $(item).parent()
initIsBacklog = parent.hasClass('backlog-table-body')
if initIsBacklog && $el.hasClass("active-filters")
filterError() filterError()
return false
return true
}) })
$el.on "multiplesortreceive", (event, ui) -> drake.on 'drag', (item, container) ->
if $el.hasClass("active-filters") parent = $(item).parent()
ui.source.sortable("cancel") initIsBacklog = parent.hasClass('backlog-table-body')
filterError()
return $(document.body).addClass("drag-active")
itemUs = ui.item.scope().us isChecked = $(item).find("input[type='checkbox']").is(":checked")
itemIndex = getUsIndex(ui.item)
deleteElement(ui.item) window.dragMultiple.start(item, container)
$scope.$emit("sprint:us:move", [itemUs], itemIndex, null) drake.on 'cloned', (item) ->
ui.item.find('a').removeClass('noclick') $(item).addClass('backlog-us-mirror')
$el.on "multiplesortstop", (event, ui) -> drake.on 'dragend', (item) ->
# When parent not exists, do nothing $('.doom-line').remove()
if $(ui.items[0]).parent().length == 0
return
if $el.hasClass("active-filters") parent = $(item).parent()
return isBacklog = parent.hasClass('backlog-table-body') || parent.hasClass('empty-backlog')
items = _.sortBy ui.items, (item) -> sameContainer = (initIsBacklog == isBacklog)
return $(item).index()
index = _.min _.map items, (item) -> dragMultipleItems = window.dragMultiple.stop()
return getUsIndex(item)
us = _.map items, (item) -> $(document.body).removeClass("drag-active")
item = $(item)
itemUs = item.scope().us
# HACK: setTimeout prevents that firefox click items = $(item).parent().find('.row')
# event fires just after drag ends
setTimeout ( =>
item.find('a').removeClass('noclick')
), 300
return itemUs sprint = null
$scope.$emit("sprint:us:move", us, index, null) firstElement = if dragMultipleItems.length then dragMultipleItems[0] else item
$el.on "sortstart", (event, ui) -> if isBacklog
ui.item.find('a').addClass('noclick') index = $(firstElement).index(".backlog-table-body .row")
else
index = $(firstElement).index()
sprint = parent.scope().sprint.id
$scope.$on "$destroy", -> if !sameContainer
$el.off() if dragMultipleItems.length
usList = _.map dragMultipleItems, (item) ->
return {link: link} return item = $(item).scope().us
else
BacklogEmptySortableDirective = ($repo, $rs, $rootscope) -> usList = [$(item).scope().us]
# Notes about jquery bug:
# http://stackoverflow.com/questions/5791886/jquery-draggable-shows-
# helper-in-wrong-place-when-scrolled-down-page
link = ($scope, $el, $attrs) ->
bindOnce $scope, "project", (project) ->
# If the user has not enough permissions we don't enable the sortable
if project.my_permissions.indexOf("modify_us") > -1
$el.sortable({
items: ".us-item-row",
dropOnEmpty: true
})
$el.on "sortreceive", (event, ui) ->
itemUs = ui.item.scope().us
itemIndex = ui.item.index()
deleteElement(ui.item)
$scope.$emit("sprint:us:move", [itemUs], itemIndex, null)
ui.item.find('a').removeClass('noclick')
$scope.$on "$destroy", ->
$el.off()
return {link: link}
SprintSortableDirective = ($repo, $rs, $rootscope) ->
link = ($scope, $el, $attrs) ->
bindOnce $scope, "project", (project) ->
# If the user has not enough permissions we don't enable the sortable
if project.my_permissions.indexOf("modify_us") > -1
$el.sortable({
scroll: true
dropOnEmpty: true
items: ".sprint-table .milestone-us-item-row"
disableHorizontalScroll: true
connectWith: ".sprint,.backlog-table-body,.empty-backlog"
placeholder: "row us-item-row sortable-placeholder"
forcePlaceholderSize:true
})
$el.on "multiplesortreceive", (event, ui) ->
items = _.sortBy ui.items, (item) ->
return $(item).index()
index = _.min _.map items, (item) ->
return $(item).index()
us = _.map items, (item) ->
item = $(item)
itemUs = item.scope().us
if (dragMultipleItems.length)
_.each dragMultipleItems, (item) ->
deleteElement(item)
else
deleteElement(item) deleteElement(item)
else
if dragMultipleItems.length
usList = _.map dragMultipleItems, (item) ->
return item = $(item).scope().us
else
usList = _.map items, (item) ->
item = $(item)
itemUs = item.scope().us
return itemUs return itemUs
$scope.$emit("sprint:us:move", us, index, $scope.sprint.id) $scope.$emit("sprint:us:move", usList, index, sprint)
$el.on "multiplesortstop", (event, ui) -> scroll = autoScroll([window], {
# When parent not exists, do nothing margin: 20,
if ui.item.parent().length == 0 pixels: 30,
return scrollWhenOutside: true,
autoScroll: () ->
return this.down && drake.dragging;
})
itemUs = ui.item.scope().us $scope.$on "$destroy", ->
itemIndex = ui.item.index() $el.off()
drake.destroy()
# HACK: setTimeout prevents that firefox click
# event fires just after drag ends
setTimeout ( =>
ui.item.find('a').removeClass('noclick')
), 300
$scope.$emit("sprint:us:move", [itemUs], itemIndex, $scope.sprint.id)
$el.on "sortstart", (event, ui) ->
ui.item.find('a').addClass('noclick')
return {link:link}
return {link: link}
module.directive("tgBacklogSortable", [ module.directive("tgBacklogSortable", [
"$tgRepo", "$tgRepo",
@ -228,17 +158,3 @@ module.directive("tgBacklogSortable", [
"$translate", "$translate",
BacklogSortableDirective BacklogSortableDirective
]) ])
module.directive("tgBacklogEmptySortable", [
"$tgRepo",
"$tgResources",
"$rootScope",
BacklogEmptySortableDirective
])
module.directive("tgSprintSortable", [
"$tgRepo",
"$tgResources",
"$rootScope",
SprintSortableDirective
])

View File

@ -108,6 +108,8 @@ LbTagLineDirective = ($rs, $template, $compile) ->
templateTags = $template.get("common/tag/lb-tag-line-tags.html", true) templateTags = $template.get("common/tag/lb-tag-line-tags.html", true)
autocomplete = null
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
## Render ## Render
renderTags = (tags, tagsColors) -> renderTags = (tags, tagsColors) ->
@ -130,7 +132,7 @@ LbTagLineDirective = ($rs, $template, $compile) ->
resetInput = -> resetInput = ->
$el.find("input").val("") $el.find("input").val("")
$el.find("input").autocomplete("close") autocomplete.close()
## Aux methods ## Aux methods
addValue = (value) -> addValue = (value) ->
@ -190,22 +192,15 @@ LbTagLineDirective = ($rs, $template, $compile) ->
deleteValue(value) deleteValue(value)
bindOnce $scope, "project", (project) -> bindOnce $scope, "project", (project) ->
positioningFunction = (position, elements) -> input = $el.find("input")
menu = elements.element.element
menu.css("width", elements.target.width)
menu.css("top", position.top)
menu.css("left", position.left)
$el.find("input").autocomplete({ autocomplete = new Awesomplete(input[0], {
source: _.keys(project.tags_colors) list: _.keys(project.tags_colors)
position: { });
my: "left top",
using: positioningFunction input.on "awesomplete-selectcomplete", () ->
} addValue(input.val())
select: (event, ui) -> input.val("")
addValue(ui.item.value)
ui.item.value = ""
})
$scope.$watch $attrs.ngModel, (tags) -> $scope.$watch $attrs.ngModel, (tags) ->
tagsColors = $scope.project?.tags_colors or [] tagsColors = $scope.project?.tags_colors or []
@ -235,6 +230,8 @@ TagLineDirective = ($rootScope, $repo, $rs, $confirm, $qqueue, $template, $compi
templateTags = $template.get("common/tag/tags-line-tags.html", true) templateTags = $template.get("common/tag/tags-line-tags.html", true)
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
autocomplete = null
isEditable = -> isEditable = ->
if $attrs.requiredPerm? if $attrs.requiredPerm?
return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1 return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
@ -268,7 +265,8 @@ TagLineDirective = ($rootScope, $repo, $rs, $confirm, $qqueue, $template, $compi
hideInput = -> $el.find("input").addClass("hidden").blur() hideInput = -> $el.find("input").addClass("hidden").blur()
resetInput = -> resetInput = ->
$el.find("input").val("") $el.find("input").val("")
$el.find("input").autocomplete("close")
autocomplete.close()
## Aux methods ## Aux methods
addValue = $qqueue.bindAdd (value) -> addValue = $qqueue.bindAdd (value) ->
@ -366,22 +364,15 @@ TagLineDirective = ($rootScope, $repo, $rs, $confirm, $qqueue, $template, $compi
showAddTagButton() showAddTagButton()
positioningFunction = (position, elements) -> input = $el.find("input")
menu = elements.element.element
menu.css("width", elements.target.width)
menu.css("top", position.top)
menu.css("left", position.left)
$el.find("input").autocomplete({ autocomplete = new Awesomplete(input[0], {
source: _.keys(tags_colors) list: _.keys(tags_colors)
position: { });
my: "left top",
using: positioningFunction input.on "awesomplete-selectcomplete", () ->
} addValue(input.val())
select: (event, ui) -> input.val("")
addValue(ui.item.value)
ui.item.value = ""
})
$scope.$watch $attrs.ngModel, (model) -> $scope.$watch $attrs.ngModel, (model) ->
return if not model return if not model

View File

@ -418,8 +418,6 @@ module.directive("tgKanbanArchivedStatusIntro", ["$translate", KanbanArchivedSta
KanbanUserstoryDirective = ($rootscope, $loading, $rs, $rs2) -> KanbanUserstoryDirective = ($rootscope, $loading, $rs, $rs2) ->
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
$el.disableSelection()
$scope.$watch "us", (us) -> $scope.$watch "us", (us) ->
if us.is_blocked and not $el.hasClass("blocked") if us.is_blocked and not $el.hasClass("blocked")
$el.addClass("blocked") $el.addClass("blocked")
@ -498,8 +496,6 @@ module.directive("tgKanbanSquishColumn", ["$tgResources", KanbanSquishColumnDire
KanbanWipLimitDirective = -> KanbanWipLimitDirective = ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
$el.disableSelection()
status = $scope.$eval($attrs.tgKanbanWipLimit) status = $scope.$eval($attrs.tgKanbanWipLimit)
redrawWipLimit = => redrawWipLimit = =>

View File

@ -55,16 +55,23 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
itemEl.off() itemEl.off()
itemEl.remove() itemEl.remove()
tdom.sortable({ containers = _.map $el.find('.task-column'), (item) ->
handle: ".kanban-task-inner" return item
dropOnEmpty: true
connectWith: ".kanban-uses-box" drake = dragula(containers, {
revert: 400 copySortSource: false,
copy: false,
mirrorContainer: tdom[0],
moves: (item) ->
return $(item).hasClass('kanban-task')
}) })
tdom.on "sortstop", (event, ui) -> drake.on 'drag', (item) ->
parentEl = ui.item.parent() oldParentScope = $(item).parent().scope()
itemEl = ui.item
drake.on 'dragend', (item) ->
parentEl = $(item).parent()
itemEl = $(item)
itemUs = itemEl.scope().us itemUs = itemEl.scope().us
itemIndex = itemEl.index() itemIndex = itemEl.index()
newParentScope = parentEl.scope() newParentScope = parentEl.scope()
@ -78,14 +85,17 @@ KanbanSortableDirective = ($repo, $rs, $rootscope) ->
$scope.$apply -> $scope.$apply ->
$rootscope.$broadcast("kanban:us:move", itemUs, itemUs.status, newStatusId, itemIndex) $rootscope.$broadcast("kanban:us:move", itemUs, itemUs.status, newStatusId, itemIndex)
ui.item.find('a').removeClass('noclick') scroll = autoScroll(containers, {
margin: 20,
pixels: 30,
scrollWhenOutside: true,
autoScroll: () ->
return this.down && drake.dragging;
})
tdom.on "sortstart", (event, ui) -> $scope.$on "$destroy", ->
oldParentScope = ui.item.parent().scope() $el.off()
ui.item.find('a').addClass('noclick') drake.destroy()
$scope.$on "$destroy", ->
$el.off()
return {link: link} return {link: link}

View File

@ -309,8 +309,6 @@ module.directive("tgTaskboard", ["$rootScope", TaskboardDirective])
TaskboardTaskDirective = ($rootscope, $loading, $rs, $rs2) -> TaskboardTaskDirective = ($rootscope, $loading, $rs, $rs2) ->
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
$el.disableSelection()
$scope.$watch "task", (task) -> $scope.$watch "task", (task) ->
if task.is_blocked and not $el.hasClass("blocked") if task.is_blocked and not $el.hasClass("blocked")
$el.addClass("blocked") $el.addClass("blocked")

View File

@ -39,9 +39,9 @@ module = angular.module("taigaBacklog")
TaskboardSortableDirective = ($repo, $rs, $rootscope) -> TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
bindOnce $scope, "project", (project) -> bindOnce $scope, "tasks", (xx) ->
# If the user has not enough permissions we don't enable the sortable # If the user has not enough permissions we don't enable the sortable
if not (project.my_permissions.indexOf("modify_us") > -1) if not ($scope.project.my_permissions.indexOf("modify_us") > -1)
return return
oldParentScope = null oldParentScope = null
@ -55,16 +55,22 @@ TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
itemEl.off() itemEl.off()
itemEl.remove() itemEl.remove()
tdom.sortable({ containers = _.map $el.find('.task-column'), (item) ->
handle: ".taskboard-task-inner", return item
dropOnEmpty: true
connectWith: ".taskboard-tasks-box" drake = dragula(containers, {
revert: 400 copySortSource: false,
copy: false,
mirrorContainer: $el[0],
moves: (item) -> return $(item).hasClass('taskboard-task')
}) })
tdom.on "sortstop", (event, ui) -> drake.on 'drag', (item) ->
parentEl = ui.item.parent() oldParentScope = $(item).parent().scope()
itemEl = ui.item
drake.on 'dragend', (item) ->
parentEl = $(item).parent()
itemEl = $(item)
itemTask = itemEl.scope().task itemTask = itemEl.scope().task
itemIndex = itemEl.index() itemIndex = itemEl.index()
newParentScope = parentEl.scope() newParentScope = parentEl.scope()
@ -80,14 +86,17 @@ TaskboardSortableDirective = ($repo, $rs, $rootscope) ->
$scope.$apply -> $scope.$apply ->
$rootscope.$broadcast("taskboard:task:move", itemTask, newUsId, newStatusId, itemIndex) $rootscope.$broadcast("taskboard:task:move", itemTask, newUsId, newStatusId, itemIndex)
ui.item.find('a').removeClass('noclick') scroll = autoScroll(containers, {
margin: 20,
pixels: 30,
scrollWhenOutside: true,
autoScroll: () ->
return this.down && drake.dragging;
})
tdom.on "sortstart", (event, ui) -> $scope.$on "$destroy", ->
oldParentScope = ui.item.parent().scope() $el.off()
ui.item.find('a').addClass('noclick') drake.destroy()
$scope.$on "$destroy", ->
$el.off()
return {link: link} return {link: link}

573
app/js/dom-autoscroller.js Normal file
View File

@ -0,0 +1,573 @@
// The MIT License (MIT)
// Copyright (c) 2016 Quentin Engles
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
(function() {
// more_events
function MoreEvents(context){
this.listeners = {};
this.__context = context || this;
}
MoreEvents.prototype = {
constructor: MoreEvents,
on: function(event, listener){
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push(listener);
return this;
},
one: function(event, listener){
function onceListener(){
listener.apply(this, arguments);
this.off(event, onceListener);
return this;
}
return this.on(event, onceListener);
},
emit: function(event){
if(typeof this.listeners[event] === 'undefined' || !this.listeners[event].length)
return this;
var args = Array.prototype.slice.call(arguments, 1),
canRun = this.listeners[event].length;
do{
this.listeners[event][--canRun].apply(this.__context, args);
}while(canRun);
return this;
},
off: function(event, listener){
if(this.listeners[event] === undefined || !this.listeners[event].length)
return this;
this.listeners[event] = this.listeners[event].filter(function(item){
return item !== listener;
});
return this;
},
dispose: function(){
for(var n in this){
this[n] = null;
}
}
};
// pointer_point
var Emitter = MoreEvents;
if(!Date.now){ Date.now = function(){ return new Date().getTime() } }
function LocalDimensions(point, rect){
for(var n in rect)
setProp(this, n, rect[n]);
setProp(this, 'x', point.x - rect.left+1);
setProp(this, 'y', point.y - rect.top+1);
setProp(this, 'north', (((rect.bottom - rect.top) / 2)-this.y));
setProp(this, 'south', ((-(rect.bottom - rect.top) / 2)+this.y));
setProp(this, 'east', (((rect.right - rect.left) / 2)-this.x));
setProp(this, 'west', ((-(rect.right - rect.left) / 2)+this.x));
function setProp(self, name, value){
Object.defineProperty(self, name, {
value: value,
configurable: true,
writable: false
});
}
}
function Point(elements){
var self = this, el = [];
if(typeof elements.length === 'undefined'){
elements = [elements];
}
for(var i=0; i<elements.length; i++){
if(elements[i] !== undefined){
if(typeof elements[i] === 'string'){
try{
el.push(document.querySelector(e));
}catch(err){
throw new Error(e + ' is not a valid selector used by pointer.');
}
}else{
el.push(elements[i]);
}
}
}
var pos = {}, direction = {}, rect, local,
lastmousex=-1, lastmousey=-1, timestamp, mousetravel = 0,
startX=-1, startY=-1, scrolling = false, buf = 10, timeOut = false,
downTime;
var special = {
hold: []
};
this.emitter = new Emitter(this);
this.origin = null;
this.current = null;
this.previous = null;
window.addEventListener('mousedown', onDown, false);
window.addEventListener('mousemove', onMove, false);
window.addEventListener("mouseup", onUp, false);
window.addEventListener('touchstart', onDown, false);
window.addEventListener('touchmove', onMove, false);
window.addEventListener('touchend', onUp, false);
window.addEventListener('scroll', function(e){
scrolling = true;
clearTimeout(timeOut)
timeOut = setTimeout(function(){
scrolling = false;
}, 100)
});
function onDown(e){
downTime = Date.now();
toPoint(e);
self.down = true;
self.up = false;
if(self.current){
self.origin = self.current;
self.emitter.emit('down', self.current, local);
}
startX = self.x;
startY = self.y;
}
function onMove(e){
toPoint(e);
self.emitter.emit('move', self.current, local);
if(self.down && self.current){
self.emitter.emit('stroke', self.current, local);
}
}
function onUp(e){
self.down = false;
self.up = true;
if(self.current){
self.emitter.emit('up', self.current, local);
}
if(e.targetTouches){
//Allow click within buf. A 20x20 square.
if(!(self.y > (startY - buf) && self.y < (startY + buf) &&
self.x > (startX - buf) && self.x < (startX + buf))){
//If there is scrolling there was a touch flick.
if(!scrolling){
//No touch flick so
self.previous = null;
self.origin = null;
e.preventDefault();
return false;
}
}
}
scrolling = false;
self.previous = null;
self.origin = null;
}
function toPoint(event){
var dot, eventDoc, doc, body, pageX, pageY;
var target, newTarget = null, leaving = null;
event = event || window.event; // IE-ism
target = event.target || event.srcElement;
//Supporting touch
//http://www.creativebloq.com/javascript/make-your-site-work-touch-devices-51411644
if(event.targetTouches) {
event.pageX = event.targetTouches[0].clientX;
event.pageY = event.targetTouches[0].clientY;
event.clientX = event.targetTouches[0].clientX;
event.clientY = event.targetTouches[0].clientY;
}else
// If pageX/Y aren't available and clientX/Y are,
// calculate pageX/Y - logic taken from jQuery.
// (This is to support old IE)
if (event.pageX === null && event.clientX !== null) {
eventDoc = (event.target && event.target.ownerDocument) || document;
doc = eventDoc.documentElement;
body = eventDoc.body;
event.pageX = event.clientX +
(doc && doc.scrollLeft || body && body.scrollLeft || 0) -
(doc && doc.clientLeft || body && body.clientLeft || 0);
event.pageY = event.clientY +
(doc && doc.scrollTop || body && body.scrollTop || 0) -
(doc && doc.clientTop || body && body.clientTop || 0 );
}
if(self.x && self.y){
if(event.pageX < self.x)
direction.h = 'left';
else if(event.pageX > self.x)
direction.h = 'right';
if(event.pageY < self.y)
direction.v = 'up';
else if(event.pageY > self.y)
direction.v = 'down';
lastmousex = self.x;
lastmousey = self.y;
}
pos = {};
//Prefer the viewport with clientX, and clientY.
//pageX, and pageY change too often.
pos.x = event.clientX;//event.pageX;
pos.y = event.clientY;//event.pageY;
if(self.current === null || self.outside(self.current)){
for(var i=0; i<el.length; i++){
//console.log('inside el['+i+'] '+self.inside(el[i]));
if(el[i] === target || self.inside(el[i])){
//if(el[i] === target){
newTarget = el[i];
break;
}
}
leaving = self.current;
if(newTarget){
self.previous = self.current;
self.current = newTarget;
}
}
rect = self.current ? getRect(self.current) : null;
local = rect ? new LocalDimensions(self, rect) : null;
if(leaving){
if(!newTarget)
self.current = null;
self.emitter.emit('leave', leaving, local);
}
if(newTarget){
self.emitter.emit('enter', self.current, local);
}
}
//Get speed
//http://stackoverflow.com/questions/6417036/track-mouse-speed-with-js
Object.defineProperty(this, 'speedX', {
get: function(){
var now = Date.now() / 1000;
var dt = now - timestamp;
var dx = self.x - lastmousex;
timestamp = now;
return Math.round(dx / dt);// * 1000);
}
});
Object.defineProperty(this, 'speedY', {
get: function(){
var now = Date.now() / 1000;
var dt = now - timestamp;
var dy = self.y - lastmousey;
timestamp = now;
return Math.round(dy / dt);// * 1000);
}
});
Object.defineProperty(this, 'x', {
get: function(){
return pos.x;
}
});
Object.defineProperty(this, 'y', {
get: function(){
return pos.y;
}
});
Object.defineProperty(this, 'h', {
get: function(){
return direction.h;
}
});
Object.defineProperty(this, 'v', {
get: function(){
return direction.v;
}
});
this.emitter.on('up', function(el, rect){
if(downTime){
for(var i=0; i<special.hold.length; i++){
if(Date.now() > downTime + (special.hold[i].data || 2000)){
special.hold[i].callback.call(this, el, rect);
}
}
}
downTime = 0;
});
function removeSpecial(event, cb){
for(var i=0; i<special[event].length; i++){
if(special[event][i].callback === cb){
special[event].splice(i, 1);
return;
}
}
}
function addSpecial(event, data, cb){
if(typeof cb === 'undefined'){
cb = data;
data = null;
}
special[event].push({
data: data,
callback: cb
})
}
this.on = function(event, cb){
if(special[event]){
addSpecial(event, cb, arguments[2]);
return this;
}
this.emitter.on(event, cb);
return this;
};
this.off = function(event, cb){
if(special[event]){
removeSpecial(event, cb);
return this;
}
this.emitter.off(event, cb);
return this;
};
this.add = function(element){
if(typeof element === 'string'){
try{
el.push(document.querySelector(e));
}catch(err){
throw new Error(e + ' is not a valid selector, and can\'t be used add to pointer.');
}
}else if(!element){
throw new Error(e + ' can not be added to pointer.');
}
el.push(element);
};
this.destroy = function(){
window.removeEventListener('mousedown', onDown, false);
window.removeEventListener('mousemove', onMove, false);
window.removeEventListener('mouseup', onUp, false);
window.removeEventListener('touchstart', onDown, false);
window.removeEventListener('touchmove', onMove, false);
window.removeEventListener('touchend', onUp, false);
el = null;
self = null;
pos = null;
direction = null;
};
}
Point.prototype = {
constructor: Point,
inside: function(el){
if(!el) throw new TypeError('Cannot be inside '+el);
var rect = getRect(el);
return (this.y > rect.top && this.y < rect.bottom &&
this.x > rect.left && this.x < rect.right);
},
outside: function(el){
if(!el) throw new TypeError('Cannot be outside '+el);
return !this.inside(el);
}
};
function elementFromPoint(x, y){
if(document.getElementFromPoint)
return document.getElementFromPoint(x, y);
else
return document.elementFromPoint(x, y);
return null;
}
function safeObject(src){
var obj = {};
for(var n in src)
obj[n] = src[n];
return obj;
}
function getRect(el){
if(el === window){
return {
top: 0,
left: 0,
right: window.innerWidth,
bottom: window.innerHeight,
width: window.innerWidth,
height: window.innerHeight
};
}else{
return el.getBoundingClientRect();
}
}
var pointer = function(element){
return new Point(element);
};
// Autscroller
function AutoScrollerFactory(element, options){
return new AutoScroller(element, options);
}
function AutoScroller(elements, options){
var self = this, pixels = 2;
options = options || {};
this.margin = options.margin || -1;
this.scrolling = false;
this.scrollWhenOutside = options.scrollWhenOutside || false;
this.point = pointer(elements);
if(!isNaN(options.pixels)){
pixels = options.pixels;
}
if(typeof options.autoScroll === 'boolean'){
this.autoScroll = options.autoScroll ? function(){return true;} : function(){return false;};
}else if(typeof options.autoScroll === 'undefined'){
this.autoScroll = function(){return false;};
}else if(typeof options.autoScroll === 'function'){
this.autoScroll = options.autoScroll;
}
this.destroy = function() {
this.point.destroy();
};
Object.defineProperties(this, {
down: {
get: function(){ return self.point.down; }
},
interval: {
get: function(){ return 1/pixels * 1000; }
},
pixels: {
set: function(i){ pixels = i; },
get: function(){ return pixels; }
}
});
this.point.on('move', function(el, rect){
if(!el) return;
if(!self.autoScroll()) return;
if(!self.scrollWhenOutside && this.outside(el)) return;
if(self.point.y < rect.top + self.margin){
autoScrollV(el, -1, rect);
}else if(self.point.y > rect.bottom - self.margin){
autoScrollV(el, 1, rect);
}
if(self.point.x < rect.left + self.margin){
autoScrollH(el, -1, rect);
}else if(self.point.x > rect.right - self.margin){
autoScrollH(el, 1, rect);
}
});
function autoScrollV(el, amount, rect){
//if(!self.down) return;
if(!self.autoScroll()) return;
if(!self.scrollWhenOutside && self.point.outside(el)) return;
if(el === window){
window.scrollTo(el.pageXOffset, el.pageYOffset + amount);
}else{
el.scrollTop = el.scrollTop + amount;
}
setTimeout(function(){
if(self.point.y < rect.top + self.margin){
autoScrollV(el, amount, rect);
}else if(self.point.y > rect.bottom - self.margin){
autoScrollV(el, amount, rect);
}
}, self.interval);
}
function autoScrollH(el, amount, rect){
//if(!self.down) return;
if(!self.autoScroll()) return;
if(!self.scrollWhenOutside && self.point.outside(el)) return;
if(el === window){
window.scrollTo(el.pageXOffset + amount, el.pageYOffset);
}else{
el.scrollLeft = el.scrollLeft + amount;
}
setTimeout(function(){
if(self.point.x < rect.left + self.margin){
autoScrollH(el, amount, rect);
}else if(self.point.x > rect.right - self.margin){
autoScrollH(el, amount, rect);
}
}, self.interval);
}
}
window.autoScroll = AutoScrollerFactory;
}());

View File

@ -0,0 +1,221 @@
(function() {
var multipleSortableClass = 'ui-multisortable-multiple';
var mainClass = 'main-drag-item';
var inProgress = false;
var reset = function(elm) {
$(elm)
.removeAttr('style')
.removeClass('tg-backlog-us-mirror')
.removeClass('backlog-us-mirror')
.data('dragMultipleIndex', null)
.data('dragMultipleActive', false);
};
var sort = function(positions) {
var current = dragMultiple.items.elm;
positions.after.reverse();
$.each(positions.after, function () {
reset(this);
current.after(this);
});
$.each(positions.before, function () {
reset(this);
current.before(this);
});
};
var drag = function() {
var current = dragMultiple.items.elm;
var container = dragMultiple.items.container;
var shadow = dragMultiple.items.shadow;
// following the drag element
var currentLeft = shadow.position().left;
var currentTop = shadow.position().top;
var height = shadow.outerHeight();
_.forEach(dragMultiple.items.draggingItems, function(elm, index) {
var elmIndex = parseInt(elm.data('dragMultipleIndex'), 10);
var top = currentTop + (elmIndex * height);
elm
.css({
top: top,
left: currentLeft
});
});
};
var stop = function() {
inProgress = false;
refreshOriginal();
var current = dragMultiple.items.elm;
var container = dragMultiple.items.container;
$(window).off('mousemove.dragmultiple');
// reset
dragMultiple.items = {};
$('.' + mainClass).removeClass(mainClass);
$('.tg-backlog-us-mirror').remove();
$('.backlog-us-mirror').removeClass('backlog-us-mirror');
$('.tg-backlog-us-dragging')
.removeClass('tg-backlog-us-dragging')
.show();
return $('.' + multipleSortableClass);
};
var refreshOriginal = function() {
var index = parseInt(dragMultiple.items.elm.data('dragMultipleIndex'), 10);
var after = [];
var before = [];
_.forEach(dragMultiple.items.draggedItemsOriginal, function(item) {
if (parseInt($(item).data('dragMultipleIndex'), 10) > index) {
after.push(item);
} else {
before.push(item);
}
});
after.reverse();
_.forEach(after, function(item) {
$(item).insertAfter(dragMultiple.items.elm);
});
_.forEach(before, function(item) {
$(item).insertBefore(dragMultiple.items.elm);
});
};
var isMultiple = function(elm, container) {
var items = $(container).find('.' + multipleSortableClass);
if (!$(elm).hasClass(multipleSortableClass) || !(items.length > 1)) {
return false;
}
return true;
};
var setIndex = function(items) {
var before = [];
var after = [];
var mainFound = false;
_.forEach(items, function(item, index) {
if ($(item).data('dragMultipleIndex') === 0) {
mainFound = true;
return;
}
if (mainFound) {
after.push(item);
} else {
before.push(item);
}
});
before.reverse();
_.forEach(after, function(item, index) {
$(item).data('dragMultipleIndex', index + 1);
});
_.forEach(before, function(item, index) {
$(item).data('dragMultipleIndex', -index - 1);
});
};
var dragMultiple = {};
dragMultiple.prepare = function(elm, container) {
inProgress = true;
var items = $(container).find('.' + multipleSortableClass);
$(elm)
.data('dragmultiple:originalPosition', $(elm).position())
.data('dragMultipleActive', true);
dragMultiple.items = {};
dragMultiple.items.elm = $(elm);
dragMultiple.items.container = $(container);
dragMultiple.items.elm.data('dragMultipleIndex', 0);
setIndex(items);
dragMultiple.items.shadow = $('.gu-mirror');
dragMultiple.items.elm.addClass(mainClass);
items = _.filter(items, function(item) {
return !$(item).hasClass(mainClass);
});
dragMultiple.items.draggedItemsOriginal = items;
var itemsCloned = _.map(items, function (item) {
clone = $(item).clone(true);
clone
.addClass('backlog-us-mirror')
.addClass('tg-backlog-us-mirror')
.data('dragmultiple:originalPosition', $(item).position())
.data('dragMultipleActive', true)
.css({
zIndex: '9999',
opacity: '0.8',
position: 'fixed',
width: dragMultiple.items.elm.outerWidth(),
height: dragMultiple.items.elm.outerHeight()
});
$(item)
.hide()
.addClass('tg-backlog-us-dragging');
return clone;
});
dragMultiple.items.draggingItems = itemsCloned;
$(document.body).append(itemsCloned);
};
dragMultiple.start = function(item, container) {
if (isMultiple(item, container)) {
$(window).on('mousemove.dragmultiple', function() {
if (!inProgress) {
dragMultiple.prepare(item, container);
}
drag();
});
}
};
dragMultiple.stop = function() {
if (inProgress) {
return stop();
} else {
return [];
}
};
window.dragMultiple = dragMultiple;
}());

View File

@ -1,161 +0,0 @@
(function($) {
var multipleSortableClass = 'ui-multisortable-multiple';
var dragStarted = false;
var multiSort = {};
multiSort.isBelow = function(elm, compare) {
var elmOriginalPosition = elm.data('dragmultiple:originalPosition');
var compareOriginalPosition = compare.data('dragmultiple:originalPosition');
return elmOriginalPosition.top > compareOriginalPosition.top;
};
multiSort.reset = function(elm) {
$(elm)
.removeClass("ui-sortable-helper")
.removeAttr('style')
.data('dragMultipleActive', false);
};
multiSort.sort = function(current, positions) {
positions.after.reverse();
$.each(positions.after, function () {
multiSort.reset(this);
current.after(this);
});
$.each(positions.before, function () {
multiSort.reset(this);
current.before(this);
});
};
multiSort.sortPositions = function(elm, current) {
//saved if the elements are after or before the current
var insertAfter = [];
var insertBefore = [];
$(elm).find('.' + multipleSortableClass).each(function () {
var elm = $(this);
if (elm[0] === current[0] || !current.hasClass(multipleSortableClass)) return;
if (multiSort.isBelow(elm, current)) {
insertAfter.push(elm);
} else {
insertBefore.push(elm);
}
});
return {'after': insertAfter, 'before': insertBefore};
};
$.widget( "ui.sortable", $.ui.sortable, {
_mouseStart: function() {
dragStarted = false;
this._superApply( arguments );
},
_createHelper: function () {
var helper = this._superApply( arguments );
if ($(helper).hasClass(multipleSortableClass)) {
$(this.element).find('.' + multipleSortableClass).each(function () {
$(this)
.data('dragmultiple:originalPosition', $(this).position())
.data('dragMultipleActive', true);
});
}
return helper;
},
_mouseStop: function (event, ui) {
var current = this.helper;
var elms = [];
if (current.hasClass(multipleSortableClass)) {
elms = $(this.element).find('.' + multipleSortableClass);
}
if (!elms.length) {
elms = [current];
}
//save the order of the elements relative to the main
var positions = multiSort.sortPositions(this.element, current);
this._superApply( arguments );
if (this.element !== this.currentContainer.element) {
// change to another sortable list
multiSort.sort(current, positions);
$(this.currentContainer.element).trigger('multiplesortreceive', {
'item': current,
'items': elms,
'source': this.element
});
} else if (current.hasClass(multipleSortableClass)) {
// sort in the same list
multiSort.sort(current, positions);
}
$(this.element).trigger('multiplesortstop', {
'item': current,
'items': elms
});
},
_mouseDrag: function(key, value) {
this._super(key, value);
var current = this.helper;
if (!current.hasClass(multipleSortableClass)) return;
// following the drag element
var currentLeft = current.position().left;
var currentTop = current.position().top;
var currentZIndex = current.css('z-index');
var currentPosition = current.css('position');
var positions = multiSort.sortPositions(this.element, current);
positions.before.reverse();
[{'positions': positions.after, type: 'after'},
{'positions': positions.before, type: 'before'}]
.forEach(function (item) {
$.each(item.positions, function (index, elm) {
var top;
if (item.type === 'after') {
top = currentTop + ((index + 1) * current.outerHeight());
} else {
top = currentTop - ((index + 1) * current.outerHeight());
}
elm
.addClass("ui-sortable-helper")
.css({
width: elm.outerWidth(),
height: elm.outerHeight(),
position: currentPosition,
zIndex: currentZIndex,
top: top,
left: currentLeft
});
});
});
// it only refresh position the first time because
// jquery-ui has saved the old positions of the draggable elements
// and with this will remove all elements with dragMultipleActive
if (!dragStarted) {
dragStarted = true;
this.refreshPositions();
}
}
});
}(jQuery))

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 20112014, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);

View File

@ -1,5 +1,20 @@
.attachments { .attachments {
margin-bottom: 4rem; margin-bottom: 4rem;
.gu-transit {
background: $whitish;
height: 40px;
* {
display: none;
}
}
.gu-mirror {
opacity: 1;
form {
background: lighten($primary, 60%);
box-shadow: 1px 1px 10px rgba($black, .1);
transition: background .2s ease-in;
}
}
} }
.attachments-header { .attachments-header {
@ -78,15 +93,6 @@
.single-attachment { .single-attachment {
@extend %small; @extend %small;
background: rgba($white, .9); background: rgba($white, .9);
&.ui-sortable-helper {
background: lighten($primary, 60%);
box-shadow: 1px 1px 10px rgba($black, .1);
transition: background .2s ease-in;
}
&.sortable-placeholder {
background: $whitish;
height: 40px;
}
.attachment-name { .attachment-name {
@extend %bold; @extend %bold;
padding-right: 1rem; padding-right: 1rem;

View File

@ -21,25 +21,34 @@ AttachmentSortableDirective = ($parse) ->
link = (scope, el, attrs) -> link = (scope, el, attrs) ->
callback = $parse(attrs.tgAttachmentsSortable) callback = $parse(attrs.tgAttachmentsSortable)
el.sortable({ drake = dragula([el[0]], {
items: "div[tg-bind-scope]" copySortSource: false,
handle: ".settings .icon" copy: false,
containment: ".attachments" mirrorContainer: el[0],
dropOnEmpty: true moves: (item) -> return $(item).is('div[tg-bind-scope]')
helper: 'clone'
scroll: false
tolerance: "pointer"
placeholder: "sortable-placeholder single-attachment"
}) })
el.on "sortstop", (event, ui) -> drake.on 'dragend', (item) ->
attachment = ui.item.scope().attachment item = $(item)
newIndex = ui.item.index()
attachment = item.scope().attachment
newIndex = item.index()
scope.$apply () -> scope.$apply () ->
callback(scope, {attachment: attachment, index: newIndex}) callback(scope, {attachment: attachment, index: newIndex})
scope.$on "$destroy", -> el.off() scroll = autoScroll(window, {
margin: 20,
pixels: 30,
scrollWhenOutside: true,
autoScroll: () ->
return this.down && drake.dragging;
})
scope.$on "$destroy", ->
el.off()
drake.destroy()
return { return {
link: link link: link

View File

@ -21,17 +21,15 @@ SortProjectsDirective = (currentUserService) ->
link = (scope, el, attrs, ctrl) -> link = (scope, el, attrs, ctrl) ->
itemEl = null itemEl = null
el.sortable({ drake = dragula([el[0]], {
dropOnEmpty: true copySortSource: false,
revert: 200 copy: false,
axis: "y" mirrorContainer: el[0],
opacity: .95 moves: (item) -> return $(item).hasClass('list-itemtype-project')
placeholder: 'placeholder'
cancel: '.project-name'
}) })
el.on "sortstop", (event, ui) -> drake.on 'dragend', (item) ->
itemEl = ui.item itemEl = $(item)
project = itemEl.scope().project project = itemEl.scope().project
index = itemEl.index() index = itemEl.index()
@ -46,6 +44,18 @@ SortProjectsDirective = (currentUserService) ->
currentUserService.bulkUpdateProjectsOrder(sortData) currentUserService.bulkUpdateProjectsOrder(sortData)
scroll = autoScroll(window, {
margin: 20,
pixels: 30,
scrollWhenOutside: true,
autoScroll: () ->
return this.down && drake.dragging;
})
scope.$on "$destroy", ->
el.off()
drake.destroy()
directive = { directive = {
scope: { scope: {
projects: "=tgSortProjects" projects: "=tgSortProjects"

View File

@ -49,10 +49,6 @@
color: $gray-light; color: $gray-light;
width: 200px; width: 200px;
} }
.placeholder {
background-color: lighten($whitish, 3%);
height: 5rem;
}
.list-itemtype-project { .list-itemtype-project {
background: rgba($white, .6); background: rgba($white, .6);
&:hover { &:hover {
@ -96,4 +92,16 @@
width: 1.1rem; width: 1.1rem;
} }
} }
.gu-transit {
background-color: lighten($whitish, 3%);
height: 5rem;
opacity: 1;
* {
display: none;
}
}
.gu-mirror {
background: lighten($primary, 63%);
opacity: 1;
}
} }

View File

@ -17,6 +17,8 @@
# File: app-meta.service.spec.coffee # File: app-meta.service.spec.coffee
### ###
angular.module("taigaCommon").provider("$exceptionHandler", angular.mock.$ExceptionHandlerProvider)
describe "AppMetaService", -> describe "AppMetaService", ->
appMetaService = null appMetaService = null
$rootScope = null $rootScope = null

View File

@ -1,54 +0,0 @@
###
# 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: scope-event.service.coffee
###
class ScopeEvent
scopes: {},
_searchDuplicatedScopes: (id) ->
return _.find Object.keys(@scopes), (key) =>
return @scopes[key].$id == id
_create: (name, scope) ->
duplicatedScopeName = @._searchDuplicatedScopes(scope.$id)
if duplicatedScopeName
throw new Error("scopeEvent: this scope is already
register with the name \"" + duplicatedScopeName + "\"")
if @scopes[name]
throw new Error("scopeEvent: \"" + name + "\" already in use")
else
scope._tgEmitter = new EventEmitter2()
scope.$on "$destroy", () =>
scope._tgEmitter.removeAllListeners()
delete @scopes[name]
@scopes[name] = scope
emitter: (name, scope) ->
if scope
scope = @._create(name, scope)
else if @scopes[name]
scope = @scopes[name]
else
throw new Error("scopeEvent: \"" + name + "\" scope doesn't exist'")
return scope._tgEmitter
angular.module("taigaCommon").service("tgScopeEvent", ScopeEvent)

View File

@ -1,102 +0,0 @@
###
# 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: scope-event.service.spec.coffee
###
angular.module("taigaCommon").provider("$exceptionHandler", angular.mock.$ExceptionHandlerProvider)
describe "tgScopeEvent", ->
scopeEvent = null
$rootScope = null
_inject = ->
inject (_tgScopeEvent_, _$rootScope_) ->
scopeEvent = _tgScopeEvent_
scopeEvent.scopes = {}
$rootScope = _$rootScope_
_setup = ->
_inject()
beforeEach ->
module "taigaCommon"
_setup()
it "get non-existent emitter", () ->
fn = () -> scopeEvent.emitter("test")
expect(fn).to.throw(Error, "scopeEvent: \"test\" scope doesn't exist'")
it "create emitter", () ->
scope = $rootScope.$new()
emitter = scopeEvent.emitter("test", scope)
expect(emitter).to.be.an.instanceof(EventEmitter2)
it "get emitter", () ->
scope = $rootScope.$new()
scopeEvent.emitter("test", scope)
emitter = scopeEvent.emitter("test")
expect(emitter).to.be.an.instanceof(EventEmitter2)
it "duplicate emitter name", () ->
scope = $rootScope.$new()
scope2 = $rootScope.$new()
scopeEvent.emitter("test", scope)
fn = () -> scopeEvent.emitter("test", scope2)
expect(fn).to.throw(Error, "scopeEvent: \"test\" already in use")
it "duplicate scope", () ->
scope = $rootScope.$new()
scopeEvent.emitter("test", scope)
fn = () -> scopeEvent.emitter("test2", scope)
expect(fn).to.throw(Error, "scopeEvent: this scope is already register with the name \"test\"")
it "create listener", () ->
scope = $rootScope.$new()
emitter = scopeEvent.emitter("test", scope)
emitter.on "test_listener", () -> return
expect(scope._tgEmitter.listeners("test_listener")[0]).to.be.ok
it "remove emitter and listeners after scope destroy", () ->
scope = $rootScope.$new()
emitter = scopeEvent.emitter("test", scope)
emitter.on "test_listener", () -> return
expect(scope._tgEmitter.listeners("test_listener")).to.have.length(1)
expect(scopeEvent.scopes['test']).to.be.ok
scope.$destroy()
expect(scopeEvent.scopes['test']).to.be.undefined
expect(scope._tgEmitter.listeners("test_listener")).to.have.length(0)

View File

@ -54,5 +54,5 @@ div.row.us-item-row(
div.points(tg-backlog-us-points="us") div.points(tg-backlog-us-points="us")
a.us-points(href="", title="{{'COMMON.FIELDS.POINTS' | translate}}") a.us-points(href="", title="{{'COMMON.FIELDS.POINTS' | translate}}")
a(tg-check-permission="modify_us", href="", title="{{'COMMON.DRAG' | translate}}") a.e2e-drag(tg-check-permission="modify_us", href="", title="{{'COMMON.DRAG' | translate}}")
tg-svg(svg-icon="icon-drag") tg-svg(svg-icon="icon-drag")

View File

@ -1,4 +1,4 @@
div.kanban-table(tg-kanban-squish-column) div.kanban-table(tg-kanban-squish-column, tg-kanban-sortable)
div.kanban-table-header div.kanban-table-header
div.kanban-table-inner div.kanban-table-inner
h2.task-colum-name(ng-repeat="s in usStatusList track by s.id", h2.task-colum-name(ng-repeat="s in usStatusList track by s.id",
@ -66,7 +66,6 @@ div.kanban-table(tg-kanban-squish-column)
div.kanban-table-inner div.kanban-table-inner
div.kanban-uses-box.task-column(ng-class='{vfold:folds[s.id]}', div.kanban-uses-box.task-column(ng-class='{vfold:folds[s.id]}',
ng-repeat="s in usStatusList track by s.id", ng-repeat="s in usStatusList track by s.id",
tg-kanban-sortable,
tg-kanban-wip-limit="s", tg-kanban-wip-limit="s",
tg-kanban-column-height-fixer, tg-kanban-column-height-fixer,
tg-bind-scope tg-bind-scope

View File

@ -2,7 +2,7 @@ header(tg-backlog-sprint-header, ng-model="sprint")
div.sprint-progress-bar(tg-progress-bar="100 * sprint.closed_points / sprint.total_points") div.sprint-progress-bar(tg-progress-bar="100 * sprint.closed_points / sprint.total_points")
div.sprint-table div.sprint-table(tg-bind-scope, ng-class="{'sprint-empty-wrapper': !sprint.user_stories.length}")
div.sprint-empty(ng-if="!sprint.user_stories.length") div.sprint-empty(ng-if="!sprint.user_stories.length")
span(tg-class-permission="{'hidden': 'modify_us'}") {{ 'BACKLOG.SPRINTS.WARNING_EMPTY_SPRINT_ANONYMOUS' | translate }} span(tg-class-permission="{'hidden': 'modify_us'}") {{ 'BACKLOG.SPRINTS.WARNING_EMPTY_SPRINT_ANONYMOUS' | translate }}
span(tg-class-permission="{'hidden': '!modify_us'}") {{ 'BACKLOG.SPRINTS.WARNING_EMPTY_SPRINT' | translate }} span(tg-class-permission="{'hidden': '!modify_us'}") {{ 'BACKLOG.SPRINTS.WARNING_EMPTY_SPRINT' | translate }}

View File

@ -1,4 +1,4 @@
div.taskboard-table(tg-taskboard-squish-column) div.taskboard-table(tg-taskboard-squish-column, tg-taskboard-sortable)
div.taskboard-table-header div.taskboard-table-header
div.taskboard-table-inner div.taskboard-table-inner
h2.task-colum-name(translate="TASKBOARD.TABLE.COLUMN") h2.task-colum-name(translate="TASKBOARD.TABLE.COLUMN")
@ -49,7 +49,7 @@ div.taskboard-table(tg-taskboard-squish-column)
span(translate="TASKBOARD.TABLE.FIELD_POINTS") span(translate="TASKBOARD.TABLE.FIELD_POINTS")
include ../components/addnewtask include ../components/addnewtask
div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", tg-taskboard-sortable, class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}", tg-bind-scope) div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}", tg-bind-scope)
div.taskboard-task( div.taskboard-task(
ng-repeat="task in usTasks[us.id][st.id] track by task.id" ng-repeat="task in usTasks[us.id][st.id] track by task.id"
tg-bind-scope tg-bind-scope
@ -82,7 +82,7 @@ div.taskboard-table(tg-taskboard-squish-column)
h3.us-title h3.us-title
span(translate="TASKBOARD.TABLE.ROW_UNASSIGED_TASKS_TITLE") span(translate="TASKBOARD.TABLE.ROW_UNASSIGED_TASKS_TITLE")
include ../components/addnewtask.jade include ../components/addnewtask.jade
div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", tg-taskboard-sortable, class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}", tg-bind-scope) div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}", tg-bind-scope)
div.taskboard-task( div.taskboard-task(
ng-repeat="task in usTasks[null][st.id] track by task.id" ng-repeat="task in usTasks[null][st.id] track by task.id"
tg-bind-scope tg-bind-scope

View File

@ -16,13 +16,11 @@
transition: color .3s linear, opacity .3s linear; transition: color .3s linear, opacity .3s linear;
} }
} }
&.ui-sortable-helper { &.gu-mirror {
box-shadow: 1px 1px 15px rgba($black, .4); box-shadow: 1px 1px 15px rgba($black, .4);
opacity: 1;
transition: box-shadow .3s linear; transition: box-shadow .3s linear;
} }
&.ui-sortable-placeholder {
background: $grayer;
}
&.blocked { &.blocked {
background: $red; background: $red;
border: 1px solid darken($red, 10%); border: 1px solid darken($red, 10%);

View File

@ -14,13 +14,10 @@
transition: color .3s linear, opacity .3s linear; transition: color .3s linear, opacity .3s linear;
} }
} }
&.ui-sortable-helper { &.gu-mirror {
box-shadow: 1px 1px 15px rgba($black, .4); box-shadow: 1px 1px 15px rgba($black, .4);
transition: box-shadow .3s linear; transition: box-shadow .3s linear;
} }
&.ui-sortable-placeholder {
background: $grayer;
}
.blocked { .blocked {
background: $red; background: $red;
border: 1px solid darken($red, 10%); border: 1px solid darken($red, 10%);

View File

@ -88,3 +88,39 @@ svg {
max-width: 2rem; max-width: 2rem;
} }
} }
// scss-lint:disable QualifyingElement
div.awesomplete {
> ul {
background: rgba($black, .95);
color: $primary-light;
top: 2.25rem;
transition: all .2s ease;
&::before {
background: rgba($black, .95);
}
&[hidden] {
position: absolute;
top: 1.5rem;
transform: scale(1);
}
> li {
&:hover {
background: $primary-light;
color: $black;
}
}
}
mark {
background: $primary-light;
color: $black;
}
li {
&:hover {
mark {
background: $primary-light;
color: $black;
}
}
}
}

View File

@ -27,3 +27,25 @@
margin-left: .2rem; margin-left: .2rem;
} }
} }
.backlog-us-mirror {
background: $white;
border-radius: 4px;
box-shadow: 2px 2px 5px $gray;
min-height: calc(40px + 1rem);
opacity: .9;
padding: 1rem;
.tags-block,
.votes,
.us-settings,
.status,
.points,
.icon-drag,
input {
display: none;
}
&.is-checked,
&:hover {
background: $white;
}
}

View File

@ -165,7 +165,7 @@
&:last-child { &:last-child {
border-bottom: 0; border-bottom: 0;
} }
&.ui-sortable-helper { &.gu-mirror {
background: lighten($primary, 60%); background: lighten($primary, 60%);
box-shadow: 1px 1px 10px rgba($black, .1); box-shadow: 1px 1px 10px rgba($black, .1);
opacity: .9; opacity: .9;
@ -183,10 +183,13 @@
width: .7rem; width: .7rem;
} }
} }
.sortable-placeholder { .gu-transit {
background: $whitish; background: $whitish;
height: 40px; height: 40px;
width: 100%; width: 100%;
* {
display: none;
}
} }
.row-selected, .row-selected,
.is-checked { .is-checked {
@ -301,6 +304,9 @@
@extend %light; @extend %light;
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
.row {
display: none;
}
img { img {
margin-bottom: 1rem; margin-bottom: 1rem;
} }

View File

@ -158,7 +158,7 @@
text-align: left; text-align: left;
width: 100%; width: 100%;
&:hover { &:hover {
background: lighten($gray-light, 12%); background: rgba($gray-light, .2);
cursor: move; cursor: move;
transition: background .2s ease-in; transition: background .2s ease-in;
} }
@ -168,10 +168,6 @@
&.readonly { &.readonly {
cursor: auto; cursor: auto;
} }
&.sortable-placeholder {
background: lighten($gray-light, 12%);
height: 40px;
}
&.ui-sortable-helper { &.ui-sortable-helper {
background: lighten($primary, 60%); background: lighten($primary, 60%);
box-shadow: 1px 1px 10px rgba($black, .1); box-shadow: 1px 1px 10px rgba($black, .1);
@ -180,6 +176,13 @@
} }
} }
.gu-transit {
background: lighten($gray-light, 12%);
height: 40px;
* {
display: none;
}
}
.column-us { .column-us {
@extend %small; @extend %small;
flex-flow: 3; flex-flow: 3;
@ -206,6 +209,11 @@
color: $red; color: $red;
} }
} }
&.sprint-empty-wrapper { // hide dragging items
.row {
display: none;
}
}
} }
.button-gray { .button-gray {
display: block; display: block;

View File

@ -25,9 +25,6 @@ $column-margin: 0 10px 0 0;
.icon { .icon {
display: none; display: none;
} }
&.ui-sortable-helper {
box-shadow: none;
}
} }
&.task-column, &.task-column,
.task-column { .task-column {
@ -54,6 +51,12 @@ $column-margin: 0 10px 0 0;
&.readonly { &.readonly {
cursor: auto; cursor: auto;
} }
&.gu-mirror {
opacity: 1;
.avatar-task-link {
display: none;
}
}
} }
} }

View File

@ -141,4 +141,13 @@
} }
} }
} }
.gu-mirror {
background: lighten($primary, 60%);
opacity: 1;
}
.gu-transit {
* {
visibility: hidden;
}
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -56,10 +56,6 @@
"angular-sanitize": "1.4.7", "angular-sanitize": "1.4.7",
"checksley": "~0.6.0", "checksley": "~0.6.0",
"jquery": "~2.1.1", "jquery": "~2.1.1",
"select2": "~3.4.8",
"angular-ui-select2": "~0.0.5",
"google-diff-match-patch-js": "~1.0.0",
"underscore.string": "~2.3.3",
"markitup-1x": "~1.1.14", "markitup-1x": "~1.1.14",
"jquery-textcomplete": "yuku-t/jquery-textcomplete#~0.7", "jquery-textcomplete": "yuku-t/jquery-textcomplete#~0.7",
"flot-axislabels": "markrcote/flot-axislabels", "flot-axislabels": "markrcote/flot-axislabels",
@ -67,10 +63,7 @@
"flot.tooltip": "~0.8.4", "flot.tooltip": "~0.8.4",
"Sortable": "~0.1.8", "Sortable": "~0.1.8",
"moment": "~2.10.6", "moment": "~2.10.6",
"isMobile": "~0.3.1",
"favico.js": "0.3.9",
"pikaday": "~1.4.0", "pikaday": "~1.4.0",
"malihu-custom-scrollbar-plugin": "~3.1.0",
"raven-js": "~1.1.16", "raven-js": "~1.1.16",
"l.js": "~0.1.0", "l.js": "~0.1.0",
"angular-translate": "~2.8.1", "angular-translate": "~2.8.1",
@ -78,17 +71,19 @@
"angular-translate-loader-static-files": "~2.8.1", "angular-translate-loader-static-files": "~2.8.1",
"angular-translate-interpolation-messageformat": "~2.8.1", "angular-translate-interpolation-messageformat": "~2.8.1",
"ngInfiniteScroll": "1.2.1", "ngInfiniteScroll": "1.2.1",
"eventemitter2": "~0.4.14",
"immutable": "~3.7.6", "immutable": "~3.7.6",
"bluebird": "~2.10.2", "bluebird": "~2.10.2",
"intro.js": "~1.1.1", "intro.js": "~1.1.1",
"lodash": "~4.0.0" "lodash": "~4.0.0",
"messageformat": "^0.1.8",
"dragula.js": "dragula#^3.6.6"
}, },
"resolutions": { "resolutions": {
"lodash": "~4.0.0", "lodash": "~4.0.0",
"moment": "~2.10.6", "moment": "~2.10.6",
"jquery": "~2.1.1", "jquery": "~2.1.1",
"angular": "1.4.7" "angular": "1.4.7",
"messageformat": "0.1.8"
}, },
"private": true "private": true
} }

View File

@ -22,9 +22,9 @@ exports.config = {
compilers: 'js:babel-register', compilers: 'js:babel-register',
require: 'babel-polyfill' require: 'babel-polyfill'
}, },
capabilities: { // capabilities: {
'browserName': 'firefox' // 'browserName': 'firefox'
}, // },
// capabilities: { // capabilities: {
// browserName: 'internet explorer', // browserName: 'internet explorer',
// version: '11' // version: '11'

View File

@ -1,5 +1,6 @@
var exports = module.exports = {}; var exports = module.exports = {};
var fs = require("fs"); var fs = require("fs");
var gutil = require('gulp-util');
var Theme = function() { var Theme = function() {
var defaultTheme = "taiga"; var defaultTheme = "taiga";
@ -72,3 +73,88 @@ exports.themes = {
return Theme(); return Theme();
} }
}; };
exports.unusedCss = function(options) {
var through = require('through2');
var css = require('css');
var path = require('path');
var content = fs.readFileSync(options.css, "utf8");
var ast = css.parse(content, {});
var files = [];
var validsSelectors = [];
ast.stylesheet.rules.forEach(function(rule) {
if (rule.selectors) {
rule.selectors.forEach(function(selectorRule) {
var selectors = selectorRule.split(" ");
selectors.forEach(function(selector) {
var valid = false;
if (selector.slice(0, 2) === 'tg') {
valid = true;
} else if (selector[0] === '.') {
valid = true;
selector = '.' + selector.split('.')[1]; // '.class1.class2 -> .class1'
}
selector = selector.split('::')[0];
selector = selector.split(':')[0];
if(valid && validsSelectors.indexOf(selector) === -1) {
validsSelectors.push(selector);
}
});
});
}
});
var addFile = function(file, encoding, cb) {
files.push(file);
cb();
};
var searchUnusedCss = function(cb) {
var invalid = [];
validsSelectors.forEach(function(validSelector) {
var finded = false;
files.every(function(file) {
var content = file.contents.toString();
var ext = path.extname(file.path);
var pattern = validSelector;
if (ext === '.html') {
pattern = validSelector.slice(1);
}
if(content.indexOf(pattern) !== -1) {
finded = true;
return false;
}
return true;
});
if (!finded) {
invalid.push(validSelector);
}
});
for(var i = 0; i < invalid.length; i++) {
gutil.log(gutil.colors.magenta(invalid[i]));
}
cb();
};
return through.obj(addFile, searchUnusedCss);
};

View File

@ -27,9 +27,10 @@ var gulp = require("gulp"),
del = require("del"), del = require("del"),
livereload = require('gulp-livereload'), livereload = require('gulp-livereload'),
gulpFilter = require('gulp-filter'), gulpFilter = require('gulp-filter'),
addsrc = require('gulp-add-src');
mergeStream = require('merge-stream'), mergeStream = require('merge-stream'),
path = require('path'), path = require('path'),
addsrc = require('gulp-add-src'),
jsonminify = require('gulp-jsonminify'),
coffeelint = require('gulp-coffeelint'); coffeelint = require('gulp-coffeelint');
var argv = require('minimist')(process.argv.slice(2)); var argv = require('minimist')(process.argv.slice(2));
@ -51,6 +52,7 @@ paths.distVersion = paths.dist + version + "/";
paths.tmp = "tmp/"; paths.tmp = "tmp/";
paths.extras = "extras/"; paths.extras = "extras/";
paths.vendor = "vendor/"; paths.vendor = "vendor/";
paths.modules = "node_modules/";
paths.jade = [ paths.jade = [
paths.app + "**/*.jade" paths.app + "**/*.jade"
@ -66,8 +68,10 @@ paths.htmlPartials = [
paths.images = paths.app + "images/**/*"; paths.images = paths.app + "images/**/*";
paths.svg = paths.app + "svg/**/*"; paths.svg = paths.app + "svg/**/*";
paths.css_vendor = [ paths.css_vendor = [
paths.app + "styles/vendor/*.css", paths.vendor + "intro.js/introjs.css",
paths.vendor + "intro.js/introjs.css" paths.vendor + "dragula.js/dist/dragula.css",
paths.modules + "awesomplete/awesomplete.css",
paths.app + "styles/vendor/*.css"
]; ];
paths.locales = paths.app + "locales/**/*.json"; paths.locales = paths.app + "locales/**/*.json";
paths.modulesLocales = paths.app + "modules/**/locales/*.json"; paths.modulesLocales = paths.app + "modules/**/locales/*.json";
@ -157,7 +161,7 @@ paths.libs = [
paths.vendor + "angular-translate-loader-partial/angular-translate-loader-partial.js", paths.vendor + "angular-translate-loader-partial/angular-translate-loader-partial.js",
paths.vendor + "angular-translate-loader-static-files/angular-translate-loader-static-files.js", paths.vendor + "angular-translate-loader-static-files/angular-translate-loader-static-files.js",
paths.vendor + "angular-translate-interpolation-messageformat/angular-translate-interpolation-messageformat.js", paths.vendor + "angular-translate-interpolation-messageformat/angular-translate-interpolation-messageformat.js",
paths.vendor + "moment/min/moment-with-locales.js", paths.vendor + "moment/moment.js",
paths.vendor + "checksley/checksley.js", paths.vendor + "checksley/checksley.js",
paths.vendor + "pikaday/pikaday.js", paths.vendor + "pikaday/pikaday.js",
paths.vendor + "jquery-flot/jquery.flot.js", paths.vendor + "jquery-flot/jquery.flot.js",
@ -167,20 +171,19 @@ paths.libs = [
paths.vendor + "flot.tooltip/js/jquery.flot.tooltip.js", paths.vendor + "flot.tooltip/js/jquery.flot.tooltip.js",
paths.vendor + "jquery-textcomplete/dist/jquery.textcomplete.js", paths.vendor + "jquery-textcomplete/dist/jquery.textcomplete.js",
paths.vendor + "markitup-1x/markitup/jquery.markitup.js", paths.vendor + "markitup-1x/markitup/jquery.markitup.js",
paths.vendor + "malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.concat.min.js",
paths.vendor + "raven-js/dist/raven.js", paths.vendor + "raven-js/dist/raven.js",
paths.vendor + "l.js/l.js", paths.vendor + "l.js/l.js",
paths.vendor + "messageformat/locale/*.js", paths.vendor + "messageformat/locale/*.js",
paths.vendor + "ngInfiniteScroll/build/ng-infinite-scroll.js", paths.vendor + "ngInfiniteScroll/build/ng-infinite-scroll.js",
paths.vendor + "eventemitter2/lib/eventemitter2.js",
paths.vendor + "immutable/dist/immutable.js", paths.vendor + "immutable/dist/immutable.js",
paths.vendor + "intro.js/intro.js", paths.vendor + "intro.js/intro.js",
paths.app + "js/jquery.ui.git-custom.js", paths.vendor + "dragula.js/dist/dragula.js",
paths.app + "js/jquery-ui.drag-multiple-custom.js", paths.app + "js/dom-autoscroller.js",
paths.app + "js/jquery.ui.touch-punch.min.js", paths.app + "js/dragula-drag-multiple.js",
paths.app + "js/tg-repeat.js", paths.app + "js/tg-repeat.js",
paths.app + "js/sha1-custom.js", paths.app + "js/sha1-custom.js",
paths.app + "js/murmurhash3_gc.js" paths.app + "js/murmurhash3_gc.js",
paths.modules + "awesomplete/awesomplete.js"
]; ];
var isDeploy = argv["_"].indexOf("deploy") !== -1; var isDeploy = argv["_"].indexOf("deploy") !== -1;
@ -218,6 +221,7 @@ gulp.task("copy-index", function() {
gulp.task("template-cache", function() { gulp.task("template-cache", function() {
return gulp.src(paths.htmlPartials) return gulp.src(paths.htmlPartials)
.pipe(templateCache({standalone: true})) .pipe(templateCache({standalone: true}))
.pipe(gulpif(isDeploy, uglify()))
.pipe(gulp.dest(paths.distVersion + "js/")) .pipe(gulp.dest(paths.distVersion + "js/"))
.pipe(gulpif(!isDeploy, livereload())); .pipe(gulpif(!isDeploy, livereload()));
}); });
@ -378,6 +382,7 @@ gulp.task("app-loader", function() {
return gulp.src("app-loader/app-loader.coffee") return gulp.src("app-loader/app-loader.coffee")
.pipe(replace("___VERSION___", version)) .pipe(replace("___VERSION___", version))
.pipe(coffee()) .pipe(coffee())
.pipe(gulpif(isDeploy, uglify()))
.pipe(gulp.dest(paths.distVersion + "js/")); .pipe(gulp.dest(paths.distVersion + "js/"));
}); });
@ -391,7 +396,10 @@ gulp.task("locales", function() {
var pluginFolder = pluginPath.split('/').pop(); var pluginFolder = pluginPath.split('/').pop();
localeFile.dirname = pluginFolder; localeFile.dirname = pluginFolder;
})) }));
return gulp.src(paths.locales)
.pipe(gulpif(isDeploy, jsonminify()))
.pipe(gulp.dest(paths.distVersion + "locales")); .pipe(gulp.dest(paths.distVersion + "locales"));
var core = gulp.src(paths.locales) var core = gulp.src(paths.locales)
@ -439,6 +447,12 @@ gulp.task("coffee", function() {
.pipe(livereload()); .pipe(livereload());
}); });
gulp.task("moment-locales", function() {
return gulp.src(paths.vendor + "moment/locale/*")
.pipe(gulpif(isDeploy, uglify()))
.pipe(gulp.dest(paths.distVersion + "locales/moment-locales/"));
});
gulp.task("jslibs-watch", function() { gulp.task("jslibs-watch", function() {
return gulp.src(paths.libs) return gulp.src(paths.libs)
.pipe(plumber()) .pipe(plumber())
@ -451,17 +465,17 @@ gulp.task("jslibs-deploy", function() {
.pipe(plumber()) .pipe(plumber())
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
.pipe(concat("libs.js")) .pipe(concat("libs.js"))
.pipe(uglify({mangle:false, preserveComments: false})) .pipe(uglify())
.pipe(sourcemaps.write("./maps")) .pipe(sourcemaps.write("./maps"))
.pipe(gulp.dest(paths.distVersion + "js/")); .pipe(gulp.dest(paths.distVersion + "js/"));
}); });
gulp.task("app-watch", ["coffee", "conf", "locales", "app-loader"]); gulp.task("app-watch", ["coffee", "conf", "locales", "moment-locales", "app-loader"]);
gulp.task("app-deploy", ["coffee", "conf", "locales", "app-loader"], function() { gulp.task("app-deploy", ["coffee", "conf", "locales", "moment-locales", "app-loader"], function() {
return gulp.src(paths.distVersion + "js/app.js") return gulp.src(paths.distVersion + "js/app.js")
.pipe(sourcemaps.init()) .pipe(sourcemaps.init())
.pipe(uglify({mangle:false, preserveComments: false})) .pipe(uglify())
.pipe(sourcemaps.write("./maps")) .pipe(sourcemaps.write("./maps"))
.pipe(gulp.dest(paths.distVersion + "js/")); .pipe(gulp.dest(paths.distVersion + "js/"));
}); });
@ -531,10 +545,24 @@ gulp.task("delete-tmp", function() {
del.sync(paths.tmp); del.sync(paths.tmp);
}); });
gulp.task("unused-css", ["default"], function() {
return gulp.src([
paths.distVersion + "js/app.js",
paths.tmp + "**/*.html"
])
.pipe(utils.unusedCss({
css: paths.distVersion + "styles/theme-taiga.css"
}));
});
gulp.task("express", function() { gulp.task("express", function() {
var express = require("express"); var express = require("express");
var compression = require('compression');
var app = express(); var app = express();
app.use(compression()); //gzip
app.use("/" + version + "/js", express.static(__dirname + "/dist/" + version + "/js")); app.use("/" + version + "/js", express.static(__dirname + "/dist/" + version + "/js"));
app.use("/" + version + "/styles", express.static(__dirname + "/dist/" + version + "/styles")); app.use("/" + version + "/styles", express.static(__dirname + "/dist/" + version + "/styles"));
app.use("/" + version + "/images", express.static(__dirname + "/dist/" + version + "/images")); app.use("/" + version + "/images", express.static(__dirname + "/dist/" + version + "/images"));

View File

@ -32,10 +32,12 @@
"chai-jquery": "^2.0.0", "chai-jquery": "^2.0.0",
"cli-color": "^1.0.0", "cli-color": "^1.0.0",
"coffee-script": "^1.9.1", "coffee-script": "^1.9.1",
"compression": "^1.6.1",
"connect-livereload": "^0.5.4", "connect-livereload": "^0.5.4",
"css": "^2.2.1",
"del": "^2.0.2", "del": "^2.0.2",
"express": "^4.12.0", "express": "^4.12.0",
"glob": "^5.0.14", "glob": "^5.0.15",
"gulp": "^3.8.11", "gulp": "^3.8.11",
"gulp-add-src": "^0.2.0", "gulp-add-src": "^0.2.0",
"gulp-angular-templatecache": "^1.5.0", "gulp-angular-templatecache": "^1.5.0",
@ -53,6 +55,7 @@
"gulp-insert": "^0.5.0", "gulp-insert": "^0.5.0",
"gulp-jade": "^1.0.0", "gulp-jade": "^1.0.0",
"gulp-jade-inheritance": "0.5.3", "gulp-jade-inheritance": "0.5.3",
"gulp-jsonminify": "^1.0.0",
"gulp-livereload": "^3.8.1", "gulp-livereload": "^3.8.1",
"gulp-minify-css": "^0.4.6", "gulp-minify-css": "^0.4.6",
"gulp-order": "^1.1.1", "gulp-order": "^1.1.1",
@ -62,9 +65,11 @@
"gulp-replace": "^0.5.3", "gulp-replace": "^0.5.3",
"gulp-sass": "^2.0.4", "gulp-sass": "^2.0.4",
"gulp-scss-lint": "0.3.6", "gulp-scss-lint": "0.3.6",
"gulp-size": "^2.0.0",
"gulp-sourcemaps": "^1.5.0", "gulp-sourcemaps": "^1.5.0",
"gulp-template": "^3.0.0", "gulp-template": "^3.0.0",
"gulp-uglify": "~1.4.1", "gulp-uglify": "~1.4.1",
"gulp-util": "^3.0.7",
"gulp-wrap": "^0.11.0", "gulp-wrap": "^0.11.0",
"image-size": "^0.3.5", "image-size": "^0.3.5",
"inquirer": "^0.10.0", "inquirer": "^0.10.0",
@ -84,10 +89,14 @@
"readable-stream": "~2.0.2", "readable-stream": "~2.0.2",
"run-sequence": "^1.0.2", "run-sequence": "^1.0.2",
"sinon": "^1.14.1", "sinon": "^1.14.1",
"through2": "~2.0.0", "through2": "^2.0.1",
"vinyl-fs": "^2.1.1" "vinyl-fs": "^2.1.1"
}, },
"pre-commit": [ "pre-commit": [
"scss-lint" "scss-lint"
] ],
"dependencies": {
"awesomplete": "^1.0.0",
"dom-autoscroller": "^1.2.3"
}
} }