tg-repeat
parent
3f34ed1412
commit
c9ee2dbf84
|
@ -350,7 +350,8 @@ modules = [
|
|||
"ngRoute",
|
||||
"ngAnimate",
|
||||
"pascalprecht.translate",
|
||||
"infinite-scroll"
|
||||
"infinite-scroll",
|
||||
"tgRepeat"
|
||||
].concat(_.map(@.taigaContribPlugins, (plugin) -> plugin.module))
|
||||
|
||||
# Main module definition
|
||||
|
|
|
@ -158,13 +158,18 @@ replaceTags = (str, tags, replace) ->
|
|||
|
||||
return str
|
||||
|
||||
defineImmutableProperty = (obj, name, variable) =>
|
||||
defineImmutableProperty = (obj, name, fn) =>
|
||||
Object.defineProperty obj, name, {
|
||||
get: () =>
|
||||
if _.isFunction(variable)
|
||||
return variable.call(obj)
|
||||
else
|
||||
return variable
|
||||
if !_.isFunction(fn)
|
||||
throw "defineImmutableProperty third param must be a function"
|
||||
|
||||
fn_result = fn()
|
||||
if fn_result && _.isObject(fn_result)
|
||||
if fn_result.size == undefined
|
||||
throw "defineImmutableProperty must return immutable data"
|
||||
|
||||
return fn_result
|
||||
}
|
||||
|
||||
taiga = @.taiga
|
||||
|
|
|
@ -0,0 +1,310 @@
|
|||
/*
|
||||
--replace
|
||||
minError -> angular.$$minErr
|
||||
isString -> angular.isString
|
||||
isArray -> angular.isArray
|
||||
var expression = $attr.ngRepeat; -> var expression = $attr.tgRepeat;
|
||||
forEach(nextBlockOrder, function(block) { -> nextBlockOrder.forEach(function(block) {
|
||||
$scope.$watchCollection(rhs, function ngRepeatAction(collection) {
|
||||
->
|
||||
$scope.$watch(rhs, function ngRepeatAction(immutable_collection) {
|
||||
var collection = []
|
||||
|
||||
if (immutable_collection.toJS) {
|
||||
collection = immutable_collection.toJS();
|
||||
}
|
||||
--copy from angular
|
||||
copy angular hashKey
|
||||
copy angular createMap
|
||||
copy angular isArrayLike
|
||||
copy angular isWindow
|
||||
copy angular NODE_TYPE_ELEMENT
|
||||
copy angular nextUid
|
||||
copy angular getBlockNodes
|
||||
--add
|
||||
jqLite = $
|
||||
var uid = 0;
|
||||
*/
|
||||
|
||||
(function() {
|
||||
var NODE_TYPE_ELEMENT = 1;
|
||||
var uid = 0;
|
||||
|
||||
function nextUid() {
|
||||
return ++uid;
|
||||
}
|
||||
|
||||
function hashKey(obj, nextUidFn) {
|
||||
var key = obj && obj.$$hashKey;
|
||||
|
||||
if (key) {
|
||||
if (typeof key === 'function') {
|
||||
key = obj.$$hashKey();
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
var objType = typeof obj;
|
||||
if (objType == 'function' || (objType == 'object' && obj !== null)) {
|
||||
key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)();
|
||||
} else {
|
||||
key = objType + ':' + obj;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
function createMap() {
|
||||
return Object.create(null);
|
||||
}
|
||||
|
||||
function isArrayLike(obj) {
|
||||
if (obj == null || isWindow(obj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var length = obj.length;
|
||||
|
||||
if (obj.nodeType === NODE_TYPE_ELEMENT && length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return angular.isString(obj) || angular.isArray(obj) || length === 0 ||
|
||||
typeof length === 'number' && length > 0 && (length - 1) in obj;
|
||||
}
|
||||
|
||||
function isWindow(obj) {
|
||||
return obj && obj.window === obj;
|
||||
}
|
||||
|
||||
function isString(value) {return typeof value === 'string';}
|
||||
|
||||
function getBlockNodes(nodes) {
|
||||
// TODO(perf): just check if all items in `nodes` are siblings and if they are return the original
|
||||
// collection, otherwise update the original collection.
|
||||
var node = nodes[0];
|
||||
var endNode = nodes[nodes.length - 1];
|
||||
var blockNodes = [node];
|
||||
|
||||
do {
|
||||
node = node.nextSibling;
|
||||
if (!node) break;
|
||||
blockNodes.push(node);
|
||||
} while (node !== endNode);
|
||||
|
||||
return jqLite(blockNodes);
|
||||
}
|
||||
|
||||
var isArray = Array.isArray;
|
||||
|
||||
var jqLite = $;
|
||||
|
||||
var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
|
||||
var NG_REMOVED = '$$NG_REMOVED';
|
||||
var ngRepeatMinErr = angular.$$minErr('ngRepeat');
|
||||
var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) {
|
||||
// TODO(perf): generate setters to shave off ~40ms or 1-1.5%
|
||||
scope[valueIdentifier] = value;
|
||||
if (keyIdentifier) scope[keyIdentifier] = key;
|
||||
scope.$index = index;
|
||||
scope.$first = (index === 0);
|
||||
scope.$last = (index === (arrayLength - 1));
|
||||
scope.$middle = !(scope.$first || scope.$last);
|
||||
// jshint bitwise: false
|
||||
scope.$odd = !(scope.$even = (index&1) === 0);
|
||||
// jshint bitwise: true
|
||||
};
|
||||
var getBlockStart = function(block) {
|
||||
return block.clone[0];
|
||||
};
|
||||
var getBlockEnd = function(block) {
|
||||
return block.clone[block.clone.length - 1];
|
||||
};
|
||||
return {
|
||||
restrict: 'A',
|
||||
multiElement: true,
|
||||
transclude: 'element',
|
||||
priority: 1000,
|
||||
terminal: true,
|
||||
$$tlb: true,
|
||||
compile: function ngRepeatCompile($element, $attr) {
|
||||
var expression = $attr.tgRepeat;
|
||||
var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' ');
|
||||
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
|
||||
if (!match) {
|
||||
throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
|
||||
expression);
|
||||
}
|
||||
var lhs = match[1];
|
||||
var rhs = match[2];
|
||||
var aliasAs = match[3];
|
||||
var trackByExp = match[4];
|
||||
match = lhs.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);
|
||||
if (!match) {
|
||||
throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
|
||||
lhs);
|
||||
}
|
||||
var valueIdentifier = match[3] || match[1];
|
||||
var keyIdentifier = match[2];
|
||||
if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) ||
|
||||
/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(aliasAs))) {
|
||||
throw ngRepeatMinErr('badident', "alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.",
|
||||
aliasAs);
|
||||
}
|
||||
var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn;
|
||||
var hashFnLocals = {$id: hashKey};
|
||||
if (trackByExp) {
|
||||
trackByExpGetter = $parse(trackByExp);
|
||||
} else {
|
||||
trackByIdArrayFn = function(key, value) {
|
||||
return hashKey(value);
|
||||
};
|
||||
trackByIdObjFn = function(key) {
|
||||
return key;
|
||||
};
|
||||
}
|
||||
return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
|
||||
if (trackByExpGetter) {
|
||||
trackByIdExpFn = function(key, value, index) {
|
||||
// assign key, value, and $index to the locals so that they can be used in hash functions
|
||||
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
|
||||
hashFnLocals[valueIdentifier] = value;
|
||||
hashFnLocals.$index = index;
|
||||
return trackByExpGetter($scope, hashFnLocals);
|
||||
};
|
||||
}
|
||||
// Store a list of elements from previous run. This is a hash where key is the item from the
|
||||
// iterator, and the value is objects with following properties.
|
||||
// - scope: bound scope
|
||||
// - element: previous element.
|
||||
// - index: position
|
||||
//
|
||||
// We are using no-proto object so that we don't need to guard against inherited props via
|
||||
// hasOwnProperty.
|
||||
var lastBlockMap = createMap();
|
||||
$scope.$watch(rhs, function ngRepeatAction(immutable_collection) {
|
||||
var collection = []
|
||||
|
||||
if (immutable_collection && immutable_collection.toJS) {
|
||||
collection = immutable_collection.toJS();
|
||||
}
|
||||
|
||||
var index, length,
|
||||
previousNode = $element[0], // node that cloned nodes should be inserted after
|
||||
// initialized to the comment node anchor
|
||||
nextNode,
|
||||
// Same as lastBlockMap but it has the current state. It will become the
|
||||
// lastBlockMap on the next iteration.
|
||||
nextBlockMap = createMap(),
|
||||
collectionLength,
|
||||
key, value, // key/value of iteration
|
||||
trackById,
|
||||
trackByIdFn,
|
||||
collectionKeys,
|
||||
block, // last object information {scope, element, id}
|
||||
nextBlockOrder,
|
||||
elementsToRemove;
|
||||
if (aliasAs) {
|
||||
$scope[aliasAs] = collection;
|
||||
}
|
||||
if (isArrayLike(collection)) {
|
||||
collectionKeys = collection;
|
||||
trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
|
||||
} else {
|
||||
trackByIdFn = trackByIdExpFn || trackByIdObjFn;
|
||||
// if object, extract keys, in enumeration order, unsorted
|
||||
collectionKeys = [];
|
||||
for (var itemKey in collection) {
|
||||
if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') {
|
||||
collectionKeys.push(itemKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
collectionLength = collectionKeys.length;
|
||||
nextBlockOrder = new Array(collectionLength);
|
||||
// locate existing items
|
||||
for (index = 0; index < collectionLength; index++) {
|
||||
key = (collection === collectionKeys) ? index : collectionKeys[index];
|
||||
value = collection[key];
|
||||
trackById = trackByIdFn(key, value, index);
|
||||
if (lastBlockMap[trackById]) {
|
||||
// found previously seen block
|
||||
block = lastBlockMap[trackById];
|
||||
delete lastBlockMap[trackById];
|
||||
nextBlockMap[trackById] = block;
|
||||
nextBlockOrder[index] = block;
|
||||
} else if (nextBlockMap[trackById]) {
|
||||
// if collision detected. restore lastBlockMap and throw an error
|
||||
nextBlockOrder.forEach(function(block) {
|
||||
if (block && block.scope) lastBlockMap[block.id] = block;
|
||||
});
|
||||
throw ngRepeatMinErr('dupes',
|
||||
"Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}",
|
||||
expression, trackById, value);
|
||||
} else {
|
||||
// new never before seen block
|
||||
nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined};
|
||||
nextBlockMap[trackById] = true;
|
||||
}
|
||||
}
|
||||
// remove leftover items
|
||||
for (var blockKey in lastBlockMap) {
|
||||
block = lastBlockMap[blockKey];
|
||||
elementsToRemove = getBlockNodes(block.clone);
|
||||
$animate.leave(elementsToRemove);
|
||||
if (elementsToRemove[0].parentNode) {
|
||||
// if the element was not removed yet because of pending animation, mark it as deleted
|
||||
// so that we can ignore it later
|
||||
for (index = 0, length = elementsToRemove.length; index < length; index++) {
|
||||
elementsToRemove[index][NG_REMOVED] = true;
|
||||
}
|
||||
}
|
||||
block.scope.$destroy();
|
||||
}
|
||||
// we are not using forEach for perf reasons (trying to avoid #call)
|
||||
for (index = 0; index < collectionLength; index++) {
|
||||
key = (collection === collectionKeys) ? index : collectionKeys[index];
|
||||
value = collection[key];
|
||||
block = nextBlockOrder[index];
|
||||
if (block.scope) {
|
||||
// if we have already seen this object, then we need to reuse the
|
||||
// associated scope/element
|
||||
nextNode = previousNode;
|
||||
// skip nodes that are already pending removal via leave animation
|
||||
do {
|
||||
nextNode = nextNode.nextSibling;
|
||||
} while (nextNode && nextNode[NG_REMOVED]);
|
||||
if (getBlockStart(block) != nextNode) {
|
||||
// existing item which got moved
|
||||
$animate.move(getBlockNodes(block.clone), null, jqLite(previousNode));
|
||||
}
|
||||
previousNode = getBlockEnd(block);
|
||||
updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
|
||||
} else {
|
||||
// new item which we don't know about
|
||||
$transclude(function ngRepeatTransclude(clone, scope) {
|
||||
block.scope = scope;
|
||||
// http://jsperf.com/clone-vs-createcomment
|
||||
var endNode = ngRepeatEndComment.cloneNode(false);
|
||||
clone[clone.length++] = endNode;
|
||||
// TODO(perf): support naked previousNode in `enter` to avoid creation of jqLite wrapper?
|
||||
$animate.enter(clone, null, jqLite(previousNode));
|
||||
previousNode = endNode;
|
||||
// Note: We only need the first/last node of the cloned nodes.
|
||||
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
|
||||
// by a directive with templateUrl when its template arrives.
|
||||
block.clone = clone;
|
||||
nextBlockMap[block.id] = block;
|
||||
updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
|
||||
});
|
||||
}
|
||||
}
|
||||
lastBlockMap = nextBlockMap;
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
}];
|
||||
|
||||
angular.module("tgRepeat", []).directive("tgRepeat", ngRepeatDirective);
|
||||
})();
|
|
@ -1,7 +1,9 @@
|
|||
DropdownProjectListDirective = (projectsService) ->
|
||||
link = (scope, el, attrs, ctrl) ->
|
||||
scope.vm = {}
|
||||
taiga.defineImmutableProperty(scope.vm, "projects", projectsService.projects)
|
||||
|
||||
taiga.defineImmutableProperty(scope.vm, "projects", () -> projectsService.projects.get("recents"))
|
||||
|
||||
scope.vm.newProject = ->
|
||||
projectsService.newProject()
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ a(href="", title="Projects", tg-nav="projects")
|
|||
div.navbar-dropdown.dropdown-project-list
|
||||
ul
|
||||
a(href="#",
|
||||
ng-repeat="project in vm.projects.recents",
|
||||
tg-repeat="project in vm.projects",
|
||||
ng-bind="::project.name"
|
||||
tg-nav="project:project=project.slug")
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
NavigationBarDirective = (projectsService) ->
|
||||
link = (scope, el, attrs, ctrl) ->
|
||||
scope.vm = {}
|
||||
scope.vm.projects = projectsService.projects
|
||||
|
||||
taiga.defineImmutableProperty(scope.vm, "projects", () -> projectsService.projects.get("recents"))
|
||||
|
||||
directive = {
|
||||
templateUrl: "navigation-bar/navigation-bar.html"
|
||||
|
|
|
@ -26,7 +26,7 @@ nav.navbar
|
|||
|
||||
include ../../svg/dashboard.svg
|
||||
|
||||
div.topnav-dropdown-wrapper(ng-show="vm.projects.recents", tg-dropdown-project-list)
|
||||
div.topnav-dropdown-wrapper(ng-show="vm.projects.size", tg-dropdown-project-list)
|
||||
//div.topnav-dropdown-wrapper(tg-dropdown-organization-list)
|
||||
div.topnav-dropdown-wrapper(tg-dropdown-user)
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@ ProjectsListingDirective = (projectsService) ->
|
|||
itemEl = ui.item
|
||||
project = itemEl.scope().project
|
||||
index = itemEl.index()
|
||||
sorted_project_ids = _.map(scope.vm.projects.all, (p) -> p.id)
|
||||
|
||||
sorted_project_ids = _.map(scope.vm.projects.toArray(), (p) -> p.id)
|
||||
sorted_project_ids = _.without(sorted_project_ids, project.id)
|
||||
sorted_project_ids.splice(index, 0, project.id)
|
||||
sortData = []
|
||||
|
@ -25,7 +26,7 @@ ProjectsListingDirective = (projectsService) ->
|
|||
|
||||
projectsService.bulkUpdateProjectsOrder(sortData)
|
||||
|
||||
taiga.defineImmutableProperty(scope.vm, "projects", projectsService.projects)
|
||||
taiga.defineImmutableProperty(scope.vm, "projects", () -> projectsService.projects.get("all"))
|
||||
|
||||
scope.vm.newProject = ->
|
||||
projectsService.newProject()
|
||||
|
|
|
@ -10,16 +10,16 @@ div.project-list-wrapper.centered
|
|||
section.project-list-section
|
||||
div.project-list
|
||||
ul.js-sortable
|
||||
li.project-list-single(tg-bind-scope, ng-repeat="project in vm.projects.all")
|
||||
li.project-list-single(tg-bind-scope, tg-repeat="project in vm.projects")
|
||||
div.project-list-single-left
|
||||
div.project-list-single-title
|
||||
h1
|
||||
a(href="#", ng-bind="::project.name", tg-nav="project:project=project.slug", title="{{ ::project.name }}")
|
||||
span {{project.is_private}}
|
||||
span {{::project.is_private}}
|
||||
p {{ ::project.description | limitTo:300 }}
|
||||
span(ng-if="::project.description.length > 300") ...
|
||||
|
||||
div.project-list-single-tags.tags-container(ng-if="project.tags")
|
||||
div.project-list-single-tags.tags-container(ng-if="::project.tags")
|
||||
div.tags-block(tg-colorize-tags="project.tags", tg-colorize-tags-type="backlog")
|
||||
|
||||
div.project-list-single-right
|
||||
|
|
|
@ -2,7 +2,7 @@ class ProjectsService extends taiga.Service
|
|||
@.$inject = ["$q", "$tgResources", "$rootScope", "$projectUrl"]
|
||||
|
||||
constructor: (@q, @rs, @rootScope, @projectUrl) ->
|
||||
@.projects = {all: [], recent: []}
|
||||
@.projects = Immutable.Map()
|
||||
@.inProgress = false
|
||||
@.projectsPromise = null
|
||||
@.fetchProjects()
|
||||
|
@ -15,8 +15,10 @@ class ProjectsService extends taiga.Service
|
|||
for project in projects
|
||||
project.url = @projectUrl.get(project)
|
||||
|
||||
@.projects.recents = projects.slice(0, 10)
|
||||
@.projects.all = projects
|
||||
@.projects = Immutable.fromJS({
|
||||
all: projects,
|
||||
recents: projects.slice(0, 10)
|
||||
})
|
||||
|
||||
return @.projects
|
||||
|
||||
|
|
|
@ -78,7 +78,8 @@
|
|||
"angular-translate-loader-static-files": "~2.6.1",
|
||||
"angular-translate-interpolation-messageformat": "~2.6.1",
|
||||
"ngInfiniteScroll": "1.0.0",
|
||||
"eventemitter2": "~0.4.14"
|
||||
"eventemitter2": "~0.4.14",
|
||||
"immutable": "~3.7.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"lodash": "~2.4.1",
|
||||
|
|
|
@ -144,9 +144,11 @@ paths.libs = [
|
|||
paths.vendor + "messageformat/locale/*.js",
|
||||
paths.vendor + "ngInfiniteScroll/build/ng-infinite-scroll.js",
|
||||
paths.vendor + "eventemitter2/lib/eventemitter2.js",
|
||||
paths.vendor + "immutable/dist/immutable.js",
|
||||
paths.app + "js/jquery.ui.git-custom.js",
|
||||
paths.app + "js/jquery-ui.drag-multiple-custom.js",
|
||||
paths.app + "js/jquery.ui.touch-punch.min.js",
|
||||
paths.app + "js/tg-repeat.js",
|
||||
paths.app + "js/sha1-custom.js"
|
||||
];
|
||||
|
||||
|
|
Loading…
Reference in New Issue