Select Git revision
content.js 34.50 KiB
/*jslint eqeq: true, forin: true, nomen: true*/
/*jshint unused:false, loopfunc:true */
/*global _, MAPJS, observable*/
MAPJS.content = function (contentAggregate, sessionKey) {
'use strict';
var cachedId,
invalidateIdCache = function () {
cachedId = undefined;
},
maxId = function maxId(idea) {
idea = idea || contentAggregate;
if (!idea.ideas) {
return parseInt(idea.id, 10) || 0;
}
return _.reduce(
idea.ideas,
function (result, subidea) {
return Math.max(result, maxId(subidea));
},
parseInt(idea.id, 10) || 0
);
},
nextId = function nextId(originSession) {
originSession = originSession || sessionKey;
if (!cachedId) {
cachedId = maxId();
}
cachedId += 1;
if (originSession) {
return cachedId + '.' + originSession;
}
return cachedId;
},
init = function (contentIdea, originSession) {
if (!contentIdea.id) {
contentIdea.id = nextId(originSession);
} else {
invalidateIdCache();
}
if (contentIdea.ideas) {
_.each(contentIdea.ideas, function (value, key) {
contentIdea.ideas[parseFloat(key)] = init(value, originSession);
});
}
if (!contentIdea.title) {
contentIdea.title = '';
}
contentIdea.containsDirectChild = contentIdea.findChildRankById = function (childIdeaId) {
return parseFloat(
_.reduce(
contentIdea.ideas,
function (res, value, key) {
return value.id == childIdeaId ? key : res;
},
undefined
)
);
};
contentIdea.findSubIdeaById = function (childIdeaId) {
var myChild = _.find(contentIdea.ideas, function (idea) {
return idea.id == childIdeaId;
});
return myChild || _.reduce(contentIdea.ideas, function (result, idea) {
return result || idea.findSubIdeaById(childIdeaId);
}, undefined);
};
contentIdea.find = function (predicate) {
var current = predicate(contentIdea) ? [_.pick(contentIdea, 'id', 'title')] : [];
if (_.size(contentIdea.ideas) === 0) {
return current;
}
return _.reduce(contentIdea.ideas, function (result, idea) {
return _.union(result, idea.find(predicate));
}, current);
};
contentIdea.getAttr = function (name) {
if (contentIdea.attr && contentIdea.attr[name]) {
return _.clone(contentIdea.attr[name]);
}
return false;
};
contentIdea.sortedSubIdeas = function () {
if (!contentIdea.ideas) {
return [];
}
var result = [],
childKeys = _.groupBy(_.map(_.keys(contentIdea.ideas), parseFloat), function (key) {
return key > 0;
}),
sortedChildKeys = _.sortBy(childKeys[true], Math.abs).concat(_.sortBy(childKeys[false], Math.abs));
_.each(sortedChildKeys, function (key) {
result.push(contentIdea.ideas[key]);
});
return result;
};
contentIdea.traverse = function (iterator, postOrder) {
if (!postOrder) {
iterator(contentIdea);
}
_.each(contentIdea.sortedSubIdeas(), function (subIdea) {
subIdea.traverse(iterator, postOrder);
});
if (postOrder) {
iterator(contentIdea);
}
};
return contentIdea;
},
maxKey = function (kvMap, sign) {
sign = sign || 1;
if (_.size(kvMap) === 0) {
return 0;
}
var currentKeys = _.keys(kvMap);
currentKeys.push(0);
/* ensure at least 0 is there for negative ranks */
return _.max(_.map(currentKeys, parseFloat), function (x) {
return x * sign;
});
},
nextChildRank = function (parentIdea) {
var newRank, counts, childRankSign = 1;
if (parentIdea.id == contentAggregate.id) {
counts = _.countBy(parentIdea.ideas, function (v, k) {
return k < 0;
});
if ((counts['true'] || 0) < counts['false']) {
childRankSign = -1;
}
}
newRank = maxKey(parentIdea.ideas, childRankSign) + childRankSign;
return newRank;
},
appendSubIdea = function (parentIdea, subIdea) {
var rank;
parentIdea.ideas = parentIdea.ideas || {};
rank = nextChildRank(parentIdea);
parentIdea.ideas[rank] = subIdea;
return rank;
},
findIdeaById = function (ideaId) {
return contentAggregate.id == ideaId ? contentAggregate : contentAggregate.findSubIdeaById(ideaId);
},
sameSideSiblingRanks = function (parentIdea, ideaRank) {
return _(_.map(_.keys(parentIdea.ideas), parseFloat)).reject(function (k) {
return k * ideaRank < 0;
});
},
sign = function (number) {
/* intentionally not returning 0 case, to help with split sorting into 2 groups */
return number < 0 ? -1 : 1;
},
eventStacks = {},
redoStacks = {},
isRedoInProgress = false,
batches = {},
notifyChange = function (method, args, originSession) {
if (originSession) {
contentAggregate.dispatchEvent('changed', method, args, originSession);
} else {
contentAggregate.dispatchEvent('changed', method, args);
}
},
appendChange = function (method, args, undofunc, originSession) {
var prev;
if (method === 'batch' || batches[originSession] || !eventStacks || !eventStacks[originSession] || eventStacks[originSession].length === 0) {
logChange(method, args, undofunc, originSession);
return;
} else {
prev = eventStacks[originSession].pop();
if (prev.eventMethod === 'batch') {
eventStacks[originSession].push({
eventMethod: 'batch',
eventArgs: prev.eventArgs.concat([[method].concat(args)]),
undoFunction: function () {
undofunc();
prev.undoFunction();
}
});
} else {
eventStacks[originSession].push({
eventMethod: 'batch',
eventArgs: [[prev.eventMethod].concat(prev.eventArgs)].concat([[method].concat(args)]),
undoFunction: function () {
undofunc();
prev.undoFunction();
}
});
}
}
if (isRedoInProgress) {
contentAggregate.dispatchEvent('changed', 'redo', undefined, originSession);
} else {
notifyChange(method, args, originSession);
redoStacks[originSession] = [];
}
},
logChange = function (method, args, undofunc, originSession) {
var event = {eventMethod: method, eventArgs: args, undoFunction: undofunc};
if (batches[originSession]) {
batches[originSession].push(event);
return;
}
if (!eventStacks[originSession]) {
eventStacks[originSession] = [];
}
eventStacks[originSession].push(event);
if (isRedoInProgress) {
contentAggregate.dispatchEvent('changed', 'redo', undefined, originSession);
} else {
notifyChange(method, args, originSession);
redoStacks[originSession] = [];
}
},
reorderChild = function (parentIdea, newRank, oldRank) {
parentIdea.ideas[newRank] = parentIdea.ideas[oldRank];
delete parentIdea.ideas[oldRank];
},
upgrade = function (idea) {
if (idea.style) {
idea.attr = {};
var collapsed = idea.style.collapsed;
delete idea.style.collapsed;
idea.attr.style = idea.style;
if (collapsed) {
idea.attr.collapsed = collapsed;
}
delete idea.style;
}
if (idea.ideas) {
_.each(idea.ideas, upgrade);
}
},
sessionFromId = function (id) {
var dotIndex = String(id).indexOf('.');
return dotIndex > 0 && id.substr(dotIndex + 1);
},
commandProcessors = {},
configuration = {},
uniqueResourcePostfix = '/xxxxxxxx-yxxx-yxxx-yxxx-xxxxxxxxxxxx/'.replace(/[xy]/g, function (c) {
/*jshint bitwise: false*/
// jscs:disable
var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
// jscs:enable
return v.toString(16);
}) + (sessionKey || ''),
updateAttr = function (object, attrName, attrValue) {
var oldAttr;
if (!object) {
return false;
}
oldAttr = _.extend({}, object.attr);
object.attr = _.extend({}, object.attr);
if (!attrValue || attrValue === 'false' || (_.isObject(attrValue) && _.isEmpty(attrValue))) {
if (!object.attr[attrName]) {
return false;
}
delete object.attr[attrName];
} else {
if (_.isEqual(object.attr[attrName], attrValue)) {
return false;
}
object.attr[attrName] = JSON.parse(JSON.stringify(attrValue));
}
if (_.size(object.attr) === 0) {
delete object.attr;
}
return function () {
object.attr = oldAttr;
};
};
contentAggregate.logChange = logChange;
contentAggregate.setConfiguration = function (config) {
configuration = config || {};
};
contentAggregate.getSessionKey = function () {
return sessionKey;
};
contentAggregate.nextSiblingId = function (subIdeaId) {
var parentIdea = contentAggregate.findParent(subIdeaId),
currentRank,
candidateSiblingRanks,
siblingsAfter;
if (!parentIdea) {
return false;
}
currentRank = parentIdea.findChildRankById(subIdeaId);
candidateSiblingRanks = sameSideSiblingRanks(parentIdea, currentRank);
siblingsAfter = _.reject(candidateSiblingRanks, function (k) {
return Math.abs(k) <= Math.abs(currentRank);
});
if (siblingsAfter.length === 0) {
return false;
}
return parentIdea.ideas[_.min(siblingsAfter, Math.abs)].id;
};
contentAggregate.sameSideSiblingIds = function (subIdeaId) {
var parentIdea = contentAggregate.findParent(subIdeaId),
currentRank = parentIdea.findChildRankById(subIdeaId);
return _.without(_.map(_.pick(parentIdea.ideas, sameSideSiblingRanks(parentIdea, currentRank)), function (i) {
return i.id;
}), subIdeaId);
};
contentAggregate.getAttrById = function (ideaId, attrName) {
var idea = findIdeaById(ideaId);
return idea && idea.getAttr(attrName);
};
contentAggregate.previousSiblingId = function (subIdeaId) {
var parentIdea = contentAggregate.findParent(subIdeaId),
currentRank,
candidateSiblingRanks,
siblingsBefore;
if (!parentIdea) {
return false;
}
currentRank = parentIdea.findChildRankById(subIdeaId);
candidateSiblingRanks = sameSideSiblingRanks(parentIdea, currentRank);
siblingsBefore = _.reject(candidateSiblingRanks, function (k) {
return Math.abs(k) >= Math.abs(currentRank);
});
if (siblingsBefore.length === 0) {
return false;
}
return parentIdea.ideas[_.max(siblingsBefore, Math.abs)].id;
};
contentAggregate.clone = function (subIdeaId) {
var toClone = (subIdeaId && subIdeaId != contentAggregate.id && contentAggregate.findSubIdeaById(subIdeaId)) || contentAggregate;
return JSON.parse(JSON.stringify(toClone));
};
contentAggregate.cloneMultiple = function (subIdeaIdArray) {
return _.map(subIdeaIdArray, contentAggregate.clone);
};
contentAggregate.calculatePath = function (ideaId, currentPath, potentialParent) {
if (contentAggregate.id == ideaId) {
return [];
}
currentPath = currentPath || [contentAggregate];
potentialParent = potentialParent || contentAggregate;
if (potentialParent.containsDirectChild(ideaId)) {
return currentPath;
}
return _.reduce(
potentialParent.ideas,
function (result, child) {
return result || contentAggregate.calculatePath(ideaId, [child].concat(currentPath), child);
},
false
);
};
contentAggregate.getSubTreeIds = function (rootIdeaId) {
var result = [],
collectIds = function (idea) {
if (_.isEmpty(idea.ideas)) {
return [];
}
_.each(idea.sortedSubIdeas(), function (child) {
collectIds(child);
result.push(child.id);
});
};
collectIds(contentAggregate.findSubIdeaById(rootIdeaId) || contentAggregate);
return result;
};
contentAggregate.findParent = function (subIdeaId, parentIdea) {
parentIdea = parentIdea || contentAggregate;
if (parentIdea.containsDirectChild(subIdeaId)) {
return parentIdea;
}
return _.reduce(
parentIdea.ideas,
function (result, child) {
return result || contentAggregate.findParent(subIdeaId, child);
},
false
);
};
/**** aggregate command processing methods ****/
contentAggregate.getCommandProcessors = function () {
return commandProcessors;
};
contentAggregate.startBatch = function (originSession) {
var activeSession = originSession || sessionKey;
contentAggregate.endBatch(originSession);
batches[activeSession] = [];
};
contentAggregate.endBatch = function (originSession) {
var activeSession = originSession || sessionKey,
inBatch = batches[activeSession],
batchArgs,
batchUndoFunctions,
undo;
batches[activeSession] = undefined;
if (_.isEmpty(inBatch)) {
return;
}
if (_.size(inBatch) === 1) {
logChange(inBatch[0].eventMethod, inBatch[0].eventArgs, inBatch[0].undoFunction, activeSession);
} else {
batchArgs = _.map(inBatch, function (event) {
return [event.eventMethod].concat(event.eventArgs);
});
batchUndoFunctions = _.sortBy(
_.map(inBatch, function (event) {
return event.undoFunction;
}),
function (f, idx) {
return -1 * idx;
}
);
undo = function () {
_.each(batchUndoFunctions, function (eventUndo) {
eventUndo();
});
};
logChange('batch', batchArgs, undo, activeSession);
}
};
contentAggregate.execCommand = function (cmd, args, originSession) {
if (!commandProcessors[cmd]) {
return false;
}
return commandProcessors[cmd].apply(contentAggregate, [originSession || sessionKey].concat(_.toArray(args)));
};
contentAggregate.batch = function (batchOp) {
contentAggregate.startBatch();
try {
batchOp();
}
finally {
contentAggregate.endBatch();
}
};
commandProcessors.batch = function (originSession) {
contentAggregate.startBatch(originSession);
try {
_.each(_.toArray(arguments).slice(1), function (event) {
contentAggregate.execCommand(event[0], event.slice(1), originSession);
});
}
finally {
contentAggregate.endBatch(originSession);
}
};
contentAggregate.pasteMultiple = function (parentIdeaId, jsonArrayToPaste) {
contentAggregate.startBatch();
var results = _.map(jsonArrayToPaste, function (json) {
return contentAggregate.paste(parentIdeaId, json);
});
contentAggregate.endBatch();
return results;
};
contentAggregate.paste = function (parentIdeaId, jsonToPaste, initialId) {
return contentAggregate.execCommand('paste', arguments);
};
commandProcessors.paste = function (originSession, parentIdeaId, jsonToPaste, initialId) {
var pasteParent = (parentIdeaId == contentAggregate.id) ? contentAggregate : contentAggregate.findSubIdeaById(parentIdeaId),
cleanUp = function (json) {
var result = _.omit(json, 'ideas', 'id', 'attr'), index = 1, childKeys, sortedChildKeys;
result.attr = _.omit(json.attr, configuration.nonClonedAttributes);
if (_.isEmpty(result.attr)) {
delete result.attr;
}
if (json.ideas) {
childKeys = _.groupBy(_.map(_.keys(json.ideas), parseFloat), function (key) {
return key > 0;
});
sortedChildKeys = _.sortBy(childKeys[true], Math.abs).concat(_.sortBy(childKeys[false], Math.abs));
result.ideas = {};
_.each(sortedChildKeys, function (key) {
result.ideas[index++] = cleanUp(json.ideas[key]);
});
}
return result;
},
newIdea,
newRank,
oldPosition;
if (initialId) {
cachedId = parseInt(initialId, 10) - 1;
}
newIdea = jsonToPaste && (jsonToPaste.title || jsonToPaste.attr) && init(cleanUp(jsonToPaste), sessionFromId(initialId));
if (!pasteParent || !newIdea) {
return false;
}
if (!contentAggregate.getYsy().validator.validate("paste", pasteParent, newIdea)) return false;
newRank = appendSubIdea(pasteParent, newIdea);
if (initialId) {
invalidateIdCache();
}
updateAttr(newIdea, 'position');
logChange('paste', [parentIdeaId, jsonToPaste, newIdea.id], function () {
delete pasteParent.ideas[newRank];
}, originSession);
return newIdea.id;
};
contentAggregate.flip = function (ideaId) {
return contentAggregate.execCommand('flip', arguments);
};
commandProcessors.flip = function (originSession, ideaId) {
var newRank, maxRank, currentRank = contentAggregate.findChildRankById(ideaId);
if (!currentRank) {
return false;
}
maxRank = maxKey(contentAggregate.ideas, -1 * sign(currentRank));
newRank = maxRank - 10 * sign(currentRank);
reorderChild(contentAggregate, newRank, currentRank);
logChange('flip', [ideaId], function () {
reorderChild(contentAggregate, currentRank, newRank);
}, originSession);
return true;
};
contentAggregate.initialiseTitle = function (ideaId, title) {
return contentAggregate.execCommand('initialiseTitle', arguments);
};
commandProcessors.initialiseTitle = function (originSession, ideaId, title) {
var idea = findIdeaById(ideaId), originalTitle;
if (!idea) {
return false;
}
originalTitle = idea.title;
if (originalTitle == title) {
return false;
}
idea.title = title;
appendChange('initialiseTitle', [ideaId, title], function () {
idea.title = originalTitle;
}, originSession);
return true;
};
contentAggregate.updateTitle = function (ideaId, title) {
return contentAggregate.execCommand('updateTitle', arguments);
};
commandProcessors.updateTitle = function (originSession, ideaId, title) {
var idea = findIdeaById(ideaId), originalTitle;
if (!idea) {
return false;
}
originalTitle = idea.title;
if (originalTitle == title) {
return false;
}
idea.title = title;
logChange('updateTitle', [ideaId, title], function () {
idea.title = originalTitle;
}, originSession);
return true;
};
contentAggregate.addSubIdea = function (parentId, ideaTitle, optionalNewId) {
return contentAggregate.execCommand('addSubIdea', arguments);
};
commandProcessors.addSubIdea = function (originSession, parentId, ideaTitle, optionalNewId) {
var idea, parent = findIdeaById(parentId), newRank;
if (!parent) {
return false;
}
if (optionalNewId && findIdeaById(optionalNewId)) {
return false;
}
idea = init({
title: ideaTitle,
id: optionalNewId
});
if (!contentAggregate.getYsy().validator.validate("addSubIdea", parent, idea)) return false;
newRank = appendSubIdea(parent, idea);
logChange('addSubIdea', [parentId, ideaTitle, idea.id], function () {
delete parent.ideas[newRank];
}, originSession);
return idea.id;
};
contentAggregate.removeMultiple = function (subIdeaIdArray) {
contentAggregate.startBatch();
var results = _.map(subIdeaIdArray, contentAggregate.removeSubIdea);
contentAggregate.endBatch();
return results;
};
contentAggregate.removeSubIdea = function (subIdeaId) {
return contentAggregate.execCommand('removeSubIdea', arguments);
};
commandProcessors.removeSubIdea = function (originSession, subIdeaId) {
if (!contentAggregate.getYsy().validator.validate("removeSubIdea", subIdeaId, originSession)) return false;
var parent = contentAggregate.findParent(subIdeaId), oldRank, oldIdea, oldLinks;
if (parent) {
oldRank = parent.findChildRankById(subIdeaId);
oldIdea = parent.ideas[oldRank];
delete parent.ideas[oldRank];
oldLinks = contentAggregate.links;
contentAggregate.links = _.reject(contentAggregate.links, function (link) {
return link.ideaIdFrom == subIdeaId || link.ideaIdTo == subIdeaId;
});
logChange('removeSubIdea', [subIdeaId], function () {
parent.ideas[oldRank] = oldIdea;
contentAggregate.links = oldLinks;
}, originSession);
return true;
}
return false;
};
contentAggregate.insertIntermediateMultiple = function (idArray) {
contentAggregate.startBatch();
var newId = contentAggregate.insertIntermediate(idArray[0]);
_.each(idArray.slice(1), function (id) {
contentAggregate.changeParent(id, newId);
});
contentAggregate.endBatch();
return newId;
};
contentAggregate.insertIntermediate = function (inFrontOfIdeaId, title, optionalNewId) {
return contentAggregate.execCommand('insertIntermediate', arguments);
};
commandProcessors.insertIntermediate = function (originSession, inFrontOfIdeaId, title, optionalNewId) {
if (contentAggregate.id == inFrontOfIdeaId) {
return false;
}
var childRank, oldIdea, newIdea, parentIdea = contentAggregate.findParent(inFrontOfIdeaId);
if (!parentIdea) {
return false;
}
if (optionalNewId && findIdeaById(optionalNewId)) {
return false;
}
childRank = parentIdea.findChildRankById(inFrontOfIdeaId);
if (!childRank) {
return false;
}
oldIdea = parentIdea.ideas[childRank];
newIdea = init({
title: title,
id: optionalNewId
});
if (!contentAggregate.getYsy().validator.validate("insertIntermediate", parentIdea, oldIdea, newIdea)) return false;
parentIdea.ideas[childRank] = newIdea;
newIdea.ideas = {
1: oldIdea
};
logChange('insertIntermediate', [inFrontOfIdeaId, title, newIdea.id], function () {
parentIdea.ideas[childRank] = oldIdea;
}, originSession);
return newIdea.id;
};
contentAggregate.changeParent = function (ideaId, newParentId) {
return contentAggregate.execCommand('changeParent', arguments);
};
commandProcessors.changeParent = function (originSession, ideaId, newParentId) {
var oldParent, oldRank, newRank, idea, parent = findIdeaById(newParentId), oldPosition;
if (ideaId == newParentId) {
return false;
}
if (!parent) {
return false;
}
idea = contentAggregate.findSubIdeaById(ideaId);
if (!idea) {
return false;
}
if (idea.findSubIdeaById(newParentId)) {
return false;
}
if (parent.containsDirectChild(ideaId)) {
return false;
}
if (!contentAggregate.getYsy().validator.changeParent(idea, parent)) return false;
oldParent = contentAggregate.findParent(ideaId);
if (!oldParent) {
return false;
}
oldRank = oldParent.findChildRankById(ideaId);
newRank = appendSubIdea(parent, idea);
oldPosition = idea.getAttr('position');
updateAttr(idea, 'position');
delete oldParent.ideas[oldRank];
logChange('changeParent', [ideaId, newParentId, oldParent.id], function () {
updateAttr(idea, 'position', oldPosition);
oldParent.ideas[oldRank] = idea;
delete parent.ideas[newRank];
}, originSession);
return true;
};
contentAggregate.mergeAttrProperty = function (ideaId, attrName, attrPropertyName, attrPropertyValue) {
var val = contentAggregate.getAttrById(ideaId, attrName) || {};
if (attrPropertyValue) {
val[attrPropertyName] = attrPropertyValue;
} else {
delete val[attrPropertyName];
}
if (_.isEmpty(val)) {
val = false;
}
return contentAggregate.updateAttr(ideaId, attrName, val);
};
contentAggregate.updateAttr = function (ideaId, attrName, attrValue) {
return contentAggregate.execCommand('updateAttr', arguments);
};
commandProcessors.updateAttr = function (originSession, ideaId, attrName, attrValue) {
var idea = findIdeaById(ideaId), undoAction;
undoAction = updateAttr(idea, attrName, attrValue);
if (undoAction) {
logChange('updateAttr', [ideaId, attrName, attrValue], undoAction, originSession);
}
return !!undoAction;
};
contentAggregate.moveRelative = function (ideaId, relativeMovement) {
var parentIdea = contentAggregate.findParent(ideaId),
currentRank = parentIdea && parentIdea.findChildRankById(ideaId),
siblingRanks = currentRank && _.sortBy(sameSideSiblingRanks(parentIdea, currentRank), Math.abs),
currentIndex = siblingRanks && siblingRanks.indexOf(currentRank),
/* we call positionBefore, so movement down is actually 2 spaces, not 1 */
newIndex = currentIndex + (relativeMovement > 0 ? relativeMovement + 1 : relativeMovement),
beforeSibling = (newIndex >= 0) && parentIdea && siblingRanks && parentIdea.ideas[siblingRanks[newIndex]];
if (newIndex < 0 || !parentIdea) {
return false;
}
return contentAggregate.positionBefore(ideaId, beforeSibling && beforeSibling.id, parentIdea);
};
contentAggregate.positionBefore = function (ideaId, positionBeforeIdeaId, parentIdea) {
return contentAggregate.execCommand('positionBefore', arguments);
};
commandProcessors.positionBefore = function (originSession, ideaId, positionBeforeIdeaId, parentIdea) {
parentIdea = parentIdea || contentAggregate;
var newRank, afterRank, siblingRanks, candidateSiblings, beforeRank, maxRank, currentRank;
currentRank = parentIdea.findChildRankById(ideaId);
if (!currentRank) {
return _.reduce(
parentIdea.ideas,
function (result, idea) {
return result || commandProcessors.positionBefore(originSession, ideaId, positionBeforeIdeaId, idea);
},
false
);
}
if (ideaId == positionBeforeIdeaId) {
return false;
}
newRank = 0;
if (positionBeforeIdeaId) {
afterRank = parentIdea.findChildRankById(positionBeforeIdeaId);
if (!afterRank) {
return false;
}
siblingRanks = sameSideSiblingRanks(parentIdea, currentRank);
candidateSiblings = _.reject(_.sortBy(siblingRanks, Math.abs), function (k) {
return Math.abs(k) >= Math.abs(afterRank);
});
beforeRank = candidateSiblings.length > 0 ? _.max(candidateSiblings, Math.abs) : 0;
if (beforeRank == currentRank) {
return false;
}
newRank = beforeRank + (afterRank - beforeRank) / 2;
} else {
maxRank = maxKey(parentIdea.ideas, currentRank < 0 ? -1 : 1);
if (maxRank == currentRank) {
return false;
}
newRank = maxRank + 10 * (currentRank < 0 ? -1 : 1);
}
if (newRank == currentRank) {
return false;
}
reorderChild(parentIdea, newRank, currentRank);
logChange('positionBefore', [ideaId, positionBeforeIdeaId], function () {
reorderChild(parentIdea, currentRank, newRank);
}, originSession);
return true;
};
observable(contentAggregate);
(function () {
var isLinkValid = function (ideaIdFrom, ideaIdTo) {
var isParentChild, ideaFrom, ideaTo;
if (ideaIdFrom === ideaIdTo) {
return false;
}
ideaFrom = findIdeaById(ideaIdFrom);
if (!ideaFrom) {
return false;
}
ideaTo = findIdeaById(ideaIdTo);
if (!ideaTo) {
return false;
}
isParentChild = _.find(
ideaFrom.ideas,
function (node) {
return node.id === ideaIdTo;
}
) || _.find(
ideaTo.ideas,
function (node) {
return node.id === ideaIdFrom;
}
);
if (isParentChild) {
return false;
}
return true;
};
contentAggregate.addLink = function (ideaIdFrom, ideaIdTo) {
return contentAggregate.execCommand('addLink', arguments);
};
commandProcessors.addLink = function (originSession, ideaIdFrom, ideaIdTo) {
var alreadyExists, link;
if (!isLinkValid(ideaIdFrom, ideaIdTo)) {
return false;
}
alreadyExists = _.find(
contentAggregate.links,
function (link) {
return (link.ideaIdFrom === ideaIdFrom && link.ideaIdTo === ideaIdTo) || (link.ideaIdFrom === ideaIdTo && link.ideaIdTo === ideaIdFrom);
}
);
if (alreadyExists) {
return false;
}
contentAggregate.links = contentAggregate.links || [];
link = {
ideaIdFrom: ideaIdFrom,
ideaIdTo: ideaIdTo,
attr: {
style: {
color: '#FF0000',
lineStyle: 'dashed'
}
}
};
contentAggregate.links.push(link);
logChange('addLink', [ideaIdFrom, ideaIdTo], function () {
contentAggregate.links.pop();
}, originSession);
return true;
};
contentAggregate.removeLink = function (ideaIdOne, ideaIdTwo) {
return contentAggregate.execCommand('removeLink', arguments);
};
commandProcessors.removeLink = function (originSession, ideaIdOne, ideaIdTwo) {
var i = 0, link;
while (contentAggregate.links && i < contentAggregate.links.length) {
link = contentAggregate.links[i];
if (String(link.ideaIdFrom) === String(ideaIdOne) && String(link.ideaIdTo) === String(ideaIdTwo)) {
contentAggregate.links.splice(i, 1);
logChange('removeLink', [ideaIdOne, ideaIdTwo], function () {
contentAggregate.links.push(_.clone(link));
}, originSession);
return true;
}
i += 1;
}
return false;
};
contentAggregate.getLinkAttr = function (ideaIdFrom, ideaIdTo, name) {
var link = _.find(
contentAggregate.links,
function (link) {
return link.ideaIdFrom == ideaIdFrom && link.ideaIdTo == ideaIdTo;
}
);
if (link && link.attr && link.attr[name]) {
return link.attr[name];
}
return false;
};
contentAggregate.updateLinkAttr = function (ideaIdFrom, ideaIdTo, attrName, attrValue) {
return contentAggregate.execCommand('updateLinkAttr', arguments);
};
commandProcessors.updateLinkAttr = function (originSession, ideaIdFrom, ideaIdTo, attrName, attrValue) {
var link = _.find(
contentAggregate.links,
function (link) {
return link.ideaIdFrom == ideaIdFrom && link.ideaIdTo == ideaIdTo;
}
), undoAction;
undoAction = updateAttr(link, attrName, attrValue);
if (undoAction) {
logChange('updateLinkAttr', [ideaIdFrom, ideaIdTo, attrName, attrValue], undoAction, originSession);
}
return !!undoAction;
};
}());
/* undo/redo */
contentAggregate.resetHistory = function () {
eventStacks = {};
redoStacks = {};
};
contentAggregate.canUndo = function () {
return !!(eventStacks[sessionKey] && eventStacks[sessionKey].length > 0);
};
contentAggregate.canRedo = function () {
return !!(redoStacks[sessionKey] && redoStacks[sessionKey].length > 0);
};
contentAggregate.undo = function () {
return contentAggregate.execCommand('undo', arguments);
};
commandProcessors.undo = function (originSession) {
contentAggregate.endBatch();
var topEvent;
topEvent = eventStacks[originSession] && eventStacks[originSession].pop();
if (topEvent && topEvent.undoFunction) {
topEvent.undoFunction();
if (!redoStacks[originSession]) {
redoStacks[originSession] = [];
}
redoStacks[originSession].push(topEvent);
contentAggregate.dispatchEvent('changed', 'undo', [], originSession);
return true;
}
return false;
};
contentAggregate.redo = function () {
return contentAggregate.execCommand('redo', arguments);
};
commandProcessors.redo = function (originSession) {
contentAggregate.endBatch();
var topEvent;
topEvent = redoStacks[originSession] && redoStacks[originSession].pop();
if (topEvent) {
isRedoInProgress = true;
contentAggregate.execCommand(topEvent.eventMethod, topEvent.eventArgs, originSession);
isRedoInProgress = false;
return true;
}
return false;
};
contentAggregate.storeResource = function (/*resourceBody, optionalKey*/) {
return contentAggregate.execCommand('storeResource', arguments);
};
commandProcessors.storeResource = function (originSession, resourceBody, optionalKey) {
var existingId, id,
maxIdForSession = function () {
if (_.isEmpty(contentAggregate.resources)) {
return 0;
}
var toInt = function (string) {
return parseInt(string, 10);
},
keys = _.keys(contentAggregate.resources),
filteredKeys = sessionKey ? _.filter(keys, RegExp.prototype.test.bind(new RegExp('\\/' + sessionKey + '$'))) : keys,
intKeys = _.map(filteredKeys, toInt);
return _.isEmpty(intKeys) ? 0 : _.max(intKeys);
},
nextResourceId = function () {
var intId = maxIdForSession() + 1;
return intId + uniqueResourcePostfix;
};
if (!optionalKey && contentAggregate.resources) {
existingId = _.find(_.keys(contentAggregate.resources), function (key) {
return contentAggregate.resources[key] === resourceBody;
});
if (existingId) {
return existingId;
}
}
id = optionalKey || nextResourceId();
contentAggregate.resources = contentAggregate.resources || {};
contentAggregate.resources[id] = resourceBody;
contentAggregate.dispatchEvent('resourceStored', resourceBody, id, originSession);
return id;
};
contentAggregate.getResource = function (id) {
return contentAggregate.resources && contentAggregate.resources[id];
};
contentAggregate.hasSiblings = function (id) {
if (id === contentAggregate.id) {
return false;
}
var parent = contentAggregate.findParent(id);
return parent && _.size(parent.ideas) > 1;
};
if (contentAggregate.formatVersion != 2) {
upgrade(contentAggregate);
contentAggregate.formatVersion = 2;
}
init(contentAggregate);
return contentAggregate;
};