[mb-commits] branch, beta, updated. Merge remote-tracking branch 'bitmap/knockout-2.2.0-upgrade' into beta Upgrad...
MusicBrainz Git Server <[email protected]>
| Newsgroups | gmane.comp.audio.musicbrainz.cvs |
|---|---|
| Message-ID | <E1TTZxb-0005nX-P0@wiley> |
The branch, beta has been updated
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=c5ccae2d806a22dfdd81d6206f8c41441a336769 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=6adb200d24e881ea378024ec157e246899f8423c (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=08ea6ce9c82e3d436eb70eb94abda77f54054292 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=fd647f86b613b72bc49b92c98ea31636198f347c (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=684cc3e815c33a43c0b21e9238e2599a484b585c (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=ac01ec72b245c38b340366dd51a7c9d6f29c8d51 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=a9a9cd279e10e4a3f4d28673ca2e5ef85d1b0fa0 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=c3075e51689a2cf3ab04d65366797ca9255ce626 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=63041d75886c85863c37cc1ddd60c393dfebd529 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=0aa7752ed2ee645974f0318e091358350341434d (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=8393ef47433de047a7eee98eef531139888e42d5 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=49aa509da1ebb8b837b4b3cafd5aa14e775a5a8f (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=ae8998748bd0e70e23e7ef07b3b63e1f8c8bb17a (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=070070138957f693768b57a7a1ce28f388ab8de9 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=231fbd6576b11b06d0523efd04944cb339b2d666 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=a3297c80d5d70091a595bfc4a480be9c254a2234 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=e8ab6a5159a38465f46f4722486c8bacb8cd911b (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=b89cf93efaf0678881d43af27b76f97f073495c8 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=3d66a69744bc014f3e063c8e718f12669705d675 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=ffc2aafea8dac4f24fb62c7ed043da9c351c7173 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=0650b786f211914d9b01f66a30ed7f4dab6ebfa6 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=2b57f54d138f3e6d16bcaa8b207efaf213a0a573 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=90461ef717ee409ca872fccf44b68d815e794bc4 (commit)
via http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=ab6011c012d1277e9f2634cbd05f9693b2aa42d9 (commit)
from http://git.musicbrainz.org/gitweb/?p=musicbrainz-server/core.git;a=commit;h=0daca74fc9bfbbeca8aa185e9c217efa8e0cf97b (commit)
Summary of changes:
.../Server/Controller/RelationshipEditor.pm | 16 +-
root/edit/index.tt | 6 +-
root/release/edit_relationships.tt | 126 +-
root/scripts/text_strings.tt | 3 +-
root/static/lib/knockout/knockout-latest.debug.js | 1264 +++++++++++---------
.../lib/knockout/knockout.mapping-latest.debug.js | 716 -----------
root/static/lib/libraries.txt | 10 +-
root/static/relationship-editor.js.manifest | 1 -
root/static/scripts/common/multiselect.js | 20 +-
root/static/scripts/relationship-editor/Dialog.js | 436 ++++----
root/static/scripts/relationship-editor/Entity.js | 209 ++--
root/static/scripts/relationship-editor/Fields.js | 169 ++--
.../scripts/relationship-editor/Relationship.js | 393 +++----
.../relationship-editor/RelationshipEditor.js | 59 +-
root/static/scripts/relationship-editor/Util.js | 108 +--
root/static/scripts/tests/RelationshipEditor.js | 390 +++---
root/static/scripts/tests/all.html | 1 -
root/static/styles/relationship-editor.css | 1 +
root/static/styles/widgets.css | 6 +
root/work/edit_form.tt | 6 +-
20 files changed, 1561 insertions(+), 2379 deletions(-)
delete mode 100644 root/static/lib/knockout/knockout.mapping-latest.debug.js
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit c5ccae2d806a22dfdd81d6206f8c41441a336769
Merge: 6adb200 49aa509
Author: Oliver Charles <[email protected]>
Date: Wed Oct 31 15:01:59 2012 +0000
Merge remote-tracking branch 'bitmap/knockout-2.2.0-upgrade' into beta
* bitmap/knockout-2.2.0-upgrade:
Upgrade knockout.js to 2.2.0
diff --cc root/static/scripts/relationship-editor/Dialog.js
index 766bae0,f412855..078e8cc
--- a/root/static/scripts/relationship-editor/Dialog.js
+++ b/root/static/scripts/relationship-editor/Dialog.js
@@@ -316,20 -289,8 +315,8 @@@ var Dialog = UI.Dialog =
showAttributesHelp: ko.observable(false),
showLinkTypeHelp: ko.observable(false),
- relationship: (function() {
- var value = ko.observable(null);
-
- return ko.computed({
- read: value,
- write: function(newValue) {
- var oldValue = value();
- if (oldValue !== newValue) value(newValue);
- }
- });
- }()),
-
init: function() {
- var entity = [RE.Entity({type: "artist"}), RE.Entity({type: "recording"})];
+ var self = this, entity = [RE.Entity({type: "artist"}), RE.Entity({type: "recording"})];
// this is used as an "empty" state when the dialog is hidden, so that
// none of the bindings error out.
commit 6adb200d24e881ea378024ec157e246899f8423c
Merge: 08ea6ce 0aa7752
Author: Oliver Charles <[email protected]>
Date: Wed Oct 31 15:01:25 2012 +0000
Merge remote-tracking branch 'mineo/add-vote-button-for-review' into beta
* mineo/add-vote-button-for-review:
Do not show 'and vote' on the note/vote submit button for closed edits
commit 08ea6ce9c82e3d436eb70eb94abda77f54054292
Merge: fd647f8 8393ef4
Author: Oliver Charles <[email protected]>
Date: Wed Oct 31 14:58:45 2012 +0000
Merge remote-tracking branch 'bitmap/mbs-5538' into beta
* bitmap/mbs-5538:
MBS-5538: Pressing enter in work add adds/removes ISWC instead of submitting
commit fd647f86b613b72bc49b92c98ea31636198f347c
Merge: 684cc3e e8ab6a5
Author: Oliver Charles <[email protected]>
Date: Wed Oct 31 14:57:10 2012 +0000
Merge remote-tracking branch 'bitmap/mbs-5521' into beta
* bitmap/mbs-5521:
MBS-5521: Date placeholders in the relationship editor are not translated
commit 684cc3e815c33a43c0b21e9238e2599a484b585c
Merge: ac01ec7 231fbd6
Author: Oliver Charles <[email protected]>
Date: Wed Oct 31 14:56:40 2012 +0000
Merge remote-tracking branch 'bitmap/mbs-5438-change-batch-tools-ui' into beta
* bitmap/mbs-5438-change-batch-tools-ui:
Rearrange the rel editor's batch ui
commit ac01ec72b245c38b340366dd51a7c9d6f29c8d51
Merge: a9a9cd2 0700701
Author: Oliver Charles <[email protected]>
Date: Wed Oct 31 14:56:12 2012 +0000
Merge remote-tracking branch 'bitmap/mbs-5529' into beta
* bitmap/mbs-5529:
Relationship editor could partially submit edits
commit a9a9cd279e10e4a3f4d28673ca2e5ef85d1b0fa0
Merge: c3075e5 ae89987
Author: Oliver Charles <[email protected]>
Date: Wed Oct 31 14:55:45 2012 +0000
Merge remote-tracking branch 'bitmap/mbs-5480' into beta
* bitmap/mbs-5480:
MBS-5480: limit the size of the autocomplete menu
diff --cc root/static/scripts/relationship-editor/Dialog.js
index 40c27d3,414968e..766bae0
--- a/root/static/scripts/relationship-editor/Dialog.js
+++ b/root/static/scripts/relationship-editor/Dialog.js
@@@ -209,7 -209,15 +209,9 @@@ ko.bindingHandlers.autocomplete = (func
$("#target-type").val(type).trigger("change");
}
- function closeOnEnter(event) {
- if (event.keyCode == 13 && !Dialog.relationship.peek().hasErrors.peek() &&
- !event.isDefaultPrevented())
- Dialog.instance.peek().accept();
- }
-
function changeTarget(event, data) {
+ if (!data.gid) return;
+
// XXX release groups' numeric "type" conflicts with the entity type
data.type = _.isNumber(data.type) ? "release_group" : (data.type || Dialog.target.type);
commit c3075e51689a2cf3ab04d65366797ca9255ce626
Merge: 63041d7 b89cf93
Author: Oliver Charles <[email protected]>
Date: Wed Oct 31 14:55:01 2012 +0000
Merge remote-tracking branch 'bitmap/mbs-5476' into beta
* bitmap/mbs-5476:
Extraneous semicolon
MBS-5476 / MBS-5445: Misc. fixes pressing <enter> in dialog
commit 63041d75886c85863c37cc1ddd60c393dfebd529
Merge: 0daca74 a3297c8
Author: Oliver Charles <[email protected]>
Date: Wed Oct 31 14:54:17 2012 +0000
Merge remote-tracking branch 'bitmap/mbs-5389' into beta
* bitmap/mbs-5389:
Fix open edits link in the rel editor dialog
Fix the "Create a new work" link not displaying
Avoid re-rendering the dialog attrs
Correctly handle relating a recording on the release to another recording on the release (MBS-5389)
commit 0aa7752ed2ee645974f0318e091358350341434d
Author: Wieland Hoffmann <[email protected]>
Date: Sat Oct 27 15:45:36 2012 +0200
Do not show 'and vote' on the note/vote submit button for closed edits
diff --git a/root/edit/index.tt b/root/edit/index.tt
index 9bb2f8c..57d66c6 100644
--- a/root/edit/index.tt
+++ b/root/edit/index.tt
@@ -78,8 +78,10 @@
<h2>[% l('Edit notes') %]</h2>
[% INCLUDE 'edit/notes.tt' %]
- [%- IF edit.editor_may_add_note(c.user) || edit.editor_may_vote -%]
- [%- form_submit(l('Add note and vote')) -%]
+ [%- IF edit.editor_may_vote(c.user) -%]
+ [%- form_submit(l('Add note and vote')) -%]
+ [%- ELSIF edit.editor_may_add_note(c.user) -%]
+ [%- form_submit(l('Add note')) -%]
[%- END -%]
</form>
</div>
commit 8393ef47433de047a7eee98eef531139888e42d5
Author: Michael Wiencek <[email protected]>
Date: Tue Oct 30 18:29:00 2012 +0000
MBS-5538: Pressing enter in work add adds/removes ISWC instead of submitting
diff --git a/root/work/edit_form.tt b/root/work/edit_form.tt
index ea741ab..446f126 100644
--- a/root/work/edit_form.tt
+++ b/root/work/edit_form.tt
@@ -1,4 +1,4 @@
-<form action="[% c.req.uri %]" method="post">
+<form action="[% c.req.uri %]" method="post" class="edit-work">
[%- USE r = FormRenderer(form) -%]
<div class="half-width">
@@ -23,3 +23,7 @@
</div>
</form>
+
+<script type="text/javascript">
+ MB.utility.setDefaultAction ('form.edit-work', 'button.submit.positive');
+</script>
commit 49aa509da1ebb8b837b4b3cafd5aa14e775a5a8f
Author: Michael Wiencek <[email protected]>
Date: Tue Oct 30 02:07:16 2012 +0000
Upgrade knockout.js to 2.2.0
Also make use of the improved css binding, and remove unneeded code.
diff --git a/root/release/edit_relationships.tt b/root/release/edit_relationships.tt
index 296cd5e..44c999a 100644
--- a/root/release/edit_relationships.tt
+++ b/root/release/edit_relationships.tt
@@ -276,12 +276,9 @@
</tr>
</script>
-<script type="text/html" id="template.link.phrase"><span class="link-phrase"
- data-bind="text: linkPhrase($parents[1]), css: {
- 'error-field': hasErrors,
- 'rel-add': action() == 'add',
- 'rel-edit': action() == 'edit',
- 'rel-remove': action() == 'remove'}"></span></script>
+<script type="text/html" id="template.link.phrase"><!--
+ --><span class="link-phrase" data-bind="text: linkPhrase($parents[1]), css: css()"></span><!--
+--></script>
<script type="text/html" id="template.relationship">
<div class="ar">
diff --git a/root/static/lib/knockout/knockout-latest.debug.js b/root/static/lib/knockout/knockout-latest.debug.js
index 29dc24b..61c2a0e 100644
--- a/root/static/lib/knockout/knockout-latest.debug.js
+++ b/root/static/lib/knockout/knockout-latest.debug.js
@@ -1,4 +1,4 @@
-// Knockout JavaScript library v2.1.0
+// Knockout JavaScript library v2.2.0
// (c) Steven Sanderson - http://knockoutjs.com/
// License: MIT (http://www.opensource.org/licenses/mit-license.php)
@@ -37,7 +37,7 @@ ko.exportSymbol = function(koPath, object) {
ko.exportProperty = function(owner, publicName, object) {
owner[publicName] = object;
};
-ko.version = "2.1.0";
+ko.version = "2.2.0";
ko.exportSymbol('version', ko.version);
ko.utils = new (function () {
@@ -58,6 +58,9 @@ ko.utils = new (function () {
var eventsThatMustBeRegisteredUsingAttachEvent = { 'propertychange': true }; // Workaround for an IE9 issue - https://github.com/SteveSanderson/knockout/issues/406
// Detect IE versions for bug workarounds (uses IE conditionals, not UA string, for robustness)
+ // Note that, since IE 10 does not support conditional comments, the following logic only detects IE < 10.
+ // Currently this is by design, since IE 10+ behaves correctly when treated as a standard browser.
+ // If there is a future need to detect specific versions of IE10+, we will amend this.
var ieVersion = (function() {
var version = 3, div = document.createElement('div'), iElems = div.getElementsByTagName('i');
@@ -168,12 +171,19 @@ ko.utils = new (function () {
var container = document.createElement('div');
for (var i = 0, j = nodesArray.length; i < j; i++) {
- ko.cleanNode(nodesArray[i]);
- container.appendChild(nodesArray[i]);
+ container.appendChild(ko.cleanNode(nodesArray[i]));
}
return container;
},
+ cloneNodes: function (nodesArray, shouldCleanNodes) {
+ for (var i = 0, j = nodesArray.length, newNodesArray = []; i < j; i++) {
+ var clonedNode = nodesArray[i].cloneNode(true);
+ newNodesArray.push(shouldCleanNodes ? ko.cleanNode(clonedNode) : clonedNode);
+ }
+ return newNodesArray;
+ },
+
setDomNodeChildren: function (domNode, childNodes) {
ko.utils.emptyDomNode(domNode);
if (childNodes) {
@@ -225,17 +235,6 @@ ko.utils = new (function () {
return string.substring(0, startsWith.length) === startsWith;
},
- buildEvalWithinScopeFunction: function (expression, scopeLevels) {
- // Build the source for a function that evaluates "expression"
- // For each scope variable, add an extra level of "with" nesting
- // Example result: with(sc[1]) { with(sc[0]) { return (expression) } }
- var functionBody = "return (" + expression + ")";
- for (var i = 0; i < scopeLevels; i++) {
- functionBody = "with(sc[" + i + "]) { " + functionBody + " } ";
- }
- return new Function("sc", functionBody);
- },
-
domNodeIsContainedBy: function (node, containedByNode) {
if (containedByNode.compareDocumentPosition)
return (containedByNode.compareDocumentPosition(node) & 16) == 16;
@@ -321,6 +320,10 @@ ko.utils = new (function () {
return ko.isObservable(value) ? value() : value;
},
+ peekObservable: function (value) {
+ return ko.isObservable(value) ? value.peek() : value;
+ },
+
toggleDomNodeCssClass: function (node, classNames, shouldHaveClass) {
if (classNames) {
var cssClassNameRegex = /[\w-]+/g,
@@ -344,17 +347,21 @@ ko.utils = new (function () {
if ((value === null) || (value === undefined))
value = "";
- // We need there to be exactly one child: a text node.
- // If there are no children, more than one, or if it's not a text node,
- // we'll clear everything and create a single text node.
- var innerTextNode = ko.virtualElements.firstChild(element);
- if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) {
- ko.virtualElements.setDomNodeChildren(element, [document.createTextNode(value)]);
+ if (element.nodeType === 3) {
+ element.data = value;
} else {
- innerTextNode.data = value;
- }
+ // We need there to be exactly one child: a text node.
+ // If there are no children, more than one, or if it's not a text node,
+ // we'll clear everything and create a single text node.
+ var innerTextNode = ko.virtualElements.firstChild(element);
+ if (!innerTextNode || innerTextNode.nodeType != 3 || ko.virtualElements.nextSibling(innerTextNode)) {
+ ko.virtualElements.setDomNodeChildren(element, [document.createTextNode(value)]);
+ } else {
+ innerTextNode.data = value;
+ }
- ko.utils.forceRefresh(element);
+ ko.utils.forceRefresh(element);
+ }
},
setElementName: function(element, name) {
@@ -496,6 +503,7 @@ ko.exportSymbol('utils.arrayRemoveItem', ko.utils.arrayRemoveItem);
ko.exportSymbol('utils.extend', ko.utils.extend);
ko.exportSymbol('utils.fieldsIncludedWithJsonPost', ko.utils.fieldsIncludedWithJsonPost);
ko.exportSymbol('utils.getFormFields', ko.utils.getFormFields);
+ko.exportSymbol('utils.peekObservable', ko.utils.peekObservable);
ko.exportSymbol('utils.postJson', ko.utils.postJson);
ko.exportSymbol('utils.parseJson', ko.utils.parseJson);
ko.exportSymbol('utils.registerEventHandler', ko.utils.registerEventHandler);
@@ -536,7 +544,7 @@ ko.utils.domData = new (function () {
},
getAll: function (node, createIfNotFound) {
var dataStoreKey = node[dataStoreKeyExpandoPropertyName];
- var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null");
+ var hasExistingDataStore = dataStoreKey && (dataStoreKey !== "null") && dataStore[dataStoreKey];
if (!hasExistingDataStore) {
if (!createIfNotFound)
return undefined;
@@ -550,7 +558,9 @@ ko.utils.domData = new (function () {
if (dataStoreKey) {
delete dataStore[dataStoreKey];
node[dataStoreKeyExpandoPropertyName] = null;
+ return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended
}
+ return false;
}
}
})();
@@ -638,6 +648,7 @@ ko.utils.domNodeDisposal = new (function () {
cleanSingleNode(descendants[i]);
}
}
+ return node;
},
removeNode : function(node) {
@@ -952,10 +963,10 @@ ko.dependencyDetection = (function () {
}
},
- ignore: function(callback, callbackTarget) {
+ ignore: function(callback, callbackTarget, callbackArgs) {
try {
_frames.push(null);
- callback.call(callbackTarget);
+ return callback.apply(callbackTarget, callbackArgs || []);
} finally {
_frames.pop();
}
@@ -1167,41 +1178,20 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
if (!readFunction)
readFunction = options["read"];
}
- // By here, "options" is always non-null
if (typeof readFunction != "function")
throw new Error("Pass a function that returns the value of the ko.computed");
- var writeFunction = options["write"];
- if (!evaluatorFunctionTarget)
- evaluatorFunctionTarget = options["owner"];
+ function addSubscriptionToDependency(subscribable) {
+ _subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync));
+ }
- var _subscriptionsToDependencies = [];
function disposeAllSubscriptionsToDependencies() {
ko.utils.arrayForEach(_subscriptionsToDependencies, function (subscription) {
subscription.dispose();
});
_subscriptionsToDependencies = [];
}
- var dispose = disposeAllSubscriptionsToDependencies;
-
- // Build "disposeWhenNodeIsRemoved" and "disposeWhenNodeIsRemovedCallback" option values
- // (Note: "disposeWhenNodeIsRemoved" option both proactively disposes as soon as the node is removed using ko.removeNode(),
- // plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.)
- var disposeWhenNodeIsRemoved = (typeof options["disposeWhenNodeIsRemoved"] == "object") ? options["disposeWhenNodeIsRemoved"] : null;
- var disposeWhen = options["disposeWhen"] || function() { return false; };
- if (disposeWhenNodeIsRemoved) {
- dispose = function() {
- ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, arguments.callee);
- disposeAllSubscriptionsToDependencies();
- };
- ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, dispose);
- var existingDisposeWhenFunction = disposeWhen;
- disposeWhen = function () {
- return !ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved) || existingDisposeWhenFunction();
- }
- }
- var evaluationTimeoutInstance = null;
function evaluatePossiblyAsync() {
var throttleEvaluationTimeout = dependentObservable['throttleEvaluation'];
if (throttleEvaluationTimeout && throttleEvaluationTimeout >= 0) {
@@ -1239,7 +1229,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
if ((inOld = ko.utils.arrayIndexOf(disposalCandidates, subscribable)) >= 0)
disposalCandidates[inOld] = undefined; // Don't want to dispose this subscription, as it's still being used
else
- _subscriptionsToDependencies.push(subscribable.subscribe(evaluatePossiblyAsync)); // Brand new subscription - add it
+ addSubscriptionToDependency(subscribable); // Brand new subscription - add it
});
var newValue = readFunction.call(evaluatorFunctionTarget);
@@ -1260,7 +1250,8 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
dependentObservable["notifySubscribers"](_latestValue);
_isBeingEvaluated = false;
-
+ if (!_subscriptionsToDependencies.length)
+ dispose();
}
function dependentObservable() {
@@ -1271,6 +1262,7 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
} else {
throw new Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");
}
+ return this; // Permits chained assignments
} else {
// Reading the value
if (!_hasBeenEvaluated)
@@ -1280,26 +1272,61 @@ ko.dependentObservable = function (evaluatorFunctionOrOptions, evaluatorFunction
}
}
- dependentObservable.peek = function () {
+ function peek() {
if (!_hasBeenEvaluated)
evaluateImmediate();
return _latestValue;
}
+ function isActive() {
+ return !_hasBeenEvaluated || _subscriptionsToDependencies.length > 0;
+ }
+
+ // By here, "options" is always non-null
+ var writeFunction = options["write"],
+ disposeWhenNodeIsRemoved = options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null,
+ disposeWhen = options["disposeWhen"] || options.disposeWhen || function() { return false; },
+ dispose = disposeAllSubscriptionsToDependencies,
+ _subscriptionsToDependencies = [],
+ evaluationTimeoutInstance = null;
+
+ if (!evaluatorFunctionTarget)
+ evaluatorFunctionTarget = options["owner"];
+
+ dependentObservable.peek = peek;
dependentObservable.getDependenciesCount = function () { return _subscriptionsToDependencies.length; };
dependentObservable.hasWriteFunction = typeof options["write"] === "function";
dependentObservable.dispose = function () { dispose(); };
+ dependentObservable.isActive = isActive;
ko.subscribable.call(dependentObservable);
ko.utils.extend(dependentObservable, ko.dependentObservable['fn']);
- if (options['deferEvaluation'] !== true)
- evaluateImmediate();
-
ko.exportProperty(dependentObservable, 'peek', dependentObservable.peek);
ko.exportProperty(dependentObservable, 'dispose', dependentObservable.dispose);
+ ko.exportProperty(dependentObservable, 'isActive', dependentObservable.isActive);
ko.exportProperty(dependentObservable, 'getDependenciesCount', dependentObservable.getDependenciesCount);
+ // Evaluate, unless deferEvaluation is true
+ if (options['deferEvaluation'] !== true)
+ evaluateImmediate();
+
+ // Build "disposeWhenNodeIsRemoved" and "disposeWhenNodeIsRemovedCallback" option values.
+ // But skip if isActive is false (there will never be any dependencies to dispose).
+ // (Note: "disposeWhenNodeIsRemoved" option both proactively disposes as soon as the node is removed using ko.removeNode(),
+ // plus adds a "disposeWhen" callback that, on each evaluation, disposes if the node was removed by some other means.)
+ if (disposeWhenNodeIsRemoved && isActive()) {
+ dispose = function() {
+ ko.utils.domNodeDisposal.removeDisposeCallback(disposeWhenNodeIsRemoved, arguments.callee);
+ disposeAllSubscriptionsToDependencies();
+ };
+ ko.utils.domNodeDisposal.addDisposeCallback(disposeWhenNodeIsRemoved, dispose);
+ var existingDisposeWhenFunction = disposeWhen;
+ disposeWhen = function () {
+ return !ko.utils.domNodeIsAttachedToDocument(disposeWhenNodeIsRemoved) || existingDisposeWhenFunction();
+ }
+ }
+
return dependentObservable;
};
@@ -1473,9 +1500,12 @@ ko.exportSymbol('selectExtensions.readValue', ko.selectExtensions.readValue);
ko.exportSymbol('selectExtensions.writeValue', ko.selectExtensions.writeValue);
ko.expressionRewriting = (function () {
var restoreCapturedTokensRegex = /\@ko_token_(\d+)\@/g;
- var javaScriptAssignmentTarget = /^[\_$a-z][\_$a-z0-9]*(\[.*?\])*(\.[\_$a-z][\_$a-z0-9]*(\[.*?\])*)*$/i;
var javaScriptReservedWords = ["true", "false"];
+ // Matches something that can be assigned to--either an isolated identifier or something ending with a property accessor
+ // This is designed to be simple and avoid false negatives, but could produce false positives (e.g., a+b.c).
+ var javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;
+
function restoreTokens(string, tokens) {
var prevValue = null;
while (string != prevValue) { // Keep restoring tokens until it no longer makes a difference (they may be nested)
@@ -1487,10 +1517,11 @@ ko.expressionRewriting = (function () {
return string;
}
- function isWriteableValue(expression) {
+ function getWriteableValue(expression) {
if (ko.utils.arrayIndexOf(javaScriptReservedWords, ko.utils.stringTrim(expression).toLowerCase()) >= 0)
return false;
- return expression.match(javaScriptAssignmentTarget) !== null;
+ var match = expression.match(javaScriptAssignmentTarget);
+ return match === null ? false : match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
}
function ensureQuoted(key) {
@@ -1610,7 +1641,7 @@ ko.expressionRewriting = (function () {
resultStrings.push(":");
resultStrings.push(val);
- if (isWriteableValue(ko.utils.stringTrim(val))) {
+ if (val = getWriteableValue(ko.utils.stringTrim(val))) {
if (propertyAccessorResultStrings.length > 0)
propertyAccessorResultStrings.push(", ");
propertyAccessorResultStrings.push(quotedKey + " : function(__ko_value) { " + val + " = __ko_value; }");
@@ -1650,7 +1681,7 @@ ko.expressionRewriting = (function () {
var propWriters = allBindingsAccessor()['_ko_property_writers'];
if (propWriters && propWriters[key])
propWriters[key](value);
- } else if (!checkIfDifferent || property() !== value) {
+ } else if (!checkIfDifferent || property.peek() !== value) {
property(value);
}
}
@@ -1679,7 +1710,7 @@ ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.ex
// So, use node.text where available, and node.nodeValue elsewhere
var commentNodesHaveTextProperty = document.createComment("test").text === "<!--test-->";
- var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko\s+(.*\:.*)\s*-->$/ : /^\s*ko\s+(.*\:.*)\s*$/;
+ var startCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*-->$/ : /^\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*$/;
var endCommentRegex = commentNodesHaveTextProperty ? /^<!--\s*\/ko\s*-->$/ : /^\s*\/ko\s*$/;
var htmlTagsWithOptionallyClosingChildren = { 'ul': true, 'ol': true };
@@ -1785,7 +1816,9 @@ ko.exportSymbol('jsonExpressionRewriting.insertPropertyAccessorsIntoJson', ko.ex
},
insertAfter: function(containerNode, nodeToInsert, insertAfterNode) {
- if (!isStartComment(containerNode)) {
+ if (!insertAfterNode) {
+ ko.virtualElements.prepend(containerNode, nodeToInsert);
+ } else if (!isStartComment(containerNode)) {
// Insert after insertion point
if (insertAfterNode.nextSibling)
containerNode.insertBefore(nodeToInsert, insertAfterNode.nextSibling);
@@ -1874,7 +1907,7 @@ ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomN
'getBindings': function(node, bindingContext) {
var bindingsString = this['getBindingsString'](node, bindingContext);
- return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext) : null;
+ return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null;
},
// The following function is only used internally by this default provider.
@@ -1889,12 +1922,10 @@ ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomN
// The following function is only used internally by this default provider.
// It's not part of the interface definition for a general binding provider.
- 'parseBindingsString': function(bindingsString, bindingContext) {
+ 'parseBindingsString': function(bindingsString, bindingContext, node) {
try {
- var viewModel = bindingContext['$data'],
- scopes = (typeof viewModel == 'object' && viewModel != null) ? [viewModel, bindingContext] : [bindingContext],
- bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, scopes.length, this.bindingCache);
- return bindingFunction(scopes);
+ var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, this.bindingCache);
+ return bindingFunction(bindingContext, node);
} catch (ex) {
throw new Error("Unable to parse bindings.\nMessage: " + ex + ";\nBindings value: " + bindingsString);
}
@@ -1903,15 +1934,19 @@ ko.exportSymbol('virtualElements.setDomNodeChildren', ko.virtualElements.setDomN
ko.bindingProvider['instance'] = new ko.bindingProvider();
- function createBindingsStringEvaluatorViaCache(bindingsString, scopesCount, cache) {
- var cacheKey = scopesCount + '_' + bindingsString;
+ function createBindingsStringEvaluatorViaCache(bindingsString, cache) {
+ var cacheKey = bindingsString;
return cache[cacheKey]
- || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, scopesCount));
+ || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString));
}
- function createBindingsStringEvaluator(bindingsString, scopesCount) {
- var rewrittenBindings = " { " + ko.expressionRewriting.preProcessBindings(bindingsString) + " } ";
- return ko.utils.buildEvalWithinScopeFunction(rewrittenBindings, scopesCount);
+ function createBindingsStringEvaluator(bindingsString) {
+ // Build the source for a function that evaluates "expression"
+ // For each scope variable, add an extra level of "with" nesting
+ // Example result: with(sc1) { with(sc0) { return (expression) } }
+ var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString),
+ functionBody = "with($context){with($data||{}){return{" + rewrittenBindings + "}}}";
+ return new Function("$context", "$element", functionBody);
}
})();
@@ -1919,7 +1954,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
(function () {
ko.bindingHandlers = {};
- ko.bindingContext = function(dataItem, parentBindingContext) {
+ ko.bindingContext = function(dataItem, parentBindingContext, dataItemAlias) {
if (parentBindingContext) {
ko.utils.extend(this, parentBindingContext); // Inherit $root and any custom properties
this['$parentContext'] = parentBindingContext;
@@ -1935,9 +1970,11 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
this['ko'] = ko;
}
this['$data'] = dataItem;
+ if (dataItemAlias)
+ this[dataItemAlias] = dataItem;
}
- ko.bindingContext.prototype['createChildContext'] = function (dataItem) {
- return new ko.bindingContext(dataItem, this);
+ ko.bindingContext.prototype['createChildContext'] = function (dataItem, dataItemAlias) {
+ return new ko.bindingContext(dataItem, this, dataItemAlias);
};
ko.bindingContext.prototype['extend'] = function(properties) {
var clone = ko.utils.extend(new ko.bindingContext(), this);
@@ -2020,7 +2057,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
ko.storedBindingContextForNode(node, bindingContextInstance);
// Use evaluatedBindings if given, otherwise fall back on asking the bindings provider to give us some bindings
- var evaluatedBindings = (typeof bindings == "function") ? bindings() : bindings;
+ var evaluatedBindings = (typeof bindings == "function") ? bindings(bindingContextInstance, node) : bindings;
parsedBindings = evaluatedBindings || ko.bindingProvider['instance']['getBindings'](node, bindingContextInstance);
if (parsedBindings) {
@@ -2060,7 +2097,7 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
}
},
null,
- { 'disposeWhenNodeIsRemoved' : node }
+ { disposeWhenNodeIsRemoved : node }
);
return {
@@ -2120,10 +2157,128 @@ ko.exportSymbol('bindingProvider', ko.bindingProvider);
ko.exportSymbol('contextFor', ko.contextFor);
ko.exportSymbol('dataFor', ko.dataFor);
})();
+var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' };
+ko.bindingHandlers['attr'] = {
+ 'update': function(element, valueAccessor, allBindingsAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor()) || {};
+ for (var attrName in value) {
+ if (typeof attrName == "string") {
+ var attrValue = ko.utils.unwrapObservable(value[attrName]);
+
+ // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
+ // when someProp is a "no value"-like value (strictly null, false, or undefined)
+ // (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
+ var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined);
+ if (toRemove)
+ element.removeAttribute(attrName);
+
+ // In IE <= 7 and IE8 Quirks Mode, you have to use the Javascript property name instead of the
+ // HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior,
+ // but instead of figuring out the mode, we'll just set the attribute through the Javascript
+ // property for IE <= 8.
+ if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap) {
+ attrName = attrHtmlToJavascriptMap[attrName];
+ if (toRemove)
+ element.removeAttribute(attrName);
+ else
+ element[attrName] = attrValue;
+ } else if (!toRemove) {
+ element.setAttribute(attrName, attrValue.toString());
+ }
+
+ // Treat "name" specially - although you can think of it as an attribute, it also needs
+ // special handling on older versions of IE (https://github.com/SteveSanderson/knockout/pull/333)
+ // Deliberately being case-sensitive here because XHTML would regard "Name" as a different thing
+ // entirely, and there's no strong reason to allow for such casing in HTML.
+ if (attrName === "name") {
+ ko.utils.setElementName(element, toRemove ? "" : attrValue.toString());
+ }
+ }
+ }
+ }
+};
+ko.bindingHandlers['checked'] = {
+ 'init': function (element, valueAccessor, allBindingsAccessor) {
+ var updateHandler = function() {
+ var valueToWrite;
+ if (element.type == "checkbox") {
+ valueToWrite = element.checked;
+ } else if ((element.type == "radio") && (element.checked)) {
+ valueToWrite = element.value;
+ } else {
+ return; // "checked" binding only responds to checkboxes and selected radio buttons
+ }
+
+ var modelValue = valueAccessor(), unwrappedValue = ko.utils.unwrapObservable(modelValue);
+ if ((element.type == "checkbox") && (unwrappedValue instanceof Array)) {
+ // For checkboxes bound to an array, we add/remove the checkbox value to that array
+ // This works for both observable and non-observable arrays
+ var existingEntryIndex = ko.utils.arrayIndexOf(unwrappedValue, element.value);
+ if (element.checked && (existingEntryIndex < 0))
+ modelValue.push(element.value);
+ else if ((!element.checked) && (existingEntryIndex >= 0))
+ modelValue.splice(existingEntryIndex, 1);
+ } else {
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'checked', valueToWrite, true);
+ }
+ };
+ ko.utils.registerEventHandler(element, "click", updateHandler);
+
+ // IE 6 won't allow radio buttons to be selected unless they have a name
+ if ((element.type == "radio") && !element.name)
+ ko.bindingHandlers['uniqueName']['init'](element, function() { return true });
+ },
+ 'update': function (element, valueAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+
+ if (element.type == "checkbox") {
+ if (value instanceof Array) {
+ // When bound to an array, the checkbox being checked represents its value being present in that array
+ element.checked = ko.utils.arrayIndexOf(value, element.value) >= 0;
+ } else {
+ // When bound to anything other value (not an array), the checkbox being checked represents the value being trueish
+ element.checked = value;
+ }
+ } else if (element.type == "radio") {
+ element.checked = (element.value == value);
+ }
+ }
+};
+var classesWrittenByBindingKey = '__ko__cssValue';
+ko.bindingHandlers['css'] = {
+ 'update': function (element, valueAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+ if (typeof value == "object") {
+ for (var className in value) {
+ var shouldHaveClass = ko.utils.unwrapObservable(value[className]);
+ ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass);
+ }
+ } else {
+ value = String(value || ''); // Make sure we don't try to store or set a non-string value
+ ko.utils.toggleDomNodeCssClass(element, element[classesWrittenByBindingKey], false);
+ element[classesWrittenByBindingKey] = value;
+ ko.utils.toggleDomNodeCssClass(element, value, true);
+ }
+ }
+};
+ko.bindingHandlers['enable'] = {
+ 'update': function (element, valueAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+ if (value && element.disabled)
+ element.removeAttribute("disabled");
+ else if ((!value) && (!element.disabled))
+ element.disabled = true;
+ }
+};
+
+ko.bindingHandlers['disable'] = {
+ 'update': function (element, valueAccessor) {
+ ko.bindingHandlers['enable']['update'](element, function() { return !ko.utils.unwrapObservable(valueAccessor()) });
+ }
+};
// For certain common events (currently just 'click'), allow a simplified data-binding syntax
// e.g. click:handler instead of the usual full-length event:{click:handler}
-var eventHandlersWithShortcuts = ['click'];
-ko.utils.arrayForEach(eventHandlersWithShortcuts, function(eventName) {
+function makeEventHandlerShortcut(eventName) {
ko.bindingHandlers[eventName] = {
'init': function(element, valueAccessor, allBindingsAccessor, viewModel) {
var newValueAccessor = function () {
@@ -2134,8 +2289,7 @@ ko.utils.arrayForEach(eventHandlersWithShortcuts, function(eventName) {
return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindingsAccessor, viewModel);
}
}
-});
-
+}
ko.bindingHandlers['event'] = {
'init' : function (element, valueAccessor, allBindingsAccessor, viewModel) {
@@ -2177,140 +2331,145 @@ ko.bindingHandlers['event'] = {
}
}
};
+// "foreach: someExpression" is equivalent to "template: { foreach: someExpression }"
+// "foreach: { data: someExpression, afterAdd: myfn }" is equivalent to "template: { foreach: someExpression, afterAdd: myfn }"
+ko.bindingHandlers['foreach'] = {
+ makeTemplateValueAccessor: function(valueAccessor) {
+ return function() {
+ var modelValue = valueAccessor(),
+ unwrappedValue = ko.utils.peekObservable(modelValue); // Unwrap without setting a dependency here
-ko.bindingHandlers['submit'] = {
- 'init': function (element, valueAccessor, allBindingsAccessor, viewModel) {
- if (typeof valueAccessor() != "function")
- throw new Error("The value for a submit binding must be a function");
- ko.utils.registerEventHandler(element, "submit", function (event) {
- var handlerReturnValue;
- var value = valueAccessor();
- try { handlerReturnValue = value.call(viewModel, element); }
- finally {
- if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
- if (event.preventDefault)
- event.preventDefault();
- else
- event.returnValue = false;
- }
- }
- });
- }
-};
+ // If unwrappedValue is the array, pass in the wrapped value on its own
+ // The value will be unwrapped and tracked within the template binding
+ // (See https://github.com/SteveSanderson/knockout/issues/523)
+ if ((!unwrappedValue) || typeof unwrappedValue.length == "number")
+ return { 'foreach': modelValue, 'templateEngine': ko.nativeTemplateEngine.instance };
-ko.bindingHandlers['visible'] = {
- 'update': function (element, valueAccessor) {
- var value = ko.utils.unwrapObservable(valueAccessor());
- var isCurrentlyVisible = !(element.style.display == "none");
- if (value && !isCurrentlyVisible)
- element.style.display = "";
- else if ((!value) && isCurrentlyVisible)
- element.style.display = "none";
+ // If unwrappedValue.data is the array, preserve all relevant options and unwrap again value so we get updates
+ ko.utils.unwrapObservable(modelValue);
+ return {
+ 'foreach': unwrappedValue['data'],
+ 'as': unwrappedValue['as'],
+ 'includeDestroyed': unwrappedValue['includeDestroyed'],
+ 'afterAdd': unwrappedValue['afterAdd'],
+ 'beforeRemove': unwrappedValue['beforeRemove'],
+ 'afterRender': unwrappedValue['afterRender'],
+ 'beforeMove': unwrappedValue['beforeMove'],
+ 'afterMove': unwrappedValue['afterMove'],
+ 'templateEngine': ko.nativeTemplateEngine.instance
+ };
+ };
+ },
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor));
+ },
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
}
-}
+};
+ko.expressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
+ko.virtualElements.allowedBindings['foreach'] = true;
+var hasfocusUpdatingProperty = '__ko_hasfocusUpdating';
+ko.bindingHandlers['hasfocus'] = {
+ 'init': function(element, valueAccessor, allBindingsAccessor) {
+ var handleElementFocusChange = function(isFocused) {
+ // Where possible, ignore which event was raised and determine focus state using activeElement,
+ // as this avoids phantom focus/blur events raised when changing tabs in modern browsers.
+ // However, not all KO-targeted browsers (Firefox 2) support activeElement. For those browsers,
+ // prevent a loss of focus when changing tabs/windows by setting a flag that prevents hasfocus
+ // from calling 'blur()' on the element when it loses focus.
+ // Discussion at https://github.com/SteveSanderson/knockout/pull/352
+ element[hasfocusUpdatingProperty] = true;
+ var ownerDoc = element.ownerDocument;
+ if ("activeElement" in ownerDoc) {
+ isFocused = (ownerDoc.activeElement === element);
+ }
+ var modelValue = valueAccessor();
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'hasfocus', isFocused, true);
+ element[hasfocusUpdatingProperty] = false;
+ };
+ var handleElementFocusIn = handleElementFocusChange.bind(null, true);
+ var handleElementFocusOut = handleElementFocusChange.bind(null, false);
-ko.bindingHandlers['enable'] = {
- 'update': function (element, valueAccessor) {
+ ko.utils.registerEventHandler(element, "focus", handleElementFocusIn);
+ ko.utils.registerEventHandler(element, "focusin", handleElementFocusIn); // For IE
+ ko.utils.registerEventHandler(element, "blur", handleElementFocusOut);
+ ko.utils.registerEventHandler(element, "focusout", handleElementFocusOut); // For IE
+ },
+ 'update': function(element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
- if (value && element.disabled)
- element.removeAttribute("disabled");
- else if ((!value) && (!element.disabled))
- element.disabled = true;
+ if (!element[hasfocusUpdatingProperty]) {
+ value ? element.focus() : element.blur();
+ ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, value ? "focusin" : "focusout"]); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
+ }
}
};
-
-ko.bindingHandlers['disable'] = {
+ko.bindingHandlers['html'] = {
+ 'init': function() {
+ // Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications)
+ return { 'controlsDescendantBindings': true };
+ },
'update': function (element, valueAccessor) {
- ko.bindingHandlers['enable']['update'](element, function() { return !ko.utils.unwrapObservable(valueAccessor()) });
+ // setHtml will unwrap the value if needed
+ ko.utils.setHtml(element, valueAccessor());
}
};
+var withIfDomDataKey = '__ko_withIfBindingData';
+// Makes a binding like with or if
+function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) {
+ ko.bindingHandlers[bindingKey] = {
+ 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ ko.utils.domData.set(element, withIfDomDataKey, {});
+ return { 'controlsDescendantBindings': true };
+ },
+ 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ var withIfData = ko.utils.domData.get(element, withIfDomDataKey),
+ dataValue = ko.utils.unwrapObservable(valueAccessor()),
+ shouldDisplay = !isNot !== !dataValue, // equivalent to isNot ? !dataValue : !!dataValue
+ isFirstRender = !withIfData.savedNodes,
+ needsRefresh = isFirstRender || isWith || (shouldDisplay !== withIfData.didDisplayOnLastUpdate);
-function ensureDropdownSelectionIsConsistentWithModelValue(element, modelValue, preferModelValue) {
- if (preferModelValue) {
- if (modelValue !== ko.selectExtensions.readValue(element))
- ko.selectExtensions.writeValue(element, modelValue);
- }
-
- // No matter which direction we're syncing in, we want the end result to be equality between dropdown value and model value.
- // If they aren't equal, either we prefer the dropdown value, or the model value couldn't be represented, so either way,
- // change the model value to match the dropdown.
- if (modelValue !== ko.selectExtensions.readValue(element))
- ko.utils.triggerEvent(element, "change");
-};
-
-ko.bindingHandlers['value'] = {
- 'init': function (element, valueAccessor, allBindingsAccessor) {
- // Always catch "change" event; possibly other events too if asked
- var eventsToCatch = ["change"];
- var requestedEventsToCatch = allBindingsAccessor()["valueUpdate"];
- if (requestedEventsToCatch) {
- if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names
- requestedEventsToCatch = [requestedEventsToCatch];
- ko.utils.arrayPushAll(eventsToCatch, requestedEventsToCatch);
- eventsToCatch = ko.utils.arrayGetDistinctValues(eventsToCatch);
- }
-
- var valueUpdateHandler = function() {
- var modelValue = valueAccessor();
- var elementValue = ko.selectExtensions.readValue(element);
- ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'value', elementValue, /* checkIfDifferent: */ true);
- }
+ if (needsRefresh) {
+ if (isFirstRender) {
+ withIfData.savedNodes = ko.utils.cloneNodes(ko.virtualElements.childNodes(element), true /* shouldCleanNodes */);
+ }
- // Workaround for https://github.com/SteveSanderson/knockout/issues/122
- // IE doesn't fire "change" events on textboxes if the user selects a value from its autocomplete list
- var ieAutoCompleteHackNeeded = ko.utils.ieVersion && element.tagName.toLowerCase() == "input" && element.type == "text"
- && element.autocomplete != "off" && (!element.form || element.form.autocomplete != "off");
- if (ieAutoCompleteHackNeeded && ko.utils.arrayIndexOf(eventsToCatch, "propertychange") == -1) {
- var propertyChangedFired = false;
- ko.utils.registerEventHandler(element, "propertychange", function () { propertyChangedFired = true });
- ko.utils.registerEventHandler(element, "blur", function() {
- if (propertyChangedFired) {
- propertyChangedFired = false;
- valueUpdateHandler();
+ if (shouldDisplay) {
+ if (!isFirstRender) {
+ ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(withIfData.savedNodes));
+ }
+ ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, dataValue) : bindingContext, element);
+ } else {
+ ko.virtualElements.emptyNode(element);
}
- });
- }
- ko.utils.arrayForEach(eventsToCatch, function(eventName) {
- // The syntax "after<eventname>" means "run the handler asynchronously after the event"
- // This is useful, for example, to catch "keydown" events after the browser has updated the control
- // (otherwise, ko.selectExtensions.readValue(this) will receive the control's value *before* the key event)
- var handler = valueUpdateHandler;
- if (ko.utils.stringStartsWith(eventName, "after")) {
- handler = function() { setTimeout(valueUpdateHandler, 0) };
- eventName = eventName.substring("after".length);
+ withIfData.didDisplayOnLastUpdate = shouldDisplay;
}
- ko.utils.registerEventHandler(element, eventName, handler);
- });
- },
- 'update': function (element, valueAccessor) {
- var valueIsSelectOption = ko.utils.tagNameLower(element) === "select";
- var newValue = ko.utils.unwrapObservable(valueAccessor());
- var elementValue = ko.selectExtensions.readValue(element);
- var valueHasChanged = (newValue != elementValue);
-
- // JavaScript's 0 == "" behavious is unfortunate here as it prevents writing 0 to an empty text box (loose equality suggests the values are the same).
- // We don't want to do a strict equality comparison as that is more confusing for developers in certain cases, so we specifically special case 0 != "" here.
- if ((newValue === 0) && (elementValue !== 0) && (elementValue !== "0"))
- valueHasChanged = true;
-
- if (valueHasChanged) {
- var applyValueAction = function () { ko.selectExtensions.writeValue(element, newValue); };
- applyValueAction();
-
- // Workaround for IE6 bug: It won't reliably apply values to SELECT nodes during the same execution thread
- // right after you've changed the set of OPTION nodes on it. So for that node type, we'll schedule a second thread
- // to apply the value as well.
- var alsoApplyAsynchronously = valueIsSelectOption;
- if (alsoApplyAsynchronously)
- setTimeout(applyValueAction, 0);
}
+ };
+ ko.expressionRewriting.bindingRewriteValidators[bindingKey] = false; // Can't rewrite control flow bindings
+ ko.virtualElements.allowedBindings[bindingKey] = true;
+}
- // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
- // because you're not allowed to have a model value that disagrees with a visible UI selection.
- if (valueIsSelectOption && (element.length > 0))
- ensureDropdownSelectionIsConsistentWithModelValue(element, newValue, /* preferModelValue */ false);
+// Construct the actual binding handlers
+makeWithIfBinding('if');
+makeWithIfBinding('ifnot', false /* isWith */, true /* isNot */);
+makeWithIfBinding('with', true /* isWith */, false /* isNot */,
+ function(bindingContext, dataValue) {
+ return bindingContext['createChildContext'](dataValue);
+ }
+);
+function ensureDropdownSelectionIsConsistentWithModelValue(element, modelValue, preferModelValue) {
+ if (preferModelValue) {
+ if (modelValue !== ko.selectExtensions.readValue(element))
+ ko.selectExtensions.writeValue(element, modelValue);
}
+
+ // No matter which direction we're syncing in, we want the end result to be equality between dropdown value and model value.
+ // If they aren't equal, either we prefer the dropdown value, or the model value couldn't be represented, so either way,
+ // change the model value to match the dropdown.
+ if (modelValue !== ko.selectExtensions.readValue(element))
+ ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
};
ko.bindingHandlers['options'] = {
@@ -2395,7 +2554,7 @@ ko.bindingHandlers['options'] = {
// Ensure consistency between model value and selected option.
// If the dropdown is being populated for the first time here (or was otherwise previously empty),
// the dropdown selection state is meaningless, so we preserve the model value.
- ensureDropdownSelectionIsConsistentWithModelValue(element, ko.utils.unwrapObservable(allBindings['value']), /* preferModelValue */ true);
+ ensureDropdownSelectionIsConsistentWithModelValue(element, ko.utils.peekObservable(allBindings['value']), /* preferModelValue */ true);
}
// Workaround for IE9 bug
@@ -2404,7 +2563,6 @@ ko.bindingHandlers['options'] = {
}
};
ko.bindingHandlers['options'].optionValueDomDataKey = '__ko.optionValueDomData__';
-
ko.bindingHandlers['selectedOptions'] = {
'init': function (element, valueAccessor, allBindingsAccessor) {
ko.utils.registerEventHandler(element, "change", function () {
@@ -2429,43 +2587,6 @@ ko.bindingHandlers['selectedOptions'] = {
}
}
};
-
-ko.bindingHandlers['text'] = {
- 'update': function (element, valueAccessor) {
- ko.utils.setTextContent(element, valueAccessor());
- }
-};
-ko.virtualElements.allowedBindings['text'] = true;
-
-ko.bindingHandlers['html'] = {
- 'init': function() {
- // Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications)
- return { 'controlsDescendantBindings': true };
- },
- 'update': function (element, valueAccessor) {
- // setHtml will unwrap the value if needed
- ko.utils.setHtml(element, valueAccessor());
- }
-};
-
-var classesWrittenByBindingKey = '__ko__cssValue';
-ko.bindingHandlers['css'] = {
- 'update': function (element, valueAccessor) {
- var value = ko.utils.unwrapObservable(valueAccessor());
- if (typeof value == "object") {
- for (var className in value) {
- var shouldHaveClass = ko.utils.unwrapObservable(value[className]);
- ko.utils.toggleDomNodeCssClass(element, className, shouldHaveClass);
- }
- } else {
- value = String(value || ''); // Make sure we don't try to store or set a non-string value
- ko.utils.toggleDomNodeCssClass(element, element[classesWrittenByBindingKey], false);
- element[classesWrittenByBindingKey] = value;
- ko.utils.toggleDomNodeCssClass(element, value, true);
- }
- }
-};
-
ko.bindingHandlers['style'] = {
'update': function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor() || {});
@@ -2477,7 +2598,31 @@ ko.bindingHandlers['style'] = {
}
}
};
-
+ko.bindingHandlers['submit'] = {
+ 'init': function (element, valueAccessor, allBindingsAccessor, viewModel) {
+ if (typeof valueAccessor() != "function")
+ throw new Error("The value for a submit binding must be a function");
+ ko.utils.registerEventHandler(element, "submit", function (event) {
+ var handlerReturnValue;
+ var value = valueAccessor();
+ try { handlerReturnValue = value.call(viewModel, element); }
+ finally {
+ if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
+ if (event.preventDefault)
+ event.preventDefault();
+ else
+ event.returnValue = false;
+ }
+ }
+ });
+ }
+};
+ko.bindingHandlers['text'] = {
+ 'update': function (element, valueAccessor) {
+ ko.utils.setTextContent(element, valueAccessor());
+ }
+};
+ko.virtualElements.allowedBindings['text'] = true;
ko.bindingHandlers['uniqueName'] = {
'init': function (element, valueAccessor) {
if (valueAccessor()) {
@@ -2487,190 +2632,92 @@ ko.bindingHandlers['uniqueName'] = {
}
};
ko.bindingHandlers['uniqueName'].currentIndex = 0;
-
-ko.bindingHandlers['checked'] = {
+ko.bindingHandlers['value'] = {
'init': function (element, valueAccessor, allBindingsAccessor) {
- var updateHandler = function() {
- var valueToWrite;
- if (element.type == "checkbox") {
- valueToWrite = element.checked;
- } else if ((element.type == "radio") && (element.checked)) {
- valueToWrite = element.value;
- } else {
- return; // "checked" binding only responds to checkboxes and selected radio buttons
- }
+ // Always catch "change" event; possibly other events too if asked
+ var eventsToCatch = ["change"];
+ var requestedEventsToCatch = allBindingsAccessor()["valueUpdate"];
+ var propertyChangedFired = false;
+ if (requestedEventsToCatch) {
+ if (typeof requestedEventsToCatch == "string") // Allow both individual event names, and arrays of event names
+ requestedEventsToCatch = [requestedEventsToCatch];
+ ko.utils.arrayPushAll(eventsToCatch, requestedEventsToCatch);
+ eventsToCatch = ko.utils.arrayGetDistinctValues(eventsToCatch);
+ }
+ var valueUpdateHandler = function() {
+ propertyChangedFired = false;
var modelValue = valueAccessor();
- if ((element.type == "checkbox") && (ko.utils.unwrapObservable(modelValue) instanceof Array)) {
- // For checkboxes bound to an array, we add/remove the checkbox value to that array
- // This works for both observable and non-observable arrays
- var existingEntryIndex = ko.utils.arrayIndexOf(ko.utils.unwrapObservable(modelValue), element.value);
- if (element.checked && (existingEntryIndex < 0))
- modelValue.push(element.value);
- else if ((!element.checked) && (existingEntryIndex >= 0))
- modelValue.splice(existingEntryIndex, 1);
- } else {
- ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'checked', valueToWrite, true);
- }
- };
- ko.utils.registerEventHandler(element, "click", updateHandler);
-
- // IE 6 won't allow radio buttons to be selected unless they have a name
- if ((element.type == "radio") && !element.name)
- ko.bindingHandlers['uniqueName']['init'](element, function() { return true });
- },
- 'update': function (element, valueAccessor) {
- var value = ko.utils.unwrapObservable(valueAccessor());
-
- if (element.type == "checkbox") {
- if (value instanceof Array) {
- // When bound to an array, the checkbox being checked represents its value being present in that array
- element.checked = ko.utils.arrayIndexOf(value, element.value) >= 0;
- } else {
- // When bound to anything other value (not an array), the checkbox being checked represents the value being trueish
- element.checked = value;
- }
- } else if (element.type == "radio") {
- element.checked = (element.value == value);
+ var elementValue = ko.selectExtensions.readValue(element);
+ ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'value', elementValue);
}
- }
-};
-var attrHtmlToJavascriptMap = { 'class': 'className', 'for': 'htmlFor' };
-ko.bindingHandlers['attr'] = {
- 'update': function(element, valueAccessor, allBindingsAccessor) {
- var value = ko.utils.unwrapObservable(valueAccessor()) || {};
- for (var attrName in value) {
- if (typeof attrName == "string") {
- var attrValue = ko.utils.unwrapObservable(value[attrName]);
-
- // To cover cases like "attr: { checked:someProp }", we want to remove the attribute entirely
- // when someProp is a "no value"-like value (strictly null, false, or undefined)
- // (because the absence of the "checked" attr is how to mark an element as not checked, etc.)
- var toRemove = (attrValue === false) || (attrValue === null) || (attrValue === undefined);
- if (toRemove)
- element.removeAttribute(attrName);
-
- // In IE <= 7 and IE8 Quirks Mode, you have to use the Javascript property name instead of the
- // HTML attribute name for certain attributes. IE8 Standards Mode supports the correct behavior,
- // but instead of figuring out the mode, we'll just set the attribute through the Javascript
- // property for IE <= 8.
- if (ko.utils.ieVersion <= 8 && attrName in attrHtmlToJavascriptMap) {
- attrName = attrHtmlToJavascriptMap[attrName];
- if (toRemove)
- element.removeAttribute(attrName);
- else
- element[attrName] = attrValue;
- } else if (!toRemove) {
- element.setAttribute(attrName, attrValue.toString());
- }
-
- // Treat "name" specially - although you can think of it as an attribute, it also needs
- // special handling on older versions of IE (https://github.com/SteveSanderson/knockout/pull/333)
- // Deliberately being case-sensitive here because XHTML would regard "Name" as a different thing
- // entirely, and there's no strong reason to allow for such casing in HTML.
- if (attrName === "name") {
- ko.utils.setElementName(element, toRemove ? "" : attrValue.toString());
+ // Workaround for https://github.com/SteveSanderson/knockout/issues/122
+ // IE doesn't fire "change" events on textboxes if the user selects a value from its autocomplete list
+ var ieAutoCompleteHackNeeded = ko.utils.ieVersion && element.tagName.toLowerCase() == "input" && element.type == "text"
+ && element.autocomplete != "off" && (!element.form || element.form.autocomplete != "off");
+ if (ieAutoCompleteHackNeeded && ko.utils.arrayIndexOf(eventsToCatch, "propertychange") == -1) {
+ ko.utils.registerEventHandler(element, "propertychange", function () { propertyChangedFired = true });
+ ko.utils.registerEventHandler(element, "blur", function() {
+ if (propertyChangedFired) {
+ valueUpdateHandler();
}
- }
+ });
}
- }
-};
-ko.bindingHandlers['hasfocus'] = {
- 'init': function(element, valueAccessor, allBindingsAccessor) {
- var writeValue = function(valueToWrite) {
- var modelValue = valueAccessor();
- ko.expressionRewriting.writeValueToProperty(modelValue, allBindingsAccessor, 'hasfocus', valueToWrite, true);
- };
- ko.utils.registerEventHandler(element, "focus", function() { writeValue(true) });
- ko.utils.registerEventHandler(element, "focusin", function() { writeValue(true) }); // For IE
- ko.utils.registerEventHandler(element, "blur", function() { writeValue(false) });
- ko.utils.registerEventHandler(element, "focusout", function() { writeValue(false) }); // For IE
+ ko.utils.arrayForEach(eventsToCatch, function(eventName) {
+ // The syntax "after<eventname>" means "run the handler asynchronously after the event"
+ // This is useful, for example, to catch "keydown" events after the browser has updated the control
+ // (otherwise, ko.selectExtensions.readValue(this) will receive the control's value *before* the key event)
+ var handler = valueUpdateHandler;
+ if (ko.utils.stringStartsWith(eventName, "after")) {
+ handler = function() { setTimeout(valueUpdateHandler, 0) };
+ eventName = eventName.substring("after".length);
+ }
+ ko.utils.registerEventHandler(element, eventName, handler);
+ });
},
- 'update': function(element, valueAccessor) {
- var value = ko.utils.unwrapObservable(valueAccessor());
- value ? element.focus() : element.blur();
- ko.utils.triggerEvent(element, value ? "focusin" : "focusout"); // For IE, which doesn't reliably fire "focus" or "blur" events synchronously
- }
-};
+ 'update': function (element, valueAccessor) {
+ var valueIsSelectOption = ko.utils.tagNameLower(element) === "select";
+ var newValue = ko.utils.unwrapObservable(valueAccessor());
+ var elementValue = ko.selectExtensions.readValue(element);
+ var valueHasChanged = (newValue != elementValue);
-// "with: someExpression" is equivalent to "template: { if: someExpression, data: someExpression }"
-ko.bindingHandlers['with'] = {
- makeTemplateValueAccessor: function(valueAccessor) {
- return function() { var value = valueAccessor(); return { 'if': value, 'data': value, 'templateEngine': ko.nativeTemplateEngine.instance } };
- },
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['with'].makeTemplateValueAccessor(valueAccessor));
- },
- 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['with'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
- }
-};
-ko.expressionRewriting.bindingRewriteValidators['with'] = false; // Can't rewrite control flow bindings
-ko.virtualElements.allowedBindings['with'] = true;
+ // JavaScript's 0 == "" behavious is unfortunate here as it prevents writing 0 to an empty text box (loose equality suggests the values are the same).
+ // We don't want to do a strict equality comparison as that is more confusing for developers in certain cases, so we specifically special case 0 != "" here.
+ if ((newValue === 0) && (elementValue !== 0) && (elementValue !== "0"))
+ valueHasChanged = true;
-// "if: someExpression" is equivalent to "template: { if: someExpression }"
-ko.bindingHandlers['if'] = {
- makeTemplateValueAccessor: function(valueAccessor) {
- return function() { return { 'if': valueAccessor(), 'templateEngine': ko.nativeTemplateEngine.instance } };
- },
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['if'].makeTemplateValueAccessor(valueAccessor));
- },
- 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['if'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
- }
-};
-ko.expressionRewriting.bindingRewriteValidators['if'] = false; // Can't rewrite control flow bindings
-ko.virtualElements.allowedBindings['if'] = true;
+ if (valueHasChanged) {
+ var applyValueAction = function () { ko.selectExtensions.writeValue(element, newValue); };
+ applyValueAction();
-// "ifnot: someExpression" is equivalent to "template: { ifnot: someExpression }"
-ko.bindingHandlers['ifnot'] = {
- makeTemplateValueAccessor: function(valueAccessor) {
- return function() { return { 'ifnot': valueAccessor(), 'templateEngine': ko.nativeTemplateEngine.instance } };
- },
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['ifnot'].makeTemplateValueAccessor(valueAccessor));
- },
- 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['ifnot'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
+ // Workaround for IE6 bug: It won't reliably apply values to SELECT nodes during the same execution thread
+ // right after you've changed the set of OPTION nodes on it. So for that node type, we'll schedule a second thread
+ // to apply the value as well.
+ var alsoApplyAsynchronously = valueIsSelectOption;
+ if (alsoApplyAsynchronously)
+ setTimeout(applyValueAction, 0);
+ }
+
+ // If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
+ // because you're not allowed to have a model value that disagrees with a visible UI selection.
+ if (valueIsSelectOption && (element.length > 0))
+ ensureDropdownSelectionIsConsistentWithModelValue(element, newValue, /* preferModelValue */ false);
}
};
-ko.expressionRewriting.bindingRewriteValidators['ifnot'] = false; // Can't rewrite control flow bindings
-ko.virtualElements.allowedBindings['ifnot'] = true;
-
-// "foreach: someExpression" is equivalent to "template: { foreach: someExpression }"
-// "foreach: { data: someExpression, afterAdd: myfn }" is equivalent to "template: { foreach: someExpression, afterAdd: myfn }"
-ko.bindingHandlers['foreach'] = {
- makeTemplateValueAccessor: function(valueAccessor) {
- return function() {
- var bindingValue = ko.utils.unwrapObservable(valueAccessor());
-
- // If bindingValue is the array, just pass it on its own
- if ((!bindingValue) || typeof bindingValue.length == "number")
- return { 'foreach': bindingValue, 'templateEngine': ko.nativeTemplateEngine.instance };
-
- // If bindingValue.data is the array, preserve all relevant options
- return {
- 'foreach': bindingValue['data'],
- 'includeDestroyed': bindingValue['includeDestroyed'],
- 'afterAdd': bindingValue['afterAdd'],
- 'beforeRemove': bindingValue['beforeRemove'],
- 'afterRender': bindingValue['afterRender'],
- 'templateEngine': ko.nativeTemplateEngine.instance
- };
- };
- },
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- return ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor));
- },
- 'update': function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- return ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindingsAccessor, viewModel, bindingContext);
+ko.bindingHandlers['visible'] = {
+ 'update': function (element, valueAccessor) {
+ var value = ko.utils.unwrapObservable(valueAccessor());
+ var isCurrentlyVisible = !(element.style.display == "none");
+ if (value && !isCurrentlyVisible)
+ element.style.display = "";
+ else if ((!value) && isCurrentlyVisible)
+ element.style.display = "none";
}
};
-ko.expressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
-ko.virtualElements.allowedBindings['foreach'] = true;
+// 'click' is just a shorthand for the usual full-length event:{click:handler}
+makeEventHandlerShortcut('click');
// If you want to make a custom template engine,
//
// [1] Inherit from this class (like ko.nativeTemplateEngine does)
@@ -2774,7 +2821,7 @@ ko.templateRewriting = (function () {
// anonymous function, even though Opera's built-in debugger can evaluate it anyway. No other browser requires this
// extra indirection.
var applyBindingsToNextSiblingScript =
- "ko.__tr_ambtns(function(){return(function(){return{" + rewrittenDataBindAttributeValue + "} })()})";
+ "ko.__tr_ambtns(function($context,$element){return(function(){return{ " + rewrittenDataBindAttributeValue + " } })()})";
return templateEngine['createJavaScriptEvaluatorBlock'](applyBindingsToNextSiblingScript) + tagToRetain;
}
@@ -2973,7 +3020,7 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
if (haveAddedNodesToParent) {
activateBindingsOnContinuousNodeArray(renderedNodesArray, bindingContext);
if (options['afterRender'])
- options['afterRender'](renderedNodesArray, bindingContext['$data']);
+ ko.dependencyDetection.ignore(options['afterRender'], null, [renderedNodesArray, bindingContext['$data']]);
}
return renderedNodesArray;
@@ -3008,7 +3055,7 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
}
},
null,
- { 'disposeWhen': whenToDispose, 'disposeWhenNodeIsRemoved': activelyDisposeWhenNodeIsRemoved }
+ { disposeWhen: whenToDispose, disposeWhenNodeIsRemoved: activelyDisposeWhenNodeIsRemoved }
);
} else {
// We don't yet have a DOM node to evaluate, so use a memo and render the template later when there is a DOM node
@@ -3026,7 +3073,7 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
// This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
var executeTemplateForArrayItem = function (arrayValue, index) {
// Support selecting template as a function of the data being rendered
- arrayItemContext = parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue));
+ arrayItemContext = parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue), options['as']);
arrayItemContext['$index'] = index;
var templateName = typeof(template) == 'function' ? template(arrayValue, arrayItemContext) : template;
return executeTemplate(null, "ignoreTargetNode", templateName, arrayItemContext, options);
@@ -3049,17 +3096,19 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
return options['includeDestroyed'] || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
});
- ko.utils.setDomNodeChildrenFromArrayMapping(targetNode, filteredArray, executeTemplateForArrayItem, options, activateBindingsCallback);
+ // Call setDomNodeChildrenFromArrayMapping, ignoring any observables unwrapped within (most likely from a callback function).
+ // If the array items are observables, though, they will be unwrapped in executeTemplateForArrayItem and managed within setDomNodeChildrenFromArrayMapping.
+ ko.dependencyDetection.ignore(ko.utils.setDomNodeChildrenFromArrayMapping, null, [targetNode, filteredArray, executeTemplateForArrayItem, options, activateBindingsCallback]);
- }, null, { 'disposeWhenNodeIsRemoved': targetNode });
+ }, null, { disposeWhenNodeIsRemoved: targetNode });
};
- var templateSubscriptionDomDataKey = '__ko__templateSubscriptionDomDataKey__';
- function disposeOldSubscriptionAndStoreNewOne(element, newSubscription) {
- var oldSubscription = ko.utils.domData.get(element, templateSubscriptionDomDataKey);
- if (oldSubscription && (typeof(oldSubscription.dispose) == 'function'))
- oldSubscription.dispose();
- ko.utils.domData.set(element, templateSubscriptionDomDataKey, newSubscription);
+ var templateComputedDomDataKey = '__ko__templateComputedDomDataKey__';
+ function disposeOldComputedAndStoreNewOne(element, newComputed) {
+ var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);
+ if (oldComputed && (typeof(oldComputed.dispose) == 'function'))
+ oldComputed.dispose();
+ ko.utils.domData.set(element, templateComputedDomDataKey, (newComputed && newComputed.isActive()) ? newComputed : undefined);
}
ko.bindingHandlers['template'] = {
@@ -3075,41 +3124,41 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
return { 'controlsDescendantBindings': true };
},
'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
- var bindingValue = ko.utils.unwrapObservable(valueAccessor());
- var templateName;
- var shouldDisplay = true;
+ var templateName = ko.utils.unwrapObservable(valueAccessor()),
+ options = {},
+ shouldDisplay = true,
+ dataValue,
+ templateComputed = null;
- if (typeof bindingValue == "string") {
- templateName = bindingValue;
- } else {
- templateName = bindingValue['name'];
+ if (typeof templateName != "string") {
+ options = templateName;
+ templateName = options['name'];
// Support "if"/"ifnot" conditions
- if ('if' in bindingValue)
- shouldDisplay = shouldDisplay && ko.utils.unwrapObservable(bindingValue['if']);
- if ('ifnot' in bindingValue)
- shouldDisplay = shouldDisplay && !ko.utils.unwrapObservable(bindingValue['ifnot']);
- }
+ if ('if' in options)
+ shouldDisplay = ko.utils.unwrapObservable(options['if']);
+ if (shouldDisplay && 'ifnot' in options)
+ shouldDisplay = !ko.utils.unwrapObservable(options['ifnot']);
- var templateSubscription = null;
+ dataValue = ko.utils.unwrapObservable(options['data']);
+ }
- if ((typeof bindingValue === 'object') && ('foreach' in bindingValue)) { // Note: can't use 'in' operator on strings
+ if ('foreach' in options) {
// Render once for each data point (treating data set as empty if shouldDisplay==false)
- var dataArray = (shouldDisplay && bindingValue['foreach']) || [];
- templateSubscription = ko.renderTemplateForEach(templateName || element, dataArray, /* options: */ bindingValue, element, bindingContext);
+ var dataArray = (shouldDisplay && options['foreach']) || [];
+ templateComputed = ko.renderTemplateForEach(templateName || element, dataArray, options, element, bindingContext);
+ } else if (!shouldDisplay) {
+ ko.virtualElements.emptyNode(element);
} else {
- if (shouldDisplay) {
- // Render once for this single data point (or use the viewModel if no data was provided)
- var innerBindingContext = (typeof bindingValue == 'object') && ('data' in bindingValue)
- ? bindingContext['createChildContext'](ko.utils.unwrapObservable(bindingValue['data'])) // Given an explitit 'data' value, we create a child binding context for it
- : bindingContext; // Given no explicit 'data' value, we retain the same binding context
- templateSubscription = ko.renderTemplate(templateName || element, innerBindingContext, /* options: */ bindingValue, element);
- } else
- ko.virtualElements.emptyNode(element);
+ // Render once for this single data point (or use the viewModel if no data was provided)
+ var innerBindingContext = ('data' in options) ?
+ bindingContext['createChildContext'](dataValue, options['as']) : // Given an explitit 'data' value, we create a child binding context for it
+ bindingContext; // Given no explicit 'data' value, we retain the same binding context
+ templateComputed = ko.renderTemplate(templateName || element, innerBindingContext, options, element);
}
- // It only makes sense to have a single template subscription per element (otherwise which one should have its output displayed?)
- disposeOldSubscriptionAndStoreNewOne(element, templateSubscription);
+ // It only makes sense to have a single template computed per element (otherwise which one should have its output displayed?)
+ disposeOldComputedAndStoreNewOne(element, templateComputed);
}
};
@@ -3131,85 +3180,95 @@ ko.exportSymbol('__tr_ambtns', ko.templateRewriting.applyMemoizedBindingsToNextS
ko.exportSymbol('setTemplateEngine', ko.setTemplateEngine);
ko.exportSymbol('renderTemplate', ko.renderTemplate);
-(function () {
+ko.utils.compareArrays = (function () {
+ var statusNotInOld = 'added', statusNotInNew = 'deleted';
+
// Simple calculation based on Levenshtein distance.
- function calculateEditDistanceMatrix(oldArray, newArray, maxAllowedDistance) {
- var distances = [];
- for (var i = 0; i <= newArray.length; i++)
- distances[i] = [];
-
- // Top row - transform old array into empty array via deletions
- for (var i = 0, j = Math.min(oldArray.length, maxAllowedDistance); i <= j; i++)
- distances[0][i] = i;
-
- // Left row - transform empty array into new array via additions
- for (var i = 1, j = Math.min(newArray.length, maxAllowedDistance); i <= j; i++) {
- distances[i][0] = i;
- }
-
- // Fill out the body of the array
- var oldIndex, oldIndexMax = oldArray.length, newIndex, newIndexMax = newArray.length;
- var distanceViaAddition, distanceViaDeletion;
- for (oldIndex = 1; oldIndex <= oldIndexMax; oldIndex++) {
- var newIndexMinForRow = Math.max(1, oldIndex - maxAllowedDistance);
- var newIndexMaxForRow = Math.min(newIndexMax, oldIndex + maxAllowedDistance);
- for (newIndex = newIndexMinForRow; newIndex <= newIndexMaxForRow; newIndex++) {
- if (oldArray[oldIndex - 1] === newArray[newIndex - 1])
- distances[newIndex][oldIndex] = distances[newIndex - 1][oldIndex - 1];
+ function compareArrays(oldArray, newArray, dontLimitMoves) {
+ oldArray = oldArray || [];
+ newArray = newArray || [];
+
+ if (oldArray.length <= newArray.length)
+ return compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew, dontLimitMoves);
+ else
+ return compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld, dontLimitMoves);
+ }
+
+ function compareSmallArrayToBigArray(smlArray, bigArray, statusNotInSml, statusNotInBig, dontLimitMoves) {
+ var myMin = Math.min,
+ myMax = Math.max,
+ editDistanceMatrix = [],
+ smlIndex, smlIndexMax = smlArray.length,
+ bigIndex, bigIndexMax = bigArray.length,
+ compareRange = (bigIndexMax - smlIndexMax) || 1,
+ maxDistance = smlIndexMax + bigIndexMax + 1,
+ thisRow, lastRow,
+ bigIndexMaxForRow, bigIndexMinForRow;
+
+ for (smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) {
+ lastRow = thisRow;
+ editDistanceMatrix.push(thisRow = []);
+ bigIndexMaxForRow = myMin(bigIndexMax, smlIndex + compareRange);
+ bigIndexMinForRow = myMax(0, smlIndex - 1);
+ for (bigIndex = bigIndexMinForRow; bigIndex <= bigIndexMaxForRow; bigIndex++) {
+ if (!bigIndex)
+ thisRow[bigIndex] = smlIndex + 1;
+ else if (!smlIndex) // Top row - transform empty array into new array via additions
+ thisRow[bigIndex] = bigIndex + 1;
+ else if (smlArray[smlIndex - 1] === bigArray[bigIndex - 1])
+ thisRow[bigIndex] = lastRow[bigIndex - 1]; // copy value (no edit)
else {
- var northDistance = distances[newIndex - 1][oldIndex] === undefined ? Number.MAX_VALUE : distances[newIndex - 1][oldIndex] + 1;
- var westDistance = distances[newIndex][oldIndex - 1] === undefined ? Number.MAX_VALUE : distances[newIndex][oldIndex - 1] + 1;
- distances[newIndex][oldIndex] = Math.min(northDistance, westDistance);
+ var northDistance = lastRow[bigIndex] || maxDistance; // not in big (deletion)
+ var westDistance = thisRow[bigIndex - 1] || maxDistance; // not in small (addition)
+ thisRow[bigIndex] = myMin(northDistance, westDistance) + 1;
}
}
}
- return distances;
- }
-
- function findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray) {
- var oldIndex = oldArray.length;
- var newIndex = newArray.length;
- var editScript = [];
- var maxDistance = editDistanceMatrix[newIndex][oldIndex];
- if (maxDistance === undefined)
- return null; // maxAllowedDistance must be too small
- while ((oldIndex > 0) || (newIndex > 0)) {
- var me = editDistanceMatrix[newIndex][oldIndex];
- var distanceViaAdd = (newIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex] : maxDistance + 1;
- var distanceViaDelete = (oldIndex > 0) ? editDistanceMatrix[newIndex][oldIndex - 1] : maxDistance + 1;
- var distanceViaRetain = (newIndex > 0) && (oldIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex - 1] : maxDistance + 1;
- if ((distanceViaAdd === undefined) || (distanceViaAdd < me - 1)) distanceViaAdd = maxDistance + 1;
- if ((distanceViaDelete === undefined) || (distanceViaDelete < me - 1)) distanceViaDelete = maxDistance + 1;
- if (distanceViaRetain < me - 1) distanceViaRetain = maxDistance + 1;
-
- if ((distanceViaAdd <= distanceViaDelete) && (distanceViaAdd < distanceViaRetain)) {
- editScript.push({ status: "added", value: newArray[newIndex - 1] });
- newIndex--;
- } else if ((distanceViaDelete < distanceViaAdd) && (distanceViaDelete < distanceViaRetain)) {
- editScript.push({ status: "deleted", value: oldArray[oldIndex - 1] });
- oldIndex--;
+ var editScript = [], meMinusOne, notInSml = [], notInBig = [];
+ for (smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex;) {
+ meMinusOne = editDistanceMatrix[smlIndex][bigIndex] - 1;
+ if (bigIndex && meMinusOne === editDistanceMatrix[smlIndex][bigIndex-1]) {
+ notInSml.push(editScript[editScript.length] = { // added
+ 'status': statusNotInSml,
+ 'value': bigArray[--bigIndex],
+ 'index': bigIndex });
+ } else if (smlIndex && meMinusOne === editDistanceMatrix[smlIndex - 1][bigIndex]) {
+ notInBig.push(editScript[editScript.length] = { // deleted
+ 'status': statusNotInBig,
+ 'value': smlArray[--smlIndex],
+ 'index': smlIndex });
} else {
- editScript.push({ status: "retained", value: oldArray[oldIndex - 1] });
- newIndex--;
- oldIndex--;
+ editScript.push({
+ 'status': "retained",
+ 'value': bigArray[--bigIndex] });
+ --smlIndex;
+ }
+ }
+
+ if (notInSml.length && notInBig.length) {
+ // Set a limit on the number of consecutive non-matching comparisons; having it a multiple of
+ // smlIndexMax keeps the time complexity of this algorithm linear.
+ var limitFailedCompares = smlIndexMax * 10, failedCompares,
+ a, d, notInSmlItem, notInBigItem;
+ // Go through the items that have been added and deleted and try to find matches between them.
+ for (failedCompares = a = 0; (dontLimitMoves || failedCompares < limitFailedCompares) && (notInSmlItem = notInSml[a]); a++) {
+ for (d = 0; notInBigItem = notInBig[d]; d++) {
+ if (notInSmlItem['value'] === notInBigItem['value']) {
+ notInSmlItem['moved'] = notInBigItem['index'];
+ notInBigItem['moved'] = notInSmlItem['index'];
+ notInBig.splice(d,1); // This item is marked as moved; so remove it from notInBig list
+ failedCompares = d = 0; // Reset failed compares count because we're checking for consecutive failures
+ break;
+ }
+ }
+ failedCompares += d;
}
}
return editScript.reverse();
}
- ko.utils.compareArrays = function (oldArray, newArray, maxEditsToConsider) {
- if (maxEditsToConsider === undefined) {
- return ko.utils.compareArrays(oldArray, newArray, 1) // First consider likely case where there is at most one edit (very fast)
- || ko.utils.compareArrays(oldArray, newArray, 10) // If that fails, account for a fair number of changes while still being fast
- || ko.utils.compareArrays(oldArray, newArray, Number.MAX_VALUE); // Ultimately give the right answer, even though it may take a long time
- } else {
- oldArray = oldArray || [];
- newArray = newArray || [];
- var editDistanceMatrix = calculateEditDistanceMatrix(oldArray, newArray, maxEditsToConsider);
- return findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray);
- }
- };
+ return compareArrays;
})();
ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
@@ -3225,8 +3284,8 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
// "callbackAfterAddingNodes" will be invoked after any "mapping"-generated nodes are inserted into the container node
// You can use this, for example, to activate bindings on those nodes.
- function fixUpNodesToBeRemoved(contiguousNodeArray) {
- // Before deleting or replacing a set of nodes that were previously outputted by the "map" function, we have to reconcile
+ function fixUpNodesToBeMovedOrRemoved(contiguousNodeArray) {
+ // Before moving, deleting, or replacing a set of nodes that were previously outputted by the "map" function, we have to reconcile
// them against what is in the DOM right now. It may be that some of the nodes have already been removed from the document,
// or that new nodes might have been inserted in the middle, for example by a binding. Also, there may previously have been
// leading comment nodes (created by rewritten string-based templates) that have since been removed during binding.
@@ -3258,6 +3317,7 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
// (The following line replaces the contents of contiguousNodeArray with newContiguousSet)
Array.prototype.splice.apply(contiguousNodeArray, [0, contiguousNodeArray.length].concat(newContiguousSet));
}
+ return contiguousNodeArray;
}
function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes, index) {
@@ -3268,18 +3328,17 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
// On subsequent evaluations, just replace the previously-inserted DOM nodes
if (mappedNodes.length > 0) {
- fixUpNodesToBeRemoved(mappedNodes);
- ko.utils.replaceDomNodes(mappedNodes, newMappedNodes);
+ ko.utils.replaceDomNodes(fixUpNodesToBeMovedOrRemoved(mappedNodes), newMappedNodes);
if (callbackAfterAddingNodes)
- callbackAfterAddingNodes(valueToMap, newMappedNodes);
+ ko.dependencyDetection.ignore(callbackAfterAddingNodes, null, [valueToMap, newMappedNodes, index]);
}
// Replace the contents of the mappedNodes array, thereby updating the record
// of which nodes would be deleted if valueToMap was itself later removed
mappedNodes.splice(0, mappedNodes.length);
ko.utils.arrayPushAll(mappedNodes, newMappedNodes);
- }, null, { 'disposeWhenNodeIsRemoved': containerNode, 'disposeWhen': function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } });
- return { mappedNodes : mappedNodes, dependentObservable : dependentObservable };
+ }, null, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: function() { return (mappedNodes.length == 0) || !ko.utils.domNodeIsAttachedToDocument(mappedNodes[0]) } });
+ return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) };
}
var lastMappingResultDomDataKey = "setDomNodeChildrenFromArrayMapping_lastMappingResult";
@@ -3296,96 +3355,113 @@ ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);
// Build the new mapping result
var newMappingResult = [];
var lastMappingResultIndex = 0;
- var nodesToDelete = [];
var newMappingResultIndex = 0;
- var nodesAdded = [];
- var insertAfterNode = null;
- for (var i = 0, j = editScript.length; i < j; i++) {
- switch (editScript[i].status) {
- case "retained":
- // Just keep the information - don't touch the nodes
- var dataToRetain = lastMappingResult[lastMappingResultIndex];
- dataToRetain.indexObservable(newMappingResultIndex);
- newMappingResultIndex = newMappingResult.push(dataToRetain);
- if (dataToRetain.domNodes.length > 0)
- insertAfterNode = dataToRetain.domNodes[dataToRetain.domNodes.length - 1];
- lastMappingResultIndex++;
- break;
- case "deleted":
- // Stop tracking changes to the mapping for these nodes
- lastMappingResult[lastMappingResultIndex].dependentObservable.dispose();
-
- // Queue these nodes for later removal
- fixUpNodesToBeRemoved(lastMappingResult[lastMappingResultIndex].domNodes);
- ko.utils.arrayForEach(lastMappingResult[lastMappingResultIndex].domNodes, function (node) {
- nodesToDelete.push({
- element: node,
- index: i,
- value: editScript[i].value
+ var nodesToDelete = [];
+ var itemsToProcess = [];
+ var itemsForBeforeRemoveCallbacks = [];
+ var itemsForMoveCallbacks = [];
+ var itemsForAfterAddCallbacks = [];
+ var mapData;
+
+ function itemMovedOrRetained(editScriptIndex, oldPosition) {
+ mapData = lastMappingResult[oldPosition];
+ if (newMappingResultIndex !== oldPosition)
+ itemsForMoveCallbacks[editScriptIndex] = mapData;
+ // Since updating the index might change the nodes, do so before calling fixUpNodesToBeMovedOrRemoved
+ mapData.indexObservable(newMappingResultIndex++);
+ fixUpNodesToBeMovedOrRemoved(mapData.mappedNodes);
+ newMappingResult.push(mapData);
+ itemsToProcess.push(mapData);
+ }
+
+ function callCallback(callback, items) {
+ if (callback) {
+ for (var i = 0, n = items.length; i < n; i++) {
+ if (items[i]) {
+ ko.utils.arrayForEach(items[i].mappedNodes, function(node) {
+ callback(node, i, items[i].arrayEntry);
});
- insertAfterNode = node;
- });
+ }
+ }
+ }
+ }
+
+ for (var i = 0, editScriptItem, movedIndex; editScriptItem = editScript[i]; i++) {
+ movedIndex = editScriptItem['moved'];
+ switch (editScriptItem['status']) {
+ case "deleted":
+ if (movedIndex === undefined) {
+ mapData = lastMappingResult[lastMappingResultIndex];
+
+ // Stop tracking changes to the mapping for these nodes
+ if (mapData.dependentObservable)
+ mapData.dependentObservable.dispose();
+
+ // Queue these nodes for later removal
+ nodesToDelete.push.apply(nodesToDelete, fixUpNodesToBeMovedOrRemoved(mapData.mappedNodes));
+ if (options['beforeRemove']) {
+ itemsForBeforeRemoveCallbacks[i] = mapData;
+ itemsToProcess.push(mapData);
+ }
+ }
lastMappingResultIndex++;
break;
+ case "retained":
+ itemMovedOrRetained(i, lastMappingResultIndex++);
+ break;
+
case "added":
- var valueToMap = editScript[i].value;
- var indexObservable = ko.observable(newMappingResultIndex);
- var mapData = mapNodeAndRefreshWhenChanged(domNode, mapping, valueToMap, callbackAfterAddingNodes, indexObservable);
- var mappedNodes = mapData.mappedNodes;
-
- // On the first evaluation, insert the nodes at the current insertion point
- newMappingResultIndex = newMappingResult.push({
- arrayEntry: editScript[i].value,
- domNodes: mappedNodes,
- dependentObservable: mapData.dependentObservable,
- indexObservable: indexObservable
- });
- for (var nodeIndex = 0, nodeIndexMax = mappedNodes.length; nodeIndex < nodeIndexMax; nodeIndex++) {
- var node = mappedNodes[nodeIndex];
- nodesAdded.push({
- element: node,
- index: i,
- value: editScript[i].value
- });
- if (insertAfterNode == null) {
- // Insert "node" (the newly-created node) as domNode's first child
- ko.virtualElements.prepend(domNode, node);
- } else {
- // Insert "node" into "domNode" immediately after "insertAfterNode"
- ko.virtualElements.insertAfter(domNode, node, insertAfterNode);
- }
- insertAfterNode = node;
+ if (movedIndex !== undefined) {
+ itemMovedOrRetained(i, movedIndex);
+ } else {
+ mapData = { arrayEntry: editScriptItem['value'], indexObservable: ko.observable(newMappingResultIndex++) };
+ newMappingResult.push(mapData);
+ itemsToProcess.push(mapData);
+ if (!isFirstExecution)
+ itemsForAfterAddCallbacks[i] = mapData;
}
- if (callbackAfterAddingNodes)
- callbackAfterAddingNodes(valueToMap, mappedNodes, indexObservable);
break;
}
}
- ko.utils.arrayForEach(nodesToDelete, function (node) { ko.cleanNode(node.element) });
+ // Call beforeMove first before any changes have been made to the DOM
+ callCallback(options['beforeMove'], itemsForMoveCallbacks);
- var invokedBeforeRemoveCallback = false;
- if (!isFirstExecution) {
- if (options['afterAdd']) {
- for (var i = 0; i < nodesAdded.length; i++)
- options['afterAdd'](nodesAdded[i].element, nodesAdded[i].index, nodesAdded[i].value);
- }
- if (options['beforeRemove']) {
- for (var i = 0; i < nodesToDelete.length; i++)
- options['beforeRemove'](nodesToDelete[i].element, nodesToDelete[i].index, nodesToDelete[i].value);
- invokedBeforeRemoveCallback = true;
+ // Next remove nodes for deleted items (or just clean if there's a beforeRemove callback)
+ ko.utils.arrayForEach(nodesToDelete, options['beforeRemove'] ? ko.cleanNode : ko.removeNode);
+
+ // Next add/reorder the remaining items (will include deleted items if there's a beforeRemove callback)
+ for (var i = 0, nextNode = ko.virtualElements.firstChild(domNode), lastNode, node; mapData = itemsToProcess[i]; i++) {
+ // Get nodes for newly added items
+ if (!mapData.mappedNodes)
+ ko.utils.extend(mapData, mapNodeAndRefreshWhenChanged(domNode, mapping, mapData.arrayEntry, callbackAfterAddingNodes, mapData.indexObservable));
+
+ // Put nodes in the right place if they aren't there already
+ for (var j = 0; node = mapData.mappedNodes[j]; nextNode = node.nextSibling, lastNode = node, j++) {
+ if (node !== nextNode)
+ ko.virtualElements.insertAfter(domNode, node, lastNode);
}
- }
- if (!invokedBeforeRemoveCallback && nodesToDelete.length) {
- for (var i = 0; i < nodesToDelete.length; i++) {
- var element = nodesToDelete[i].element;
- if (element.parentNode)
- element.parentNode.removeChild(element);
+
+ // Run the callbacks for newly added nodes (for example, to apply bindings, etc.)
+ if (!mapData.initialized && callbackAfterAddingNodes) {
+ callbackAfterAddingNodes(mapData.arrayEntry, mapData.mappedNodes, mapData.indexObservable);
+ mapData.initialized = true;
}
}
+ // If there's a beforeRemove callback, call it after reordering.
+ // Note that we assume that the beforeRemove callback will usually be used to remove the nodes using
+ // some sort of animation, which is why we first reorder the nodes that will be removed. If the
+ // callback instead removes the nodes right away, it would be more efficient to skip reordering them.
+ // Perhaps we'll make that change in the future if this scenario becomes more common.
+ callCallback(options['beforeRemove'], itemsForBeforeRemoveCallbacks);
+
+ // Finally call afterMove and afterAdd callbacks
+ callCallback(options['afterMove'], itemsForMoveCallbacks);
+ callCallback(options['afterAdd'], itemsForAfterAddCallbacks);
+
// Store a copy of the array items we just considered so we can difference it next time
ko.utils.domData.set(domNode, lastMappingResultDomDataKey, newMappingResult);
}
diff --git a/root/static/lib/libraries.txt b/root/static/lib/libraries.txt
index 9e99edf..1f7fdbd 100644
--- a/root/static/lib/libraries.txt
+++ b/root/static/lib/libraries.txt
@@ -94,7 +94,7 @@ url: http://tablesorter.com
library: Knockout
license: MIT
-version: GIT, 2012-07-23
+version: 2.2.0
author: Steven Sanderson
url: http://knockoutjs.com
diff --git a/root/static/scripts/relationship-editor/Dialog.js b/root/static/scripts/relationship-editor/Dialog.js
index f2d6c69..f412855 100644
--- a/root/static/scripts/relationship-editor/Dialog.js
+++ b/root/static/scripts/relationship-editor/Dialog.js
@@ -283,38 +283,29 @@ var Dialog = UI.Dialog = {
mode: ko.observable(""),
loading: ko.observable(false),
batchWorksError: ko.observable(false),
- backward: ko.observable(false),
showAutocomplete: ko.observable(false),
showCreateWorkLink: ko.observable(false),
showAttributesHelp: ko.observable(false),
showLinkTypeHelp: ko.observable(false),
- relationship: (function() {
- var value = ko.observable(null);
-
- return ko.computed({
- read: value,
- write: function(newValue) {
- var oldValue = value();
- if (oldValue !== newValue) value(newValue);
- }
- });
- }()),
-
init: function() {
var entity = [RE.Entity({type: "artist"}), RE.Entity({type: "recording"})];
// this is used as an "empty" state when the dialog is hidden, so that
// none of the bindings error out.
this.emptyRelationship = RE.Relationship({entity: entity});
+ this.relationship = ko.observable(this.emptyRelationship);
- this.backward(true);
- this.relationship(this.emptyRelationship);
+ this.backward = ko.observable(true);
this.sourceField = ko.observable(null);
this.targetField = ko.observable(null);
this.source = entity[1];
+ this.linkTypeDescription = ko.computed(function() {
+ return (Util.typeInfo(Dialog.relationship().link_type()) || {}).descr || "";
+ });
+
ko.computed(function() {
var relationship = Dialog.relationship(),
entity0 = relationship.entity[0],
@@ -404,17 +395,6 @@ var Dialog = UI.Dialog = {
this.showAttributesHelp(!this.showAttributesHelp());
},
- linkTypeDescription: ko.computed({
- read: function() {
- var info = Util.typeInfo(Dialog.relationship().link_type());
- return info ? info.descr : "";
- },
- // at the time this is declared, Dialog.relationship() is not set,
- // and can't be because relationships require the link type info from
- // the server. so defer this.
- deferEvaluation: true
- }),
-
changeDirection: function() {
var relationship = this.relationship.peek(),
entity0 = relationship.entity[0].peek(),
diff --git a/root/static/scripts/relationship-editor/Relationship.js b/root/static/scripts/relationship-editor/Relationship.js
index 779d770..549b810 100644
--- a/root/static/scripts/relationship-editor/Relationship.js
+++ b/root/static/scripts/relationship-editor/Relationship.js
@@ -282,6 +282,12 @@ Relationship.prototype.openEdits = function() {
);
};
+
+Relationship.prototype.css = function() {
+ var action = this.action();
+ return _.trim((this.hasErrors() ? "error-field" : "") + " " + (action ? "rel-" + action : ""));
+};
+
return RE;
}(MB.RelationshipEditor || {}));
commit ae8998748bd0e70e23e7ef07b3b63e1f8c8bb17a
Author: Michael Wiencek <[email protected]>
Date: Mon Oct 29 20:10:55 2012 +0000
MBS-5480: limit the size of the autocomplete menu
diff --git a/root/static/scripts/relationship-editor/Dialog.js b/root/static/scripts/relationship-editor/Dialog.js
index f2d6c69..414968e 100644
--- a/root/static/scripts/relationship-editor/Dialog.js
+++ b/root/static/scripts/relationship-editor/Dialog.js
@@ -216,6 +216,8 @@ ko.bindingHandlers.autocomplete = (function() {
}
function changeTarget(event, data) {
+ if (!data.gid) return;
+
// XXX release groups' numeric "type" conflicts with the entity type
data.type = _.isNumber(data.type) ? "release_group" : (data.type || Dialog.target.type);
@@ -257,7 +259,6 @@ ko.bindingHandlers.autocomplete = (function() {
Dialog.autocomplete = MB.Control.EntityAutocomplete({
inputs: $autocomplete,
- position: {collision: "fit"},
entity: Dialog.target.type,
setEntity: setEntity
});
diff --git a/root/static/styles/widgets.css b/root/static/styles/widgets.css
index da3d82e..feba02a 100644
--- a/root/static/styles/widgets.css
+++ b/root/static/styles/widgets.css
@@ -334,3 +334,9 @@ ul.favicons li img {
#recaptcha_widget_div {
display: inline-block;
}
+
+ul.ui-autocomplete.ui-menu {
+ max-height: 300px !important;
+ overflow-y: auto !important;
+ overflow-x: hidden !important;
+}
commit 070070138957f693768b57a7a1ce28f388ab8de9
Author: Michael Wiencek <[email protected]>
Date: Mon Oct 29 01:45:00 2012 +0000
Relationship editor could partially submit edits
diff --git a/lib/MusicBrainz/Server/Controller/RelationshipEditor.pm b/lib/MusicBrainz/Server/Controller/RelationshipEditor.pm
index 259b0ab..4bcf4cf 100644
--- a/lib/MusicBrainz/Server/Controller/RelationshipEditor.pm
+++ b/lib/MusicBrainz/Server/Controller/RelationshipEditor.pm
@@ -34,7 +34,9 @@ sub base : Path('/relationship-editor') Args(0) Edit RequireAuth {
}
}
if ($form->submitted_and_valid($c->req->body_parameters)) {
- $self->submit_edits($c, $form);
+ $c->model('MB')->with_transaction(sub {
+ $self->submit_edits($c, $form);
+ });
$c->res->body(encode_json({message => 'OK'}));
} else {
$c->res->status(400);
@@ -199,13 +201,11 @@ sub remove_relationship {
my $id = $field->field('id')->value;
my $relationship = $c->stash->{loaded_relationships}->{$types}->{$id};
- $c->model('MB')->with_transaction(sub {
- $self->_insert_edit(
- $c, $form,
- edit_type => $EDIT_RELATIONSHIP_DELETE,
- relationship => $relationship,
- );
- });
+ $self->_insert_edit(
+ $c, $form,
+ edit_type => $EDIT_RELATIONSHIP_DELETE,
+ relationship => $relationship,
+ );
}
sub add_relationship {
diff --git a/root/scripts/text_strings.tt b/root/scripts/text_strings.tt
index ef1f9aa..5efc125 100644
--- a/root/scripts/text_strings.tt
+++ b/root/scripts/text_strings.tt
@@ -116,6 +116,7 @@
AddNote: '[% l('Add Note') | js %]',
DeleteNote: '[% l('Delete Note') | js %]',
ConfirmSwap: '[% l('This tracklist has artist credits with information that will be lost if you swap artist credits with track titles. This cannot be undone. Do you wish to continue?') | js %]',
- ConfirmNavigation: '[% l('All of your changes will be lost if you leave this page.') | js %]'
+ ConfirmNavigation: '[% l('All of your changes will be lost if you leave this page.') | js %]',
+ SubmissionError: '[% l('We\'re very sorry, an error occurred during submission. The server may be under heavy load; please wait a moment and try again.') | js %]'
});
})(MB);
diff --git a/root/static/scripts/relationship-editor/RelationshipEditor.js b/root/static/scripts/relationship-editor/RelationshipEditor.js
index 291109f..fd818f1 100644
--- a/root/static/scripts/relationship-editor/RelationshipEditor.js
+++ b/root/static/scripts/relationship-editor/RelationshipEditor.js
@@ -101,12 +101,12 @@ RE.releaseViewModel = {
.success(function() {
window.location.replace("/release/" + self.GID);
})
- .error(function(jqXHR, statusText) {
+ .error(function(jqXHR) {
try {
self.handlerErrors(JSON.parse(jqXHR.responseText), changed);
} catch(e) {
self.submissionLoading(false);
- self.submissionError(statusText);
+ self.submissionError(MB.text.SubmissionError);
}
if (beforeUnload) window.onbeforeunload = beforeUnload;
});
commit 231fbd6576b11b06d0523efd04944cb339b2d666
Author: Michael Wiencek <[email protected]>
Date: Sun Oct 28 03:15:47 2012 +0000
Rearrange the rel editor's batch ui
diff --git a/root/release/edit_relationships.tt b/root/release/edit_relationships.tt
index 296cd5e..0bdbcc7 100644
--- a/root/release/edit_relationships.tt
+++ b/root/release/edit_relationships.tt
@@ -327,9 +327,9 @@
<p>[% l('To <span class="rel-edit">edit</span> a relationship, click on its name (e.g. "composer").') %]</p>
- <h2>[% l('Track Relationships') %]</h2>
+ <p>[% l('To use the batch tools, select some recordings or works using the checkboxes.') %]</p>
- <h3>[% l('Tools:') %]</h3>
+ <h2>[% l('Track Relationships') %]</h2>
[%# These are here to make sure the strings get added to the message catalog.
The strings are later obtained via /ws/js/plurals. %]
@@ -337,38 +337,40 @@
<!-- [% ln('{n} recording selected', '{n} recordings selected', 0) %]
[% ln('{n} work selected', '{n} works selected', 0) %] -->
- <div>
- <div style="float: left;" data-bind="with: checkboxes">
- <a id="batch-recording" class="btn" data-bind="css: {disabled: recordingCount() == 0}">
- <img src="/static/images/icons/add.png" class="bottom"/>
- [% l('Batch-add a relationship to recordings') %]
- </a>
- <!-- ko text: recordingMessage --><!-- /ko --><br/>
- <a id="batch-work" class="btn" data-bind="css: {disabled: workCount() == 0}">
- <img src="/static/images/icons/add.png" class="bottom"/>
- [% l('Batch-add a relationship to works') %]
- </a>
- <!-- ko text: workMessage --><!-- /ko --><br/>
- <a id="batch-create-works" class="btn" data-bind="css: {disabled: recordingCount() == 0}">
- <img src="/static/images/icons/add.png" class="bottom"/>
- [% l('Batch-create new works') %]
- </a>
- <!-- ko text: recordingMessage --><!-- /ko -->
- </div>
- <div class="documentation">
- <div class="bubble">
- [% l('To use these tools, select some recordings or works using the checkboxes.') %]
- </div>
- </div>
- </div>
+ <table id="batch-tools" data-bind="with: checkboxes">
+ <tr>
+ <td>
+ <a id="batch-recording" class="btn" data-bind="css: {disabled: recordingCount() == 0}">
+ <img src="/static/images/icons/add.png" class="bottom"/>
+ [% l('Batch-add a relationship to recordings') %]
+ </a>
+ </td>
+ <td>
+ <a id="batch-create-works" class="btn" data-bind="css: {disabled: recordingCount() == 0}">
+ <img src="/static/images/icons/add.png" class="bottom"/>
+ [% l('Batch-create new works') %]
+ </a>
+ </td>
+ <td>
+ <a id="batch-work" class="btn" data-bind="css: {disabled: workCount() == 0}">
+ <img src="/static/images/icons/add.png" class="bottom"/>
+ [% l('Batch-add a relationship to works') %]
+ </a>
+ </td>
+ </tr>
+ </table>
<table class="tbl" id="tracklist">
- <thead>
+ <thead data-bind="with: checkboxes">
<tr>
<th class="pos t">#</th>
- <th class="recordings">[% l('Recording') %]</th>
- <th style="border-left: none;"> </th>
- <th class="works">[% l('Related Works') %]</th>
+ <th class="recordings">
+ [% l('Recording') %] <!-- ko text: recordingMessage --><!-- /ko -->
+ </th>
+ <th style="border-left: none;"></th>
+ <th class="works">
+ [% l('Related Works') %] <!-- ko text: workMessage --><!-- /ko -->
+ </th>
</tr>
</thead>
<tbody data-bind="foreach: media">
diff --git a/root/static/styles/relationship-editor.css b/root/static/styles/relationship-editor.css
index e7c4109..0a39d2f 100644
--- a/root/static/styles/relationship-editor.css
+++ b/root/static/styles/relationship-editor.css
@@ -1,5 +1,6 @@
#content.rel-editor #tracklist td {vertical-align: top;}
#content.rel-editor #tracklist td.midcol {vertical-align: middle; font-size: 0.9em;}
+#content.rel-editor #batch-tools {width: 100%; margin: 0.8em;}
#content.rel-editor div.ars {padding: 0; margin: 0.5em 0; margin-left: 1.5em;}
#content.rel-editor span.link-phrase {color: #000; cursor: pointer; text-decoration: none;}
#content.rel-editor span.link-phrase:hover {background: #FD9;}
commit a3297c80d5d70091a595bfc4a480be9c254a2234
Author: Michael Wiencek <[email protected]>
Date: Sun Oct 28 02:02:05 2012 +0000
Fix open edits link in the rel editor dialog
diff --git a/root/release/edit_relationships.tt b/root/release/edit_relationships.tt
index 6bc1c1b..296cd5e 100644
--- a/root/release/edit_relationships.tt
+++ b/root/release/edit_relationships.tt
@@ -16,7 +16,7 @@
<!-- ko if: relationship() && relationship().edits_pending -->
<p class="msg warning">
[% l('Warning: This relationship has pending edits.
- <a target="_blank" data-bind="attr: {href: relationship().openEdits}">Click here</a>
+ <a target="_blank" data-bind="attr: {href: relationship().openEdits()}">Click here</a>
to view these edits and make sure they do not conflict with your own.') %]
</p>
<!-- /ko -->
diff --git a/root/static/scripts/relationship-editor/Relationship.js b/root/static/scripts/relationship-editor/Relationship.js
index c6123c8..779d770 100644
--- a/root/static/scripts/relationship-editor/Relationship.js
+++ b/root/static/scripts/relationship-editor/Relationship.js
@@ -261,9 +261,9 @@ Relationship.prototype.isDuplicate = function(other) {
};
-Relationship.prototype.buildOpenEdits = function() {
- var entity0 = this.original_fields.entity[0],
- entity1 = this.original_fields.entity[1];
+Relationship.prototype.openEdits = function() {
+ var entity0 = RE.Entity(this.original_fields.entity[0]),
+ entity1 = RE.Entity(this.original_fields.entity[1]);
return _.sprintf(
'/search/edits?auto_edit_filter=&order=desc&negation=0&combinator=and' +
commit e8ab6a5159a38465f46f4722486c8bacb8cd911b
Author: Michael Wiencek <[email protected]>
Date: Sun Oct 28 01:15:34 2012 +0000
MBS-5521: Date placeholders in the relationship editor are not translated
diff --git a/root/release/edit_relationships.tt b/root/release/edit_relationships.tt
index 6bc1c1b..0d2ad52 100644
--- a/root/release/edit_relationships.tt
+++ b/root/release/edit_relationships.tt
@@ -129,29 +129,29 @@
<!-- /ko -->
<tr>
<td class="section">[% l('Begin date:') %]</td>
- <td data-bind="with: relationship().period">
- <input data-bind="value: begin_date().year, valueUpdate: 'afterkeydown'" maxlength="4" placeholder="YYYY" size="4"/>-[%-
- -%]<input data-bind="value: begin_date().month, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="MM" size="2"/>-[%-
- -%]<input data-bind="value: begin_date().day, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="DD" size="2"/>
- <div class="error" data-bind="text: begin_date.error"></div>
+ <td class="partial-date">
+ <input data-bind="value: relationship().period.begin_date().year, valueUpdate: 'afterkeydown'" maxlength="4" placeholder="[% l('YYYY') %]" size="4"/>-[%-
+ -%]<input data-bind="value: relationship().period.begin_date().month, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="[% l('MM') %]" size="2"/>-[%-
+ -%]<input data-bind="value: relationship().period.begin_date().day, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="[% l('DD') %]" size="2"/>
+ <div class="error" data-bind="text: relationship().period.begin_date.error"></div>
</td>
</tr>
<tr>
<td class="section">[% l('End date:') %]</td>
- <td data-bind="with: relationship().period">
- <input data-bind="value: end_date().year, valueUpdate: 'afterkeydown'" maxlength="4" placeholder="YYYY" size="4"/>-[%-
- -%]<input data-bind="value: end_date().month, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="MM" size="2"/>-[%-
- -%]<input data-bind="value: end_date().day, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="DD" size="2"/>
- <div class="error" data-bind="text: end_date.error"></div>
+ <td class="partial-date">
+ <input data-bind="value: relationship().period.end_date().year, valueUpdate: 'afterkeydown'" maxlength="4" placeholder="[% l('YYYY') %]" size="4"/>-[%-
+ -%]<input data-bind="value: relationship().period.end_date().month, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="[% l('MM') %]" size="2"/>-[%-
+ -%]<input data-bind="value: relationship().period.end_date().day, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="[% l('DD') %]" size="2"/>
+ <div class="error" data-bind="text: relationship().period.end_date.error"></div>
</td>
</tr>
<tr>
- <td colspan="2" data-bind="with: relationship().period">
+ <td colspan="2">
<label>
- <input type="checkbox" data-bind="checked: ended"/>
+ <input type="checkbox" data-bind="checked: relationship().period.ended"/>
[% l('This relationship has ended.') %]
</label>
- <div class="error" data-bind="text: ended.error"></div>
+ <div class="error" data-bind="text: relationship().period.ended.error"></div>
</td>
</tr>
<!-- ko if: loading -->
commit b89cf93efaf0678881d43af27b76f97f073495c8
Author: Michael Wiencek <[email protected]>
Date: Sat Oct 27 06:58:29 2012 +0000
Extraneous semicolon
diff --git a/root/static/scripts/common/multiselect.js b/root/static/scripts/common/multiselect.js
index 2292eaf..3ab9759 100644
--- a/root/static/scripts/common/multiselect.js
+++ b/root/static/scripts/common/multiselect.js
@@ -193,7 +193,7 @@ var multiselect = function(input, placeholder, cacheKey) {
.on("keydown", function(event) {
if (event.keyCode == 13) { // enter
if (self.hoverOption) {
- self.select(self.hoverOption);;
+ self.select(self.hoverOption);
killEvent(event);
}
} else if (event.keyCode == 38) { // up
commit 3d66a69744bc014f3e063c8e718f12669705d675
Merge: ffc2aaf 0650b78
Author: Michael Wiencek <[email protected]>
Date: Sat Oct 27 06:28:44 2012 +0000
Merge branch 'mbs-5389' into mbs-5476
commit ffc2aafea8dac4f24fb62c7ed043da9c351c7173
Author: Michael Wiencek <[email protected]>
Date: Sat Oct 27 06:23:44 2012 +0000
MBS-5476 / MBS-5445: Misc. fixes pressing <enter> in dialog
MBS-5476: Pressing enter in empty fields selects instruments / vocals
MBS-5445: All the widgets forms should submit when hitting <Enter> in a
field
diff --git a/root/static/scripts/common/multiselect.js b/root/static/scripts/common/multiselect.js
index dec19a3..2292eaf 100644
--- a/root/static/scripts/common/multiselect.js
+++ b/root/static/scripts/common/multiselect.js
@@ -192,18 +192,17 @@ var multiselect = function(input, placeholder, cacheKey) {
})
.on("keydown", function(event) {
if (event.keyCode == 13) { // enter
- if (self.hoverOption)
- self.select(self.hoverOption);
-
+ if (self.hoverOption) {
+ self.select(self.hoverOption);;
+ killEvent(event);
+ }
} else if (event.keyCode == 38) { // up
- if (self.activeOption()) return;
-
// opera skips these when tabbing, so focus them explicitly.
- if (self.items.lastChild) {
+ if (!self.activeOption() && self.items.lastChild) {
self.hide();
self.items.lastChild.focus();
- } else return;
-
+ killEvent(event);
+ }
} else if (event.keyCode == 40) { // down
self.show();
@@ -214,9 +213,9 @@ var multiselect = function(input, placeholder, cacheKey) {
var option = self.activeOption() || self.firstOption();
option ? self.activateOption(option) : self.hide();
}, 1);
- } else return;
- killEvent(event);
+ killEvent(event);
+ }
})
.on("input", function(event) {
self.lookup(this.value);
@@ -293,7 +292,6 @@ multiselect.prototype.activateOption = function(option, focus) {
if (option === this.hoverOption) return;
this.hoverOption && (this.hoverOption.className = "");
- this.hoverOption = null;
this.hoverOption = option;
option.className = "hover";
};
@@ -358,7 +356,7 @@ multiselect.prototype.matchesTerm = function(option) {
}
multiselect.prototype.lookup = function(term) {
- var self = this, first = true;
+ var self = this, first = Boolean(term);
this.term = term.toLowerCase();
if (!this.term) this.hide();
diff --git a/root/static/scripts/relationship-editor/Dialog.js b/root/static/scripts/relationship-editor/Dialog.js
index a839928..83f6480 100644
--- a/root/static/scripts/relationship-editor/Dialog.js
+++ b/root/static/scripts/relationship-editor/Dialog.js
@@ -209,12 +209,6 @@ ko.bindingHandlers.autocomplete = (function() {
$("#target-type").val(type).trigger("change");
}
- function closeOnEnter(event) {
- if (event.keyCode == 13 && !Dialog.relationship.peek().hasErrors.peek() &&
- !event.isDefaultPrevented())
- Dialog.instance.peek().accept();
- }
-
function changeTarget(event, data) {
// XXX release groups' numeric "type" conflicts with the entity type
data.type = _.isNumber(data.type) ? "release_group" : (data.type || Dialog.target.type);
@@ -251,6 +245,14 @@ ko.bindingHandlers.autocomplete = (function() {
}
}
+ // In Opera 10, when the keydown event on the autocomplete bubbles up to the
+ // dialog, isDefaultPrevented returns false even though here it returns true.
+ // Other browsers work fine.
+ function stopEnter(event) {
+ if (event.keyCode == 13 && event.isDefaultPrevented())
+ event.stopPropagation();
+ }
+
return {
init: function(element) {
var $autocomplete = Dialog.$autocomplete = $(element);
@@ -265,8 +267,8 @@ ko.bindingHandlers.autocomplete = (function() {
$autocomplete
.on("lookup-performed", changeTarget)
.find("input.name")
- .on("keyup focus click", showRecentEntities)
- .on("keydown", closeOnEnter);
+ .on("keydown", stopEnter)
+ .on("keyup focus click", showRecentEntities);
setAutocompleteEntity(Dialog.target, Dialog.mode() != "edit");
@@ -278,6 +280,29 @@ ko.bindingHandlers.autocomplete = (function() {
}());
+var BaseDialog = (function() {
+ var inputRegex = /^input|button|select$/;
+
+ function submit(event) {
+ if (event.keyCode == 13 && this.canSubmit() && !event.isDefaultPrevented() &&
+ inputRegex.test(event.target.nodeName.toLowerCase()))
+ this.accept();
+ }
+
+ function cancel(event) {
+ if (event.keyCode == 13) {
+ event.preventDefault();
+ this.hide();
+ }
+ }
+
+ return function(options) {
+ options.$dialog.on("keydown", _.bind(submit, options))
+ .find("button.negative").on("keydown", _.bind(cancel, options));
+ };
+}());
+
+
var Dialog = UI.Dialog = {
MB: MB,
mode: ko.observable(""),
@@ -303,7 +328,7 @@ var Dialog = UI.Dialog = {
}()),
init: function() {
- var entity = [RE.Entity({type: "artist"}), RE.Entity({type: "recording"})];
+ var self = this, entity = [RE.Entity({type: "artist"}), RE.Entity({type: "recording"})];
// this is used as an "empty" state when the dialog is hidden, so that
// none of the bindings error out.
@@ -334,7 +359,16 @@ var Dialog = UI.Dialog = {
});
this.$overlay = $("#overlay");
- this.$dialog = $("#dialog");
+ this.$dialog = $("#dialog");
+
+ BaseDialog({
+ $dialog: this.$dialog,
+ canSubmit: function() {
+ return !self.relationship.peek().hasErrors.peek();
+ },
+ accept: function() {self.instance.peek().accept()},
+ hide: function() {self.instance.peek().hide()}
+ });
Dialog.instance = ko.observable(this);
ko.applyBindings(this, this.$dialog[0]);
@@ -676,11 +710,16 @@ var WorkDialog = UI.WorkDialog = {
editNote: ko.observable(""),
init: function() {
- var self = this, $dialog = $("#new-work-dialog")
- .on("keydown", "#work-name", function(event) {
- if (event.keyCode == 13 && self.name.peek() && !self.loading.peek())
- self.accept();
- });
+ var self = this, $dialog = $("#new-work-dialog");
+
+ BaseDialog({
+ $dialog: $dialog,
+ canSubmit: function() {
+ return self.name.peek() && !self.loading.peek();
+ },
+ accept: this.accept,
+ hide: this.hide
+ });
ko.applyBindings(this, $dialog[0]);
},
commit 0650b786f211914d9b01f66a30ed7f4dab6ebfa6
Author: Michael Wiencek <[email protected]>
Date: Sat Oct 27 05:33:38 2012 +0000
Fix the "Create a new work" link not displaying
diff --git a/root/static/scripts/relationship-editor/Dialog.js b/root/static/scripts/relationship-editor/Dialog.js
index a839928..f2d6c69 100644
--- a/root/static/scripts/relationship-editor/Dialog.js
+++ b/root/static/scripts/relationship-editor/Dialog.js
@@ -341,8 +341,7 @@ var Dialog = UI.Dialog = {
},
show: function(options) {
- var dlg = Dialog, relationship = dlg.relationship.peek(),
- notBatchWorks = dlg.mode.peek() != "batch.create.works";
+ var dlg = Dialog, notBatchWorks = dlg.mode.peek() != "batch.create.works";
dlg.source = options.source;
dlg.relationship(options.relationship);
@@ -353,7 +352,7 @@ var Dialog = UI.Dialog = {
dlg.instance(this);
dlg.showAutocomplete(notBatchWorks);
- dlg.showCreateWorkLink(relationship.type == "recording-work" && notBatchWorks);
+ dlg.showCreateWorkLink(options.relationship.type == "recording-work" && notBatchWorks);
dlg.$overlay.show();
// prevents the page from jumping. these will be adjusted in positionDialog.
commit 2b57f54d138f3e6d16bcaa8b207efaf213a0a573
Author: Michael Wiencek <[email protected]>
Date: Sat Oct 27 02:20:35 2012 +0000
Avoid re-rendering the dialog attrs
diff --git a/root/static/scripts/relationship-editor/Dialog.js b/root/static/scripts/relationship-editor/Dialog.js
index 1466f18..a839928 100644
--- a/root/static/scripts/relationship-editor/Dialog.js
+++ b/root/static/scripts/relationship-editor/Dialog.js
@@ -483,7 +483,7 @@ function positionDialog($dialog, posx, posy) {
Dialog.attrs = (function() {
var Attribute = function(relationship, attr, info) {
- this.value = relationship.attrs()[attr.name];
+ this.value = relationship.attrs.peek()[attr.name];
this.data = attr;
this.min = info[0];
this.max = info[1];
commit 90461ef717ee409ca872fccf44b68d815e794bc4
Merge: ab6011c 69906c8
Author: Michael Wiencek <[email protected]>
Date: Tue Oct 23 23:10:28 2012 +0000
Merge branch 'beta' of git://git.musicbrainz.org/musicbrainz-server into mbs-5389
commit ab6011c012d1277e9f2634cbd05f9693b2aa42d9
Author: Michael Wiencek <[email protected]>
Date: Tue Oct 23 23:07:25 2012 +0000
Correctly handle relating a recording on the release to another recording on the release (MBS-5389)
Re-works a lot of code so that relationships no longer have a "source" or "target."
Removes the knockout mapping plugin, which created more problems than it solved.
diff --git a/root/release/edit_relationships.tt b/root/release/edit_relationships.tt
index c2ffb6b..6bc1c1b 100644
--- a/root/release/edit_relationships.tt
+++ b/root/release/edit_relationships.tt
@@ -51,9 +51,10 @@
<tbody>
<!-- ko if: mode().match(/^batch/) == null -->
<tr>
- <td class="section" data-bind="text: MB.text.Entity[relationship().source.type] + ':'"></td>
+ <td class="section" data-bind="text: MB.text.Entity[sourceField()().type] + ':'"></td>
<td>
- <span data-bind="html: relationship().source.rendering"></span>
+ <span data-bind="html: sourceField()().rendering"></span>
+ <div class="error" data-bind="text: sourceField().error"></div>
</td>
</tr>
<!-- /ko -->
@@ -61,7 +62,7 @@
<td class="section">[% l('Type') %]:</td>
<td>
<select id="link-type" data-bind="linkType: relationship, value: relationship().link_type"></select>
- <!-- ko template: {"if": relationship().source.type == relationship().target().type, afterRender: resize} -->
+ <!-- ko template: {"if": sourceField()().type == targetField()().type, afterRender: resize} -->
<button data-bind="click: changeDirection, text: MB.text.ChangeDirection"></button>
<!-- /ko -->
(<a href="#" data-bind="click: toggleLinkTypeHelp">[% l('help') %]</a>)
@@ -73,7 +74,7 @@
<tr>
<td class="section">
<select id="target-type" data-bind="targetType: $data,
- disable: mode() == 'edit' || relationship().type() == 'recording-work'"></select>
+ disable: mode() == 'edit' || relationship().type == 'recording-work'"></select>
</td>
<td>
<span id="autocomplete" class="autocomplete" data-bind="autocomplete: relationship">
@@ -82,7 +83,7 @@
<input type="hidden" class="id"/>
<input type="hidden" class="gid"/>
</span>
- <div class="error" data-bind="text: relationship().target.error"></div>
+ <div class="error" data-bind="text: targetField().error"></div>
</td>
</tr>
<!-- /ko -->
@@ -102,13 +103,13 @@
<td id="batch-work-lang"></td>
</tr>
<!-- /ko -->
- <!-- ko template: {"if": attributes().length > 0, afterRender: resize} -->
+ <!-- ko template: {"if": attrs().length > 0, afterRender: resize} -->
<tr>
<td class="section">
[% l('Attributes:') %]<br/>
(<a href="#" data-bind="click: toggleAttributesHelp">[% l('help') %]</a>)
</td>
- <td data-bind="foreach: attributes">
+ <td data-bind="foreach: attrs">
<div>
<label>
<!-- ko if: type == 'boolean' -->
@@ -120,7 +121,7 @@
<select data-bind="selectAttribute: $data"></select>
<!-- /ko -->
</label>
- <div class="error" data-bind="text: $root.relationship.peek().attributes.peek()[$data.data.name].error"></div>
+ <div class="error" data-bind="text: value.error"></div>
<div class="ar-descr" data-bind="html: data.descr, visible: $root.showAttributesHelp"></div>
</div>
</td>
@@ -128,7 +129,7 @@
<!-- /ko -->
<tr>
<td class="section">[% l('Begin date:') %]</td>
- <td data-bind="with: relationship">
+ <td data-bind="with: relationship().period">
<input data-bind="value: begin_date().year, valueUpdate: 'afterkeydown'" maxlength="4" placeholder="YYYY" size="4"/>-[%-
-%]<input data-bind="value: begin_date().month, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="MM" size="2"/>-[%-
-%]<input data-bind="value: begin_date().day, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="DD" size="2"/>
@@ -137,7 +138,7 @@
</tr>
<tr>
<td class="section">[% l('End date:') %]</td>
- <td data-bind="with: relationship">
+ <td data-bind="with: relationship().period">
<input data-bind="value: end_date().year, valueUpdate: 'afterkeydown'" maxlength="4" placeholder="YYYY" size="4"/>-[%-
-%]<input data-bind="value: end_date().month, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="MM" size="2"/>-[%-
-%]<input data-bind="value: end_date().day, valueUpdate: 'afterkeydown'" maxlength="2" placeholder="DD" size="2"/>
@@ -145,7 +146,7 @@
</td>
</tr>
<tr>
- <td colspan="2" data-bind="with: relationship">
+ <td colspan="2" data-bind="with: relationship().period">
<label>
<input type="checkbox" data-bind="checked: ended"/>
[% l('This relationship has ended.') %]
@@ -276,17 +277,17 @@
</script>
<script type="text/html" id="template.link.phrase"><span class="link-phrase"
- data-bind="text: linkPhrase, css: {
+ data-bind="text: linkPhrase($parents[1]), css: {
'error-field': hasErrors,
'rel-add': action() == 'add',
'rel-edit': action() == 'edit',
'rel-remove': action() == 'remove'}"></span></script>
<script type="text/html" id="template.relationship">
- <div class="ar" data-bind="with: $data">
+ <div class="ar">
<span class="remove-button">×</span>
<!-- ko template: {name: 'template.link.phrase', data: $data} --><!-- /ko -->:
- <span class="entity" data-bind="html: target().rendering, css: {'rel-edit': edits_pending}"></span>
+ <span class="entity" data-bind="html: target($parents[0]).rendering, css: {'rel-edit': edits_pending}"></span>
<!-- ko if: dateRendering -->
(<!-- ko text: dateRendering --><!-- /ko -->)
<!-- /ko -->
@@ -295,8 +296,8 @@
<script type="text/html" id="template.relationship.performance">
<div class="ar">
- <!-- ko with: target --><input type="checkbox"/><!-- /ko -->
- <span class="entity" data-bind="html: target().rendering, css: {'rel-edit': edits_pending}"></span>
+ <!-- ko with: entity[1] --><input type="checkbox"/><!-- /ko -->
+ <span class="entity" data-bind="html: entity[1]().rendering, css: {'rel-edit': edits_pending}"></span>
(<!-- ko template: {name: 'template.link.phrase', data: $data} --><!-- /ko -->)
<!-- ko if: dateRendering -->
(<!-- ko text: dateRendering --><!-- /ko -->)
@@ -306,7 +307,7 @@
<div data-bind="html: $root.RE.UI.loadingIndicator"></div>
<!-- /ko -->
<!-- ko if: !loadingWork() -->
- <!-- ko template: {name: 'template.relationships', data: target} --><!-- /ko -->
+ <!-- ko template: {name: 'template.relationships', data: entity[1]} --><!-- /ko -->
<!-- /ko -->
<!-- /ko -->
</div>
diff --git a/root/static/lib/knockout/knockout.mapping-latest.debug.js b/root/static/lib/knockout/knockout.mapping-latest.debug.js
deleted file mode 100644
index 4bee64e..0000000
--- a/root/static/lib/knockout/knockout.mapping-latest.debug.js
+++ /dev/null
@@ -1,716 +0,0 @@
-// Knockout Mapping plugin v2.2.3
-// (c) 2012 Steven Sanderson, Roy Jacobs - http://knockoutjs.com/
-// License: MIT (http://www.opensource.org/licenses/mit-license.php)
-
-(function (factory) {
- // Module systems magic dance.
-
- if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
- // CommonJS or Node: hard-coded dependency on "knockout"
- factory(require("knockout"), exports);
- } else if (typeof define === "function" && define["amd"]) {
- // AMD anonymous module with hard-coded dependency on "knockout"
- define(["knockout", "exports"], factory);
- } else {
- // <script> tag: use the global `ko` object, attaching a `mapping` property
- factory(ko, ko.mapping = {});
- }
-}(function (ko, exports) {
- var DEBUG=true;
- var mappingProperty = "__ko_mapping__";
- var realKoDependentObservable = ko.dependentObservable;
- var mappingNesting = 0;
- var dependentObservables;
- var visitedObjects;
-
- var _defaultOptions = {
- include: ["_destroy"],
- ignore: [],
- copy: []
- };
- var defaultOptions = _defaultOptions;
-
- function extendObject(destination, source) {
- for (var key in source) {
- if (source.hasOwnProperty(key) && source[key]) {
- if (key && destination[key] && !(exports.getType(destination[key]) === "array")) {
- extendObject(destination[key], source[key]);
- } else {
- destination[key] = source[key];
- }
- }
- }
- }
-
- function merge(obj1, obj2) {
- var merged = {};
- extendObject(merged, obj1);
- extendObject(merged, obj2);
-
- return merged;
- }
-
- exports.isMapped = function (viewModel) {
- var unwrapped = ko.utils.unwrapObservable(viewModel);
- return unwrapped && unwrapped[mappingProperty];
- }
-
- exports.fromJS = function (jsObject /*, inputOptions, target*/ ) {
- if (arguments.length == 0) throw new Error("When calling ko.fromJS, pass the object you want to convert.");
-
- // When mapping is completed, even with an exception, reset the nesting level
- window.setTimeout(function () {
- mappingNesting = 0;
- }, 0);
-
- if (!mappingNesting++) {
- dependentObservables = [];
- visitedObjects = new objectLookup();
- }
-
- var options;
- var target;
-
- if (arguments.length == 2) {
- if (arguments[1][mappingProperty]) {
- target = arguments[1];
- } else {
- options = arguments[1];
- }
- }
- if (arguments.length == 3) {
- options = arguments[1];
- target = arguments[2];
- }
-
- if (target) {
- options = merge(options, target[mappingProperty]);
- }
- options = fillOptions(options);
-
- var result = updateViewModel(target, jsObject, options);
- if (target) {
- result = target;
- }
-
- // Evaluate any dependent observables that were proxied.
- // Do this in a timeout to defer execution. Basically, any user code that explicitly looks up the DO will perform the first evaluation. Otherwise,
- // it will be done by this code.
- if (!--mappingNesting) {
- window.setTimeout(function () {
- while (dependentObservables.length) {
- var DO = dependentObservables.pop();
- if (DO) DO();
- }
- }, 0);
- }
-
- // Save any new mapping options in the view model, so that updateFromJS can use them later.
- result[mappingProperty] = merge(result[mappingProperty], options);
-
- return result;
- };
-
- exports.fromJSON = function (jsonString /*, options, target*/ ) {
- var parsed = ko.utils.parseJson(jsonString);
- arguments[0] = parsed;
- return exports.fromJS.apply(this, arguments);
- };
-
- exports.updateFromJS = function (viewModel) {
- throw new Error("ko.mapping.updateFromJS, use ko.mapping.fromJS instead. Please note that the order of parameters is different!");
- };
-
- exports.updateFromJSON = function (viewModel) {
- throw new Error("ko.mapping.updateFromJSON, use ko.mapping.fromJSON instead. Please note that the order of parameters is different!");
- };
-
- exports.toJS = function (rootObject, options) {
- if (!defaultOptions) exports.resetDefaultOptions();
-
- if (arguments.length == 0) throw new Error("When calling ko.mapping.toJS, pass the object you want to convert.");
- if (exports.getType(defaultOptions.ignore) !== "array") throw new Error("ko.mapping.defaultOptions().ignore should be an array.");
- if (exports.getType(defaultOptions.include) !== "array") throw new Error("ko.mapping.defaultOptions().include should be an array.");
- if (exports.getType(defaultOptions.copy) !== "array") throw new Error("ko.mapping.defaultOptions().copy should be an array.");
-
- // Merge in the options used in fromJS
- options = fillOptions(options, rootObject[mappingProperty]);
-
- // We just unwrap everything at every level in the object graph
- return exports.visitModel(rootObject, function (x) {
- return ko.utils.unwrapObservable(x)
- }, options);
- };
-
- exports.toJSON = function (rootObject, options) {
- var plainJavaScriptObject = exports.toJS(rootObject, options);
- return ko.utils.stringifyJson(plainJavaScriptObject);
- };
-
- exports.defaultOptions = function () {
- if (arguments.length > 0) {
- defaultOptions = arguments[0];
- } else {
- return defaultOptions;
- }
- };
-
- exports.resetDefaultOptions = function () {
- defaultOptions = {
- include: _defaultOptions.include.slice(0),
- ignore: _defaultOptions.ignore.slice(0),
- copy: _defaultOptions.copy.slice(0)
- };
- };
-
- exports.getType = function(x) {
- if ((x) && (typeof (x) === "object")) {
- if (x.constructor == (new Date).constructor) return "date";
- if (Object.prototype.toString.call(x) === "[object Array]") return "array";
- }
- return typeof x;
- }
-
- function fillOptions(options, otherOptions) {
- options = options || {};
-
- // Is there only a root-level mapping present?
- if ((options.create instanceof Function) || (options.update instanceof Function) || (options.key instanceof Function) || (options.arrayChanged instanceof Function)) {
- options = {
- "": options
- };
- }
-
- if (otherOptions) {
- options.ignore = mergeArrays(otherOptions.ignore, options.ignore);
- options.include = mergeArrays(otherOptions.include, options.include);
- options.copy = mergeArrays(otherOptions.copy, options.copy);
- }
- options.ignore = mergeArrays(options.ignore, defaultOptions.ignore);
- options.include = mergeArrays(options.include, defaultOptions.include);
- options.copy = mergeArrays(options.copy, defaultOptions.copy);
-
- options.mappedProperties = options.mappedProperties || {};
- return options;
- }
-
- function mergeArrays(a, b) {
- if (exports.getType(a) !== "array") {
- if (exports.getType(a) === "undefined") a = [];
- else a = [a];
- }
- if (exports.getType(b) !== "array") {
- if (exports.getType(b) === "undefined") b = [];
- else b = [b];
- }
-
- return ko.utils.arrayGetDistinctValues(a.concat(b));
- }
-
- // When using a 'create' callback, we proxy the dependent observable so that it doesn't immediately evaluate on creation.
- // The reason is that the dependent observables in the user-specified callback may contain references to properties that have not been mapped yet.
- function withProxyDependentObservable(dependentObservables, callback) {
- var localDO = ko.dependentObservable;
- ko.dependentObservable = function (read, owner, options) {
- options = options || {};
-
- if (read && typeof read == "object") { // mirrors condition in knockout implementation of DO's
- options = read;
- }
-
- var realDeferEvaluation = options.deferEvaluation;
-
- var isRemoved = false;
-
- // We wrap the original dependent observable so that we can remove it from the 'dependentObservables' list we need to evaluate after mapping has
- // completed if the user already evaluated the DO themselves in the meantime.
- var wrap = function (DO) {
- var wrapped = realKoDependentObservable({
- read: function () {
- if (!isRemoved) {
- ko.utils.arrayRemoveItem(dependentObservables, DO);
- isRemoved = true;
- }
- return DO.apply(DO, arguments);
- },
- write: function (val) {
- return DO(val);
- },
- deferEvaluation: true
- });
- if(DEBUG) wrapped._wrapper = true;
- return wrapped;
- };
-
- options.deferEvaluation = true; // will either set for just options, or both read/options.
- var realDependentObservable = new realKoDependentObservable(read, owner, options);
-
- if (!realDeferEvaluation) {
- realDependentObservable = wrap(realDependentObservable);
- dependentObservables.push(realDependentObservable);
- }
-
- return realDependentObservable;
- }
- ko.dependentObservable.fn = realKoDependentObservable.fn;
- ko.computed = ko.dependentObservable;
- var result = callback();
- ko.dependentObservable = localDO;
- ko.computed = ko.dependentObservable;
- return result;
- }
-
- function updateViewModel(mappedRootObject, rootObject, options, parentName, parent, parentPropertyName) {
- var isArray = exports.getType(ko.utils.unwrapObservable(rootObject)) === "array";
-
- parentPropertyName = parentPropertyName || "";
-
- // If this object was already mapped previously, take the options from there and merge them with our existing ones.
- if (exports.isMapped(mappedRootObject)) {
- var previousMapping = ko.utils.unwrapObservable(mappedRootObject)[mappingProperty];
- options = merge(previousMapping, options);
- }
-
- var callbackParams = {
- data: rootObject,
- parent: parent
- };
-
- var hasCreateCallback = function () {
- return options[parentName] && options[parentName].create instanceof Function;
- };
-
- var createCallback = function (data) {
- return withProxyDependentObservable(dependentObservables, function () {
- return options[parentName].create({
- data: data || callbackParams.data,
- parent: callbackParams.parent
- });
- });
- };
-
- var hasUpdateCallback = function () {
- return options[parentName] && options[parentName].update instanceof Function;
- };
-
- var updateCallback = function (obj, data) {
- var params = {
- data: data || callbackParams.data,
- parent: callbackParams.parent,
- target: ko.utils.unwrapObservable(obj)
- };
-
- if (ko.isWriteableObservable(obj)) {
- params.observable = obj;
- }
-
- return options[parentName].update(params);
- }
-
- var alreadyMapped = visitedObjects.get(rootObject);
- if (alreadyMapped) {
- return alreadyMapped;
- }
-
- parentName = parentName || "";
-
- if (!isArray) {
- // For atomic types, do a direct update on the observable
- if (!canHaveProperties(rootObject)) {
- switch (exports.getType(rootObject)) {
- case "function":
- if (hasUpdateCallback()) {
- if (ko.isWriteableObservable(rootObject)) {
- rootObject(updateCallback(rootObject));
- mappedRootObject = rootObject;
- } else {
- mappedRootObject = updateCallback(rootObject);
- }
- } else {
- mappedRootObject = rootObject;
- }
- break;
- default:
- if (ko.isWriteableObservable(mappedRootObject)) {
- if (hasUpdateCallback()) {
- mappedRootObject(updateCallback(mappedRootObject));
- } else {
- mappedRootObject(ko.utils.unwrapObservable(rootObject));
- }
- } else {
- if (hasCreateCallback()) {
- mappedRootObject = createCallback();
- } else {
- mappedRootObject = ko.observable(ko.utils.unwrapObservable(rootObject));
- }
-
- if (hasUpdateCallback()) {
- mappedRootObject(updateCallback(mappedRootObject));
- }
- }
- break;
- }
-
- } else {
- mappedRootObject = ko.utils.unwrapObservable(mappedRootObject);
- if (!mappedRootObject) {
- if (hasCreateCallback()) {
- var result = createCallback();
-
- if (hasUpdateCallback()) {
- result = updateCallback(result);
- }
-
- return result;
- } else {
- if (hasUpdateCallback()) {
- return updateCallback(result);
- }
-
- mappedRootObject = {};
- }
- }
-
- if (hasUpdateCallback()) {
- mappedRootObject = updateCallback(mappedRootObject);
- }
-
- visitedObjects.save(rootObject, mappedRootObject);
-
- // For non-atomic types, visit all properties and update recursively
- visitPropertiesOrArrayEntries(rootObject, function (indexer) {
- var fullPropertyName = parentPropertyName.length ? parentPropertyName + "." + indexer : indexer;
-
- if (ko.utils.arrayIndexOf(options.ignore, fullPropertyName) != -1) {
- return;
- }
-
- if (ko.utils.arrayIndexOf(options.copy, fullPropertyName) != -1) {
- mappedRootObject[indexer] = rootObject[indexer];
- return;
- }
-
- // In case we are adding an already mapped property, fill it with the previously mapped property value to prevent recursion.
- // If this is a property that was generated by fromJS, we should use the options specified there
- var prevMappedProperty = visitedObjects.get(rootObject[indexer]);
- var value = prevMappedProperty || updateViewModel(mappedRootObject[indexer], rootObject[indexer], options, indexer, mappedRootObject, fullPropertyName);
-
- if (ko.isWriteableObservable(mappedRootObject[indexer])) {
- mappedRootObject[indexer](ko.utils.unwrapObservable(value));
- } else {
- mappedRootObject[indexer] = value;
- }
-
- options.mappedProperties[fullPropertyName] = true;
- });
- }
- } else {
- var changes = [];
-
- var hasKeyCallback = false;
- var keyCallback = function (x) {
- return x;
- }
- if (options[parentName] && options[parentName].key) {
- keyCallback = options[parentName].key;
- hasKeyCallback = true;
- }
-
- if (!ko.isObservable(mappedRootObject)) {
- // When creating the new observable array, also add a bunch of utility functions that take the 'key' of the array items into account.
- mappedRootObject = ko.observableArray([]);
-
- mappedRootObject.mappedRemove = function (valueOrPredicate) {
- var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) {
- return value === keyCallback(valueOrPredicate);
- };
- return mappedRootObject.remove(function (item) {
- return predicate(keyCallback(item));
- });
- }
-
- mappedRootObject.mappedRemoveAll = function (arrayOfValues) {
- var arrayOfKeys = filterArrayByKey(arrayOfValues, keyCallback);
- return mappedRootObject.remove(function (item) {
- return ko.utils.arrayIndexOf(arrayOfKeys, keyCallback(item)) != -1;
- });
- }
-
- mappedRootObject.mappedDestroy = function (valueOrPredicate) {
- var predicate = typeof valueOrPredicate == "function" ? valueOrPredicate : function (value) {
- return value === keyCallback(valueOrPredicate);
- };
- return mappedRootObject.destroy(function (item) {
- return predicate(keyCallback(item));
- });
- }
-
- mappedRootObject.mappedDestroyAll = function (arrayOfValues) {
- var arrayOfKeys = filterArrayByKey(arrayOfValues, keyCallback);
- return mappedRootObject.destroy(function (item) {
- return ko.utils.arrayIndexOf(arrayOfKeys, keyCallback(item)) != -1;
- });
- }
-
- mappedRootObject.mappedIndexOf = function (item) {
- var keys = filterArrayByKey(mappedRootObject(), keyCallback);
- var key = keyCallback(item);
- return ko.utils.arrayIndexOf(keys, key);
- }
-
- mappedRootObject.mappedCreate = function (value) {
- if (mappedRootObject.mappedIndexOf(value) !== -1) {
- throw new Error("There already is an object with the key that you specified.");
- }
-
- var item = hasCreateCallback() ? createCallback(value) : value;
- if (hasUpdateCallback()) {
- var newValue = updateCallback(item, value);
- if (ko.isWriteableObservable(item)) {
- item(newValue);
- } else {
- item = newValue;
- }
- }
- mappedRootObject.push(item);
- return item;
- }
- }
-
- var currentArrayKeys = filterArrayByKey(ko.utils.unwrapObservable(mappedRootObject), keyCallback).sort();
- var newArrayKeys = filterArrayByKey(rootObject, keyCallback);
- if (hasKeyCallback) newArrayKeys.sort();
- var editScript = ko.utils.compareArrays(currentArrayKeys, newArrayKeys);
-
- var ignoreIndexOf = {};
-
- var i, j;
-
- var unwrappedRootObject = ko.utils.unwrapObservable(rootObject);
- var itemsByKey = {};
- var optimizedKeys = true;
- for (i = 0, j = unwrappedRootObject.length; i < j; i++) {
- var key = keyCallback(unwrappedRootObject[i]);
- if (key === undefined || key instanceof Object) {
- optimizedKeys = false;
- break;
- }
- itemsByKey[key] = unwrappedRootObject[i];
- }
-
- var newContents = [];
- for (i = 0, j = editScript.length; i < j; i++) {
- var key = editScript[i];
- var mappedItem;
- var fullPropertyName = parentPropertyName + "[" + i + "]";
- switch (key.status) {
- case "added":
- var item = optimizedKeys ? itemsByKey[key.value] : getItemByKey(ko.utils.unwrapObservable(rootObject), key.value, keyCallback);
- mappedItem = updateViewModel(undefined, item, options, parentName, mappedRootObject, fullPropertyName);
- if(!hasCreateCallback()) {
- mappedItem = ko.utils.unwrapObservable(mappedItem);
- }
-
- var index = ignorableIndexOf(ko.utils.unwrapObservable(rootObject), item, ignoreIndexOf);
- newContents[index] = mappedItem;
- ignoreIndexOf[index] = true;
- break;
- case "retained":
- var item = optimizedKeys ? itemsByKey[key.value] : getItemByKey(ko.utils.unwrapObservable(rootObject), key.value, keyCallback);
- mappedItem = getItemByKey(mappedRootObject, key.value, keyCallback);
- updateViewModel(mappedItem, item, options, parentName, mappedRootObject, fullPropertyName);
-
- var index = ignorableIndexOf(ko.utils.unwrapObservable(rootObject), item, ignoreIndexOf);
- newContents[index] = mappedItem;
- ignoreIndexOf[index] = true;
- break;
- case "deleted":
- mappedItem = getItemByKey(mappedRootObject, key.value, keyCallback);
- break;
- }
-
- changes.push({
- event: key.status,
- item: mappedItem
- });
- }
-
- mappedRootObject(newContents);
-
- if (options[parentName] && options[parentName].arrayChanged) {
- ko.utils.arrayForEach(changes, function (change) {
- options[parentName].arrayChanged(change.event, change.item);
- });
- }
- }
-
- return mappedRootObject;
- }
-
- function ignorableIndexOf(array, item, ignoreIndices) {
- for (var i = 0, j = array.length; i < j; i++) {
- if (ignoreIndices[i] === true) continue;
- if (array[i] === item) return i;
- }
- return null;
- }
-
- function mapKey(item, callback) {
- var mappedItem;
- if (callback) mappedItem = callback(item);
- if (exports.getType(mappedItem) === "undefined") mappedItem = item;
-
- return ko.utils.unwrapObservable(mappedItem);
- }
-
- function getItemByKey(array, key, callback) {
- array = ko.utils.unwrapObservable(array);
- for (var i = 0, j = array.length; i < j; i++) {
- var item = array[i];
- if (mapKey(item, callback) === key) return item;
- }
-
- throw new Error("When calling ko.update*, the key '" + key + "' was not found!");
- }
-
- function filterArrayByKey(array, callback) {
- return ko.utils.arrayMap(ko.utils.unwrapObservable(array), function (item) {
- if (callback) {
- return mapKey(item, callback);
- } else {
- return item;
- }
- });
- }
-
- function visitPropertiesOrArrayEntries(rootObject, visitorCallback) {
- if (exports.getType(rootObject) === "array") {
- for (var i = 0; i < rootObject.length; i++)
- visitorCallback(i);
- } else {
- for (var propertyName in rootObject)
- visitorCallback(propertyName);
- }
- };
-
- function canHaveProperties(object) {
- var type = exports.getType(object);
- return ((type === "object") || (type === "array")) && (object !== null);
- }
-
- // Based on the parentName, this creates a fully classified name of a property
-
- function getPropertyName(parentName, parent, indexer) {
- var propertyName = parentName || "";
- if (exports.getType(parent) === "array") {
- if (parentName) {
- propertyName += "[" + indexer + "]";
- }
- } else {
- if (parentName) {
- propertyName += ".";
- }
- propertyName += indexer;
- }
- return propertyName;
- }
-
- exports.visitModel = function (rootObject, callback, options) {
- options = options || {};
- options.visitedObjects = options.visitedObjects || new objectLookup();
-
- var mappedRootObject;
- var unwrappedRootObject = ko.utils.unwrapObservable(rootObject);
-
- if (!canHaveProperties(unwrappedRootObject)) {
- return callback(rootObject, options.parentName);
- } else {
- options = fillOptions(options, unwrappedRootObject[mappingProperty]);
-
- // Only do a callback, but ignore the results
- callback(rootObject, options.parentName);
- mappedRootObject = exports.getType(unwrappedRootObject) === "array" ? [] : {};
- }
-
- options.visitedObjects.save(rootObject, mappedRootObject);
-
- var parentName = options.parentName;
- visitPropertiesOrArrayEntries(unwrappedRootObject, function (indexer) {
- if (options.ignore && ko.utils.arrayIndexOf(options.ignore, indexer) != -1) return;
-
- var propertyValue = unwrappedRootObject[indexer];
- options.parentName = getPropertyName(parentName, unwrappedRootObject, indexer);
-
- // If we don't want to explicitly copy the unmapped property...
- if (ko.utils.arrayIndexOf(options.copy, indexer) === -1) {
- // ...find out if it's a property we want to explicitly include
- if (ko.utils.arrayIndexOf(options.include, indexer) === -1) {
- // The mapped properties object contains all the properties that were part of the original object.
- // If a property does not exist, and it is not because it is part of an array (e.g. "myProp[3]"), then it should not be unmapped.
- if (unwrappedRootObject[mappingProperty] && unwrappedRootObject[mappingProperty].mappedProperties && !unwrappedRootObject[mappingProperty].mappedProperties[indexer] && !(exports.getType(unwrappedRootObject) === "array")) {
- return;
- }
- }
- }
-
- var outputProperty;
- switch (exports.getType(ko.utils.unwrapObservable(propertyValue))) {
- case "object":
- case "array":
- case "undefined":
- var previouslyMappedValue = options.visitedObjects.get(propertyValue);
- mappedRootObject[indexer] = (exports.getType(previouslyMappedValue) !== "undefined") ? previouslyMappedValue : exports.visitModel(propertyValue, callback);
- break;
- default:
- mappedRootObject[indexer] = callback(propertyValue, options.parentName);
- }
- });
-
- return mappedRootObject;
- }
-
- function simpleObjectLookup() {
- var keys = [];
- var values = [];
- this.save = function (key, value) {
- var existingIndex = ko.utils.arrayIndexOf(keys, key);
- if (existingIndex >= 0) values[existingIndex] = value;
- else {
- keys.push(key);
- values.push(value);
- }
- };
- this.get = function (key) {
- var existingIndex = ko.utils.arrayIndexOf(keys, key);
- var value = (existingIndex >= 0) ? values[existingIndex] : undefined;
- return value;
- };
- };
-
- function objectLookup() {
- var buckets = {};
-
- var findBucket = function(key) {
- var bucketKey;
- try {
- bucketKey = JSON.stringify(key);
- }
- catch (e) {
- bucketKey = "$$$";
- }
- var bucket = buckets[bucketKey];
- if (bucket === undefined) {
- bucket = new simpleObjectLookup();
- buckets[bucketKey] = bucket;
- }
- return bucket;
- };
-
- this.save = function (key, value) {
- findBucket(key).save(key, value);
- };
- this.get = function (key) {
- return findBucket(key).get(key);
- };
- };
-}));
diff --git a/root/static/lib/libraries.txt b/root/static/lib/libraries.txt
index 4a89c8d..9e99edf 100644
--- a/root/static/lib/libraries.txt
+++ b/root/static/lib/libraries.txt
@@ -98,14 +98,6 @@ version: GIT, 2012-07-23
author: Steven Sanderson
url: http://knockoutjs.com
--- knockout/knockout.mapping-latest.debug.js --
-
-library: Knockout mapping
-license: MIT
-version: v2.2.3
-author: Steven Sanderson, Roy Jacobs
-url: http://knockoutjs.com/documentation/plugins-mapping.html
-
-- underscore/underscore.js --
library: Underscore
diff --git a/root/static/relationship-editor.js.manifest b/root/static/relationship-editor.js.manifest
index 054ec77..af25747 100644
--- a/root/static/relationship-editor.js.manifest
+++ b/root/static/relationship-editor.js.manifest
@@ -1,4 +1,3 @@
lib/json/
lib/knockout/knockout-latest.debug.js
-lib/knockout/knockout.mapping-latest.debug.js
scripts/relationship-editor/
diff --git a/root/static/scripts/relationship-editor/Dialog.js b/root/static/scripts/relationship-editor/Dialog.js
index 3868364..1466f18 100644
--- a/root/static/scripts/relationship-editor/Dialog.js
+++ b/root/static/scripts/relationship-editor/Dialog.js
@@ -21,6 +21,13 @@ MB.RelationshipEditor = (function(RE) {
var UI = RE.UI = RE.UI || {}, Util = RE.Util = RE.Util || {}, $w = $(window);
+var allowedRelations = {
+ recording: ["artist", "label", "recording", "release"],
+ work: ["artist", "label", "work"],
+ release: ["artist", "label", "recording", "release"],
+ release_group: ["artist", "release_group"]
+};
+
// For select attributes and the link type field, we use a custom binding handler
// for performance reasons (the instrument tree is huge, for example). We also
// need to support the unaccented instrument names. the builtin options binding
@@ -46,7 +53,7 @@ ko.bindingHandlers.selectAttribute = (function() {
var getOptions = _.memoize(function(attr) {
var doc = document.createDocumentFragment();
- build(attr.relationship.attributes(), attr.data, 0, doc);
+ build(Dialog.relationship().attrs(), attr.data, 0, doc);
return doc;
}, function(attr) {return attr.data.name});
@@ -79,7 +86,7 @@ ko.bindingHandlers.selectAttribute = (function() {
ko.bindingHandlers.linkType = (function() {
- var previousType, previousDirection;
+ var previousType, previousDirection, getOptions;
function build(root, indent, backward, doc) {
var phrase = backward ? root.reverse_phrase : root.phrase;
@@ -104,7 +111,7 @@ ko.bindingHandlers.linkType = (function() {
});
};
- var getOptions = _.memoize(function(type, backward) {
+ getOptions = _.memoize(function(type, backward) {
var doc = document.createDocumentFragment();
$.each(Util.typeInfoByEntities(type), function(i, root) {
@@ -115,11 +122,11 @@ ko.bindingHandlers.linkType = (function() {
return {
update: function(element) {
- var relationship = Dialog.relationship(), type = relationship.type(),
- backward = relationship.backward();
+ var relationship = Dialog.relationship(), type = relationship.type,
+ backward = Dialog.backward();
if (type != previousType || backward != previousDirection) {
- var doc = getOptions(relationship.type(), backward).cloneNode(true);
+ var doc = getOptions(type, backward).cloneNode(true);
$(element).empty().append(doc).val(relationship.link_type());
Dialog.resize();
@@ -134,23 +141,27 @@ ko.bindingHandlers.linkType = (function() {
ko.bindingHandlers.targetType = (function() {
- var allowedRelations = {
- recording: ["artist", "label", "recording", "release"],
- work: ["artist", "label", "work"],
- release: ["artist", "label", "recording", "release"],
- release_group: ["artist", "release_group"]
- };
-
function change() {
var mode = Dialog.mode();
if (!(mode == "add" || /^batch\.(recording|work)$/.test(mode))) return;
- var relationship = Dialog.relationship.peek(), ac = Dialog.autocomplete,
- name = relationship.target().name(), target = Util.tempEntity(this.value);
+ var ac = Dialog.autocomplete, relationship = Dialog.relationship.peek(),
+ newTarget = RE.Entity({type: this.value, name: Dialog.target.name}),
+ obj = relationship.toJS();
+
+ obj.entity[Dialog.target.gid == obj.entity[0].gid ? 0 : 1] = newTarget;
- // reset the current target entity
- target.name(name);
- relationship.target(target);
+ // detect when the entity order needs to be reversed.
+ // e.g. switching from artist-recording to recording-release.
+
+ var types = [obj.entity[0].type, obj.entity[1].type],
+ type = types.join("-"), reverseType = types.reverse().join("-");
+
+ if (!Util.typeInfoByEntities(type) && Util.typeInfoByEntities(reverseType))
+ obj.entity.reverse();
+
+ Dialog.relationship(RE.Relationship(obj));
+ relationship.remove();
if (ac) {
ac.clear();
@@ -162,14 +173,14 @@ ko.bindingHandlers.targetType = (function() {
return {
init: function(element) {
var $element = $(element).change(change), relationship = Dialog.relationship(),
- types = (relationship.type.peek() == "recording-work")
- ? ["work"] : allowedRelations[relationship.source.type];
+ types = (relationship.type == "recording-work")
+ ? ["work"] : allowedRelations[Dialog.source.type];
$element.empty();
$.each(types, function(i, type) {
$element.append($("<option></option>").val(type).text(MB.text.Entity[type]));
});
- $element.val(relationship.target.peek().type);
+ $element.val(Dialog.target.type);
}
};
}());
@@ -177,11 +188,11 @@ ko.bindingHandlers.targetType = (function() {
function setAutocompleteEntity(entity, nameOnly) {
var $ac = Dialog.$autocomplete, ac = Dialog.autocomplete,
- $name = $ac.find("input.name"), name = entity.name.peek();
+ $name = $ac.find("input.name");
- ac.term = name;
+ ac.term = entity.name;
ac.selectedItem = null;
- $name.removeClass("error lookup-performed").val(name);
+ $name.removeClass("error lookup-performed").val(entity.name);
if (nameOnly === false) {
ac.currentSelection = null;
@@ -205,16 +216,23 @@ ko.bindingHandlers.autocomplete = (function() {
}
function changeTarget(event, data) {
- var target = Dialog.relationship.peek().target, recent, dup;
- data.type = target.peek().type;
+ // XXX release groups' numeric "type" conflicts with the entity type
+ data.type = _.isNumber(data.type) ? "release_group" : (data.type || Dialog.target.type);
+
+ if (allowedRelations[Dialog.source.type].indexOf(data.type) == -1 &&
+ !(Dialog.source.type == "recording" && data.type == "work")) {
+ Dialog.autocomplete.clear();
+ return;
+ }
// Add/move to the top of the recent entities menu.
- recent = recentEntities[data.type] = recentEntities[data.type] || [];
- dup = _.where(recent, {gid: data.gid})[0];
+ var recent = recentEntities[data.type] = recentEntities[data.type] || [],
+ dup = _.where(recent, {gid: data.gid})[0];
+
dup && recent.splice(recent.indexOf(dup), 1);
recent.unshift(data);
- target(RE.Entity(data));
+ Dialog.targetField.peek()(RE.Entity(data));
}
function showRecentEntities(event) {
@@ -222,7 +240,7 @@ ko.bindingHandlers.autocomplete = (function() {
(event.type == "keyup" && !_.contains([8, 40], event.keyCode)))
return;
- var recent = recentEntities[Dialog.relationship.peek().target.peek().type],
+ var recent = recentEntities[Dialog.target.type],
ac = Dialog.autocomplete.autocomplete;
if (!this.value && recent && recent.length && !ac.menu.active) {
@@ -235,13 +253,12 @@ ko.bindingHandlers.autocomplete = (function() {
return {
init: function(element) {
- var $autocomplete = Dialog.$autocomplete = $(element),
- target = Dialog.relationship.peek().target.peek();
+ var $autocomplete = Dialog.$autocomplete = $(element);
Dialog.autocomplete = MB.Control.EntityAutocomplete({
inputs: $autocomplete,
position: {collision: "fit"},
- entity: target.type,
+ entity: Dialog.target.type,
setEntity: setEntity
});
@@ -251,7 +268,7 @@ ko.bindingHandlers.autocomplete = (function() {
.on("keyup focus click", showRecentEntities)
.on("keydown", closeOnEnter);
- setAutocompleteEntity(target, Dialog.mode() != "edit");
+ setAutocompleteEntity(Dialog.target, Dialog.mode() != "edit");
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$autocomplete.autocomplete("destroy");
@@ -266,6 +283,7 @@ var Dialog = UI.Dialog = {
mode: ko.observable(""),
loading: ko.observable(false),
batchWorksError: ko.observable(false),
+ backward: ko.observable(false),
showAutocomplete: ko.observable(false),
showCreateWorkLink: ko.observable(false),
@@ -279,28 +297,41 @@ var Dialog = UI.Dialog = {
read: value,
write: function(newValue) {
var oldValue = value();
-
- if (oldValue !== newValue) {
- // if we cancelled an add dialog, the temporary relationship
- // must be deleted.
- if (oldValue && !oldValue.visible) oldValue.remove();
-
- value(newValue);
- }
+ if (oldValue !== newValue) value(newValue);
}
});
}()),
init: function() {
+ var entity = [RE.Entity({type: "artist"}), RE.Entity({type: "recording"})];
+
// this is used as an "empty" state when the dialog is hidden, so that
// none of the bindings error out.
- this.emptyRelationship = RE.Relationship({
- source: Util.tempEntity("recording"),
- target: Util.tempEntity("artist"),
- backward: true
- }, false);
+ this.emptyRelationship = RE.Relationship({entity: entity});
+ this.backward(true);
this.relationship(this.emptyRelationship);
+ this.sourceField = ko.observable(null);
+ this.targetField = ko.observable(null);
+ this.source = entity[1];
+
+ ko.computed(function() {
+ var relationship = Dialog.relationship(),
+ entity0 = relationship.entity[0],
+ entity1 = relationship.entity[1],
+ backward = (Dialog.source === entity1());
+
+ if (backward) {
+ Dialog.sourceField(entity1);
+ Dialog.targetField(entity0);
+ } else {
+ Dialog.sourceField(entity0);
+ Dialog.targetField(entity1);
+ }
+
+ Dialog.target = Dialog.targetField.peek()();
+ Dialog.backward(backward);
+ });
this.$overlay = $("#overlay");
this.$dialog = $("#dialog");
@@ -309,27 +340,26 @@ var Dialog = UI.Dialog = {
ko.applyBindings(this, this.$dialog[0]);
},
- show: function(posx, posy) {
- var dlg = Dialog;
+ show: function(options) {
+ var dlg = Dialog, relationship = dlg.relationship.peek(),
+ notBatchWorks = dlg.mode.peek() != "batch.create.works";
+
+ dlg.source = options.source;
+ dlg.relationship(options.relationship);
- dlg.posx = posx;
- dlg.posy = posy;
// important: objects down the prototype chain should set "this" when
// calling show. the template uses instance to decide which accept and
// hide methods to execute.
dlg.instance(this);
- var relationship = dlg.relationship.peek(),
- notBatchWorks = dlg.mode.peek() != "batch.create.works";
-
- dlg.showAutocomplete(relationship.target().type != "url" && notBatchWorks);
- dlg.showCreateWorkLink(relationship.type() == "recording-work" && notBatchWorks);
+ dlg.showAutocomplete(notBatchWorks);
+ dlg.showCreateWorkLink(relationship.type == "recording-work" && notBatchWorks);
dlg.$overlay.show();
// prevents the page from jumping. these will be adjusted in positionDialog.
dlg.$dialog.css({top: $w.scrollTop(), left: $w.scrollLeft()}).show();
- positionDialog(dlg.$dialog, posx, posy);
+ positionDialog(dlg.$dialog, options.posx, options.posy);
$("#link-type").focus();
},
@@ -344,6 +374,7 @@ var Dialog = UI.Dialog = {
if ($.isFunction(callback)) callback.call(dlg);
dlg.showAutocomplete(false);
+ dlg.source = dlg.emptyRelationship.entity[1].peek();
dlg.relationship(dlg.emptyRelationship);
},
@@ -354,11 +385,11 @@ var Dialog = UI.Dialog = {
WorkDialog.show(function(work) {
var target = RE.Entity(work, "work");
setAutocompleteEntity(target, false);
- Dialog.relationship.peek().target(target);
+ Dialog.targetField.peek()(target);
}, event.pageX, event.pageY);
- WorkDialog.name(Dialog.relationship.peek().source.name.peek());
+ WorkDialog.name(Dialog.source.name);
$("#work-name").focus();
},
@@ -386,19 +417,18 @@ var Dialog = UI.Dialog = {
}),
changeDirection: function() {
- var backward = this.relationship().backward;
- backward(!backward());
+ var relationship = this.relationship.peek(),
+ entity0 = relationship.entity[0].peek(),
+ entity1 = relationship.entity[1].peek();
+
+ relationship.entity[0](entity1);
+ relationship.entity[1](entity0);
this.resize();
},
toggleLinkTypeHelp: function() {
- var newValue = !this.showLinkTypeHelp.peek();
- this.showLinkTypeHelp(newValue);
-
- if (newValue)
- _.defer(function() {
- $("#link-type").parent().find("div.ar-descr a").attr("target", "_blank");
- });
+ this.showLinkTypeHelp(!this.showLinkTypeHelp.peek());
+ $("#link-type").parent().find("div.ar-descr a").attr("target", "_blank");
},
resize: function() {
@@ -418,7 +448,7 @@ function resizeDialog($dialog) {
if ($div.is(":visible")) $hidden = $hidden.add($div.hide());
});
- $d.css("width", "").css("width", $d[0].offsetWidth + 1);
+ $d.css("width", "").css("width", $d[0].offsetWidth + 2);
$hidden.show();
}
@@ -450,21 +480,22 @@ function positionDialog($dialog, posx, posy) {
}
-Dialog.attributes = (function() {
- var value = ko.observable([]), build, sub;
+Dialog.attrs = (function() {
- var Attribute = function(attr, info) {
- this.relationship = Dialog.relationship();
- this.value = this.relationship.attributes()[attr.name];
+ var Attribute = function(relationship, attr, info) {
+ this.value = relationship.attrs()[attr.name];
this.data = attr;
this.min = info[0];
this.max = info[1];
this.type = attr.children ? "select" : "boolean";
- }
+ };
+
+ return ko.computed({read: function() {
+ var relationship = Dialog.relationship(), attrs = [], id,
+ linkType = relationship.link_type(),
+ typeInfo = Util.typeInfo(linkType);
- build = function(linkType) {
- var attributes = [], typeInfo = Util.typeInfo(linkType), id;
- if (!typeInfo) return attributes;
+ if (!typeInfo) return attrs;
var allowedAttrs = typeInfo.attrs ? MB.utility.keys(typeInfo.attrs) : [];
@@ -473,82 +504,56 @@ Dialog.attributes = (function() {
});
for (var i = 0; id = allowedAttrs[i]; i++)
- attributes.push(new Attribute(Util.attrInfo(id), typeInfo.attrs[id]));
+ attrs.push(new Attribute(relationship, Util.attrInfo(id), typeInfo.attrs[id]));
- value(attributes);
- };
+ return attrs;
+ }, deferEvaluation: true});
- Dialog.relationship.subscribe(function(relationship) {
- if (sub) sub.dispose();
- build(relationship.link_type());
- sub = relationship.link_type.subscribe(build);
- });
- return value;
}());
UI.AddDialog = MB.utility.beget(Dialog);
-UI.AddDialog.show = function(options, posx, posy) {
- var target = options.target, source = options.source,
- relationship = RE.Relationship({
- source: source, target: target, action: "add"}, false);
-
- if (target.type == "work") target.name(source.name.peek());
-
+UI.AddDialog.show = function(options) {
+ options.relationship = RE.Relationship({entity: options.entity, action: "add"});
this.mode(options.mode || "add");
- this.relationship(relationship);
- Dialog.show.call(this, posx, posy);
-}
+ Dialog.show.call(this, options);
+};
UI.AddDialog.accept = function() {
var relationship = this.relationship();
if (!relationship.hasErrors()) {
- if (!relationship.source.mergeRelationship(relationship)) {
+ if (!Dialog.source.mergeRelationship(relationship))
relationship.show();
- }
Dialog.hide();
}
};
+UI.AddDialog.hide = function(cancel) {
+ Dialog.hide(function() {
+ this.relationship.peek().remove();
+ });
+};
-UI.EditDialog = MB.utility.beget(Dialog);
-UI.EditDialog.show = function(relationship, posx, posy) {
- var dlg = Dialog, target = relationship.target.peek(), name = target.name.peek();
+UI.EditDialog = MB.utility.beget(Dialog);
- dlg.mode("edit");
+UI.EditDialog.show = function(options) {
+ Dialog.mode("edit");
// originalRelationship is a copy of the relationship when the dialog was
// opened, i.e. before the user edits it. if they cancel the dialog, this is
// what gets copied back to revert their changes.
- dlg.originalRelationship = ko.mapping.toJS(relationship);
-
- // because the target is excluded from the Relationship mapping options, we
- // have to save the target as well.
- dlg.originalTarget = target;
-
- dlg.relationship(relationship);
- dlg.show.call(this, posx, posy);
+ Dialog.originalRelationship = options.relationship.toJS();
+ Dialog.show.call(this, options);
};
UI.EditDialog.hide = function(cancel) {
Dialog.hide(function() {
- if (cancel !== false) {
- var relationship = this.relationship.peek();
- ko.mapping.fromJS(this.originalRelationship, relationship);
- var observable = relationship.target, target = observable.peek();
-
- if (target !== this.originalTarget) {
-
- RE.Entity.isInstance(this.originalTarget)
- ? observable(this.originalTarget)
- : ko.mapping.fromJS(this.originalTarget, target);
- }
- }
+ if (cancel !== false)
+ this.relationship.peek().fromJS(this.originalRelationship);
delete Dialog.originalRelationship;
- delete Dialog.originalTarget;
});
};
@@ -557,65 +562,49 @@ UI.EditDialog.accept = function() {
if (!relationship.hasErrors()) {
delete Dialog.originalRelationship;
-
UI.EditDialog.hide(false);
}
};
-var BatchRelationshipDialog = MB.utility.beget(UI.AddDialog);
-
-BatchRelationshipDialog.accept = function(callback) {
- var relationship = Dialog.relationship();
- if (relationship.hasErrors()) return;
-
- var model = ko.mapping.toJS(relationship),
- hasCallback = $.isFunction(callback),
- targets = Dialog.targets;
-
- model.target = relationship.target();
-
- Util.callbackQueue(targets, function(target) {
- model.source = target;
- delete model.id;
-
- if (!hasCallback || callback(model))
- RE.Relationship(model, true, true);
- });
+UI.BatchRelationshipDialog = MB.utility.beget(UI.AddDialog);
- Dialog.hide();
-};
+UI.BatchRelationshipDialog.show = function(targets) {
+ Dialog.targets = targets;
-
-UI.BatchRecordingRelationshipDialog = MB.utility.beget(BatchRelationshipDialog);
-
-UI.BatchRecordingRelationshipDialog.show = function() {
- Dialog.targets = UI.checkedRecordings();
- if (Dialog.targets.length > 0) {
+ if (targets.length > 0) {
+ var source = targets[0];
UI.AddDialog.show.call(this, {
- source: Util.tempEntity("recording"), target: Util.tempEntity("artist"),
- mode: "batch.recording"
+ entity: [RE.Entity({type: "artist"}), source],
+ source: source,
+ mode: "batch." + source.type
});
}
};
+UI.BatchRelationshipDialog.accept = function(callback) {
+ var relationship = Dialog.relationship.peek(),
+ model = relationship.toJS(), hasCallback = $.isFunction(callback),
+ src = Dialog.backward.peek() ? 1 : 0;
-UI.BatchWorkRelationshipDialog = MB.utility.beget(BatchRelationshipDialog);
+ Util.callbackQueue(Dialog.targets, function(source) {
+ model.entity[src] = source;
+ delete model.id;
-UI.BatchWorkRelationshipDialog.show = function() {
- Dialog.targets = UI.checkedWorks();
- if (Dialog.targets.length > 0) {
+ if (!hasCallback || callback(model)) {
+ var newRelationship = RE.Relationship(model);
- UI.AddDialog.show.call(this, {
- source: Util.tempEntity("work"), target: Util.tempEntity("artist"),
- mode: "batch.work"
- });
- }
+ if (!source.mergeRelationship(newRelationship))
+ newRelationship.show();
+ }
+ });
+
+ UI.AddDialog.hide();
};
-UI.BatchCreateWorksDialog = MB.utility.beget(BatchRelationshipDialog);
+UI.BatchCreateWorksDialog = MB.utility.beget(UI.BatchRelationshipDialog);
UI.BatchCreateWorksDialog.show = function() {
Dialog.targets = _.filter(UI.checkedRecordings(), function(obj) {
@@ -623,13 +612,19 @@ UI.BatchCreateWorksDialog.show = function() {
});
if (Dialog.targets.length > 0) {
- var source = Util.tempEntity("recording"), target = Util.tempEntity("work");
+ var source = Dialog.targets[0], target = RE.Entity({type: "work"});
// the user can't edit the target in this dialog, but the gid of the
// temporary target entity has to be set to something valid, so that
- // validation passes and the dialog can be okay'd.
+ // validation passes and the dialog can be okay'd. we don't want to pass
+ // the gid to RE.Entity either, or else the entity will be cached.
target.gid = "00000000-0000-0000-0000-000000000000";
- UI.AddDialog.show.call(this, {source: source, target: target, mode: "batch.create.works"});
+
+ UI.AddDialog.show.call(this, {
+ entity: [source, target],
+ source: source,
+ mode: "batch.create.works"
+ });
}
};
@@ -637,18 +632,15 @@ UI.BatchCreateWorksDialog.accept = function() {
Dialog.loading(true);
var type_id = $("#batch-work-type > select").val(),
- language_id = $("#batch-work-lang > select").val(), works = [];
+ language_id = $("#batch-work-lang > select").val(), works;
- _.each(Dialog.targets, function(obj) {
- works.push({
- name: obj.name.peek(), comment: "",
- type: type_id, language: language_id
- });
+ works = _.map(Dialog.targets, function(obj) {
+ return {name: obj.name, comment: "", type: type_id, language: language_id};
});
function success(data) {
- BatchRelationshipDialog.accept.call(this, function(obj) {
- obj.target = RE.Entity(data.works.shift(), "work");
+ UI.BatchRelationshipDialog.accept.call(this, function(obj) {
+ obj.entity[1] = RE.Entity(data.works.shift(), "work");
if (data.works.length == 0) Dialog.loading(false);
return true;
});
@@ -664,7 +656,8 @@ UI.BatchCreateWorksDialog.accept = function() {
UI.BatchCreateWorksDialog.hide = function() {
Dialog.hide(function() {
- Dialog.batchWorksError(false);
+ this.batchWorksError(false);
+ this.relationship.peek().remove();
});
};
diff --git a/root/static/scripts/relationship-editor/Entity.js b/root/static/scripts/relationship-editor/Entity.js
index 9bbc874..60da231 100644
--- a/root/static/scripts/relationship-editor/Entity.js
+++ b/root/static/scripts/relationship-editor/Entity.js
@@ -19,183 +19,132 @@
MB.RelationshipEditor = (function(RE) {
-var Entity, Source, Artist, Label, Recording, Release, ReleaseGroup, Work, URL,
- entities, mapping, cache = {};
-
-mapping = {
- copy: ["type", "gid", "id", "artistCredit", "number", "position", "length"],
- ignore: ["refcount", "relationships", "artist", "artists", "label", "value", "isrcs", "artist_credit"],
- include: ["type", "gid", "id", "name", "sortname", "newWork", "comment", "work_type", "work_language"]
-};
-
// represents a core entitiy, either existing or newly-created.
// Entity is private - modules use RE.Entity to find or create an entity.
-Entity = function() {};
-
-Entity.prototype.init = function() {
- this.refcount = 0;
- this.name = ko.observable("");
-
- this.rendering = ko.computed({
- read: renderEntity,
- owner: this,
- deferEvaluation: true
- });
-};
-
-Entity.prototype.render = function(name, options) {
- options = $.extend({
- href: "/" + this.type + "/" + this.gid,
- target: "_blank"
- }, options);
- return MB.html.a(options, name);
-};
+var Entity = function() {};
-function renderEntity() {
- return this.render(this.name());
-}
+Entity.prototype.init = function(obj) {
+ obj = obj || {};
+ this.gid = obj.gid;
+ this.id = obj.id;
+ this.name = obj.name || "";
+ this.relationships = ko.observableArray([]);
-Entity.prototype.remove = function() {
- if (--this.refcount == 0) delete cache[this.gid];
+ var options = {href: "/" + this.type + "/" + this.gid, target: "_blank"};
+ if (obj.sortname) options.title = obj.sortname;
+ this.rendering = MB.html.a(options, this.name);
};
-Source = function() {};
-
-Source.prototype = new Entity;
-
-Source.prototype.init = function() {
- Entity.prototype.init.call(this);
- this.relationships = ko.observableArray([]);
+Entity.prototype.toJS = function() {
+ return {gid: this.gid, type: this.type};
};
// searches this entity's relationships for potential duplicate "rel"
// if it is a duplicate, remove and merge it
-Source.prototype.mergeRelationship = function(rel) {
- var relationships = rel.type.peek() == "recording-work"
- ? this.performanceRelationships() : this.relationships(),
- obj = ko.mapping.toJS(rel);
-
- delete obj.id;
- delete obj.action;
-
- // XXX figure out a faster/nicer way to merge relationship attributes
- var attrs = $.extend({}, obj.attributes),
- attrNames = MB.utility.keys(obj.attributes), name, value;
+Entity.prototype.mergeRelationship = function(rel) {
+ var relationships = (rel.type == "recording-work")
+ ? this.performanceRelationships() : this.relationships();
for (var i = 0; i < relationships.length; i++) {
var other = relationships[i];
if (rel !== other && rel.isDuplicate(other)) {
- obj.attributes = {};
- for (var i = 0; name = attrNames[i]; i++) {
- value = obj.attributes[name] = attrs[name];
+ var obj = rel.toJS(), attrs = $.extend({}, obj.attrs);
+ obj.attrs = {};
+ delete obj.id;
+ delete obj.action;
- if (!value || ($.isArray(value) && !value.length))
- obj.attributes[name] = other.attributes.peek()[name].peek();
- }
+ _.each(attrs, function(value, name) {
+ obj.attrs[name] = !value || ($.isArray(value) && !value.length)
+ ? other.attrs.peek()[name].peek() : value;
+ });
- // Merge the dates here, otherwise ko.mapping.fromJS would overwrite
- // them with whatever is in obj.begin_date and obj.end_date.
- other.begin_date(RE.Util.mergeDates(rel.begin_date, other.begin_date));
- other.end_date(RE.Util.mergeDates(rel.end_date, other.end_date));
- delete obj.begin_date;
- delete obj.end_date;
+ obj.period.begin_date = RE.Util.mergeDates(rel.period.begin_date, other.period.begin_date);
+ obj.period.end_date = RE.Util.mergeDates(rel.period.end_date, other.period.end_date);
- ko.mapping.fromJS(obj, other);
+ other.fromJS(obj);
rel.remove();
+
return true;
}
}
return false;
};
-Artist = function() {
- this.init();
- this.sortname = ko.observable("");
-
- this.rendering = ko.computed({
- read: renderArtist,
- owner: this,
- deferEvaluation: true
- });
+Artist = function(obj) {
+ obj = obj || {};
+ this.init(obj);
+ this.sortname = ko.observable(obj.sortname);
};
-function renderArtist() {
- return this.render(this.name(), {title: this.sortname()});
-}
+Artist.prototype = new Entity;
+Artist.prototype.type = "artist";
-Label = function() {
- this.init();
+Label = function(obj) {
+ this.init(obj);
};
-Recording = function() {
- this.init();
+Label.prototype = new Entity;
+Label.prototype.type = "label";
+
+Recording = function(obj) {
+ obj = obj || {};
+ this.init(obj);
+ this.number = obj.number;
+ this.position = obj.position;
+ this.length = obj.length;
+ this.artistCredit = obj.artistCredit;
this.performanceRelationships = ko.observableArray([]);
};
-Release = function() {
- this.init();
-};
-
-ReleaseGroup = function() {
- this.init();
-};
+Recording.prototype = new Entity;
+Recording.prototype.type = "recording";
-Work = function() {
- this.init();
- this.performanceRefcount = 0;
- this.comment = ko.observable("");
- this.work_type = ko.observable(null);
- this.work_language = ko.observable(null);
+Release = function(obj) {
+ this.init(obj);
};
-URL = function() {
- this.init();
+Release.prototype = new Entity;
+Release.prototype.type = "release";
- this.rendering = ko.computed({
- read: renderURL,
- owner: this,
- deferEvaluation: true
- });
+ReleaseGroup = function(obj) {
+ this.init(obj);
};
-function renderURL() {
- var name = _.prune(this.name(), 50);
- return this.render(name, {href: this.name()});
-}
+ReleaseGroup.prototype = new Entity;
+ReleaseGroup.prototype.type = "release_group";
-Artist.prototype = new Entity;
-Label.prototype = new Entity;
-Recording.prototype = new Source;
-Release.prototype = new Source;
-ReleaseGroup.prototype = new Source;
-Work.prototype = new Source;
-URL.prototype = new Entity;
-
-entities = {
- artist: Artist,
- label: Label,
- recording: Recording,
- release: Release,
- release_group: ReleaseGroup,
- work: Work,
- url: URL
+Work = function(obj) {
+ obj = obj || {};
+ this.init(obj);
+ this.performanceCount = 0;
+ this.comment = ko.observable(obj.comment || "");
+ this.work_type = ko.observable(obj.work_type || null);
+ this.work_language = ko.observable(obj.work_language || null);
};
-RE.Entity = function(obj, type) {
- if (obj instanceof Entity) return obj;
- obj.type = obj.type || type;
+Work.prototype = new Entity;
+Work.prototype.type = "work";
- var ent;
- if ((ent = cache[obj.gid]) === undefined)
- ent = cache[obj.gid] = new entities[obj.type];
+RE.Entity = (function() {
+ var entities = {
+ artist: Artist,
+ label: Label,
+ recording: Recording,
+ release: Release,
+ release_group: ReleaseGroup,
+ work: Work
+ }, cache = {};
- ko.mapping.fromJS(obj, mapping, ent);
- return ent;
-};
+ return function(obj, type) {
+ if (obj instanceof Entity) return obj;
+ if (obj.gid) return cache[obj.gid] || (cache[obj.gid] = new entities[type || obj.type](obj));
+ return new entities[type || obj.type](obj);
+ };
+}());
RE.Entity.isInstance = function(obj) {
return obj instanceof Entity;
diff --git a/root/static/scripts/relationship-editor/Fields.js b/root/static/scripts/relationship-editor/Fields.js
index 27dcfeb..f32aa47 100644
--- a/root/static/scripts/relationship-editor/Fields.js
+++ b/root/static/scripts/relationship-editor/Fields.js
@@ -33,23 +33,19 @@ var validationHandlers = {
}
},
- begin_date: function(field, value, relationship) {
- validateDatePeriod(field, relationship.end_date, validateDate(field, value));
+ "period.begin_date": function(field, value, relationship) {
+ validateDatePeriod(field, relationship.period.end_date, validateDate(field, value));
},
- end_date: function(field, value, relationship) {
- validateDatePeriod(relationship.begin_date, field, null, validateDate(field, value));
+ "period.end_date": function(field, value, relationship) {
+ validateDatePeriod(relationship.period.begin_date, field, null, validateDate(field, value));
},
- ended: function(field, value) {
+ "period.ended": function(field, value) {
_.isBoolean(value) ? field.error("") : field.error(MB.text.InvalidValue);
},
- backward: function(field, value) {
- field.error(_.isBoolean(value) ? "" : MB.text.InvalidValue);
- },
-
- attributes: function(field, value, relationship) {
+ attrs: function(field, value, relationship) {
var linkType = relationship.link_type(), typeInfo = Util.typeInfo(linkType);
if (!typeInfo) return;
@@ -79,12 +75,17 @@ var validationHandlers = {
});
},
- target: function(field, value) {
+ "entity.0": function(field, value) {
field.error(Util.isMBID(value.gid) ? "" : MB.text.RequiredField);
}
};
+validationHandlers["entity.1"] = validationHandlers["entity.0"];
+
+
function validateDate(field, value) {
+ if (field.error === undefined) return false;
+
var y = value.year(), m = value.month(), d = value.day(),
valid = (y === null && m === null && d === null) || MB.utility.validDate(y, m, d);
@@ -109,50 +110,58 @@ function validateDatePeriod(begin, end, beginValid, endValid) {
// used to track changes, handle validation, and update "action" accordingly
-ko.extenders.field = function(target, options) {
- var relationship = options[0], name = options[1], fullName = options[2] || name;
+ko.extenders.field = function(observable, options) {
+ var self = {
+ observable: observable,
+ relationship: options[0],
+ name: options[1]
+ }, validateAndCompare = options[2];
+
+ observable.error = ko.observable("");
+ observable.error.subscribe(errorChanged, self);
+ observable.hasError = false;
+ observable.changed = false;
+
+ if (validateAndCompare !== false) {
+ fieldChanged.call(self, observable());
+ observable.subscribe(fieldChanged, self);
+ }
+ return observable;
+};
- target.error = ko.observable("");
- target.hasError = false;
+var errorChanged = function(error) {
+ var hasError = Boolean(error);
- target.errorSub = target.error.subscribe(function(error) {
- var hasError = Boolean(error);
+ if (hasError != this.observable.hasError)
+ this.relationship.hasErrors(
+ (this.relationship.errorCount += (hasError ? 1 : -1)) > 0);
- if (hasError != target.hasError) {
- relationship.errorCount += (hasError ? 1 : -1);
- relationship.hasErrors(relationship.errorCount > 0);
- }
- target.hasError = hasError;
- });
+ this.observable.hasError = hasError;
+};
- delete fullName;
- var noValidationOrComparison = options[3];
+var fieldChanged = function(newValue) {
+ newValue = ko.utils.unwrapObservable(newValue);
+ var observable = this.observable, relationship = this.relationship, name = this.name;
- if (!noValidationOrComparison)
- target.validationSub = target.subscribe(function(value) {
- validationHandlers[name](target, value, relationship);
- });
+ validationHandlers[name](observable, newValue, relationship);
- if (relationship.action.peek() == "add" || noValidationOrComparison) return target;
+ if (relationship.action.peek() == "add") return;
- target.changed = false;
+ var origValue = relationship.original_fields, fields = name.split("."), changed;
+ for (var i = 0; i < fields.length; i++) origValue = origValue[fields[i]];
- target.subscribe(function(newValue) {
- newValue = ko.utils.unwrapObservable(newValue);
- // entities are unique, we compare them directly.
- if (name != "target") newValue = ko.mapping.toJS(newValue);
+ // entities are unique, we compare them directly.
+ /^entity/.test(name)
+ ? (origValue = RE.Entity(origValue))
+ : (newValue = ko.toJS(newValue));
- var origValue = Util.originalFields(relationship, name),
- changed = !_.isEqual(origValue, newValue);
+ changed = !_.isEqual(origValue, newValue);
- if (changed != target.changed)
- relationship.changeCount += (changed ? 1 : -1);
+ if (changed != observable.changed)
+ relationship.changeCount += (changed ? 1 : -1);
- target.changed = changed;
- relationship.action(relationship.changeCount > 0 ? "edit" : "");
- });
-
- return target;
+ observable.changed = changed;
+ relationship.action(relationship.changeCount > 0 ? "edit" : "");
};
@@ -194,7 +203,7 @@ Fields.PartialDate.prototype.write = function(obj) {
date.year(obj.year);
date.month(obj.month);
date.day(obj.day);
-}
+};
Fields.PartialDate.prototype.convert = function(obj) {
obj = ko.utils.unwrapObservable(obj);
@@ -208,23 +217,35 @@ Fields.PartialDate.prototype.partChanged = function() {
};
-var Attribute = function(name, value, attr, relationship) {
- this.value = ko.observable(Util.convertAttr(attr, value));
+Fields.Attribute = function(attr, value, relationship) {
this.attr = attr;
+ this.value = ko.observable(this.convert(value));
this.relationship = relationship;
return (ko.computed({read: this.value, write: this.write, owner: this})
- .extend({field: [relationship, null, "attrs." + name, true]}));
+ .extend({field: [relationship, "attrs." + attr.name, false]}));
};
-
-Attribute.prototype.write = function(newValue) {
- newValue = Util.convertAttr(this.attr, ko.utils.unwrapObservable(newValue));
+Fields.Attribute.prototype.write = function(newValue) {
+ newValue = this.convert(ko.utils.unwrapObservable(newValue));
if (!_.isEqual(this.value(), newValue)) {
this.value(newValue);
- var attrs = this.relationship.attributes;
- attrs.notifySubscribers(attrs());
+ var attrs = this.relationship.attrs;
+ attrs.notifySubscribers(attrs.peek());
+ }
+};
+
+Fields.Attribute.prototype.convert = function(value) {
+ if (this.attr.children) {
+ if (!_.isArray(value)) value = [value];
+
+ return (_.chain(value)
+ .map(function(n) {return parseInt(n, 10)})
+ .compact().uniq().value()
+ .sort(function(a, b) {return a - b}));
+ } else {
+ return Boolean($.isNumeric(value) ? parseInt(value, 10) : value);
}
};
@@ -236,7 +257,7 @@ Attribute.prototype.write = function(newValue) {
Fields.Attributes = function(relationship) {
this.value = {};
this.relationship = relationship;
- return ko.computed({read: this.read, write: this.write, owner: this, deferEvaluation: true});
+ return ko.computed({read: this.read, write: this.update, owner: this, deferEvaluation: true});
};
Fields.Attributes.prototype.read = function() {
@@ -244,18 +265,15 @@ Fields.Attributes.prototype.read = function() {
return this.value;
};
-Fields.Attributes.prototype.write = function(newValue) {
- this.update(newValue);
-};
-
Fields.Attributes.prototype.update = function(value) {
- var target = this.value, validAttrs = {}, self = this;
+ var target = this.value, validAttrs = {}, self = this,
+ typeInfo = Util.typeInfo(this.relationship.link_type()) || {};
- Util.attrsForLinkType(this.relationship.link_type(), function(attr) {
- var name = attr.name;
+ _.each(typeInfo.attrs || {}, function(info, id) {
+ var attr = Util.attrInfo(id), name = attr.name;
if (target[name] === undefined) {
- target[name] = new Attribute(name, value[name], attr, self.relationship);
+ target[name] = new Fields.Attribute(attr, value[name], self.relationship);
} else if (value[name] !== undefined) {
target[name](value[name]);
@@ -270,31 +288,26 @@ Fields.Attributes.prototype.update = function(value) {
if (validAttrs[name] === undefined) {
if (attr.hasError) attr.error("");
-
- attr.errorSub.dispose();
delete target[name];
}
}
};
-
-Fields.Target = function(target, relationship) {
- this.target = ko.observable(target);
+Fields.Entity = function(entity, relationship) {
+ this.entity = ko.observable(entity);
this.relationship = relationship;
-
- this.computed = ko.computed({read: this.target, write: this.write, owner: this})
- .extend({field: [relationship, "target"]});
-
- return this.computed
+ return ko.computed({read: this.entity, write: this.write, owner: this});
};
-Fields.Target.prototype.write = function(newTarget) {
- var relationship = this.relationship, oldTarget = this.target(),
- newTarget = RE.Entity(ko.utils.unwrapObservable(newTarget));
+Fields.Entity.prototype.write = function(entity) {
+ var currentEntity = this.entity.peek();
+ entity = RE.Entity(entity);
- if (oldTarget !== newTarget)
- relationship.changeTarget(oldTarget, newTarget, this.target);
-}
+ if (currentEntity !== entity && currentEntity.type == entity.type) {
+ this.entity(entity);
+ this.relationship.entityChanged(currentEntity, entity);
+ }
+};
return RE;
diff --git a/root/static/scripts/relationship-editor/Relationship.js b/root/static/scripts/relationship-editor/Relationship.js
index 845cd8d..c6123c8 100644
--- a/root/static/scripts/relationship-editor/Relationship.js
+++ b/root/static/scripts/relationship-editor/Relationship.js
@@ -20,263 +20,190 @@
MB.RelationshipEditor = (function(RE) {
var UI = RE.UI = RE.UI || {}, Util = RE.Util = RE.Util || {},
- Fields = RE.Fields = RE.Fields || {}, mapping, cache = {};
-
-mapping = {
- // entities (source, target) have their own mapping options in Entity.js
- ignore: ["source", "target", "visible", "direction"],
- copy: ["edits_pending", "id"],
- include: ["link_type", "action", "backward", "begin_date", "end_date", "ended", "attributes"],
- attributes: {
- update: function(options) {return $.extend(true, {}, options.data)}
- },
- begin_date: {
- update: function(options) {
- return new Fields.PartialDate(options.data);
- }
- },
- ended: {
- update: function(options) {return Boolean(options.data)}
- }
-};
-
-mapping.end_date = mapping.begin_date;
+ Fields = RE.Fields = RE.Fields || {}, cache = {};
-RE.Relationship = function(obj, tryToMerge, show) {
- obj.link_type = obj.link_type || defaultLinkType(obj.source.type, obj.target.type);
- var type = Util.types(obj.link_type), relationship, c = cache[type] = cache[type] || {};
+RE.Relationship = function(obj) {
+ var type0 = obj.entity[0].type, type1 = obj.entity[1].type, relationship, key;
+ if (!obj.link_type) obj.link_type = defaultLinkType(type0, type1);
if (!obj.id) obj.id = _.uniqueId("new-");
- relationship = c[obj.id] || (c[obj.id] = new Relationship(obj));
- if (tryToMerge && relationship.source.mergeRelationship(relationship)) {
- delete c[obj.id];
- return null;
- }
-
- if (show) relationship.show();
- return relationship;
+ key = [type0, type1, obj.id].join("-");
+ return cache[key] || (cache[key] = new Relationship(obj));
};
var Relationship = function(obj) {
var self = this;
- this.visible = false; // new relationships still being edited aren't visible
+ this.visible = false;
this.id = obj.id;
this.changeCount = 0;
this.errorCount = 0;
- this.action = ko.observable(obj.action || "");
this.hasErrors = ko.observable(false);
-
- obj.link_type = obj.link_type || defaultLinkType(obj.source.type, obj.target.type),
- obj.backward = obj.backward || (obj.source.type != Util.types(obj.link_type).split("-")[0]);
-
- this.link_type = new Fields.Integer(obj.link_type).extend({field: [this, "link_type"]});
- this.backward = ko.observable(obj.backward).extend({field: [this, "backward"]});
-
- this.begin_date = new Fields.PartialDate();
- this.end_date = new Fields.PartialDate();
- this.ended = ko.observable(false);
- this.attributes = new Fields.Attributes(this);
-
- this.dateRendering = ko.computed({read: this.renderDate, owner: this})
- .extend({throttle: 100});
-
- // entities have a refcount so that they can be deleted when they aren't
- // referenced by any relationship. we use a computed observable for the target,
- // so that we don't have to remember to decrement the refcount each time the
- // target changes.
-
- obj.source.refcount += 1;
- this.source = obj.source; // source can't change
-
- obj.target.refcount += 1;
- this.target = new Fields.Target(obj.target, this);
-
- this.type = ko.computed(function() {return Util.types(self.link_type())});
- if (this.type.peek() == "recording-work") obj.target.performanceRefcount += 1;
-
- // XXX trigger the validation subscription's callback, so that validation
- // on the target's name is registered as well.
- this.target.notifySubscribers(this.target.peek());
-
- ko.mapping.fromJS(obj, mapping, this);
-
- // add these *after* pulling in the obj mapping, otherwise they'll mark the
- // relationship as having changes.
- this.begin_date.extend({field: [this, "begin_date"]});
- this.end_date.extend({field: [this, "end_date"]});
- this.ended.extend({field: [this, "ended"]});
- this.attributes.extend({field: [this, "attributes"]});
-
- this.linkPhrase = ko.computed(this.buildLinkPhrase, this).extend({throttle: 1});
this.loadingWork = ko.observable(false);
+ this.edits_pending = Boolean(obj.edits_pending);
- this.edits_pending
- ? (this.openEdits = ko.computed(this.buildOpenEdits, this))
- : (this.edits_pending = false);
-
- delete obj;
+ this.action = ko.observable(obj.action || "");
+ this.link_type = new Fields.Integer(obj.link_type);
+ this.attrs = new Fields.Attributes(this);
+ this.period = {
+ begin_date: new Fields.PartialDate(),
+ end_date: new Fields.PartialDate(),
+ ended: ko.observable(false)
+ };
+
+ var entity0 = RE.Entity(obj.entity[0]), entity1 = RE.Entity(obj.entity[1]);
+ this.entity = [new Fields.Entity(entity0, this), new Fields.Entity(entity1, this)];
+ this.type = entity0.type + "-" + entity1.type;
+
+ this.fromJS(obj);
+ this.dateRendering = ko.computed({read: this.renderDate, owner: this});
+ this.original_fields = this.toJS();
+
+ this.entity[0].extend({field: [this, "entity.0"]});
+ this.entity[1].extend({field: [this, "entity.1"]});
+ this.link_type.extend({field: [this, "link_type"]});
+ this.attrs.extend({field: [this, "attrs"]});
+ this.period.begin_date.extend({field: [this, "period.begin_date"]});
+ this.period.end_date.extend({field: [this, "period.end_date"]});
+ this.period.ended.extend({field: [this, "period.ended"]});
};
-var defaultLinkType = function(sourceType, targetType) {
- var type = sourceType + "-" + targetType, linkType;
-
- if (!(linkType = Util.typeInfoByEntities(type)))
- linkType = Util.typeInfoByEntities(targetType + "-" + sourceType);
-
- linkType = linkType[0];
+var defaultLinkType = function(type0, type1) {
+ var linkType = Util.typeInfoByEntities(type0 + "-" + type1);
+ if (!linkType) return null; else linkType = linkType[0];
return linkType.descr ? linkType.id : linkType.children[0].id;
};
-Relationship.prototype.entity = function() {
- return this.backward() ? [this.target(), this.source] : [this.source, this.target()];
+Relationship.prototype.toJS = function() {
+ var entity0 = this.entity[0].peek(), entity1 = this.entity[1].peek();
+ return {
+ id: this.action.peek() == "action" ? undefined : this.id,
+ link_type: this.link_type.peek(),
+ action: this.action.peek(),
+ period: {
+ begin_date: ko.toJS(this.period.begin_date),
+ end_date: ko.toJS(this.period.end_date),
+ ended: this.period.ended.peek(),
+ },
+ attrs: ko.toJS(this.attrs),
+ entity: [this.entity[0].peek().toJS(), this.entity[1].peek().toJS()]
+ };
};
-Relationship.prototype.changeTarget = function(oldTarget, newTarget, observable) {
- var type = this.type.peek();
-
- observable(newTarget);
- newTarget.refcount += 1;
-
- if (oldTarget) {
- oldTarget.remove();
-
- if (type == "recording-work") oldTarget.performanceRefcount -= 1;
-
- if (oldTarget.type != newTarget.type) {
- // the type changed. our relationship cache is organized by type, so we
- // have to move the position of this relationship in the cache.
- var oldType = Util.types(this.link_type.peek()), newType;
-
- this.link_type(defaultLinkType(this.source.type, newTarget.type));
- newType = this.type();
+Relationship.prototype.fromJS = function(obj) {
+ this.link_type(obj.link_type);
+ this.period.begin_date((obj.period || {}).begin_date || "");
+ this.period.end_date((obj.period || {}).end_date || "");
+ this.period.ended(Boolean((obj.period || {}).ended));
+ this.attrs(obj.attrs || {});
+ this.entity[0](obj.entity[0]);
+ this.entity[1](obj.entity[1]);
+};
- (cache[newType] = cache[newType] || {})[this.id] = cache[oldType][this.id];
- delete cache[oldType][this.id];
- // fix the direction.
- var typeInfo = Util.typeInfo(this.link_type.peek());
- this.backward(this.source.type != newType.split("-")[0]);
- }
- }
-
- if (type == "recording-work") {
- newTarget.performanceRefcount += 1;
- this.workChanged(newTarget);
- }
+Relationship.prototype.target = function(source) {
+ var entity0 = this.entity[0](), entity1 = this.entity[1]();
+ return source === entity0 ? entity1 : entity0;
};
-// if the user changed a work, we need to request its relationships.
-var worksLoading = {};
+Relationship.prototype.entityChanged = function(oldEntity, newEntity) {
+ var entity0 = this.entity[0].peek(), entity1 = this.entity[1].peek(), self = this;
-Relationship.prototype.workChanged = function(work) {
- var gid = work.gid, self = this;
- if (worksLoading[gid]) return;
+ if (oldEntity !== entity0 && oldEntity !== entity1)
+ oldEntity.relationships.remove(this);
- this.loadingWork(true);
- worksLoading[gid] = 1;
+ if (entity0.type == "recording" && entity1.type == "work" && newEntity === entity1) {
+ oldEntity.performanceCount -= 1;
+ newEntity.performanceCount += 1;
- $.get("/ws/js/entity/" + gid + "?inc=rels")
- .success(function(data) {
- Util.parseRelationships(data);
- })
- .complete(function() {
- self.loadingWork(false);
- delete worksLoading[gid];
- });
-};
+ this.loadingWork(true);
+ $.get("/ws/js/entity/" + newEntity.gid + "?inc=rels")
+ .success(function(data) {
+ Util.parseRelationships(data);
+ })
+ .complete(function() {
+ self.loadingWork(false);
+ });
-Relationship.prototype.show = function() {
- if (!this.visible) {
- this.type.peek() == "recording-work"
- ? this.source.performanceRelationships.push(this)
- : this.source.relationships.push(this);
+ } else if (this.visible && newEntity.relationships.indexOf(this) == -1)
+ newEntity.relationships.push(this);
- this.visible = true;
- }
+ if (oldEntity.type == "recording" && entity1.type == "work")
+ oldEntity.performanceRelationships.remove(this);
};
-Relationship.prototype.reset = function(obj) {
- this.hasErrors(false);
- this.errorCount = 0;
- var fields = Util.originalFields(this);
-
- if (fields) {
- ko.mapping.fromJS(fields, this);
- this.target(fields.target);
- this.changeCount = 0;
+Relationship.prototype.show = function() {
+ if (this.type == "recording-work") {
+ this.entity[0].peek().performanceRelationships.push(this);
+ this.entity[1].peek().performanceCount += 1;
+ } else {
+ this.entity[0].peek().relationships.push(this);
+ this.entity[1].peek().relationships.push(this);
}
+ this.visible = true;
};
Relationship.prototype.remove = function() {
- // prevent this from being removed twice, otherwise it screws up refcounts
- // everywhere. this can happen if the relationship is merged into another
- // one (thus removed), and then removed again when the dialog is closed
- // (because the dialog sees that this.visible is false).
if (this.removed === true) return;
+ var entity0 = this.entity[0].peek(), entity1 = this.entity[1].peek();
- var recordingWork = (this.type() == "recording-work"),
- target = this.target.peek();
+ if (this.type == "recording-work") {
+ entity0.performanceRelationships.remove(this);
+ entity1.performanceCount -= 1;
- if (recordingWork) {
- this.source.performanceRelationships.remove(this);
- target.performanceRefcount -= 1;
- } else {
- this.source.relationships.remove(this);
- }
-
- this.source.remove();
- target.remove();
+ if (entity1.performanceCount <= 0) {
+ var relationships = entity1.relationships.slice(0);
- if (recordingWork && target.performanceRefcount <= 0) {
- var relationships = target.relationships.slice(0);
+ for (var i = 0; i < relationships.length; i++) {
+ var relationship = relationships[i],
+ target = relationship.target(entity1);
- for (var i = 0; i < relationships.length; i++)
- relationships[i].remove();
+ if (target.type != "work" || target.performanceCount <= 0)
+ relationship.remove();
+ }
+ }
+ } else {
+ entity0.relationships.remove(this);
+ entity1.relationships.remove(this);
}
- delete cache[this.type.peek()][this.id];
- this.visible = false;
+
+ delete cache[this.type + "-" + this.id];
this.removed = true;
+ this.visible = false;
};
// Constructs the link phrase to display for this relationship
-Relationship.prototype.buildLinkPhrase = function() {
- var typeInfo = Util.typeInfo(this.link_type());
- if (!typeInfo) return "";
-
- var attrs = {}, m, phrase = this.source === this.entity()[0]
- ? typeInfo.phrase : typeInfo.reverse_phrase;
+Relationship.prototype.linkPhrase = function(source) {
+ var typeInfo = Util.typeInfo(this.link_type()), attrs = {}, m,
+ phrase = source === this.entity[0]() ? typeInfo.phrase : typeInfo.reverse_phrase;
- $.each(this.attributes(), function(name, observable) {
- var value = observable(),
- str = Util.attrRoot(name).l_name,
- isArray = $.isArray(value);
+ _.each(this.attrs(), function(observable, name) {
+ var value = observable();
- if (!value || isArray && !value.length) return;
- if (isArray) {
- value = $.map(value, function(v) {return Util.attrInfo(v).l_name});
+ if (_.isArray(value) && value.length) {
+ value = _.map(value, function(v) {return Util.attrInfo(v).l_name});
var list = value.slice(0, -1).join(", ");
- str = (list && list + " & ") + (value.pop() || "");
- }
- attrs[name] = str;
+ attrs[name] = (list && list + " & ") + (value.pop() || "");
+
+ } else if (value) attrs[name] = Util.attrRoot(name).l_name;
});
+
while (m = phrase.match(/\{(.*?)(?::(.*?))?\}/)) {
- var replace = attrs[m[1]] !== undefined
+ phrase = phrase.replace(m[0], (attrs[m[1]] !== undefined)
? (m[2] && m[2].split("|")[0]) || attrs[m[1]]
- : (m[2] && m[2].split("|")[1]) || "";
- phrase = phrase.replace(m[0], replace);
+ : (m[2] && m[2].split("|")[1]) || "");
}
return _.clean(phrase);
};
@@ -292,8 +219,9 @@ function renderDate(date) {
}
Relationship.prototype.renderDate = function() {
- var begin_date = renderDate(this.begin_date.peek()),
- end_date = renderDate(this.end_date.peek()), ended = this.ended();
+ var begin_date = renderDate(this.period.begin_date.peek()),
+ end_date = renderDate(this.period.end_date.peek()),
+ ended = this.period.ended();
if (!begin_date && !end_date) return "";
if (begin_date == end_date) return begin_date;
@@ -301,58 +229,41 @@ Relationship.prototype.renderDate = function() {
return begin_date + " \u2013 " + (end_date || (ended ? "????" : ""));
};
-// Contruction of form fields
-
-var simpleFields = ["id", "link_type", "action"], dateFields = ["year", "month", "day"],
- entityFields = ["gid", "type"];
-
-var buildField = function(prefix, name, obj, fields) {
- var field, value, prefix = prefix + (name && name + ".");
-
- for (var i = 0; field = fields[i]; i++) {
- value = ko.utils.unwrapObservable(obj[field]);
- if (value) {
- field = prefix + field;
+// Construction of form fields
- if (_.isArray(value)) {
- for (var j = 0; j < value.length; j++)
- this[field + "." + j] = value[j];
- } else {
- if (_.isBoolean(value)) value = value ? 1 : 0;
- this[field] = value;
- }
- }
+var buildField = function(prefix, obj, result) {
+ if (_.isObject(obj) || _.isArray(obj)) {
+ _.each(obj, function(value, name) {
+ buildField(prefix + "." + name, value, result);
+ });
+ } else if (_.isBoolean(obj)) {
+ result[prefix] = obj ? 1 : 0;
+ } else if (_.isString(obj) || _.isNumber(obj)) {
+ result[prefix] = obj;
}
};
Relationship.prototype.buildFields = function(num, result) {
- var attrs = _.keys(this.attributes.peek()), entity = this.entity(),
- prefix = "rel-editor.rels." + num + ".", bf = _.bind(buildField, result),
- sf = (this.action.peek() == "add") ? _.rest(simpleFields) : simpleFields;
-
- bf(prefix, "", this, sf);
- bf(prefix, "period.begin_date", this.begin_date.peek(), dateFields);
- bf(prefix, "period.end_date", this.end_date.peek(), dateFields);
- result[prefix + "period.ended"] = this.ended.peek() ? 1 : 0;
- bf(prefix, "attrs", this.attributes.peek(), attrs);
- bf(prefix, "entity.0", entity[0], entityFields);
- bf(prefix, "entity.1", entity[1], entityFields);
+ var prefix = "rel-editor.rels." + num;
+ buildField(prefix, this.toJS(), result);
+ if (this.action.peek() == "add") delete result[prefix + ".id"];
};
// returns true if this relationship is a "duplicate" of the other.
// doesn't compare attributes, but does compare dates.
Relationship.prototype.isDuplicate = function(other) {
- var thisent = this.entity(), otherent = other.entity();
return (this.link_type.peek() == other.link_type.peek() &&
- thisent[0] === otherent[0] && thisent[1] === otherent[1] &&
- Util.mergeDates(this.begin_date, other.begin_date) &&
- Util.mergeDates(this.end_date, other.end_date));
+ this.entity[0].peek() === other.entity[0].peek() &&
+ this.entity[1].peek() === other.entity[1].peek() &&
+ Util.mergeDates(this.period.begin_date, other.period.begin_date) &&
+ Util.mergeDates(this.period.end_date, other.period.end_date));
};
Relationship.prototype.buildOpenEdits = function() {
- var orig = Util.originalFields(this), source = this.source, target = orig.target;
+ var entity0 = this.original_fields.entity[0],
+ entity1 = this.original_fields.entity[1];
return _.sprintf(
'/search/edits?auto_edit_filter=&order=desc&negation=0&combinator=and' +
@@ -362,12 +273,12 @@ Relationship.prototype.buildOpenEdits = function() {
'&conditions.2.operator=%%3D&conditions.2.args=90%%2C233&conditions.2.args=91' +
'&conditions.2.args=92&conditions.3.field=status&conditions.3.operator=%%3D' +
'&conditions.3.args=1&field=Please+choose+a+condition',
- encodeURIComponent(source.type),
- encodeURIComponent(source.name()),
- encodeURIComponent(source.id),
- encodeURIComponent(target.type),
- encodeURIComponent(target.name()),
- encodeURIComponent(target.id)
+ encodeURIComponent(entity0.type),
+ encodeURIComponent(entity0.name),
+ encodeURIComponent(entity0.id),
+ encodeURIComponent(entity1.type),
+ encodeURIComponent(entity1.name),
+ encodeURIComponent(entity1.id)
);
};
diff --git a/root/static/scripts/relationship-editor/RelationshipEditor.js b/root/static/scripts/relationship-editor/RelationshipEditor.js
index 36658c4..291109f 100644
--- a/root/static/scripts/relationship-editor/RelationshipEditor.js
+++ b/root/static/scripts/relationship-editor/RelationshipEditor.js
@@ -24,6 +24,8 @@ var UI = RE.UI = RE.UI || {}, Util = RE.Util = RE.Util || {},
RE.releaseViewModel = {
RE: RE,
+ release: ko.observable({relationships: []}),
+ releaseGroup: ko.observable({relationships: []}),
media: ko.observableArray([]),
checkboxes: (function() {
@@ -39,14 +41,14 @@ RE.releaseViewModel = {
msg = strings[Math.min(strings.length - 1, data.recordingCount())];
return msg ? "(" + msg + ")" : "";
- }).extend({throttle: 100});
+ });
data.workMessage = ko.computed(function() {
var strings = data.workStrings(),
msg = strings[Math.min(strings.length - 1, data.workCount())];
return msg ? "(" + msg + ")" : "";
- }).extend({throttle: 100});
+ });
return data;
}()),
@@ -72,12 +74,12 @@ RE.releaseViewModel = {
_.each(recording.performanceRelationships.peek(), function(relationship) {
addChanged(relationship);
- _.each(relationship.target.peek().relationships.peek(), addChanged);
+ _.each(relationship.entity[1].peek().relationships.peek(), addChanged);
});
});
});
- _.each(this.release.relationships.peek(), addChanged);
- _.each(this.releaseGroup.relationships.peek(), addChanged);
+ _.each(this.release.peek().relationships.peek(), addChanged);
+ _.each(this.releaseGroup.peek().relationships.peek(), addChanged);
if (changed.length == 0) {
this.submissionLoading(false);
@@ -118,10 +120,7 @@ RE.releaseViewModel = {
var parts = key.split(".");
if (parts[0] == "entity") {
- var i = parts[1];
-
- if (relationship.entity()[i] === relationship.target())
- relationship.target.error(error[0]);
+ relationship.entity[parts[1]].error(error[0]);
} else if (parts[1] == "begin_date" || parts[1] == "end_date") {
relationship[parts[1]].error(error[0]);
@@ -156,9 +155,6 @@ UI.init = function(releaseGID, releaseGroupGID, data) {
// preload image to avoid flickering
$("<img/>").attr("src", "../../../static/images/icons/add.png");
- RE.releaseViewModel.release = RE.Entity({type: "release", gid: releaseGID});
- RE.releaseViewModel.releaseGroup = RE.Entity({type: "release_group", gid: releaseGroupGID});
-
ko.applyBindings(RE.releaseViewModel, document.getElementById("content"));
if (data) {
@@ -176,9 +172,8 @@ UI.init = function(releaseGID, releaseGroupGID, data) {
releaseLoaded = function(data) {
- data.type = "release";
- data.release_group.type = "release_group";
- RE.Entity(data);
+ RE.releaseViewModel.release(RE.Entity(data, "release"));
+ RE.releaseViewModel.releaseGroup(RE.Entity(data.release_group, "release_group"));
for (var i = 0, trackCount = 0, medium; medium = data.mediums[i]; i++)
trackCount += medium.tracks.length;
@@ -342,12 +337,12 @@ function initCheckboxes(trackCount) {
function initButtons() {
$("#batch-recording").click(function() {
if (!$(this).hasClass("disabled"))
- UI.BatchRecordingRelationshipDialog.show();
+ UI.BatchRelationshipDialog.show(UI.checkedRecordings());
});
$("#batch-work").click(function() {
if (!$(this).hasClass("disabled"))
- UI.BatchWorkRelationshipDialog.show();
+ UI.BatchRelationshipDialog.show(UI.checkedWorks());
});
$("#batch-create-works").click(function() {
@@ -355,11 +350,18 @@ function initButtons() {
});
$("#content").on("click", "span.add-rel", function(event) {
- UI.AddDialog.show({source: ko.dataFor(this), target: Util.tempEntity("artist")}, event.pageX, event.pageY);
+ var source = ko.dataFor(this);
+ UI.AddDialog.show({
+ entity: [RE.Entity({type: "artist"}), source],
+ source: source,
+ posx: event.pageX,
+ posy: event.pageY
+ });
});
$("#content").on("click", "span.relate-work", function() {
- UI.AddDialog.show({source: ko.dataFor(this), target: Util.tempEntity("work")});
+ var source = ko.dataFor(this), target = RE.Entity({type: "work", name: source.name});
+ UI.AddDialog.show({entity: [source, target], source: source});
});
$("#content").on("click", "span.remove-button", function() {
@@ -372,15 +374,24 @@ function initButtons() {
return;
}
if (action == "remove") newAction = "";
- if (action == "edit" && newAction == "remove") relationship.reset();
+
+ if (action == "edit" && newAction == "remove")
+ relationship.fromJS(relationship.original_fields);
+
relationship.action(newAction);
});
$("#content").on("click", "span.link-phrase", function(event) {
- var relationship = ko.dataFor(this);
+ var relationship = ko.dataFor(this),
+ source = ko.dataFor(this.parentNode.parentNode);
if (relationship.action() != "remove")
- UI.EditDialog.show(relationship, event.pageX, event.pageY);
+ UI.EditDialog.show({
+ relationship: relationship,
+ source: source,
+ posx: event.pageX,
+ posy: event.pageY
+ });
});
}
diff --git a/root/static/scripts/relationship-editor/Util.js b/root/static/scripts/relationship-editor/Util.js
index f33d940..8967ab0 100644
--- a/root/static/scripts/relationship-editor/Util.js
+++ b/root/static/scripts/relationship-editor/Util.js
@@ -19,7 +19,7 @@
MB.RelationshipEditor = (function(RE) {
-var Util = RE.Util = RE.Util || {}, originalFields = {};
+var Util = RE.Util = RE.Util || {};
Util.init = function(typeInfo, attrInfo) {
@@ -35,11 +35,6 @@ Util.init = function(typeInfo, attrInfo) {
if (info = findItem(typeInfo[key], linkType)) return info;
});
- Util.types = _.memoize(function(linkType) {
- for (var key in typeInfo)
- if (findItem(typeInfo[key], linkType)) return key;
- });
-
Util.typeInfoByEntities = function(types) {return typeInfo[types]};
Util.attrRoot = function(name) {return attrInfo[name]};
@@ -50,59 +45,30 @@ Util.init = function(typeInfo, attrInfo) {
Util.parseRelationships = function(source) {
- var result = [];
-
- if (source.relationships) $.each(source.relationships, function(target_type, rel_types) {
+ if (source.relationships) _.each(source.relationships, function(rel_types, target_type) {
if (source.type == "work" && target_type == "recording") return;
if (target_type == "url") return; // no url support yet
- $.each(rel_types, function(rel_type, rels) {
-
+ _.each(rel_types, function(rels, rel_type) {
for (var i = 0, obj; obj = rels[i]; i++) {
- var target = obj.target;
- result.push(parseRelationship(obj, source, target_type));
- result.push.apply(result, Util.parseRelationships(target));
- }
- });
- });
- return result;
-};
+ var target = obj.target, relationship, type, orig;
+ obj.attrs = obj.attributes;
+ delete obj.attributes;
+ delete obj.target;
-var parseRelationship = _.memoize(function(obj, source, target_type) {
- var target, type = RE.Util.types(obj.link_type),
- orig = originalFields[type] = originalFields[type] || {};
+ obj.entity = [RE.Entity(source), RE.Entity(target, target_type)];
+ if (obj.direction == "backward") obj.entity.reverse();
+ obj.period = {begin_date: obj.begin_date, end_date: obj.end_date, ended: obj.ended};
- Util.attrsForLinkType(obj.link_type, function(attr) {
- var name = attr.name;
- obj.attributes[name] = Util.convertAttr(attr, obj.attributes[name]);
- });
+ relationship = RE.Relationship(obj);
+ if (!relationship.visible) relationship.show();
- obj.begin_date = Util.parseDate(obj.begin_date || "");
- obj.end_date = Util.parseDate(obj.end_date || "");
- obj.ended = Boolean(obj.ended);
- obj.backward = (obj.direction == "backward");
-
- orig = orig[obj.id] = $.extend(true, {}, obj);
- orig.target.type = target_type;
- orig.target = RE.Entity(orig.target);
- orig.attributes = ko.toJS(obj.attributes);
-
- target = obj.target;
- target.type = target_type;
-
- if (target_type == "url") {
- target.name = target.url;
- delete target.url;
- }
- obj.source = RE.Entity(source);
- obj.target = RE.Entity(target);
-
- return RE.Relationship(obj, false, true);
-
-}, function(obj, source, target_type) {
- return [source.type, target_type, obj.id].join("-");
-});
+ Util.parseRelationships(target);
+ }
+ });
+ });
+};
var dateRegex = /^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/;
@@ -138,46 +104,6 @@ Util.isMBID = function(str) {
return MBIDRegex.test(str);
};
-
-Util.tempEntity = function(type) {
- var id = _.uniqueId("new-");
- return RE.Entity({type: type, id: id, gid: id});
-};
-
-
-Util.convertAttr = function(root, value) {
- if (root.children) {
- if (!_.isArray(value)) value = [value];
-
- return (_.chain(value)
- .map(function(n) {return parseInt(n, 10)})
- .compact().uniq().value()
- .sort(function(a, b) {return a - b}));
- } else {
- return Boolean(_.isNumber(value) ? parseInt(value, 10) : value);
- }
-};
-
-
-Util.attrsForLinkType = function(linkType, callback) {
- var typeInfo = Util.typeInfo(linkType);
- if (!typeInfo || !typeInfo.attrs) return {};
-
- $.each(typeInfo.attrs, function(id, info) {
- callback(Util.attrInfo(id));
- });
-};
-
-
-Util.originalFields = function(relationship, field) {
- var type = relationship.type.peek(), fields;
-
- if (!(fields = originalFields[type])) return null;
- if (!(fields = fields[relationship.id])) return null;
-
- return field ? fields[field] : fields;
-};
-
// Attempts to merge two dates, otherwise returns false if they conflict.
Util.mergeDates = function(a, b) {
diff --git a/root/static/scripts/tests/RelationshipEditor.js b/root/static/scripts/tests/RelationshipEditor.js
index 2163b87..9a93408 100644
--- a/root/static/scripts/tests/RelationshipEditor.js
+++ b/root/static/scripts/tests/RelationshipEditor.js
@@ -109,6 +109,22 @@ var typeInfo = {
"phrase": "medley of",
"descr": 1
}
+ ],
+ "recording-recording": [
+ {
+ "children": [
+ {
+ "attrs": {"1": [0, 1]},
+ "descr": 1,
+ "id": 231,
+ "phrase": "{additional} samples",
+ "reverse_phrase": "{additional:additionally} sampled by"
+ }
+ ],
+ "id": 234,
+ "phrase": "remixes",
+ "reverse_phrase": "remixes"
+ }
]
};
@@ -281,13 +297,13 @@ var testRelease = {
$.extend(MB.text = MB.text || {}, {
Entity: {
- artist: "Artist",
- label: "Label",
- recording: "Recording",
- release: "Release",
- "release-group": "Release group",
- url: "URL",
- work: "Work",
+ artist: "Artist",
+ label: "Label",
+ recording: "Recording",
+ release: "Release",
+ release_group: "Release group",
+ url: "URL",
+ work: "Work",
},
AttributeNotSupported: "This attribute is not supported for the selected relationship type.",
AttributeTooMany: "This attribute can only be specified {max} times. You specified {n}.",
@@ -295,10 +311,9 @@ $.extend(MB.text = MB.text || {}, {
InvalidDate: "The date you've entered is not valid.",
InvalidEndDate: "The end date cannot preceed the begin date.",
InvalidValue: "The value you've entered is not valid.",
- RequiredField: "Required field.",
+ RequiredField: "Required field."
});
-MB.text.Date = {from: "from", until: "until", on: "on"};
MB.tests.RelationshipEditor.Util = function() {
QUnit.module("Relationship editor");
@@ -307,67 +322,6 @@ MB.tests.RelationshipEditor.Util = function() {
var RE = MB.RelationshipEditor, Util = RE.Util;
var tests = [
- // artist-recording
- {
- source: Util.tempEntity("recording"),
- target: Util.tempEntity("artist"),
- expected: function() {
- return [this.target, this.source];
- }
- },
- // recording-release
- {
- source: Util.tempEntity("recording"),
- target: Util.tempEntity("release"),
- expected: function() {
- return [this.source, this.target];
- }
- },
- {
- source: Util.tempEntity("recording"),
- target: Util.tempEntity("release"),
- backward: true,
- expected: function() {
- return [this.target, this.source];
- }
- },
- // recording-work
- {
- source: Util.tempEntity("recording"),
- target: Util.tempEntity("work"),
- expected: function() {
- return [this.source, this.target];
- }
- },
- {
- source: Util.tempEntity("recording"),
- target: Util.tempEntity("work"),
- backward: true,
- expected: function() {
- return [this.target, this.source];
- }
- },
- // work-work
- {
- source: Util.tempEntity("work"),
- target: Util.tempEntity("work"),
- backward: true,
- expected: function() {
- return [this.target, this.source];
- }
- }
- ];
-
- $.each(tests, function(i, test) {
- var obj = {source: test.source, target: test.target}, relationship;
- if (test.backward) obj.backward = true;
- relationship = RE.Relationship(obj);
-
- QUnit.deepEqual(relationship.entity(), test.expected(),
- relationship.type() + ", " + relationship.backward());
- });
-
- tests = [
{date: "", expected: {
year: null, month: null, day: null}
},
@@ -404,7 +358,7 @@ MB.tests.RelationshipEditor.Util = function() {
];
$.each(tests, function(i, test) {
- var result = RE.Util.convertAttr(test.root, test.value);
+ var result = new RE.Fields.Attribute(test.root, test.value)();
QUnit.deepEqual(result, test.expected, String(test.value));
});
@@ -448,13 +402,14 @@ MB.tests.RelationshipEditor.Fields = function() {
});
var relationship = RE.Relationship({
- source: RE.Util.tempEntity("recording"),
- target: RE.Util.tempEntity("work"),
+ entity: [RE.Entity({type: "recording"}), RE.Entity({type: "work"})],
action: "add",
link_type: 278
- }, false);
+ });
+
+ relationship.show();
- field = new Fields.Attributes(relationship);
+ field = relationship.attrs;
tests = [
{
@@ -477,23 +432,19 @@ MB.tests.RelationshipEditor.Fields = function() {
QUnit.deepEqual(result, test.expected, JSON.stringify(test.input));
});
- // test Fields.Target
+ // test Fields.Entity
- field = relationship.target;
+ field = relationship.entity[1];
oldTarget = field.peek();
- newTarget = RE.Util.tempEntity("work");
+ newTarget = RE.Entity({type: "work"});
- QUnit.equal(oldTarget.refcount, 1, oldTarget.id + " refcount");
- QUnit.equal(oldTarget.performanceRefcount, 1, oldTarget.id + " performanceRefcount");
- QUnit.equal(newTarget.refcount, 0, newTarget.id + " refcount");
- QUnit.equal(newTarget.performanceRefcount, 0, newTarget.id + " performanceRefcount");
+ QUnit.equal(oldTarget.performanceCount, 1, oldTarget.id + " performanceCount");
+ QUnit.equal(newTarget.performanceCount, 0, newTarget.id + " performanceCount");
field(newTarget);
- QUnit.equal(oldTarget.refcount, 0, oldTarget.id + " refcount");
- QUnit.equal(oldTarget.performanceRefcount, 0, oldTarget.id + " performanceRefcount");
- QUnit.equal(newTarget.refcount, 1, newTarget.id + " refcount");
- QUnit.equal(newTarget.performanceRefcount, 1, newTarget.id + " performanceRefcount");
+ QUnit.equal(oldTarget.performanceCount, 0, oldTarget.id + " performanceCount");
+ QUnit.equal(newTarget.performanceCount, 1, newTarget.id + " performanceCount");
// test Fields.PartialDate
@@ -518,15 +469,15 @@ MB.tests.RelationshipEditor.Relationship = function() {
QUnit.test('Relationship', function() {
var RE = MB.RelationshipEditor,
- source = RE.Util.tempEntity("recording"),
- target = RE.Util.tempEntity("artist");
+ source = RE.Entity({type: "recording"}), target = RE.Entity({type: "artist"});
var relationship = RE.Relationship({
- source: source,
- target: target,
+ entity: [target, source],
action: "add",
link_type: 148
- }, false);
+ });
+
+ relationship.show();
// link phrase construction
@@ -554,9 +505,10 @@ MB.tests.RelationshipEditor.Relationship = function() {
$.each(tests, function(i, test) {
relationship.link_type(test.linkType);
- relationship.backward(test.backward);
- relationship.attributes(test.attrs);
- var result = relationship.buildLinkPhrase();
+ relationship.attrs(test.attrs);
+
+ var result = relationship.linkPhrase(test.backward
+ ? relationship.entity[1]() : relationship.entity[0]());
QUnit.equal(result, test.expected, [test.linkType, JSON.stringify(test.attrs)].join(", "));
});
@@ -597,7 +549,8 @@ MB.tests.RelationshipEditor.Relationship = function() {
];
$.each(tests, function(i, test) {
- var a = relationship.begin_date(), b = relationship.end_date();
+ var a = relationship.period.begin_date(),
+ b = relationship.period.end_date();
a.year(test.begin_date.year);
a.month(test.begin_date.month);
@@ -607,7 +560,7 @@ MB.tests.RelationshipEditor.Relationship = function() {
b.month(test.end_date.month);
b.day(test.end_date.day);
- relationship.ended(test.ended);
+ relationship.period.ended(test.ended);
var result = relationship.renderDate();
QUnit.equal(result, test.expected, [
@@ -619,39 +572,36 @@ MB.tests.RelationshipEditor.Relationship = function() {
// test errors
- // the target has an invalid gid to start with, so errorCount = 1
- QUnit.equal(relationship.errorCount, 1, "relationship.errorCount");
-
- // backward must be either true or false
- relationship.backward("foo");
+ // the source/target have invalid gids to start with, so errorCount = 2
QUnit.equal(relationship.errorCount, 2, "relationship.errorCount");
// ended must be boolean
- relationship.ended(null);
+ relationship.period.ended(null);
QUnit.equal(relationship.errorCount, 3, "relationship.errorCount");
// date must exist
- relationship.begin_date("2001-01-32");
+ relationship.period.begin_date("2001-01-32");
QUnit.equal(relationship.errorCount, 4, "relationship.errorCount");
- relationship.begin_date("2001-01-31");
+ relationship.period.begin_date("2001-01-31");
QUnit.equal(relationship.errorCount, 3, "relationship.errorCount");
// end date must be after begin date
- relationship.end_date("2000-01-31");
+ relationship.period.end_date("2000-01-31");
QUnit.equal(relationship.errorCount, 4, "relationship.errorCount");
- relationship.end_date("2002-01-31");
+ relationship.period.end_date("2002-01-31");
QUnit.equal(relationship.errorCount, 3, "relationship.errorCount");
- relationship.target().gid = "00000000-0000-0000-0000-000000000000";
- relationship.target.notifySubscribers(relationship.target());
+ relationship.entity[0]().gid = "00000000-0000-0000-0000-000000000001";
+ relationship.entity[0].notifySubscribers(relationship.entity[0]());
QUnit.equal(relationship.errorCount, 2, "relationship.errorCount");
- relationship.backward(true);
+ relationship.entity[1]().gid = "00000000-0000-0000-0000-000000000002";
+ relationship.entity[1].notifySubscribers(relationship.entity[1]());
QUnit.equal(relationship.errorCount, 1, "relationship.errorCount");
- relationship.ended(true);
+ relationship.period.ended(true);
QUnit.equal(relationship.errorCount, 0, "relationship.errorCount");
});
};
@@ -663,84 +613,79 @@ MB.tests.RelationshipEditor.Entity = function() {
QUnit.test('Entity', function() {
var RE = MB.RelationshipEditor,
- entity = RE.Util.tempEntity("artist");
+ source = RE.Entity({type: "recording", name: "a recording"}),
+ target = RE.Entity({type: "artist", name: "foo", sortname: "bar"});
QUnit.equal(
- entity.rendering(),
- _.sprintf('<a href="/artist/%s" target="_blank" />', entity.gid),
- "artist link"
+ source.rendering,
+ _.sprintf('<a href="/recording/%s" target="_blank">a recording</a>', source.gid),
+ "recording link"
);
- entity.sortname("foo");
-
QUnit.equal(
- entity.rendering(),
- _.sprintf('<a href="/artist/%s" target="_blank" title="foo" />', entity.gid),
- "artist link w/ sortname"
+ target.rendering,
+ _.sprintf('<a href="/artist/%s" target="_blank" title="bar">foo</a>', target.gid),
+ "artist link"
);
- QUnit.equal(RE.Entity.isInstance(entity), true, "entity isInstance");
+ QUnit.equal(RE.Entity.isInstance(source), true, "entity isInstance");
QUnit.equal(RE.Entity.isInstance({}), false, "object isInstance");
var relationship = RE.Relationship({
- source: RE.Util.tempEntity("recording"),
- target: entity,
+ entity: [target, source],
action: "add",
link_type: 148,
- attributes: {instrument: [123, 277], guest: true},
- begin_date: "2001",
- end_date: ""
- }, false);
+ attrs: {instrument: [123, 277], guest: true},
+ period: {begin_date: "2001", end_date: ""}
+ });
var duplicateRelationship = RE.Relationship({
- source: relationship.source,
- target: entity,
+ entity: [target, source],
action: "add",
link_type: 148,
- attributes: {instrument: [229], solo: true},
- begin_date: "",
- end_date: "2002"
- }, false);
+ attrs: {instrument: [229], solo: true},
+ period: {begin_date: "", end_date: "2002"}
+ });
relationship.show();
duplicateRelationship.show();
- relationship.source.mergeRelationship(duplicateRelationship);
+ source.mergeRelationship(duplicateRelationship);
QUnit.deepEqual(
- ko.toJS(relationship.attributes),
+ ko.toJS(relationship.attrs),
{instrument: [229], additional: false, guest: true, solo: true},
"attributes"
);
QUnit.deepEqual(
- ko.toJS(relationship.begin_date),
+ ko.toJS(relationship.period.begin_date),
{year: 2001, month: null, day: null},
"begin date"
);
QUnit.deepEqual(
- ko.toJS(relationship.end_date),
+ ko.toJS(relationship.period.end_date),
{year: 2002, month: null, day: null},
"end date"
);
- QUnit.equal(relationship.source.relationships.indexOf(duplicateRelationship), -1,
+ QUnit.equal(source.relationships.indexOf(duplicateRelationship), -1,
"removed from source's relationships");
QUnit.equal(duplicateRelationship.removed, true,
"relationship.removed");
var notDuplicateRelationship = RE.Relationship({
- source: relationship.source,
- target: entity,
+ entity: [target, source],
action: "add",
link_type: 148,
- begin_date: "2003",
- end_date: "2004"
- }, false);
+ period: {begin_date: "2003", end_date: "2004"}
+ });
+
+ notDuplicateRelationship.show();
- QUnit.equal(relationship.source.mergeRelationship(notDuplicateRelationship),
+ QUnit.equal(source.mergeRelationship(notDuplicateRelationship),
false, "different dates -> not merged")
});
};
@@ -761,7 +706,7 @@ MB.tests.RelationshipEditor.RelationshipEditor = function() {
var recording = recordings[0];
QUnit.equal(recording.number, "A", "recording number");
QUnit.equal(recording.position, 1, "recording position");
- QUnit.equal(recording.name(), "Love Me Do", "recording name");
+ QUnit.equal(recording.name, "Love Me Do", "recording name");
QUnit.equal(recording.id, 6393661, "recording id");
QUnit.equal(recording.gid, "87ec065e-f139-41b9-b3b9-f746addf5b1e", "recording gid");
});
@@ -773,27 +718,91 @@ MB.tests.RelationshipEditor.Dialog = function() {
QUnit.test('Dialog', function() {
- var RE = MB.RelationshipEditor, UI = RE.UI, vm = RE.releaseViewModel,
- recordings = vm.media()[0].recordings(), source = recordings[0],
- target = RE.Util.tempEntity("artist");
+ var RE = MB.RelationshipEditor, UI = RE.UI, Util = RE.Util,
+ vm = RE.releaseViewModel,
+ recordings = vm.media()[0].recordings(),
+ source = recordings[0],
+ target = RE.Entity({type: "artist", gid: "00000000-0000-0000-0000-000000000003"});
- // AddDialog
+ UI.Dialog.resize = function() {};
+
+ var tests = [
+ {
+ entity: [RE.Entity({type: "recording"}), RE.Entity({type: "release"})],
+ backward: true,
+ source: function() {return this.entity[1];},
+ target: function() {return this.entity[0];}
+ },
+ {
+ entity: [RE.Entity({type: "recording"}), RE.Entity({type: "release"})],
+ backward: false,
+ source: function() {return this.entity[0];},
+ target: function() {return this.entity[1];}
+ }
+ ];
+
+ $.each(tests, function(i, test) {
+ UI.AddDialog.show({entity: test.entity, source: test.source()});
+
+ QUnit.equal(UI.Dialog.backward(), test.backward,
+ "entities should be backward: " + test.backward);
+
+ QUnit.equal(UI.Dialog.source, test.source(),
+ "source should be entity[" + (test.backward ? "1" : "0") + "]");
- target.gid = "00000000-0000-0000-0000-000000000000";
- target.name("foo");
+ QUnit.equal(UI.Dialog.target, test.target(),
+ "target should be entity[" + (test.backward ? "0" : "1") + "]");
- UI.AddDialog.show({source: source, target: target});
+ UI.AddDialog.hide();
+ });
+
+ // AddDialog
+
+ UI.AddDialog.show({entity: [target, source], source: source});
var relationship = UI.Dialog.relationship();
relationship.link_type(148);
- relationship.attributes({instrument: [229]});
+ relationship.attrs({instrument: [229]});
UI.AddDialog.accept();
QUnit.equal(source.relationships()[0], relationship, "AddDialog");
+ // AddDialog - relationship between recordings on same release (MBS-5389)
+
+ UI.AddDialog.show({entity: [recordings[0], recordings[1]], source: recordings[1]});
+ relationship = UI.Dialog.relationship();
+ relationship.link_type(231);
+
+ QUnit.equal(UI.Dialog.sourceField(), relationship.entity[1], "AddDialog sourceField");
+ QUnit.equal(UI.Dialog.targetField(), relationship.entity[0], "AddDialog targetField");
+ QUnit.equal(UI.Dialog.source, recordings[1], "AddDialog source");
+ QUnit.equal(UI.Dialog.target, recordings[0], "AddDialog target");
+ QUnit.equal(UI.Dialog.backward(), true, "AddDialog: relationship is backward");
+
+ UI.Dialog.changeDirection();
+
+ QUnit.equal(UI.Dialog.sourceField(), relationship.entity[0], "AddDialog sourceField");
+ QUnit.equal(UI.Dialog.targetField(), relationship.entity[1], "AddDialog targetField");
+ // source and target should stay the same
+ QUnit.equal(UI.Dialog.source, recordings[1], "AddDialog source");
+ QUnit.equal(UI.Dialog.target, recordings[0], "AddDialog target");
+ QUnit.equal(UI.Dialog.backward(), false, "AddDialog: relationship is not backward");
+
+ UI.AddDialog.accept();
+
+ QUnit.equal(recordings[0].relationships()[1], relationship, "relationship added to recording 0");
+ QUnit.equal(recordings[1].relationships()[0], relationship, "relationship added to recording 1");
+
+ relationship.remove();
+
+ QUnit.equal(recordings[0].relationships()[1], undefined, "relationship removed from recording 0");
+ QUnit.equal(recordings[1].relationships()[0], undefined, "relationship removed from recording 1");
+
// EditDialog
- UI.EditDialog.show(relationship);
- var dialogAttrs = UI.Dialog.attributes();
+ relationship = source.relationships()[0];
+
+ UI.EditDialog.show({relationship: relationship, source: source});
+ var dialogAttrs = UI.Dialog.attrs();
var solo = _.find(dialogAttrs, function(attr) {
return attr.data.name == "solo";
@@ -803,12 +812,12 @@ MB.tests.RelationshipEditor.Dialog = function() {
UI.EditDialog.accept();
QUnit.deepEqual(
- ko.toJS(relationship.attributes),
+ ko.toJS(relationship.attrs),
{instrument: [229], additional: false, guest: false, solo: true},
"EditDialog"
);
- UI.EditDialog.show(relationship);
+ UI.EditDialog.show({relationship: relationship, source: source});
var instrument = _.find(dialogAttrs, function(attr) {
return attr.data.name == "instrument";
@@ -816,72 +825,55 @@ MB.tests.RelationshipEditor.Dialog = function() {
instrument.value([229, 277]);
- var newTarget = RE.Util.tempEntity("artist");
- relationship.target(newTarget);
- QUnit.equal(newTarget.refcount, 1, "newTarget refcount");
+ var newTarget = RE.Entity({type: "artist"});
+ UI.Dialog.targetField()(newTarget);
// cancel should revert the change
UI.EditDialog.hide();
- QUnit.deepEqual(relationship.attributes().instrument(), [229], "attributes changed back");
- QUnit.equal(relationship.target(), target, "target changed back");
- QUnit.equal(newTarget.refcount, 0, "newTarget refcount");
+ QUnit.deepEqual(relationship.attrs().instrument(), [229], "attributes changed back");
+ QUnit.equal(relationship.entity[0](), target, "target changed back");
// BatchRecordingRelationshipDialog
- // XXX rewrite checkedRecordings so that batch tests work
- UI.checkedRecordings = function() {return recordings};
-
- UI.BatchRecordingRelationshipDialog.show();
+ UI.BatchRelationshipDialog.show(recordings);
- newTarget = RE.Util.tempEntity("work");
- newTarget.gid = "00000000-0000-0000-0000-000000000001";
- newTarget.name("workfoo");
+ newTarget = RE.Entity({type: "artist", gid: "00000000-0000-0000-0000-000000000004"});
relationship = UI.Dialog.relationship();
- relationship.target(newTarget);
- relationship.link_type(278);
- relationship.attributes().live(true);
+ UI.Dialog.targetField()(newTarget);
+ relationship.link_type(154);
+ relationship.attrs().additional(true);
- UI.BatchRecordingRelationshipDialog.accept();
+ UI.BatchRelationshipDialog.accept();
- for (var i = 0; i <= 1; i++) {
- var relationships = recordings[i].performanceRelationships();
- QUnit.equal(relationships[0].target(), newTarget, "recording " + i + " target");
- QUnit.deepEqual(
- ko.toJS(relationships[0].attributes),
- {live: true, partial: false, instrumental: false, cover: false},
- "recording " + i + " attributes"
- );
- }
+ var attrs = {additional: true, instrument: []},
+ relationships = recordings[0].relationships();
- // BatchWorkRelationshipDialog
+ QUnit.equal(relationships[1].entity[0](), newTarget, "recording 0 target");
+ QUnit.deepEqual(ko.toJS(relationships[1].attrs), attrs, "recording 0 attributes");
- var works = [newTarget];
- UI.checkedWorks = function() {return works};
+ relationships = recordings[1].relationships();
+ QUnit.equal(relationships[0].entity[0](), newTarget, "recording 0 target");
+ QUnit.deepEqual(ko.toJS(relationships[0].attrs), attrs, "recording 0 attributes");
- UI.BatchWorkRelationshipDialog.show();
+ // BatchWorkRelationshipDialog
- newTarget = RE.Util.tempEntity("artist");
- newTarget.gid = "00000000-0000-0000-0000-000000000002";
- newTarget.name("writer");
+ var works = [RE.Entity({type: "work", gid: "00000000-0000-0000-0000-000000000005"})];
+ UI.BatchRelationshipDialog.show(works);
relationship = UI.Dialog.relationship();
- relationship.target(newTarget);
+ relationship.entity[0](newTarget);
relationship.link_type(167);
- relationship.attributes().additional(true);
+ relationship.attrs().additional(true);
- UI.BatchWorkRelationshipDialog.accept();
+ UI.BatchRelationshipDialog.accept();
- var relationships = works[0].relationships();
- QUnit.equal(relationships[0].target(), newTarget, "work target");
- QUnit.deepEqual(
- ko.toJS(relationships[0].attributes),
- {additional: true},
- "work attributes"
- );
+ relationships = works[0].relationships();
+ QUnit.equal(relationships[0].entity[0](), newTarget, "work target");
+ QUnit.deepEqual(ko.toJS(relationships[0].attrs), {additional: true}, "work attributes");
});
};
diff --git a/root/static/scripts/tests/all.html b/root/static/scripts/tests/all.html
index b07c3ff..851d815 100644
--- a/root/static/scripts/tests/all.html
+++ b/root/static/scripts/tests/all.html
@@ -11,7 +11,6 @@
<script type="text/javascript" charset="utf-8" src="../../lib/qunit/qunit/qunit.js"></script>
<script type="text/javascript" charset="utf-8" src="../../lib/qunit-tap/lib/qunit-tap.js"></script>
<script type="text/javascript" charset="utf-8" src="../../lib/knockout/knockout-latest.debug.js"></script>
- <script type="text/javascript" charset="utf-8" src="../../lib/knockout/knockout.mapping-latest.debug.js"></script>
<script type="text/javascript" charset="utf-8" src="../common/MB.js"></script>
<script type="text/javascript" charset="utf-8" src="../common/MB/utility.js"></script>
<script type="text/javascript" charset="utf-8" src="../common/MB/html.js"></script>
-----------------------------------------------------------------------
hooks/post-receive
--
mb_server