';
694 | }
695 | runLoggingCallbacks( 'log', QUnit, details );
696 | config.current.assertions.push({
697 | result: false,
698 | message: output
699 | });
700 | },
701 |
702 | url: function( params ) {
703 | params = extend( extend( {}, QUnit.urlParams ), params );
704 | var querystring = "?",
705 | key;
706 | for ( key in params ) {
707 | if ( !hasOwn.call( params, key ) ) {
708 | continue;
709 | }
710 | querystring += encodeURIComponent( key ) + "=" +
711 | encodeURIComponent( params[ key ] ) + "&";
712 | }
713 | return window.location.pathname + querystring.slice( 0, -1 );
714 | },
715 |
716 | extend: extend,
717 | id: id,
718 | addEvent: addEvent
719 | });
720 |
721 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later
722 | //Doing this allows us to tell if the following methods have been overwritten on the actual
723 | //QUnit object, which is a deprecated way of using the callbacks.
724 | extend(QUnit.constructor.prototype, {
725 | // Logging callbacks; all receive a single argument with the listed properties
726 | // run test/logs.html for any related changes
727 | begin: registerLoggingCallback('begin'),
728 | // done: { failed, passed, total, runtime }
729 | done: registerLoggingCallback('done'),
730 | // log: { result, actual, expected, message }
731 | log: registerLoggingCallback('log'),
732 | // testStart: { name }
733 | testStart: registerLoggingCallback('testStart'),
734 | // testDone: { name, failed, passed, total }
735 | testDone: registerLoggingCallback('testDone'),
736 | // moduleStart: { name }
737 | moduleStart: registerLoggingCallback('moduleStart'),
738 | // moduleDone: { name, failed, passed, total }
739 | moduleDone: registerLoggingCallback('moduleDone')
740 | });
741 |
742 | if ( typeof document === "undefined" || document.readyState === "complete" ) {
743 | config.autorun = true;
744 | }
745 |
746 | QUnit.load = function() {
747 | runLoggingCallbacks( 'begin', QUnit, {} );
748 |
749 | // Initialize the config, saving the execution queue
750 | var oldconfig = extend({}, config);
751 | QUnit.init();
752 | extend(config, oldconfig);
753 |
754 | config.blocking = false;
755 |
756 | var urlConfigHtml = '', len = config.urlConfig.length;
757 | for ( var i = 0, val; i < len; i++ ) {
758 | val = config.urlConfig[i];
759 | config[val] = QUnit.urlParams[val];
760 | urlConfigHtml += '';
761 | }
762 |
763 | var userAgent = id("qunit-userAgent");
764 | if ( userAgent ) {
765 | userAgent.innerHTML = navigator.userAgent;
766 | }
767 | var banner = id("qunit-header");
768 | if ( banner ) {
769 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml;
770 | addEvent( banner, "change", function( event ) {
771 | var params = {};
772 | params[ event.target.name ] = event.target.checked ? true : undefined;
773 | window.location = QUnit.url( params );
774 | });
775 | }
776 |
777 | var toolbar = id("qunit-testrunner-toolbar");
778 | if ( toolbar ) {
779 | var filter = document.createElement("input");
780 | filter.type = "checkbox";
781 | filter.id = "qunit-filter-pass";
782 | addEvent( filter, "click", function() {
783 | var ol = document.getElementById("qunit-tests");
784 | if ( filter.checked ) {
785 | ol.className = ol.className + " hidepass";
786 | } else {
787 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " ";
788 | ol.className = tmp.replace(/ hidepass /, " ");
789 | }
790 | if ( defined.sessionStorage ) {
791 | if (filter.checked) {
792 | sessionStorage.setItem("qunit-filter-passed-tests", "true");
793 | } else {
794 | sessionStorage.removeItem("qunit-filter-passed-tests");
795 | }
796 | }
797 | });
798 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) {
799 | filter.checked = true;
800 | var ol = document.getElementById("qunit-tests");
801 | ol.className = ol.className + " hidepass";
802 | }
803 | toolbar.appendChild( filter );
804 |
805 | var label = document.createElement("label");
806 | label.setAttribute("for", "qunit-filter-pass");
807 | label.innerHTML = "Hide passed tests";
808 | toolbar.appendChild( label );
809 | }
810 |
811 | var main = id('qunit-fixture');
812 | if ( main ) {
813 | config.fixture = main.innerHTML;
814 | }
815 |
816 | if (config.autostart) {
817 | QUnit.start();
818 | }
819 | };
820 |
821 | addEvent(window, "load", QUnit.load);
822 |
823 | // addEvent(window, "error") gives us a useless event object
824 | window.onerror = function( message, file, line ) {
825 | if ( QUnit.config.current ) {
826 | QUnit.pushFailure( message, file + ":" + line );
827 | } else {
828 | QUnit.test( "global failure", function() {
829 | QUnit.pushFailure( message, file + ":" + line );
830 | });
831 | }
832 | };
833 |
834 | function done() {
835 | config.autorun = true;
836 |
837 | // Log the last module results
838 | if ( config.currentModule ) {
839 | runLoggingCallbacks( 'moduleDone', QUnit, {
840 | name: config.currentModule,
841 | failed: config.moduleStats.bad,
842 | passed: config.moduleStats.all - config.moduleStats.bad,
843 | total: config.moduleStats.all
844 | } );
845 | }
846 |
847 | var banner = id("qunit-banner"),
848 | tests = id("qunit-tests"),
849 | runtime = +new Date() - config.started,
850 | passed = config.stats.all - config.stats.bad,
851 | html = [
852 | 'Tests completed in ',
853 | runtime,
854 | ' milliseconds. ',
855 | '',
856 | passed,
857 | ' tests of ',
858 | config.stats.all,
859 | ' passed, ',
860 | config.stats.bad,
861 | ' failed.'
862 | ].join('');
863 |
864 | if ( banner ) {
865 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass");
866 | }
867 |
868 | if ( tests ) {
869 | id( "qunit-testresult" ).innerHTML = html;
870 | }
871 |
872 | if ( config.altertitle && typeof document !== "undefined" && document.title ) {
873 | // show ✖ for good, ✔ for bad suite result in title
874 | // use escape sequences in case file gets loaded with non-utf-8-charset
875 | document.title = [
876 | (config.stats.bad ? "\u2716" : "\u2714"),
877 | document.title.replace(/^[\u2714\u2716] /i, "")
878 | ].join(" ");
879 | }
880 |
881 | // clear own sessionStorage items if all tests passed
882 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) {
883 | var key;
884 | for ( var i = 0; i < sessionStorage.length; i++ ) {
885 | key = sessionStorage.key( i++ );
886 | if ( key.indexOf("qunit-test-") === 0 ) {
887 | sessionStorage.removeItem( key );
888 | }
889 | }
890 | }
891 |
892 | runLoggingCallbacks( 'done', QUnit, {
893 | failed: config.stats.bad,
894 | passed: passed,
895 | total: config.stats.all,
896 | runtime: runtime
897 | } );
898 | }
899 |
900 | function validTest( name ) {
901 | var filter = config.filter,
902 | run = false;
903 |
904 | if ( !filter ) {
905 | return true;
906 | }
907 |
908 | var not = filter.charAt( 0 ) === "!";
909 | if ( not ) {
910 | filter = filter.slice( 1 );
911 | }
912 |
913 | if ( name.indexOf( filter ) !== -1 ) {
914 | return !not;
915 | }
916 |
917 | if ( not ) {
918 | run = true;
919 | }
920 |
921 | return run;
922 | }
923 |
924 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions)
925 | // Later Safari and IE10 are supposed to support error.stack as well
926 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
927 | function extractStacktrace( e, offset ) {
928 | offset = offset || 3;
929 | if (e.stacktrace) {
930 | // Opera
931 | return e.stacktrace.split("\n")[offset + 3];
932 | } else if (e.stack) {
933 | // Firefox, Chrome
934 | var stack = e.stack.split("\n");
935 | if (/^error$/i.test(stack[0])) {
936 | stack.shift();
937 | }
938 | return stack[offset];
939 | } else if (e.sourceURL) {
940 | // Safari, PhantomJS
941 | // hopefully one day Safari provides actual stacktraces
942 | // exclude useless self-reference for generated Error objects
943 | if ( /qunit.js$/.test( e.sourceURL ) ) {
944 | return;
945 | }
946 | // for actual exceptions, this is useful
947 | return e.sourceURL + ":" + e.line;
948 | }
949 | }
950 | function sourceFromStacktrace(offset) {
951 | try {
952 | throw new Error();
953 | } catch ( e ) {
954 | return extractStacktrace( e, offset );
955 | }
956 | }
957 |
958 | function escapeInnerText(s) {
959 | if (!s) {
960 | return "";
961 | }
962 | s = s + "";
963 | return s.replace(/[\&<>]/g, function(s) {
964 | switch(s) {
965 | case "&": return "&";
966 | case "<": return "<";
967 | case ">": return ">";
968 | default: return s;
969 | }
970 | });
971 | }
972 |
973 | function synchronize( callback, last ) {
974 | config.queue.push( callback );
975 |
976 | if ( config.autorun && !config.blocking ) {
977 | process(last);
978 | }
979 | }
980 |
981 | function process( last ) {
982 | function next() {
983 | process( last );
984 | }
985 | var start = new Date().getTime();
986 | config.depth = config.depth ? config.depth + 1 : 1;
987 |
988 | while ( config.queue.length && !config.blocking ) {
989 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) {
990 | config.queue.shift()();
991 | } else {
992 | window.setTimeout( next, 13 );
993 | break;
994 | }
995 | }
996 | config.depth--;
997 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
998 | done();
999 | }
1000 | }
1001 |
1002 | function saveGlobal() {
1003 | config.pollution = [];
1004 |
1005 | if ( config.noglobals ) {
1006 | for ( var key in window ) {
1007 | if ( !hasOwn.call( window, key ) ) {
1008 | continue;
1009 | }
1010 | config.pollution.push( key );
1011 | }
1012 | }
1013 | }
1014 |
1015 | function checkPollution( name ) {
1016 | var old = config.pollution;
1017 | saveGlobal();
1018 |
1019 | var newGlobals = diff( config.pollution, old );
1020 | if ( newGlobals.length > 0 ) {
1021 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") );
1022 | }
1023 |
1024 | var deletedGlobals = diff( old, config.pollution );
1025 | if ( deletedGlobals.length > 0 ) {
1026 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") );
1027 | }
1028 | }
1029 |
1030 | // returns a new Array with the elements that are in a but not in b
1031 | function diff( a, b ) {
1032 | var result = a.slice();
1033 | for ( var i = 0; i < result.length; i++ ) {
1034 | for ( var j = 0; j < b.length; j++ ) {
1035 | if ( result[i] === b[j] ) {
1036 | result.splice(i, 1);
1037 | i--;
1038 | break;
1039 | }
1040 | }
1041 | }
1042 | return result;
1043 | }
1044 |
1045 | function extend(a, b) {
1046 | for ( var prop in b ) {
1047 | if ( b[prop] === undefined ) {
1048 | delete a[prop];
1049 |
1050 | // Avoid "Member not found" error in IE8 caused by setting window.constructor
1051 | } else if ( prop !== "constructor" || a !== window ) {
1052 | a[prop] = b[prop];
1053 | }
1054 | }
1055 |
1056 | return a;
1057 | }
1058 |
1059 | function addEvent(elem, type, fn) {
1060 | if ( elem.addEventListener ) {
1061 | elem.addEventListener( type, fn, false );
1062 | } else if ( elem.attachEvent ) {
1063 | elem.attachEvent( "on" + type, fn );
1064 | } else {
1065 | fn();
1066 | }
1067 | }
1068 |
1069 | function id(name) {
1070 | return !!(typeof document !== "undefined" && document && document.getElementById) &&
1071 | document.getElementById( name );
1072 | }
1073 |
1074 | function registerLoggingCallback(key){
1075 | return function(callback){
1076 | config[key].push( callback );
1077 | };
1078 | }
1079 |
1080 | // Supports deprecated method of completely overwriting logging callbacks
1081 | function runLoggingCallbacks(key, scope, args) {
1082 | //debugger;
1083 | var callbacks;
1084 | if ( QUnit.hasOwnProperty(key) ) {
1085 | QUnit[key].call(scope, args);
1086 | } else {
1087 | callbacks = config[key];
1088 | for( var i = 0; i < callbacks.length; i++ ) {
1089 | callbacks[i].call( scope, args );
1090 | }
1091 | }
1092 | }
1093 |
1094 | // Test for equality any JavaScript type.
1095 | // Author: Philippe Rathé
1096 | QUnit.equiv = (function() {
1097 |
1098 | var innerEquiv; // the real equiv function
1099 | var callers = []; // stack to decide between skip/abort functions
1100 | var parents = []; // stack to avoiding loops from circular referencing
1101 |
1102 | // Call the o related callback with the given arguments.
1103 | function bindCallbacks(o, callbacks, args) {
1104 | var prop = QUnit.objectType(o);
1105 | if (prop) {
1106 | if (QUnit.objectType(callbacks[prop]) === "function") {
1107 | return callbacks[prop].apply(callbacks, args);
1108 | } else {
1109 | return callbacks[prop]; // or undefined
1110 | }
1111 | }
1112 | }
1113 |
1114 | var getProto = Object.getPrototypeOf || function (obj) {
1115 | return obj.__proto__;
1116 | };
1117 |
1118 | var callbacks = (function () {
1119 |
1120 | // for string, boolean, number and null
1121 | function useStrictEquality(b, a) {
1122 | if (b instanceof a.constructor || a instanceof b.constructor) {
1123 | // to catch short annotaion VS 'new' annotation of a
1124 | // declaration
1125 | // e.g. var i = 1;
1126 | // var j = new Number(1);
1127 | return a == b;
1128 | } else {
1129 | return a === b;
1130 | }
1131 | }
1132 |
1133 | return {
1134 | "string" : useStrictEquality,
1135 | "boolean" : useStrictEquality,
1136 | "number" : useStrictEquality,
1137 | "null" : useStrictEquality,
1138 | "undefined" : useStrictEquality,
1139 |
1140 | "nan" : function(b) {
1141 | return isNaN(b);
1142 | },
1143 |
1144 | "date" : function(b, a) {
1145 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf();
1146 | },
1147 |
1148 | "regexp" : function(b, a) {
1149 | return QUnit.objectType(b) === "regexp" &&
1150 | // the regex itself
1151 | a.source === b.source &&
1152 | // and its modifers
1153 | a.global === b.global &&
1154 | // (gmi) ...
1155 | a.ignoreCase === b.ignoreCase &&
1156 | a.multiline === b.multiline;
1157 | },
1158 |
1159 | // - skip when the property is a method of an instance (OOP)
1160 | // - abort otherwise,
1161 | // initial === would have catch identical references anyway
1162 | "function" : function() {
1163 | var caller = callers[callers.length - 1];
1164 | return caller !== Object && typeof caller !== "undefined";
1165 | },
1166 |
1167 | "array" : function(b, a) {
1168 | var i, j, loop;
1169 | var len;
1170 |
1171 | // b could be an object literal here
1172 | if (QUnit.objectType(b) !== "array") {
1173 | return false;
1174 | }
1175 |
1176 | len = a.length;
1177 | if (len !== b.length) { // safe and faster
1178 | return false;
1179 | }
1180 |
1181 | // track reference to avoid circular references
1182 | parents.push(a);
1183 | for (i = 0; i < len; i++) {
1184 | loop = false;
1185 | for (j = 0; j < parents.length; j++) {
1186 | if (parents[j] === a[i]) {
1187 | loop = true;// dont rewalk array
1188 | }
1189 | }
1190 | if (!loop && !innerEquiv(a[i], b[i])) {
1191 | parents.pop();
1192 | return false;
1193 | }
1194 | }
1195 | parents.pop();
1196 | return true;
1197 | },
1198 |
1199 | "object" : function(b, a) {
1200 | var i, j, loop;
1201 | var eq = true; // unless we can proove it
1202 | var aProperties = [], bProperties = []; // collection of
1203 | // strings
1204 |
1205 | // comparing constructors is more strict than using
1206 | // instanceof
1207 | if (a.constructor !== b.constructor) {
1208 | // Allow objects with no prototype to be equivalent to
1209 | // objects with Object as their constructor.
1210 | if (!((getProto(a) === null && getProto(b) === Object.prototype) ||
1211 | (getProto(b) === null && getProto(a) === Object.prototype)))
1212 | {
1213 | return false;
1214 | }
1215 | }
1216 |
1217 | // stack constructor before traversing properties
1218 | callers.push(a.constructor);
1219 | // track reference to avoid circular references
1220 | parents.push(a);
1221 |
1222 | for (i in a) { // be strict: don't ensures hasOwnProperty
1223 | // and go deep
1224 | loop = false;
1225 | for (j = 0; j < parents.length; j++) {
1226 | if (parents[j] === a[i]) {
1227 | // don't go down the same path twice
1228 | loop = true;
1229 | }
1230 | }
1231 | aProperties.push(i); // collect a's properties
1232 |
1233 | if (!loop && !innerEquiv(a[i], b[i])) {
1234 | eq = false;
1235 | break;
1236 | }
1237 | }
1238 |
1239 | callers.pop(); // unstack, we are done
1240 | parents.pop();
1241 |
1242 | for (i in b) {
1243 | bProperties.push(i); // collect b's properties
1244 | }
1245 |
1246 | // Ensures identical properties name
1247 | return eq && innerEquiv(aProperties.sort(), bProperties.sort());
1248 | }
1249 | };
1250 | }());
1251 |
1252 | innerEquiv = function() { // can take multiple arguments
1253 | var args = Array.prototype.slice.apply(arguments);
1254 | if (args.length < 2) {
1255 | return true; // end transition
1256 | }
1257 |
1258 | return (function(a, b) {
1259 | if (a === b) {
1260 | return true; // catch the most you can
1261 | } else if (a === null || b === null || typeof a === "undefined" ||
1262 | typeof b === "undefined" ||
1263 | QUnit.objectType(a) !== QUnit.objectType(b)) {
1264 | return false; // don't lose time with error prone cases
1265 | } else {
1266 | return bindCallbacks(a, callbacks, [ b, a ]);
1267 | }
1268 |
1269 | // apply transition with (1..n) arguments
1270 | }(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length - 1)));
1271 | };
1272 |
1273 | return innerEquiv;
1274 |
1275 | }());
1276 |
1277 | /**
1278 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com |
1279 | * http://flesler.blogspot.com Licensed under BSD
1280 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008
1281 | *
1282 | * @projectDescription Advanced and extensible data dumping for Javascript.
1283 | * @version 1.0.0
1284 | * @author Ariel Flesler
1285 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
1286 | */
1287 | QUnit.jsDump = (function() {
1288 | function quote( str ) {
1289 | return '"' + str.toString().replace(/"/g, '\\"') + '"';
1290 | }
1291 | function literal( o ) {
1292 | return o + '';
1293 | }
1294 | function join( pre, arr, post ) {
1295 | var s = jsDump.separator(),
1296 | base = jsDump.indent(),
1297 | inner = jsDump.indent(1);
1298 | if ( arr.join ) {
1299 | arr = arr.join( ',' + s + inner );
1300 | }
1301 | if ( !arr ) {
1302 | return pre + post;
1303 | }
1304 | return [ pre, inner + arr, base + post ].join(s);
1305 | }
1306 | function array( arr, stack ) {
1307 | var i = arr.length, ret = new Array(i);
1308 | this.up();
1309 | while ( i-- ) {
1310 | ret[i] = this.parse( arr[i] , undefined , stack);
1311 | }
1312 | this.down();
1313 | return join( '[', ret, ']' );
1314 | }
1315 |
1316 | var reName = /^function (\w+)/;
1317 |
1318 | var jsDump = {
1319 | parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance
1320 | stack = stack || [ ];
1321 | var parser = this.parsers[ type || this.typeOf(obj) ];
1322 | type = typeof parser;
1323 | var inStack = inArray(obj, stack);
1324 | if (inStack != -1) {
1325 | return 'recursion('+(inStack - stack.length)+')';
1326 | }
1327 | //else
1328 | if (type == 'function') {
1329 | stack.push(obj);
1330 | var res = parser.call( this, obj, stack );
1331 | stack.pop();
1332 | return res;
1333 | }
1334 | // else
1335 | return (type == 'string') ? parser : this.parsers.error;
1336 | },
1337 | typeOf: function( obj ) {
1338 | var type;
1339 | if ( obj === null ) {
1340 | type = "null";
1341 | } else if (typeof obj === "undefined") {
1342 | type = "undefined";
1343 | } else if (QUnit.is("RegExp", obj)) {
1344 | type = "regexp";
1345 | } else if (QUnit.is("Date", obj)) {
1346 | type = "date";
1347 | } else if (QUnit.is("Function", obj)) {
1348 | type = "function";
1349 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") {
1350 | type = "window";
1351 | } else if (obj.nodeType === 9) {
1352 | type = "document";
1353 | } else if (obj.nodeType) {
1354 | type = "node";
1355 | } else if (
1356 | // native arrays
1357 | toString.call( obj ) === "[object Array]" ||
1358 | // NodeList objects
1359 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) )
1360 | ) {
1361 | type = "array";
1362 | } else {
1363 | type = typeof obj;
1364 | }
1365 | return type;
1366 | },
1367 | separator: function() {
1368 | return this.multiline ? this.HTML ? ' ' : '\n' : this.HTML ? ' ' : ' ';
1369 | },
1370 | indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
1371 | if ( !this.multiline ) {
1372 | return '';
1373 | }
1374 | var chr = this.indentChar;
1375 | if ( this.HTML ) {
1376 | chr = chr.replace(/\t/g,' ').replace(/ /g,' ');
1377 | }
1378 | return new Array( this._depth_ + (extra||0) ).join(chr);
1379 | },
1380 | up: function( a ) {
1381 | this._depth_ += a || 1;
1382 | },
1383 | down: function( a ) {
1384 | this._depth_ -= a || 1;
1385 | },
1386 | setParser: function( name, parser ) {
1387 | this.parsers[name] = parser;
1388 | },
1389 | // The next 3 are exposed so you can use them
1390 | quote: quote,
1391 | literal: literal,
1392 | join: join,
1393 | //
1394 | _depth_: 1,
1395 | // This is the list of parsers, to modify them, use jsDump.setParser
1396 | parsers: {
1397 | window: '[Window]',
1398 | document: '[Document]',
1399 | error: '[ERROR]', //when no parser is found, shouldn't happen
1400 | unknown: '[Unknown]',
1401 | 'null': 'null',
1402 | 'undefined': 'undefined',
1403 | 'function': function( fn ) {
1404 | var ret = 'function',
1405 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
1406 | if ( name ) {
1407 | ret += ' ' + name;
1408 | }
1409 | ret += '(';
1410 |
1411 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join('');
1412 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' );
1413 | },
1414 | array: array,
1415 | nodelist: array,
1416 | 'arguments': array,
1417 | object: function( map, stack ) {
1418 | var ret = [ ], keys, key, val, i;
1419 | QUnit.jsDump.up();
1420 | if (Object.keys) {
1421 | keys = Object.keys( map );
1422 | } else {
1423 | keys = [];
1424 | for (key in map) { keys.push( key ); }
1425 | }
1426 | keys.sort();
1427 | for (i = 0; i < keys.length; i++) {
1428 | key = keys[ i ];
1429 | val = map[ key ];
1430 | ret.push( QUnit.jsDump.parse( key, 'key' ) + ': ' + QUnit.jsDump.parse( val, undefined, stack ) );
1431 | }
1432 | QUnit.jsDump.down();
1433 | return join( '{', ret, '}' );
1434 | },
1435 | node: function( node ) {
1436 | var open = QUnit.jsDump.HTML ? '<' : '<',
1437 | close = QUnit.jsDump.HTML ? '>' : '>';
1438 |
1439 | var tag = node.nodeName.toLowerCase(),
1440 | ret = open + tag;
1441 |
1442 | for ( var a in QUnit.jsDump.DOMAttrs ) {
1443 | var val = node[QUnit.jsDump.DOMAttrs[a]];
1444 | if ( val ) {
1445 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' );
1446 | }
1447 | }
1448 | return ret + close + open + '/' + tag + close;
1449 | },
1450 | functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function
1451 | var l = fn.length;
1452 | if ( !l ) {
1453 | return '';
1454 | }
1455 |
1456 | var args = new Array(l);
1457 | while ( l-- ) {
1458 | args[l] = String.fromCharCode(97+l);//97 is 'a'
1459 | }
1460 | return ' ' + args.join(', ') + ' ';
1461 | },
1462 | key: quote, //object calls it internally, the key part of an item in a map
1463 | functionCode: '[code]', //function calls it internally, it's the content of the function
1464 | attribute: quote, //node calls it internally, it's an html attribute value
1465 | string: quote,
1466 | date: quote,
1467 | regexp: literal, //regex
1468 | number: literal,
1469 | 'boolean': literal
1470 | },
1471 | DOMAttrs:{//attributes to dump from nodes, name=>realName
1472 | id:'id',
1473 | name:'name',
1474 | 'class':'className'
1475 | },
1476 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
1477 | indentChar:' ',//indentation unit
1478 | multiline:true //if true, items in a collection, are separated by a \n, else just a space.
1479 | };
1480 |
1481 | return jsDump;
1482 | }());
1483 |
1484 | // from Sizzle.js
1485 | function getText( elems ) {
1486 | var ret = "", elem;
1487 |
1488 | for ( var i = 0; elems[i]; i++ ) {
1489 | elem = elems[i];
1490 |
1491 | // Get the text from text nodes and CDATA nodes
1492 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
1493 | ret += elem.nodeValue;
1494 |
1495 | // Traverse everything else, except comment nodes
1496 | } else if ( elem.nodeType !== 8 ) {
1497 | ret += getText( elem.childNodes );
1498 | }
1499 | }
1500 |
1501 | return ret;
1502 | }
1503 |
1504 | //from jquery.js
1505 | function inArray( elem, array ) {
1506 | if ( array.indexOf ) {
1507 | return array.indexOf( elem );
1508 | }
1509 |
1510 | for ( var i = 0, length = array.length; i < length; i++ ) {
1511 | if ( array[ i ] === elem ) {
1512 | return i;
1513 | }
1514 | }
1515 |
1516 | return -1;
1517 | }
1518 |
1519 | /*
1520 | * Javascript Diff Algorithm
1521 | * By John Resig (http://ejohn.org/)
1522 | * Modified by Chu Alan "sprite"
1523 | *
1524 | * Released under the MIT license.
1525 | *
1526 | * More Info:
1527 | * http://ejohn.org/projects/javascript-diff-algorithm/
1528 | *
1529 | * Usage: QUnit.diff(expected, actual)
1530 | *
1531 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over"
1532 | */
1533 | QUnit.diff = (function() {
1534 | function diff(o, n) {
1535 | var ns = {};
1536 | var os = {};
1537 | var i;
1538 |
1539 | for (i = 0; i < n.length; i++) {
1540 | if (ns[n[i]] == null) {
1541 | ns[n[i]] = {
1542 | rows: [],
1543 | o: null
1544 | };
1545 | }
1546 | ns[n[i]].rows.push(i);
1547 | }
1548 |
1549 | for (i = 0; i < o.length; i++) {
1550 | if (os[o[i]] == null) {
1551 | os[o[i]] = {
1552 | rows: [],
1553 | n: null
1554 | };
1555 | }
1556 | os[o[i]].rows.push(i);
1557 | }
1558 |
1559 | for (i in ns) {
1560 | if ( !hasOwn.call( ns, i ) ) {
1561 | continue;
1562 | }
1563 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) {
1564 | n[ns[i].rows[0]] = {
1565 | text: n[ns[i].rows[0]],
1566 | row: os[i].rows[0]
1567 | };
1568 | o[os[i].rows[0]] = {
1569 | text: o[os[i].rows[0]],
1570 | row: ns[i].rows[0]
1571 | };
1572 | }
1573 | }
1574 |
1575 | for (i = 0; i < n.length - 1; i++) {
1576 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null &&
1577 | n[i + 1] == o[n[i].row + 1]) {
1578 | n[i + 1] = {
1579 | text: n[i + 1],
1580 | row: n[i].row + 1
1581 | };
1582 | o[n[i].row + 1] = {
1583 | text: o[n[i].row + 1],
1584 | row: i + 1
1585 | };
1586 | }
1587 | }
1588 |
1589 | for (i = n.length - 1; i > 0; i--) {
1590 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null &&
1591 | n[i - 1] == o[n[i].row - 1]) {
1592 | n[i - 1] = {
1593 | text: n[i - 1],
1594 | row: n[i].row - 1
1595 | };
1596 | o[n[i].row - 1] = {
1597 | text: o[n[i].row - 1],
1598 | row: i - 1
1599 | };
1600 | }
1601 | }
1602 |
1603 | return {
1604 | o: o,
1605 | n: n
1606 | };
1607 | }
1608 |
1609 | return function(o, n) {
1610 | o = o.replace(/\s+$/, '');
1611 | n = n.replace(/\s+$/, '');
1612 | var out = diff(o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/));
1613 |
1614 | var str = "";
1615 | var i;
1616 |
1617 | var oSpace = o.match(/\s+/g);
1618 | if (oSpace == null) {
1619 | oSpace = [" "];
1620 | }
1621 | else {
1622 | oSpace.push(" ");
1623 | }
1624 | var nSpace = n.match(/\s+/g);
1625 | if (nSpace == null) {
1626 | nSpace = [" "];
1627 | }
1628 | else {
1629 | nSpace.push(" ");
1630 | }
1631 |
1632 | if (out.n.length === 0) {
1633 | for (i = 0; i < out.o.length; i++) {
1634 | str += '' + out.o[i] + oSpace[i] + "";
1635 | }
1636 | }
1637 | else {
1638 | if (out.n[0].text == null) {
1639 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) {
1640 | str += '' + out.o[n] + oSpace[n] + "";
1641 | }
1642 | }
1643 |
1644 | for (i = 0; i < out.n.length; i++) {
1645 | if (out.n[i].text == null) {
1646 | str += '' + out.n[i] + nSpace[i] + "";
1647 | }
1648 | else {
1649 | var pre = "";
1650 |
1651 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) {
1652 | pre += '' + out.o[n] + oSpace[n] + "";
1653 | }
1654 | str += " " + out.n[i].text + nSpace[i] + pre;
1655 | }
1656 | }
1657 | }
1658 |
1659 | return str;
1660 | };
1661 | }());
1662 |
1663 | // for CommonJS enviroments, export everything
1664 | if ( typeof exports !== "undefined" || typeof require !== "undefined" ) {
1665 | extend(exports, QUnit);
1666 | }
1667 |
1668 | // get at whatever the global object is, like window in browsers
1669 | }( (function() {return this;}.call()) ));
--------------------------------------------------------------------------------
/lib/emberjs-couchapp/templates/app/app/vendor/ember-data.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | window.DS = Ember.Namespace.create({
3 | CURRENT_API_REVISION: 4
4 | });
5 |
6 | })();
7 |
8 |
9 |
10 | (function() {
11 | var get = Ember.get, set = Ember.set;
12 |
13 | /**
14 | A record array is an array that contains records of a certain type. The record
15 | array materializes records as needed when they are retrieved for the first
16 | time. You should not create record arrays yourself. Instead, an instance of
17 | DS.RecordArray or its subclasses will be returned by your application's store
18 | in response to queries.
19 | */
20 |
21 | DS.RecordArray = Ember.ArrayProxy.extend({
22 |
23 | /**
24 | The model type contained by this record array.
25 |
26 | @type DS.Model
27 | */
28 | type: null,
29 |
30 | // The array of client ids backing the record array. When a
31 | // record is requested from the record array, the record
32 | // for the client id at the same index is materialized, if
33 | // necessary, by the store.
34 | content: null,
35 |
36 | // The store that created this record array.
37 | store: null,
38 |
39 | init: function() {
40 | this.contentWillChange();
41 | this._super();
42 | },
43 |
44 | contentWillChange: Ember.beforeObserver(function() {
45 | set(this, 'recordCache', []);
46 | }, 'content'),
47 |
48 | contentArrayDidChange: function(array, index, removed, added) {
49 | var recordCache = get(this, 'recordCache');
50 | var args = [index, 0].concat(new Array(added));
51 |
52 | recordCache.splice.apply(recordCache, args);
53 | },
54 |
55 | contentArrayWillChange: function(array, index, removed, added) {
56 | var recordCache = get(this, 'recordCache');
57 | recordCache.splice(index, removed);
58 | },
59 |
60 | objectAtContent: function(index) {
61 | var recordCache = get(this, 'recordCache');
62 | var record = recordCache[index];
63 |
64 | if (!record) {
65 | var store = get(this, 'store');
66 | var content = get(this, 'content');
67 |
68 | var contentObject = content.objectAt(index);
69 |
70 | if (contentObject !== undefined) {
71 | record = store.findByClientId(get(this, 'type'), contentObject);
72 | recordCache[index] = record;
73 | }
74 | }
75 |
76 | return record;
77 | }
78 | });
79 |
80 | })();
81 |
82 |
83 |
84 | (function() {
85 | var get = Ember.get;
86 |
87 | DS.FilteredRecordArray = DS.RecordArray.extend({
88 | filterFunction: null,
89 |
90 | replace: function() {
91 | var type = get(this, 'type').toString();
92 | throw new Error("The result of a client-side filter (on " + type + ") is immutable.");
93 | },
94 |
95 | updateFilter: Ember.observer(function() {
96 | var store = get(this, 'store');
97 | store.updateRecordArrayFilter(this, get(this, 'type'), get(this, 'filterFunction'));
98 | }, 'filterFunction')
99 | });
100 |
101 | })();
102 |
103 |
104 |
105 | (function() {
106 | var get = Ember.get, set = Ember.set;
107 |
108 | DS.AdapterPopulatedRecordArray = DS.RecordArray.extend({
109 | query: null,
110 | isLoaded: false,
111 |
112 | replace: function() {
113 | var type = get(this, 'type').toString();
114 | throw new Error("The result of a server query (on " + type + ") is immutable.");
115 | },
116 |
117 | load: function(array) {
118 | var store = get(this, 'store'), type = get(this, 'type');
119 |
120 | var clientIds = store.loadMany(type, array).clientIds;
121 |
122 | this.beginPropertyChanges();
123 | set(this, 'content', Ember.A(clientIds));
124 | set(this, 'isLoaded', true);
125 | this.endPropertyChanges();
126 | }
127 | });
128 |
129 |
130 | })();
131 |
132 |
133 |
134 | (function() {
135 | var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor;
136 |
137 | var Set = function() {
138 | this.hash = {};
139 | this.list = [];
140 | };
141 |
142 | Set.prototype = {
143 | add: function(item) {
144 | var hash = this.hash,
145 | guid = guidFor(item);
146 |
147 | if (hash.hasOwnProperty(guid)) { return; }
148 |
149 | hash[guid] = true;
150 | this.list.push(item);
151 | },
152 |
153 | remove: function(item) {
154 | var hash = this.hash,
155 | guid = guidFor(item);
156 |
157 | if (!hash.hasOwnProperty(guid)) { return; }
158 |
159 | delete hash[guid];
160 | var list = this.list,
161 | index = Ember.ArrayUtils.indexOf(this, item);
162 |
163 | list.splice(index, 1);
164 | },
165 |
166 | isEmpty: function() {
167 | return this.list.length === 0;
168 | }
169 | };
170 |
171 | var ManyArrayState = Ember.State.extend({
172 | recordWasAdded: function(manager, record) {
173 | var dirty = manager.dirty, observer;
174 | dirty.add(record);
175 |
176 | observer = function() {
177 | if (!get(record, 'isDirty')) {
178 | record.removeObserver('isDirty', observer);
179 | manager.send('childWasSaved', record);
180 | }
181 | };
182 |
183 | record.addObserver('isDirty', observer);
184 | },
185 |
186 | recordWasRemoved: function(manager, record) {
187 | var dirty = manager.dirty, observer;
188 | dirty.add(record);
189 |
190 | observer = function() {
191 | record.removeObserver('isDirty', observer);
192 | if (!get(record, 'isDirty')) { manager.send('childWasSaved', record); }
193 | };
194 |
195 | record.addObserver('isDirty', observer);
196 | }
197 | });
198 |
199 | var states = {
200 | clean: ManyArrayState.create({
201 | isDirty: false,
202 |
203 | recordWasAdded: function(manager, record) {
204 | this._super(manager, record);
205 | manager.goToState('dirty');
206 | },
207 |
208 | update: function(manager, clientIds) {
209 | var manyArray = manager.manyArray;
210 | set(manyArray, 'content', clientIds);
211 | }
212 | }),
213 |
214 | dirty: ManyArrayState.create({
215 | isDirty: true,
216 |
217 | childWasSaved: function(manager, child) {
218 | var dirty = manager.dirty;
219 | dirty.remove(child);
220 |
221 | if (dirty.isEmpty()) { manager.send('arrayBecameSaved'); }
222 | },
223 |
224 | arrayBecameSaved: function(manager) {
225 | manager.goToState('clean');
226 | }
227 | })
228 | };
229 |
230 | DS.ManyArrayStateManager = Ember.StateManager.extend({
231 | manyArray: null,
232 | initialState: 'clean',
233 | states: states,
234 |
235 | init: function() {
236 | this._super();
237 | this.dirty = new Set();
238 | }
239 | });
240 |
241 | })();
242 |
243 |
244 |
245 | (function() {
246 | var get = Ember.get, set = Ember.set, getPath = Ember.getPath;
247 |
248 | DS.ManyArray = DS.RecordArray.extend({
249 | init: function() {
250 | set(this, 'stateManager', DS.ManyArrayStateManager.create({ manyArray: this }));
251 |
252 | return this._super();
253 | },
254 |
255 | parentRecord: null,
256 |
257 | isDirty: Ember.computed(function() {
258 | return getPath(this, 'stateManager.currentState.isDirty');
259 | }).property('stateManager.currentState').cacheable(),
260 |
261 | fetch: function() {
262 | var clientIds = get(this, 'content'),
263 | store = get(this, 'store'),
264 | type = get(this, 'type');
265 |
266 | var ids = clientIds.map(function(clientId) {
267 | return store.clientIdToId[clientId];
268 | });
269 |
270 | store.fetchMany(type, ids);
271 | },
272 |
273 | // Overrides Ember.Array's replace method to implement
274 | replace: function(index, removed, added) {
275 | var parentRecord = get(this, 'parentRecord');
276 | var pendingParent = parentRecord && !get(parentRecord, 'id');
277 | var stateManager = get(this, 'stateManager');
278 |
279 | // Map the array of record objects into an array of client ids.
280 | added = added.map(function(record) {
281 | Ember.assert("You can only add records of " + (get(this, 'type') && get(this, 'type').toString()) + " to this association.", !get(this, 'type') || (get(this, 'type') === record.constructor));
282 |
283 | // If the record to which this many array belongs does not yet
284 | // have an id, notify the newly-added record that it must wait
285 | // for the parent to receive an id before the child can be
286 | // saved.
287 | if (pendingParent) {
288 | record.send('waitingOn', parentRecord);
289 | }
290 |
291 | this.assignInverse(record, parentRecord);
292 |
293 | stateManager.send('recordWasAdded', record);
294 |
295 | return record.get('clientId');
296 | }, this);
297 |
298 | var store = this.store;
299 |
300 | var len = index+removed, record;
301 | for (var i = index; i < len; i++) {
302 | // TODO: null out inverse FK
303 | record = this.objectAt(i);
304 | this.assignInverse(record, parentRecord, true);
305 |
306 | // If we put the child record into a pending state because
307 | // we were waiting on the parent record to get an id, we
308 | // can tell the child it no longer needs to wait.
309 | if (pendingParent) {
310 | record.send('doneWaitingOn', parentRecord);
311 | }
312 |
313 | stateManager.send('recordWasAdded', record);
314 | }
315 |
316 | this._super(index, removed, added);
317 | },
318 |
319 | assignInverse: function(record, parentRecord, remove) {
320 | var associationMap = get(record.constructor, 'associations'),
321 | possibleAssociations = associationMap.get(parentRecord.constructor),
322 | possible, actual;
323 |
324 | if (!possibleAssociations) { return; }
325 |
326 | for (var i = 0, l = possibleAssociations.length; i < l; i++) {
327 | possible = possibleAssociations[i];
328 |
329 | if (possible.kind === 'belongsTo') {
330 | actual = possible;
331 | break;
332 | }
333 | }
334 |
335 | if (actual) {
336 | set(record, actual.name, remove ? null : parentRecord);
337 | }
338 | },
339 |
340 | // Create a child record within the parentRecord
341 | createRecord: function(hash, transaction) {
342 | var parentRecord = get(this, 'parentRecord'),
343 | store = get(parentRecord, 'store'),
344 | type = get(this, 'type'),
345 | record;
346 |
347 | transaction = transaction || get(parentRecord, 'transaction');
348 |
349 | record = store.createRecord.call(store, type, hash, transaction);
350 | this.pushObject(record);
351 |
352 | return record;
353 | }
354 | });
355 |
356 | })();
357 |
358 |
359 |
360 | (function() {
361 |
362 | })();
363 |
364 |
365 |
366 | (function() {
367 | var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
368 |
369 | /**
370 | A transaction allows you to collect multiple records into a unit of work
371 | that can be committed or rolled back as a group.
372 |
373 | For example, if a record has local modifications that have not yet
374 | been saved, calling `commit()` on its transaction will cause those
375 | modifications to be sent to the adapter to be saved. Calling
376 | `rollback()` on its transaction would cause all of the modifications to
377 | be discarded and the record to return to the last known state before
378 | changes were made.
379 |
380 | If a newly created record's transaction is rolled back, it will
381 | immediately transition to the deleted state.
382 |
383 | If you do not explicitly create a transaction, a record is assigned to
384 | an implicit transaction called the default transaction. In these cases,
385 | you can treat your application's instance of `DS.Store` as a transaction
386 | and call the `commit()` and `rollback()` methods on the store itself.
387 |
388 | Once a record has been successfully committed or rolled back, it will
389 | be moved back to the implicit transaction. Because it will now be in
390 | a clean state, it can be moved to a new transaction if you wish.
391 |
392 | ### Creating a Transaction
393 |
394 | To create a new transaction, call the `transaction()` method of your
395 | application's `DS.Store` instance:
396 |
397 | var transaction = App.store.transaction();
398 |
399 | This will return a new instance of `DS.Transaction` with no records
400 | yet assigned to it.
401 |
402 | ### Adding Existing Records
403 |
404 | Add records to a transaction using the `add()` method:
405 |
406 | record = App.store.find(Person, 1);
407 | transaction.add(record);
408 |
409 | Note that only records whose `isDirty` flag is `false` may be added
410 | to a transaction. Once modifications to a record have been made
411 | (its `isDirty` flag is `true`), it is not longer able to be added to
412 | a transaction.
413 |
414 | ### Creating New Records
415 |
416 | Because newly created records are dirty from the time they are created,
417 | and because dirty records can not be added to a transaction, you must
418 | use the `createRecord()` method to assign new records to a transaction.
419 |
420 | For example, instead of this:
421 |
422 | var transaction = store.transaction();
423 | var person = Person.createRecord({ name: "Steve" });
424 |
425 | // won't work because person is dirty
426 | transaction.add(person);
427 |
428 | Call `createRecord()` on the transaction directly:
429 |
430 | var transaction = store.transaction();
431 | transaction.createRecord(Person, { name: "Steve" });
432 |
433 | ### Asynchronous Commits
434 |
435 | Typically, all of the records in a transaction will be committed
436 | together. However, new records that have a dependency on other new
437 | records need to wait for their parent record to be saved and assigned an
438 | ID. In that case, the child record will continue to live in the
439 | transaction until its parent is saved, at which time the transaction will
440 | attempt to commit again.
441 |
442 | For this reason, you should not re-use transactions once you have committed
443 | them. Always make a new transaction and move the desired records to it before
444 | calling commit.
445 | */
446 |
447 | DS.Transaction = Ember.Object.extend({
448 | /**
449 | @private
450 |
451 | Creates the bucket data structure used to segregate records by
452 | type.
453 | */
454 | init: function() {
455 | set(this, 'buckets', {
456 | clean: Ember.Map.create(),
457 | created: Ember.Map.create(),
458 | updated: Ember.Map.create(),
459 | deleted: Ember.Map.create(),
460 | inflight: Ember.Map.create()
461 | });
462 | },
463 |
464 | /**
465 | Creates a new record of the given type and assigns it to the transaction
466 | on which the method was called.
467 |
468 | This is useful as only clean records can be added to a transaction and
469 | new records created using other methods immediately become dirty.
470 |
471 | @param {DS.Model} type the model type to create
472 | @param {Object} hash the data hash to assign the new record
473 | */
474 | createRecord: function(type, hash) {
475 | var store = get(this, 'store');
476 |
477 | return store.createRecord(type, hash, this);
478 | },
479 |
480 | /**
481 | Adds an existing record to this transaction. Only records without
482 | modficiations (i.e., records whose `isDirty` property is `false`)
483 | can be added to a transaction.
484 |
485 | @param {DS.Model} record the record to add to the transaction
486 | */
487 | add: function(record) {
488 | // we could probably make this work if someone has a valid use case. Do you?
489 | Ember.assert("Once a record has changed, you cannot move it into a different transaction", !get(record, 'isDirty'));
490 |
491 | var recordTransaction = get(record, 'transaction'),
492 | defaultTransaction = getPath(this, 'store.defaultTransaction');
493 |
494 | Ember.assert("Models cannot belong to more than one transaction at a time.", recordTransaction === defaultTransaction);
495 |
496 | this.adoptRecord(record);
497 | },
498 |
499 | /**
500 | Commits the transaction, which causes all of the modified records that
501 | belong to the transaction to be sent to the adapter to be saved.
502 |
503 | Once you call `commit()` on a transaction, you should not re-use it.
504 |
505 | When a record is saved, it will be removed from this transaction and
506 | moved back to the store's default transaction.
507 | */
508 | commit: function() {
509 | var self = this,
510 | iterate;
511 |
512 | iterate = function(bucketType, fn, binding) {
513 | var dirty = self.bucketForType(bucketType);
514 |
515 | dirty.forEach(function(type, records) {
516 | if (records.isEmpty()) { return; }
517 |
518 | var array = [];
519 |
520 | records.forEach(function(record) {
521 | record.send('willCommit');
522 |
523 | if (get(record, 'isPending') === false) {
524 | array.push(record);
525 | }
526 | });
527 |
528 | fn.call(binding, type, array);
529 | });
530 | };
531 |
532 | var commitDetails = {
533 | updated: {
534 | eachType: function(fn, binding) { iterate('updated', fn, binding); }
535 | },
536 |
537 | created: {
538 | eachType: function(fn, binding) { iterate('created', fn, binding); }
539 | },
540 |
541 | deleted: {
542 | eachType: function(fn, binding) { iterate('deleted', fn, binding); }
543 | }
544 | };
545 |
546 | var store = get(this, 'store');
547 | var adapter = get(store, '_adapter');
548 |
549 | this.removeCleanRecords();
550 |
551 | if (adapter && adapter.commit) { adapter.commit(store, commitDetails); }
552 | else { throw fmt("Adapter is either null or does not implement `commit` method", this); }
553 | },
554 |
555 | /**
556 | Rolling back a transaction resets the records that belong to
557 | that transaction.
558 |
559 | Updated records have their properties reset to the last known
560 | value from the persistence layer. Deleted records are reverted
561 | to a clean, non-deleted state. Newly created records immediately
562 | become deleted, and are not sent to the adapter to be persisted.
563 |
564 | After the transaction is rolled back, any records that belong
565 | to it will return to the store's default transaction, and the
566 | current transaction should not be used again.
567 | */
568 | rollback: function() {
569 | var store = get(this, 'store'),
570 | dirty;
571 |
572 | // Loop through all of the records in each of the dirty states
573 | // and initiate a rollback on them. As a side effect of telling
574 | // the record to roll back, it should also move itself out of
575 | // the dirty bucket and into the clean bucket.
576 | ['created', 'updated', 'deleted', 'inflight'].forEach(function(bucketType) {
577 | dirty = this.bucketForType(bucketType);
578 |
579 | dirty.forEach(function(type, records) {
580 | records.forEach(function(record) {
581 | record.send('rollback');
582 | });
583 | });
584 | }, this);
585 |
586 | // Now that all records in the transaction are guaranteed to be
587 | // clean, migrate them all to the store's default transaction.
588 | this.removeCleanRecords();
589 | },
590 |
591 | /**
592 | @private
593 |
594 | Removes a record from this transaction and back to the store's
595 | default transaction.
596 |
597 | Note: This method is private for now, but should probably be exposed
598 | in the future once we have stricter error checking (for example, in the
599 | case of the record being dirty).
600 |
601 | @param {DS.Model} record
602 | */
603 | remove: function(record) {
604 | var defaultTransaction = getPath(this, 'store.defaultTransaction');
605 | defaultTransaction.adoptRecord(record);
606 | },
607 |
608 | /**
609 | @private
610 |
611 | Removes all of the records in the transaction's clean bucket.
612 | */
613 | removeCleanRecords: function() {
614 | var clean = this.bucketForType('clean'),
615 | self = this;
616 |
617 | clean.forEach(function(type, records) {
618 | records.forEach(function(record) {
619 | self.remove(record);
620 | });
621 | });
622 | },
623 |
624 | /**
625 | @private
626 |
627 | Returns the bucket for the given bucket type. For example, you might call
628 | `this.bucketForType('updated')` to get the `Ember.Map` that contains all
629 | of the records that have changes pending.
630 |
631 | @param {String} bucketType the type of bucket
632 | @returns Ember.Map
633 | */
634 | bucketForType: function(bucketType) {
635 | var buckets = get(this, 'buckets');
636 |
637 | return get(buckets, bucketType);
638 | },
639 |
640 | /**
641 | @private
642 |
643 | This method moves a record into a different transaction without the normal
644 | checks that ensure that the user is not doing something weird, like moving
645 | a dirty record into a new transaction.
646 |
647 | It is designed for internal use, such as when we are moving a clean record
648 | into a new transaction when the transaction is committed.
649 |
650 | This method must not be called unless the record is clean.
651 |
652 | @param {DS.Model} record
653 | */
654 | adoptRecord: function(record) {
655 | var oldTransaction = get(record, 'transaction');
656 |
657 | if (oldTransaction) {
658 | oldTransaction.removeFromBucket('clean', record);
659 | }
660 |
661 | this.addToBucket('clean', record);
662 | set(record, 'transaction', this);
663 | },
664 |
665 | /**
666 | @private
667 |
668 | Adds a record to the named bucket.
669 |
670 | @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted`
671 | */
672 | addToBucket: function(bucketType, record) {
673 | var bucket = this.bucketForType(bucketType),
674 | type = record.constructor;
675 |
676 | var records = bucket.get(type);
677 |
678 | if (!records) {
679 | records = Ember.OrderedSet.create();
680 | bucket.set(type, records);
681 | }
682 |
683 | records.add(record);
684 | },
685 |
686 | /**
687 | @private
688 |
689 | Removes a record from the named bucket.
690 |
691 | @param {String} bucketType one of `clean`, `created`, `updated`, or `deleted`
692 | */
693 | removeFromBucket: function(bucketType, record) {
694 | var bucket = this.bucketForType(bucketType),
695 | type = record.constructor;
696 |
697 | var records = bucket.get(type);
698 | records.remove(record);
699 | },
700 |
701 | /**
702 | @private
703 |
704 | Called by a record's state manager to indicate that the record has entered
705 | a dirty state. The record will be moved from the `clean` bucket and into
706 | the appropriate dirty bucket.
707 |
708 | @param {String} bucketType one of `created`, `updated`, or `deleted`
709 | */
710 | recordBecameDirty: function(bucketType, record) {
711 | this.removeFromBucket('clean', record);
712 | this.addToBucket(bucketType, record);
713 | },
714 |
715 | /**
716 | @private
717 |
718 | Called by a record's state manager to indicate that the record has entered
719 | inflight state. The record will be moved from its current dirty bucket and into
720 | the `inflight` bucket.
721 |
722 | @param {String} bucketType one of `created`, `updated`, or `deleted`
723 | */
724 | recordBecameInFlight: function(kind, record) {
725 | this.removeFromBucket(kind, record);
726 | this.addToBucket('inflight', record);
727 | },
728 |
729 | /**
730 | @private
731 |
732 | Called by a record's state manager to indicate that the record has entered
733 | a clean state. The record will be moved from its current dirty or inflight bucket and into
734 | the `clean` bucket.
735 |
736 | @param {String} bucketType one of `created`, `updated`, or `deleted`
737 | */
738 | recordBecameClean: function(kind, record) {
739 | this.removeFromBucket(kind, record);
740 |
741 | this.remove(record);
742 | }
743 | });
744 |
745 | })();
746 |
747 |
748 |
749 | (function() {
750 | /*globals Ember*/
751 | var get = Ember.get, set = Ember.set, getPath = Ember.getPath, fmt = Ember.String.fmt;
752 |
753 | var DATA_PROXY = {
754 | get: function(name) {
755 | return this.savedData[name];
756 | }
757 | };
758 |
759 | // These values are used in the data cache when clientIds are
760 | // needed but the underlying data has not yet been loaded by
761 | // the server.
762 | var UNLOADED = 'unloaded';
763 | var LOADING = 'loading';
764 |
765 | // Implementors Note:
766 | //
767 | // The variables in this file are consistently named according to the following
768 | // scheme:
769 | //
770 | // * +id+ means an identifier managed by an external source, provided inside the
771 | // data hash provided by that source.
772 | // * +clientId+ means a transient numerical identifier generated at runtime by
773 | // the data store. It is important primarily because newly created objects may
774 | // not yet have an externally generated id.
775 | // * +type+ means a subclass of DS.Model.
776 |
777 | /**
778 | The store contains all of the hashes for records loaded from the server.
779 | It is also responsible for creating instances of DS.Model when you request one
780 | of these data hashes, so that they can be bound to in your Handlebars templates.
781 |
782 | Create a new store like this:
783 |
784 | MyApp.store = DS.Store.create();
785 |
786 | You can retrieve DS.Model instances from the store in several ways. To retrieve
787 | a record for a specific id, use the `find()` method:
788 |
789 | var record = MyApp.store.find(MyApp.Contact, 123);
790 |
791 | By default, the store will talk to your backend using a standard REST mechanism.
792 | You can customize how the store talks to your backend by specifying a custom adapter:
793 |
794 | MyApp.store = DS.Store.create({
795 | adapter: 'MyApp.CustomAdapter'
796 | });
797 |
798 | You can learn more about writing a custom adapter by reading the `DS.Adapter`
799 | documentation.
800 | */
801 | DS.Store = Ember.Object.extend({
802 |
803 | /**
804 | Many methods can be invoked without specifying which store should be used.
805 | In those cases, the first store created will be used as the default. If
806 | an application has multiple stores, it should specify which store to use
807 | when performing actions, such as finding records by id.
808 |
809 | The init method registers this store as the default if none is specified.
810 | */
811 | init: function() {
812 | // Enforce API revisioning. See BREAKING_CHANGES.md for more.
813 | var revision = get(this, 'revision');
814 |
815 | if (revision !== DS.CURRENT_API_REVISION && !Ember.ENV.TESTING) {
816 | throw new Error("Error: The Ember Data library has had breaking API changes since the last time you updated the library. Please review the list of breaking changes at https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md, then update your store's `revision` property to " + DS.CURRENT_API_REVISION);
817 | }
818 |
819 | if (!get(DS, 'defaultStore') || get(this, 'isDefaultStore')) {
820 | set(DS, 'defaultStore', this);
821 | }
822 |
823 | // internal bookkeeping; not observable
824 | this.typeMaps = {};
825 | this.recordCache = [];
826 | this.clientIdToId = {};
827 | this.recordArraysByClientId = {};
828 |
829 | set(this, 'defaultTransaction', this.transaction());
830 |
831 | return this._super();
832 | },
833 |
834 | /**
835 | Returns a new transaction scoped to this store.
836 |
837 | @see {DS.Transaction}
838 | @returns DS.Transaction
839 | */
840 | transaction: function() {
841 | return DS.Transaction.create({ store: this });
842 | },
843 |
844 | /**
845 | @private
846 |
847 | This is used only by the record's DataProxy. Do not use this directly.
848 | */
849 | dataForRecord: function(record) {
850 | var type = record.constructor,
851 | clientId = get(record, 'clientId'),
852 | typeMap = this.typeMapFor(type);
853 |
854 | return typeMap.cidToHash[clientId];
855 | },
856 |
857 | /**
858 | The adapter to use to communicate to a backend server or other persistence layer.
859 |
860 | This can be specified as an instance, a class, or a property path that specifies
861 | where the adapter can be located.
862 |
863 | @property {DS.Adapter|String}
864 | */
865 | adapter: null,
866 |
867 | /**
868 | @private
869 |
870 | This property returns the adapter, after resolving a possible String.
871 |
872 | @returns DS.Adapter
873 | */
874 | _adapter: Ember.computed(function() {
875 | var adapter = get(this, 'adapter');
876 | if (typeof adapter === 'string') {
877 | return getPath(this, adapter, false) || getPath(window, adapter);
878 | }
879 | return adapter;
880 | }).property('adapter').cacheable(),
881 |
882 | // A monotonically increasing number to be used to uniquely identify
883 | // data hashes and records.
884 | clientIdCounter: 1,
885 |
886 | // .....................
887 | // . CREATE NEW RECORD .
888 | // .....................
889 |
890 | /**
891 | Create a new record in the current store. The properties passed
892 | to this method are set on the newly created record.
893 |
894 | @param {subclass of DS.Model} type
895 | @param {Object} properties a hash of properties to set on the
896 | newly created record.
897 | @returns DS.Model
898 | */
899 | createRecord: function(type, properties, transaction) {
900 | properties = properties || {};
901 |
902 | // Create a new instance of the model `type` and put it
903 | // into the specified `transaction`. If no transaction is
904 | // specified, the default transaction will be used.
905 | //
906 | // NOTE: A `transaction` is specified when the
907 | // `transaction.createRecord` API is used.
908 | var record = type._create({
909 | store: this
910 | });
911 |
912 | transaction = transaction || get(this, 'defaultTransaction');
913 | transaction.adoptRecord(record);
914 |
915 | // Extract the primary key from the `properties` hash,
916 | // based on the `primaryKey` for the model type.
917 | var primaryKey = get(record, 'primaryKey'),
918 | id = properties[primaryKey] || null;
919 |
920 | // If the passed properties do not include a primary key,
921 | // give the adapter an opportunity to generate one.
922 | var adapter;
923 | if (Ember.none(id)) {
924 | adapter = get(this, 'adapter');
925 | if (adapter && adapter.generateIdForRecord) {
926 | id = adapter.generateIdForRecord(this, record);
927 | properties.id = id;
928 | }
929 | }
930 |
931 | var hash = {}, clientId;
932 |
933 | // Push the hash into the store. If present, associate the
934 | // extracted `id` with the hash.
935 | clientId = this.pushHash(hash, id, type);
936 |
937 | record.send('didChangeData');
938 |
939 | var recordCache = get(this, 'recordCache');
940 |
941 | // Now that we have a clientId, attach it to the record we
942 | // just created.
943 | set(record, 'clientId', clientId);
944 |
945 | // Store the record we just created in the record cache for
946 | // this clientId.
947 | recordCache[clientId] = record;
948 |
949 | // Set the properties specified on the record.
950 | record.setProperties(properties);
951 |
952 | this.updateRecordArrays(type, clientId, get(record, 'data'));
953 |
954 | return record;
955 | },
956 |
957 | // .................
958 | // . DELETE RECORD .
959 | // .................
960 |
961 | /**
962 | For symmetry, a record can be deleted via the store.
963 |
964 | @param {DS.Model} record
965 | */
966 | deleteRecord: function(record) {
967 | record.send('deleteRecord');
968 | },
969 |
970 | // ................
971 | // . FIND RECORDS .
972 | // ................
973 |
974 | /**
975 | This is the main entry point into finding records. The first
976 | parameter to this method is always a subclass of `DS.Model`.
977 |
978 | You can use the `find` method on a subclass of `DS.Model`
979 | directly if your application only has one store. For
980 | example, instead of `store.find(App.Person, 1)`, you could
981 | say `App.Person.find(1)`.
982 |
983 | ---
984 |
985 | To find a record by ID, pass the `id` as the second parameter:
986 |
987 | store.find(App.Person, 1);
988 | App.Person.find(1);
989 |
990 | If the record with that `id` had not previously been loaded,
991 | the store will return an empty record immediately and ask
992 | the adapter to find the data by calling the adapter's `find`
993 | method.
994 |
995 | The `find` method will always return the same object for a
996 | given type and `id`. To check whether the adapter has populated
997 | a record, you can check its `isLoaded` property.
998 |
999 | ---
1000 |
1001 | To find all records for a type, call `find` with no additional
1002 | parameters:
1003 |
1004 | store.find(App.Person);
1005 | App.Person.find();
1006 |
1007 | This will return a `RecordArray` representing all known records
1008 | for the given type and kick off a request to the adapter's
1009 | `findAll` method to load any additional records for the type.
1010 |
1011 | The `RecordArray` returned by `find()` is live. If any more
1012 | records for the type are added at a later time through any
1013 | mechanism, it will automatically update to reflect the change.
1014 |
1015 | ---
1016 |
1017 | To find a record by a query, call `find` with a hash as the
1018 | second parameter:
1019 |
1020 | store.find(App.Person, { page: 1 });
1021 | App.Person.find({ page: 1 });
1022 |
1023 | This will return a `RecordArray` immediately, but it will always
1024 | be an empty `RecordArray` at first. It will call the adapter's
1025 | `findQuery` method, which will populate the `RecordArray` once
1026 | the server has returned results.
1027 |
1028 | You can check whether a query results `RecordArray` has loaded
1029 | by checking its `isLoaded` property.
1030 | */
1031 | find: function(type, id, query) {
1032 | if (id === undefined) {
1033 | return this.findAll(type);
1034 | }
1035 |
1036 | if (query !== undefined) {
1037 | return this.findMany(type, id, query);
1038 | } else if (Ember.typeOf(id) === 'object') {
1039 | return this.findQuery(type, id);
1040 | }
1041 |
1042 | if (Ember.isArray(id)) {
1043 | return this.findMany(type, id);
1044 | }
1045 |
1046 | var clientId = this.typeMapFor(type).idToCid[id];
1047 |
1048 | return this.findByClientId(type, clientId, id);
1049 | },
1050 |
1051 | findByClientId: function(type, clientId, id) {
1052 | var recordCache = get(this, 'recordCache'),
1053 | dataCache = this.typeMapFor(type).cidToHash,
1054 | record;
1055 |
1056 | // If there is already a clientId assigned for this
1057 | // type/id combination, try to find an existing
1058 | // record for that id and return. Otherwise,
1059 | // materialize a new record and set its data to the
1060 | // value we already have.
1061 | if (clientId !== undefined) {
1062 | record = recordCache[clientId];
1063 |
1064 | if (!record) {
1065 | // create a new instance of the model type in the
1066 | // 'isLoading' state
1067 | record = this.materializeRecord(type, clientId);
1068 |
1069 | if (typeof dataCache[clientId] === 'object') {
1070 | record.send('didChangeData');
1071 | }
1072 | }
1073 | } else {
1074 | clientId = this.pushHash(LOADING, id, type);
1075 |
1076 | // create a new instance of the model type in the
1077 | // 'isLoading' state
1078 | record = this.materializeRecord(type, clientId, id);
1079 |
1080 | // let the adapter set the data, possibly async
1081 | var adapter = get(this, '_adapter');
1082 | if (adapter && adapter.find) { adapter.find(this, type, id); }
1083 | else { throw fmt("Adapter is either null or does not implement `find` method", this); }
1084 | }
1085 |
1086 | return record;
1087 | },
1088 |
1089 | /**
1090 | @private
1091 |
1092 | Ask the adapter to fetch IDs that are not already loaded.
1093 |
1094 | This method will convert `id`s to `clientId`s, filter out
1095 | `clientId`s that already have a data hash present, and pass
1096 | the remaining `id`s to the adapter.
1097 |
1098 | @param {Class} type A model class
1099 | @param {Array} ids An array of ids
1100 | @param {Object} query
1101 |
1102 | @returns {Array} An Array of all clientIds for the
1103 | specified ids.
1104 | */
1105 | fetchMany: function(type, ids, query) {
1106 | var typeMap = this.typeMapFor(type),
1107 | idToClientIdMap = typeMap.idToCid,
1108 | dataCache = typeMap.cidToHash,
1109 | data = typeMap.cidToHash,
1110 | needed;
1111 |
1112 | var clientIds = Ember.A([]);
1113 |
1114 | if (ids) {
1115 | needed = [];
1116 |
1117 | ids.forEach(function(id) {
1118 | // Get the clientId for the given id
1119 | var clientId = idToClientIdMap[id];
1120 |
1121 | // If there is no `clientId` yet
1122 | if (clientId === undefined) {
1123 | // Create a new `clientId`, marking its data hash
1124 | // as loading. Once the adapter returns the data
1125 | // hash, it will be updated
1126 | clientId = this.pushHash(LOADING, id, type);
1127 | needed.push(id);
1128 |
1129 | // If there is a clientId, but its data hash is
1130 | // marked as unloaded (this happens when a
1131 | // hasMany association creates clientIds for its
1132 | // referenced ids before they were loaded)
1133 | } else if (clientId && data[clientId] === UNLOADED) {
1134 | // change the data hash marker to loading
1135 | dataCache[clientId] = LOADING;
1136 | needed.push(id);
1137 | }
1138 |
1139 | // this method is expected to return a list of
1140 | // all of the clientIds for the specified ids,
1141 | // unconditionally add it.
1142 | clientIds.push(clientId);
1143 | }, this);
1144 | } else {
1145 | needed = null;
1146 | }
1147 |
1148 | // If there are any needed ids, ask the adapter to load them
1149 | if ((needed && get(needed, 'length') > 0) || query) {
1150 | var adapter = get(this, '_adapter');
1151 | if (adapter && adapter.findMany) { adapter.findMany(this, type, needed, query); }
1152 | else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
1153 | }
1154 |
1155 | return clientIds;
1156 | },
1157 |
1158 | /** @private
1159 | */
1160 | findMany: function(type, ids, query) {
1161 | var clientIds = this.fetchMany(type, ids, query);
1162 |
1163 | return this.createManyArray(type, clientIds);
1164 | },
1165 |
1166 | findQuery: function(type, query) {
1167 | var array = DS.AdapterPopulatedRecordArray.create({ type: type, content: Ember.A([]), store: this });
1168 | var adapter = get(this, '_adapter');
1169 | if (adapter && adapter.findQuery) { adapter.findQuery(this, type, query, array); }
1170 | else { throw fmt("Adapter is either null or does not implement `findQuery` method", this); }
1171 | return array;
1172 | },
1173 |
1174 | findAll: function(type) {
1175 |
1176 | var typeMap = this.typeMapFor(type),
1177 | findAllCache = typeMap.findAllCache;
1178 |
1179 | if (findAllCache) { return findAllCache; }
1180 |
1181 | var array = DS.RecordArray.create({ type: type, content: Ember.A([]), store: this });
1182 | this.registerRecordArray(array, type);
1183 |
1184 | var adapter = get(this, '_adapter');
1185 | if (adapter && adapter.findAll) { adapter.findAll(this, type); }
1186 |
1187 | typeMap.findAllCache = array;
1188 | return array;
1189 | },
1190 |
1191 | filter: function(type, query, filter) {
1192 | // allow an optional server query
1193 | if (arguments.length === 3) {
1194 | this.findQuery(type, query);
1195 | } else if (arguments.length === 2) {
1196 | filter = query;
1197 | }
1198 |
1199 | var array = DS.FilteredRecordArray.create({ type: type, content: Ember.A([]), store: this, filterFunction: filter });
1200 |
1201 | this.registerRecordArray(array, type, filter);
1202 |
1203 | return array;
1204 | },
1205 |
1206 | // ............
1207 | // . UPDATING .
1208 | // ............
1209 |
1210 | hashWasUpdated: function(type, clientId, record) {
1211 | // Because hash updates are invoked at the end of the run loop,
1212 | // it is possible that a record might be deleted after its hash
1213 | // has been modified and this method was scheduled to be called.
1214 | //
1215 | // If that's the case, the record would have already been removed
1216 | // from all record arrays; calling updateRecordArrays would just
1217 | // add it back. If the record is deleted, just bail. It shouldn't
1218 | // give us any more trouble after this.
1219 |
1220 | if (get(record, 'isDeleted')) { return; }
1221 | this.updateRecordArrays(type, clientId, get(record, 'data'));
1222 | },
1223 |
1224 | // ..............
1225 | // . PERSISTING .
1226 | // ..............
1227 |
1228 | commit: function() {
1229 | var defaultTransaction = get(this, 'defaultTransaction');
1230 | set(this, 'defaultTransaction', this.transaction());
1231 |
1232 | defaultTransaction.commit();
1233 | },
1234 |
1235 | didUpdateRecords: function(array, hashes) {
1236 | if (hashes) {
1237 | array.forEach(function(record, idx) {
1238 | this.didUpdateRecord(record, hashes[idx]);
1239 | }, this);
1240 | } else {
1241 | array.forEach(function(record) {
1242 | this.didUpdateRecord(record);
1243 | }, this);
1244 | }
1245 | },
1246 |
1247 | didUpdateRecord: function(record, hash) {
1248 | if (hash) {
1249 | var clientId = get(record, 'clientId'),
1250 | dataCache = this.typeMapFor(record.constructor).cidToHash;
1251 |
1252 | dataCache[clientId] = hash;
1253 | record.send('didChangeData');
1254 | record.hashWasUpdated();
1255 | } else {
1256 | record.send('didSaveData');
1257 | }
1258 |
1259 | record.send('didCommit');
1260 | },
1261 |
1262 | didDeleteRecords: function(array) {
1263 | array.forEach(function(record) {
1264 | record.send('didCommit');
1265 | });
1266 | },
1267 |
1268 | didDeleteRecord: function(record) {
1269 | record.send('didCommit');
1270 | },
1271 |
1272 | _didCreateRecord: function(record, hash, typeMap, clientId, primaryKey) {
1273 | var recordData = get(record, 'data'), id, changes;
1274 |
1275 | if (hash) {
1276 | typeMap.cidToHash[clientId] = hash;
1277 |
1278 | // If the server returns a hash, we assume that the server's version
1279 | // of the data supercedes the local changes.
1280 | record.beginPropertyChanges();
1281 | record.send('didChangeData');
1282 | recordData.adapterDidUpdate();
1283 | record.hashWasUpdated();
1284 | record.endPropertyChanges();
1285 |
1286 | id = hash[primaryKey];
1287 |
1288 | typeMap.idToCid[id] = clientId;
1289 | this.clientIdToId[clientId] = id;
1290 | } else {
1291 | recordData.commit();
1292 | }
1293 |
1294 | record.send('didCommit');
1295 | },
1296 |
1297 |
1298 | didCreateRecords: function(type, array, hashes) {
1299 | var primaryKey = type.proto().primaryKey,
1300 | typeMap = this.typeMapFor(type),
1301 | clientId;
1302 |
1303 | for (var i=0, l=get(array, 'length'); i "created.uncommitted"
1650 |
1651 | The `DS.Model` states are themselves stateless. What we mean is that,
1652 | though each instance of a record also has a unique instance of a
1653 | `DS.StateManager`, the hierarchical states that each of *those* points
1654 | to is a shared data structure. For performance reasons, instead of each
1655 | record getting its own copy of the hierarchy of states, each state
1656 | manager points to this global, immutable shared instance. How does a
1657 | state know which record it should be acting on? We pass a reference to
1658 | the current state manager as the first parameter to every method invoked
1659 | on a state.
1660 |
1661 | The state manager passed as the first parameter is where you should stash
1662 | state about the record if needed; you should never store data on the state
1663 | object itself. If you need access to the record being acted on, you can
1664 | retrieve the state manager's `record` property. For example, if you had
1665 | an event handler `myEvent`:
1666 |
1667 | myEvent: function(manager) {
1668 | var record = manager.get('record');
1669 | record.doSomething();
1670 | }
1671 |
1672 | For more information about state managers in general, see the Ember.js
1673 | documentation on `Ember.StateManager`.
1674 |
1675 | ### Events, Flags, and Transitions
1676 |
1677 | A state may implement zero or more events, flags, or transitions.
1678 |
1679 | #### Events
1680 |
1681 | Events are named functions that are invoked when sent to a record. The
1682 | state manager will first look for a method with the given name on the
1683 | current state. If no method is found, it will search the current state's
1684 | parent, and then its grandparent, and so on until reaching the top of
1685 | the hierarchy. If the root is reached without an event handler being found,
1686 | an exception will be raised. This can be very helpful when debugging new
1687 | features.
1688 |
1689 | Here's an example implementation of a state with a `myEvent` event handler:
1690 |
1691 | aState: DS.State.create({
1692 | myEvent: function(manager, param) {
1693 | console.log("Received myEvent with "+param);
1694 | }
1695 | })
1696 |
1697 | To trigger this event:
1698 |
1699 | record.send('myEvent', 'foo');
1700 | //=> "Received myEvent with foo"
1701 |
1702 | Note that an optional parameter can be sent to a record's `send()` method,
1703 | which will be passed as the second parameter to the event handler.
1704 |
1705 | Events should transition to a different state if appropriate. This can be
1706 | done by calling the state manager's `goToState()` method with a path to the
1707 | desired state. The state manager will attempt to resolve the state path
1708 | relative to the current state. If no state is found at that path, it will
1709 | attempt to resolve it relative to the current state's parent, and then its
1710 | parent, and so on until the root is reached. For example, imagine a hierarchy
1711 | like this:
1712 |
1713 | * created
1714 | * start <-- currentState
1715 | * inFlight
1716 | * updated
1717 | * inFlight
1718 |
1719 | If we are currently in the `start` state, calling
1720 | `goToState('inFlight')` would transition to the `created.inFlight` state,
1721 | while calling `goToState('updated.inFlight')` would transition to
1722 | the `updated.inFlight` state.
1723 |
1724 | Remember that *only events* should ever cause a state transition. You should
1725 | never call `goToState()` from outside a state's event handler. If you are
1726 | tempted to do so, create a new event and send that to the state manager.
1727 |
1728 | #### Flags
1729 |
1730 | Flags are Boolean values that can be used to introspect a record's current
1731 | state in a more user-friendly way than examining its state path. For example,
1732 | instead of doing this:
1733 |
1734 | var statePath = record.getPath('stateManager.currentState.path');
1735 | if (statePath === 'created.inFlight') {
1736 | doSomething();
1737 | }
1738 |
1739 | You can say:
1740 |
1741 | if (record.get('isNew') && record.get('isSaving')) {
1742 | doSomething();
1743 | }
1744 |
1745 | If your state does not set a value for a given flag, the value will
1746 | be inherited from its parent (or the first place in the state hierarchy
1747 | where it is defined).
1748 |
1749 | The current set of flags are defined below. If you want to add a new flag,
1750 | in addition to the area below, you will also need to declare it in the
1751 | `DS.Model` class.
1752 |
1753 | #### Transitions
1754 |
1755 | Transitions are like event handlers but are called automatically upon
1756 | entering or exiting a state. To implement a transition, just call a method
1757 | either `enter` or `exit`:
1758 |
1759 | myState: DS.State.create({
1760 | // Gets called automatically when entering
1761 | // this state.
1762 | enter: function(manager) {
1763 | console.log("Entered myState");
1764 | }
1765 | })
1766 |
1767 | Note that enter and exit events are called once per transition. If the
1768 | current state changes, but changes to another child state of the parent,
1769 | the transition event on the parent will not be triggered.
1770 | */
1771 |
1772 | var stateProperty = Ember.computed(function(key) {
1773 | var parent = get(this, 'parentState');
1774 | if (parent) {
1775 | return get(parent, key);
1776 | }
1777 | }).property();
1778 |
1779 | var isEmptyObject = function(object) {
1780 | for (var name in object) {
1781 | if (object.hasOwnProperty(name)) { return false; }
1782 | }
1783 |
1784 | return true;
1785 | };
1786 |
1787 | var hasDefinedProperties = function(object) {
1788 | for (var name in object) {
1789 | if (object.hasOwnProperty(name) && object[name]) { return true; }
1790 | }
1791 |
1792 | return false;
1793 | };
1794 |
1795 | DS.State = Ember.State.extend({
1796 | isLoaded: stateProperty,
1797 | isDirty: stateProperty,
1798 | isSaving: stateProperty,
1799 | isDeleted: stateProperty,
1800 | isError: stateProperty,
1801 | isNew: stateProperty,
1802 | isValid: stateProperty,
1803 | isPending: stateProperty,
1804 |
1805 | // For states that are substates of a
1806 | // DirtyState (updated or created), it is
1807 | // useful to be able to determine which
1808 | // type of dirty state it is.
1809 | dirtyType: stateProperty
1810 | });
1811 |
1812 | var setProperty = function(manager, context) {
1813 | var key = context.key, value = context.value;
1814 |
1815 | var record = get(manager, 'record'),
1816 | data = get(record, 'data');
1817 |
1818 | set(data, key, value);
1819 | };
1820 |
1821 | var setAssociation = function(manager, context) {
1822 | var key = context.key, value = context.value;
1823 |
1824 | var record = get(manager, 'record'),
1825 | data = get(record, 'data');
1826 |
1827 | data.setAssociation(key, value);
1828 | };
1829 |
1830 | var didChangeData = function(manager) {
1831 | var record = get(manager, 'record'),
1832 | data = get(record, 'data');
1833 |
1834 | data._savedData = null;
1835 | record.notifyPropertyChange('data');
1836 | };
1837 |
1838 | // The waitingOn event shares common functionality
1839 | // between the different dirty states, but each is
1840 | // treated slightly differently. This method is exposed
1841 | // so that each implementation can invoke the common
1842 | // behavior, and then implement the behavior specific
1843 | // to the state.
1844 | var waitingOn = function(manager, object) {
1845 | var record = get(manager, 'record'),
1846 | pendingQueue = get(record, 'pendingQueue'),
1847 | objectGuid = guidFor(object);
1848 |
1849 | var observer = function() {
1850 | if (get(object, 'id')) {
1851 | manager.send('doneWaitingOn', object);
1852 | Ember.removeObserver(object, 'id', observer);
1853 | }
1854 | };
1855 |
1856 | pendingQueue[objectGuid] = [object, observer];
1857 | Ember.addObserver(object, 'id', observer);
1858 | };
1859 |
1860 | // Implementation notes:
1861 | //
1862 | // Each state has a boolean value for all of the following flags:
1863 | //
1864 | // * isLoaded: The record has a populated `data` property. When a
1865 | // record is loaded via `store.find`, `isLoaded` is false
1866 | // until the adapter sets it. When a record is created locally,
1867 | // its `isLoaded` property is always true.
1868 | // * isDirty: The record has local changes that have not yet been
1869 | // saved by the adapter. This includes records that have been
1870 | // created (but not yet saved) or deleted.
1871 | // * isSaving: The record's transaction has been committed, but
1872 | // the adapter has not yet acknowledged that the changes have
1873 | // been persisted to the backend.
1874 | // * isDeleted: The record was marked for deletion. When `isDeleted`
1875 | // is true and `isDirty` is true, the record is deleted locally
1876 | // but the deletion was not yet persisted. When `isSaving` is
1877 | // true, the change is in-flight. When both `isDirty` and
1878 | // `isSaving` are false, the change has persisted.
1879 | // * isError: The adapter reported that it was unable to save
1880 | // local changes to the backend. This may also result in the
1881 | // record having its `isValid` property become false if the
1882 | // adapter reported that server-side validations failed.
1883 | // * isNew: The record was created on the client and the adapter
1884 | // did not yet report that it was successfully saved.
1885 | // * isValid: No client-side validations have failed and the
1886 | // adapter did not report any server-side validation failures.
1887 | // * isPending: A record `isPending` when it belongs to an
1888 | // association on another record and that record has not been
1889 | // saved. A record in this state cannot be saved because it
1890 | // lacks a "foreign key" that will be supplied by its parent
1891 | // association when the parent record has been created. When
1892 | // the adapter reports that the parent has saved, the
1893 | // `isPending` property on all children will become `false`
1894 | // and the transaction will try to commit the records.
1895 |
1896 | // This mixin is mixed into various uncommitted states. Make
1897 | // sure to mix it in *after* the class definition, so its
1898 | // super points to the class definition.
1899 | var Uncommitted = Ember.Mixin.create({
1900 | setProperty: setProperty,
1901 | setAssociation: setAssociation,
1902 | });
1903 |
1904 | // These mixins are mixed into substates of the concrete
1905 | // subclasses of DirtyState.
1906 |
1907 | var CreatedUncommitted = Ember.Mixin.create({
1908 | deleteRecord: function(manager) {
1909 | var record = get(manager, 'record');
1910 | this._super(manager);
1911 |
1912 | record.withTransaction(function(t) {
1913 | t.recordBecameClean('created', record);
1914 | });
1915 | manager.goToState('deleted.saved');
1916 | }
1917 | });
1918 |
1919 | var UpdatedUncommitted = Ember.Mixin.create({
1920 | deleteRecord: function(manager) {
1921 | this._super(manager);
1922 |
1923 | var record = get(manager, 'record');
1924 |
1925 | record.withTransaction(function(t) {
1926 | t.recordBecameClean('updated', record);
1927 | });
1928 |
1929 | manager.goToState('deleted');
1930 | }
1931 | });
1932 |
1933 | // The dirty state is a abstract state whose functionality is
1934 | // shared between the `created` and `updated` states.
1935 | //
1936 | // The deleted state shares the `isDirty` flag with the
1937 | // subclasses of `DirtyState`, but with a very different
1938 | // implementation.
1939 | var DirtyState = DS.State.extend({
1940 | initialState: 'uncommitted',
1941 |
1942 | // FLAGS
1943 | isDirty: true,
1944 |
1945 | // SUBSTATES
1946 |
1947 | // When a record first becomes dirty, it is `uncommitted`.
1948 | // This means that there are local pending changes,
1949 | // but they have not yet begun to be saved.
1950 | uncommitted: DS.State.extend({
1951 | // TRANSITIONS
1952 | enter: function(manager) {
1953 | var dirtyType = get(this, 'dirtyType'),
1954 | record = get(manager, 'record');
1955 |
1956 | record.withTransaction(function (t) {
1957 | t.recordBecameDirty(dirtyType, record);
1958 | });
1959 | },
1960 |
1961 | // EVENTS
1962 | deleteRecord: Ember.K,
1963 |
1964 | waitingOn: function(manager, object) {
1965 | waitingOn(manager, object);
1966 | manager.goToState('pending');
1967 | },
1968 |
1969 | willCommit: function(manager) {
1970 | manager.goToState('inFlight');
1971 | },
1972 |
1973 | becameInvalid: function(manager) {
1974 | var dirtyType = get(this, 'dirtyType'),
1975 | record = get(manager, 'record');
1976 |
1977 | record.withTransaction(function (t) {
1978 | t.recordBecameInFlight(dirtyType, record);
1979 | });
1980 |
1981 | manager.goToState('invalid');
1982 | },
1983 |
1984 | rollback: function(manager) {
1985 | var record = get(manager, 'record'),
1986 | dirtyType = get(this, 'dirtyType'),
1987 | data = get(record, 'data');
1988 |
1989 | data.rollback();
1990 |
1991 | record.withTransaction(function(t) {
1992 | t.recordBecameClean(dirtyType, record);
1993 | });
1994 |
1995 | manager.goToState('loaded');
1996 | }
1997 | }, Uncommitted),
1998 |
1999 | // Once a record has been handed off to the adapter to be
2000 | // saved, it is in the 'in flight' state. Changes to the
2001 | // record cannot be made during this window.
2002 | inFlight: DS.State.extend({
2003 | // FLAGS
2004 | isSaving: true,
2005 |
2006 | // TRANSITIONS
2007 | enter: function(manager) {
2008 | var dirtyType = get(this, 'dirtyType'),
2009 | record = get(manager, 'record');
2010 |
2011 | record.withTransaction(function (t) {
2012 | t.recordBecameInFlight(dirtyType, record);
2013 | });
2014 | },
2015 |
2016 | // EVENTS
2017 | didCommit: function(manager) {
2018 | var dirtyType = get(this, 'dirtyType'),
2019 | record = get(manager, 'record');
2020 |
2021 | record.withTransaction(function(t) {
2022 | t.recordBecameClean('inflight', record);
2023 | });
2024 |
2025 | manager.goToState('loaded');
2026 | manager.send('invokeLifecycleCallbacks', dirtyType);
2027 | },
2028 |
2029 | becameInvalid: function(manager, errors) {
2030 | var record = get(manager, 'record');
2031 |
2032 | set(record, 'errors', errors);
2033 |
2034 | manager.goToState('invalid');
2035 | manager.send('invokeLifecycleCallbacks');
2036 | },
2037 |
2038 | becameError: function(manager) {
2039 | manager.goToState('error');
2040 | manager.send('invokeLifecycleCallbacks');
2041 | },
2042 |
2043 | didChangeData: didChangeData
2044 | }),
2045 |
2046 | // If a record becomes associated with a newly created
2047 | // parent record, it will be `pending` until the parent
2048 | // record has successfully persisted. Once this happens,
2049 | // this record can use the parent's primary key as its
2050 | // foreign key.
2051 | //
2052 | // If the record's transaction had already started to
2053 | // commit, the record will transition to the `inFlight`
2054 | // state. If it had not, the record will transition to
2055 | // the `uncommitted` state.
2056 | pending: DS.State.extend({
2057 | initialState: 'uncommitted',
2058 |
2059 | // FLAGS
2060 | isPending: true,
2061 |
2062 | // SUBSTATES
2063 |
2064 | // A pending record whose transaction has not yet
2065 | // started to commit is in this state.
2066 | uncommitted: DS.State.extend({
2067 | // EVENTS
2068 | deleteRecord: function(manager) {
2069 | var record = get(manager, 'record'),
2070 | pendingQueue = get(record, 'pendingQueue'),
2071 | tuple;
2072 |
2073 | // since we are leaving the pending state, remove any
2074 | // observers we have registered on other records.
2075 | for (var prop in pendingQueue) {
2076 | if (!pendingQueue.hasOwnProperty(prop)) { continue; }
2077 |
2078 | tuple = pendingQueue[prop];
2079 | Ember.removeObserver(tuple[0], 'id', tuple[1]);
2080 | }
2081 | },
2082 |
2083 | willCommit: function(manager) {
2084 | manager.goToState('committing');
2085 | },
2086 |
2087 | doneWaitingOn: function(manager, object) {
2088 | var record = get(manager, 'record'),
2089 | pendingQueue = get(record, 'pendingQueue'),
2090 | objectGuid = guidFor(object);
2091 |
2092 | delete pendingQueue[objectGuid];
2093 |
2094 | if (isEmptyObject(pendingQueue)) {
2095 | manager.send('doneWaiting');
2096 | }
2097 | },
2098 |
2099 | doneWaiting: function(manager) {
2100 | var dirtyType = get(this, 'dirtyType');
2101 | manager.goToState(dirtyType + '.uncommitted');
2102 | }
2103 | }, Uncommitted),
2104 |
2105 | // A pending record whose transaction has started
2106 | // to commit is in this state. Since it has not yet
2107 | // been sent to the adapter, it is not `inFlight`
2108 | // until all of its dependencies have been committed.
2109 | committing: DS.State.extend({
2110 | // FLAGS
2111 | isSaving: true,
2112 |
2113 | // EVENTS
2114 | doneWaitingOn: function(manager, object) {
2115 | var record = get(manager, 'record'),
2116 | pendingQueue = get(record, 'pendingQueue'),
2117 | objectGuid = guidFor(object);
2118 |
2119 | delete pendingQueue[objectGuid];
2120 |
2121 | if (isEmptyObject(pendingQueue)) {
2122 | manager.send('doneWaiting');
2123 | }
2124 | },
2125 |
2126 | doneWaiting: function(manager) {
2127 | var record = get(manager, 'record'),
2128 | transaction = get(record, 'transaction');
2129 |
2130 | // Now that the record is no longer pending, schedule
2131 | // the transaction to commit.
2132 | Ember.run.once(transaction, transaction.commit);
2133 | },
2134 |
2135 | willCommit: function(manager) {
2136 | var record = get(manager, 'record'),
2137 | pendingQueue = get(record, 'pendingQueue');
2138 |
2139 | if (isEmptyObject(pendingQueue)) {
2140 | var dirtyType = get(this, 'dirtyType');
2141 | manager.goToState(dirtyType + '.inFlight');
2142 | }
2143 | }
2144 | })
2145 | }),
2146 |
2147 | // A record is in the `invalid` state when its client-side
2148 | // invalidations have failed, or if the adapter has indicated
2149 | // the the record failed server-side invalidations.
2150 | invalid: DS.State.extend({
2151 | // FLAGS
2152 | isValid: false,
2153 |
2154 | exit: function(manager) {
2155 | var record = get(manager, 'record');
2156 |
2157 | record.withTransaction(function (t) {
2158 | t.recordBecameClean('inflight', record);
2159 | });
2160 | },
2161 |
2162 | // EVENTS
2163 | deleteRecord: function(manager) {
2164 | manager.goToState('deleted');
2165 | },
2166 |
2167 | setAssociation: setAssociation,
2168 |
2169 | setProperty: function(manager, context) {
2170 | setProperty(manager, context);
2171 |
2172 | var record = get(manager, 'record'),
2173 | errors = get(record, 'errors'),
2174 | key = context.key;
2175 |
2176 | delete errors[key];
2177 |
2178 | if (!hasDefinedProperties(errors)) {
2179 | manager.send('becameValid');
2180 | }
2181 | },
2182 |
2183 | rollback: function(manager) {
2184 | manager.send('becameValid');
2185 | manager.send('rollback');
2186 | },
2187 |
2188 | becameValid: function(manager) {
2189 | manager.goToState('uncommitted');
2190 | },
2191 |
2192 | invokeLifecycleCallbacks: function(manager) {
2193 | var record = get(manager, 'record');
2194 | record.fire('becameInvalid', record);
2195 | }
2196 | })
2197 | });
2198 |
2199 | // The created and updated states are created outside the state
2200 | // chart so we can reopen their substates and add mixins as
2201 | // necessary.
2202 |
2203 | var createdState = DirtyState.create({
2204 | dirtyType: 'created',
2205 |
2206 | // FLAGS
2207 | isNew: true
2208 | });
2209 |
2210 | var updatedState = DirtyState.create({
2211 | dirtyType: 'updated'
2212 | });
2213 |
2214 | // The created.uncommitted state and created.pending.uncommitted share
2215 | // some logic defined in CreatedUncommitted.
2216 | createdState.states.uncommitted.reopen(CreatedUncommitted);
2217 | createdState.states.pending.states.uncommitted.reopen(CreatedUncommitted);
2218 |
2219 | // The created.uncommitted state needs to immediately transition to the
2220 | // deleted state if it is rolled back.
2221 | createdState.states.uncommitted.reopen({
2222 | rollback: function(manager) {
2223 | this._super(manager);
2224 | manager.goToState('deleted.saved');
2225 | }
2226 | });
2227 |
2228 | // The updated.uncommitted state and updated.pending.uncommitted share
2229 | // some logic defined in UpdatedUncommitted.
2230 | updatedState.states.uncommitted.reopen(UpdatedUncommitted);
2231 | updatedState.states.pending.states.uncommitted.reopen(UpdatedUncommitted);
2232 | updatedState.states.inFlight.reopen({
2233 | didSaveData: function(manager) {
2234 | var record = get(manager, 'record'),
2235 | data = get(record, 'data');
2236 |
2237 | data.saveData();
2238 | data.adapterDidUpdate();
2239 | }
2240 | });
2241 |
2242 | var states = {
2243 | rootState: Ember.State.create({
2244 | // FLAGS
2245 | isLoaded: false,
2246 | isDirty: false,
2247 | isSaving: false,
2248 | isDeleted: false,
2249 | isError: false,
2250 | isNew: false,
2251 | isValid: true,
2252 | isPending: false,
2253 |
2254 | // SUBSTATES
2255 |
2256 | // A record begins its lifecycle in the `empty` state.
2257 | // If its data will come from the adapter, it will
2258 | // transition into the `loading` state. Otherwise, if
2259 | // the record is being created on the client, it will
2260 | // transition into the `created` state.
2261 | empty: DS.State.create({
2262 | // EVENTS
2263 | loadingData: function(manager) {
2264 | manager.goToState('loading');
2265 | },
2266 |
2267 | didChangeData: function(manager) {
2268 | didChangeData(manager);
2269 |
2270 | manager.goToState('loaded.created');
2271 | }
2272 | }),
2273 |
2274 | // A record enters this state when the store askes
2275 | // the adapter for its data. It remains in this state
2276 | // until the adapter provides the requested data.
2277 | //
2278 | // Usually, this process is asynchronous, using an
2279 | // XHR to retrieve the data.
2280 | loading: DS.State.create({
2281 | // TRANSITIONS
2282 | exit: function(manager) {
2283 | var record = get(manager, 'record');
2284 | record.fire('didLoad');
2285 | },
2286 |
2287 | // EVENTS
2288 | didChangeData: function(manager, data) {
2289 | didChangeData(manager);
2290 | manager.send('loadedData');
2291 | },
2292 |
2293 | loadedData: function(manager) {
2294 | manager.goToState('loaded');
2295 | }
2296 | }),
2297 |
2298 | // A record enters this state when its data is populated.
2299 | // Most of a record's lifecycle is spent inside substates
2300 | // of the `loaded` state.
2301 | loaded: DS.State.create({
2302 | initialState: 'saved',
2303 |
2304 | // FLAGS
2305 | isLoaded: true,
2306 |
2307 | // SUBSTATES
2308 |
2309 | // If there are no local changes to a record, it remains
2310 | // in the `saved` state.
2311 | saved: DS.State.create({
2312 |
2313 | // EVENTS
2314 | setProperty: function(manager, context) {
2315 | setProperty(manager, context);
2316 | manager.goToState('updated');
2317 | },
2318 |
2319 | setAssociation: function(manager, context) {
2320 | setAssociation(manager, context);
2321 | manager.goToState('updated');
2322 | },
2323 |
2324 | didChangeData: didChangeData,
2325 |
2326 | deleteRecord: function(manager) {
2327 | manager.goToState('deleted');
2328 | },
2329 |
2330 | waitingOn: function(manager, object) {
2331 | waitingOn(manager, object);
2332 | manager.goToState('updated.pending');
2333 | },
2334 |
2335 | invokeLifecycleCallbacks: function(manager, dirtyType) {
2336 | var record = get(manager, 'record');
2337 | if (dirtyType === 'created') {
2338 | record.fire('didCreate', record);
2339 | } else {
2340 | record.fire('didUpdate', record);
2341 | }
2342 | }
2343 | }),
2344 |
2345 | // A record is in this state after it has been locally
2346 | // created but before the adapter has indicated that
2347 | // it has been saved.
2348 | created: createdState,
2349 |
2350 | // A record is in this state if it has already been
2351 | // saved to the server, but there are new local changes
2352 | // that have not yet been saved.
2353 | updated: updatedState
2354 | }),
2355 |
2356 | // A record is in this state if it was deleted from the store.
2357 | deleted: DS.State.create({
2358 | // FLAGS
2359 | isDeleted: true,
2360 | isLoaded: true,
2361 | isDirty: true,
2362 |
2363 | // TRANSITIONS
2364 | enter: function(manager) {
2365 | var record = get(manager, 'record'),
2366 | store = get(record, 'store');
2367 |
2368 | store.removeFromRecordArrays(record);
2369 | },
2370 |
2371 | // SUBSTATES
2372 |
2373 | // When a record is deleted, it enters the `start`
2374 | // state. It will exit this state when the record's
2375 | // transaction starts to commit.
2376 | start: DS.State.create({
2377 | // TRANSITIONS
2378 | enter: function(manager) {
2379 | var record = get(manager, 'record');
2380 |
2381 | record.withTransaction(function(t) {
2382 | t.recordBecameDirty('deleted', record);
2383 | });
2384 | },
2385 |
2386 | // EVENTS
2387 | willCommit: function(manager) {
2388 | manager.goToState('inFlight');
2389 | },
2390 |
2391 | rollback: function(manager) {
2392 | var record = get(manager, 'record'),
2393 | data = get(record, 'data');
2394 |
2395 | data.rollback();
2396 | record.withTransaction(function(t) {
2397 | t.recordBecameClean('deleted', record);
2398 | });
2399 | manager.goToState('loaded');
2400 | }
2401 | }),
2402 |
2403 | // After a record's transaction is committing, but
2404 | // before the adapter indicates that the deletion
2405 | // has saved to the server, a record is in the
2406 | // `inFlight` substate of `deleted`.
2407 | inFlight: DS.State.create({
2408 | // FLAGS
2409 | isSaving: true,
2410 |
2411 | // TRANSITIONS
2412 | enter: function(manager) {
2413 | var record = get(manager, 'record');
2414 |
2415 | record.withTransaction(function (t) {
2416 | t.recordBecameInFlight('deleted', record);
2417 | });
2418 | },
2419 |
2420 | // EVENTS
2421 | didCommit: function(manager) {
2422 | var record = get(manager, 'record');
2423 |
2424 | record.withTransaction(function(t) {
2425 | t.recordBecameClean('inflight', record);
2426 | });
2427 |
2428 | manager.goToState('saved');
2429 |
2430 | manager.send('invokeLifecycleCallbacks');
2431 | }
2432 | }),
2433 |
2434 | // Once the adapter indicates that the deletion has
2435 | // been saved, the record enters the `saved` substate
2436 | // of `deleted`.
2437 | saved: DS.State.create({
2438 | // FLAGS
2439 | isDirty: false,
2440 |
2441 | invokeLifecycleCallbacks: function(manager) {
2442 | var record = get(manager, 'record');
2443 | record.fire('didDelete', record);
2444 | }
2445 | })
2446 | }),
2447 |
2448 | // If the adapter indicates that there was an unknown
2449 | // error saving a record, the record enters the `error`
2450 | // state.
2451 | error: DS.State.create({
2452 | isError: true,
2453 |
2454 | // EVENTS
2455 |
2456 | invokeLifecycleCallbacks: function(manager) {
2457 | var record = get(manager, 'record');
2458 | record.fire('becameError', record);
2459 | }
2460 | })
2461 | })
2462 | };
2463 |
2464 | DS.StateManager = Ember.StateManager.extend({
2465 | record: null,
2466 | initialState: 'rootState',
2467 | states: states
2468 | });
2469 |
2470 | })();
2471 |
2472 |
2473 |
2474 | (function() {
2475 | var get = Ember.get, set = Ember.set;
2476 |
2477 | // When a record is changed on the client, it is considered "dirty"--there are
2478 | // pending changes that need to be saved to a persistence layer, such as a
2479 | // server.
2480 | //
2481 | // If the record is rolled back, it re-enters a clean state, any changes are
2482 | // discarded, and its attributes are reset back to the last known good copy
2483 | // of the data that came from the server.
2484 | //
2485 | // If the record is committed, the changes are sent to the server to be saved,
2486 | // and once the server confirms that they are valid, the record's "canonical"
2487 | // data becomes the original canonical data plus the changes merged in.
2488 | //
2489 | // A DataProxy is an object that encapsulates this change tracking. It
2490 | // contains three buckets:
2491 | //
2492 | // * `savedData` - the last-known copy of the data from the server
2493 | // * `unsavedData` - a hash that contains any changes that have not yet
2494 | // been committed
2495 | // * `associations` - this is similar to `savedData`, but holds the client
2496 | // ids of associated records
2497 | //
2498 | // When setting a property on the object, the value is placed into the
2499 | // `unsavedData` bucket:
2500 | //
2501 | // proxy.set('key', 'value');
2502 | //
2503 | // // unsavedData:
2504 | // {
2505 | // key: "value"
2506 | // }
2507 | //
2508 | // When retrieving a property from the object, it first looks to see
2509 | // if that value exists in the `unsavedData` bucket, and returns it if so.
2510 | // Otherwise, it returns the value from the `savedData` bucket.
2511 | //
2512 | // When the adapter notifies a record that it has been saved, it merges the
2513 | // `unsavedData` bucket into the `savedData` bucket. If the record's
2514 | // transaction is rolled back, the `unsavedData` hash is simply discarded.
2515 | //
2516 | // This object is a regular JS object for performance. It is only
2517 | // used internally for bookkeeping purposes.
2518 |
2519 | var DataProxy = DS._DataProxy = function(record) {
2520 | this.record = record;
2521 |
2522 | this.unsavedData = {};
2523 |
2524 | this.associations = {};
2525 | };
2526 |
2527 | DataProxy.prototype = {
2528 | get: function(key) { return Ember.get(this, key); },
2529 | set: function(key, value) { return Ember.set(this, key, value); },
2530 |
2531 | setAssociation: function(key, value) {
2532 | this.associations[key] = value;
2533 | },
2534 |
2535 | savedData: function() {
2536 | var savedData = this._savedData;
2537 | if (savedData) { return savedData; }
2538 |
2539 | var record = this.record,
2540 | clientId = get(record, 'clientId'),
2541 | store = get(record, 'store');
2542 |
2543 | if (store) {
2544 | savedData = store.dataForRecord(record);
2545 | this._savedData = savedData;
2546 | return savedData;
2547 | }
2548 | },
2549 |
2550 | unknownProperty: function(key) {
2551 | var unsavedData = this.unsavedData,
2552 | associations = this.associations,
2553 | savedData = this.savedData(),
2554 | store;
2555 |
2556 | var value = unsavedData[key], association;
2557 |
2558 | // if this is a belongsTo association, this will
2559 | // be a clientId.
2560 | association = associations[key];
2561 |
2562 | if (association !== undefined) {
2563 | store = get(this.record, 'store');
2564 | return store.clientIdToId[association];
2565 | }
2566 |
2567 | if (savedData && value === undefined) {
2568 | value = savedData[key];
2569 | }
2570 |
2571 | return value;
2572 | },
2573 |
2574 | setUnknownProperty: function(key, value) {
2575 | var record = this.record,
2576 | unsavedData = this.unsavedData;
2577 |
2578 | unsavedData[key] = value;
2579 |
2580 | record.hashWasUpdated();
2581 |
2582 | return value;
2583 | },
2584 |
2585 | commit: function() {
2586 | this.saveData();
2587 |
2588 | this.record.notifyPropertyChange('data');
2589 | },
2590 |
2591 | rollback: function() {
2592 | this.unsavedData = {};
2593 |
2594 | this.record.notifyPropertyChange('data');
2595 | },
2596 |
2597 | saveData: function() {
2598 | var record = this.record;
2599 |
2600 | var unsavedData = this.unsavedData;
2601 | var savedData = this.savedData();
2602 |
2603 | for (var prop in unsavedData) {
2604 | if (unsavedData.hasOwnProperty(prop)) {
2605 | savedData[prop] = unsavedData[prop];
2606 | delete unsavedData[prop];
2607 | }
2608 | }
2609 | },
2610 |
2611 | adapterDidUpdate: function() {
2612 | this.unsavedData = {};
2613 | }
2614 | };
2615 |
2616 | })();
2617 |
2618 |
2619 |
2620 | (function() {
2621 | var get = Ember.get, set = Ember.set, getPath = Ember.getPath, none = Ember.none;
2622 |
2623 | var retrieveFromCurrentState = Ember.computed(function(key) {
2624 | return get(getPath(this, 'stateManager.currentState'), key);
2625 | }).property('stateManager.currentState').cacheable();
2626 |
2627 | DS.Model = Ember.Object.extend(Ember.Evented, {
2628 | isLoaded: retrieveFromCurrentState,
2629 | isDirty: retrieveFromCurrentState,
2630 | isSaving: retrieveFromCurrentState,
2631 | isDeleted: retrieveFromCurrentState,
2632 | isError: retrieveFromCurrentState,
2633 | isNew: retrieveFromCurrentState,
2634 | isPending: retrieveFromCurrentState,
2635 | isValid: retrieveFromCurrentState,
2636 |
2637 | clientId: null,
2638 | transaction: null,
2639 | stateManager: null,
2640 | pendingQueue: null,
2641 | errors: null,
2642 |
2643 | // because unknownProperty is used, any internal property
2644 | // must be initialized here.
2645 | primaryKey: 'id',
2646 | id: Ember.computed(function(key, value) {
2647 | var primaryKey = get(this, 'primaryKey'),
2648 | data = get(this, 'data');
2649 |
2650 | if (arguments.length === 2) {
2651 | set(data, primaryKey, value);
2652 | return value;
2653 | }
2654 |
2655 | var id = get(data, primaryKey);
2656 | return id ? id : this._id;
2657 | }).property('primaryKey', 'data'),
2658 |
2659 | // The following methods are callbacks invoked by `toJSON`. You
2660 | // can override one of the callbacks to override specific behavior,
2661 | // or toJSON itself.
2662 | //
2663 | // If you override toJSON, you can invoke these callbacks manually
2664 | // to get the default behavior.
2665 |
2666 | /**
2667 | Add the record's primary key to the JSON hash.
2668 |
2669 | The default implementation uses the record's specified `primaryKey`
2670 | and the `id` computed property, which are passed in as parameters.
2671 |
2672 | @param {Object} json the JSON hash being built
2673 | @param {Number|String} id the record's id
2674 | @param {String} key the primaryKey for the record
2675 | */
2676 | addIdToJSON: function(json, id, key) {
2677 | if (id) { json[key] = id; }
2678 | },
2679 |
2680 | /**
2681 | Add the attributes' current values to the JSON hash.
2682 |
2683 | The default implementation gets the current value of each
2684 | attribute from the `data`, and uses a `defaultValue` if
2685 | specified in the `DS.attr` definition.
2686 |
2687 | @param {Object} json the JSON hash being build
2688 | @param {Ember.Map} attributes a Map of attributes
2689 | @param {DataProxy} data the record's data, accessed with `get` and `set`.
2690 | */
2691 | addAttributesToJSON: function(json, attributes, data) {
2692 | attributes.forEach(function(name, meta) {
2693 | var key = meta.key(this.constructor),
2694 | value = get(data, key);
2695 |
2696 | if (value === undefined) {
2697 | value = meta.options.defaultValue;
2698 | }
2699 |
2700 | json[key] = value;
2701 | }, this);
2702 | },
2703 |
2704 | /**
2705 | Add the value of a `hasMany` association to the JSON hash.
2706 |
2707 | The default implementation honors the `embedded` option
2708 | passed to `DS.hasMany`. If embedded, `toJSON` is recursively
2709 | called on the child records. If not, the `id` of each
2710 | record is added.
2711 |
2712 | Note that if a record is not embedded and does not
2713 | yet have an `id` (usually provided by the server), it
2714 | will not be included in the output.
2715 |
2716 | @param {Object} json the JSON hash being built
2717 | @param {DataProxy} data the record's data, accessed with `get` and `set`.
2718 | @param {Object} meta information about the association
2719 | @param {Object} options options passed to `toJSON`
2720 | */
2721 | addHasManyToJSON: function(json, data, meta, options) {
2722 | var key = meta.key,
2723 | manyArray = get(this, key),
2724 | records = [], i, l,
2725 | clientId, id;
2726 |
2727 | if (meta.options.embedded) {
2728 | // TODO: Avoid materializing embedded hashes if possible
2729 | manyArray.forEach(function(record) {
2730 | records.push(record.toJSON(options));
2731 | });
2732 | } else {
2733 | var clientIds = get(manyArray, 'content');
2734 |
2735 | for (i=0, l=clientIds.length; i