/** * DEVELOPED BY * GIL LOPES BUENO * gilbueno.mail@gmail.com * * WORKS WITH: * IE8*, IE 9+, FF 4+, SF 5+, WebKit, CH 7+, OP 12+, BESEN, Rhino 1.7+ * For IE8 (and other legacy browsers) WatchJS will use dirty checking * * FORK: * https://github.com/melanke/Watch.JS * * LICENSE: MIT */ "use strict"; (function (factory) { if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like enviroments that support module.exports, // like Node. module.exports = factory(); } else if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(factory); } else { // Browser globals window.WatchJS = factory(); window.watch = window.WatchJS.watch; window.unwatch = window.WatchJS.unwatch; window.callWatchers = window.WatchJS.callWatchers; } }(function () { var WatchJS = { noMore: false, // use WatchJS.suspend(obj) instead useDirtyCheck: false, // use only dirty checking to track changes. preserveExistingSetters: false }, lengthsubjects = []; var dirtyChecklist = []; var pendingChanges = []; // used coalesce changes from defineProperty and __defineSetter__ var supportDefineProperty = false; try { supportDefineProperty = Object.defineProperty && Object.defineProperty({},'x', {}); } catch(ex) { /* not supported */ } var isFunction = function (functionToCheck) { var getType = {}; return functionToCheck && getType.toString.call(functionToCheck) == '[object Function]'; }; var isInt = function (x) { return x % 1 === 0; }; var isArray = function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; var isObject = function(obj) { return {}.toString.apply(obj) === '[object Object]'; }; var getObjDiff = function(a, b){ var aplus = [], bplus = []; if(!(typeof a == "string") && !(typeof b == "string")){ if (isArray(a) && b) { for (var i=0; i 0) { for (var prop = 0; prop < obj.length; prop++) { // watch objects in array watchAll(obj[prop],watcher,level, addNRemove); } } } else { var prop,props = []; for (prop in obj) { //for each attribute if obj is an object if (prop == "$val" || (!supportDefineProperty && prop === 'watchers')) { continue; } if (Object.prototype.hasOwnProperty.call(obj, prop)) { props.push(prop); //put in the props } } watchMany(obj, props, watcher, level, addNRemove); //watch all items of the props } if (addNRemove) { pushToLengthSubjects(obj, "$$watchlengthsubjectroot", watcher, level); } }; var watchMany = function (obj, props, watcher, level, addNRemove) { if ((typeof obj == "string") || (!(obj instanceof Object) && !isArray(obj))) { //accepts only objects and array (not string) return; } for (var i=0; i 0)){ watchAll(obj[prop], watcher, level!==undefined? level-1 : level); //recursively watch all attributes of this } defineWatcher(obj, prop, watcher, level); if(addNRemove && (level === undefined || level > 0)){ pushToLengthSubjects(obj, prop, watcher, level); } }; var unwatch = function () { if (isFunction(arguments[1])) { unwatchAll.apply(this, arguments); } else if (isArray(arguments[1])) { unwatchMany.apply(this, arguments); } else { unwatchOne.apply(this, arguments); } }; var unwatchAll = function (obj, watcher) { if (obj instanceof String || (!(obj instanceof Object) && !isArray(obj))) { //accepts only objects and array (not string) return; } if (isArray(obj)) { var props = ['__watchall__']; for (var prop = 0; prop < obj.length; prop++) { //for each item if obj is an array props.push(prop); //put in the props } unwatchMany(obj, props, watcher); //watch all itens of the props } else { var unwatchPropsInObject = function (obj2) { var props = []; for (var prop2 in obj2) { //for each attribute if obj is an object if (obj2.hasOwnProperty(prop2)) { if (obj2[prop2] instanceof Object) { unwatchPropsInObject(obj2[prop2]); //recurs into object props } else { props.push(prop2); //put in the props } } } unwatchMany(obj2, props, watcher); //unwatch all of the props }; unwatchPropsInObject(obj); } }; var unwatchMany = function (obj, props, watcher) { for (var prop2 in props) { //watch each attribute of "props" if is an object if (props.hasOwnProperty(prop2)) { unwatchOne(obj, props[prop2], watcher); } } }; var timeouts = [], timerID = null; function clearTimerID() { timerID = null; for(var i=0; i< timeouts.length; i++) { timeouts[i](); } timeouts.length = 0; } var getTimerID= function () { if (!timerID) { timerID = setTimeout(clearTimerID); } return timerID; } var registerTimeout = function(fn) { // register function to be called on timeout if (timerID==null) getTimerID(); timeouts[timeouts.length] = fn; } // Track changes made to an array, object or an object's property // and invoke callback with a single change object containing type, value, oldvalue and array splices // Syntax: // trackChange(obj, callback, recursive, addNRemove) // trackChange(obj, prop, callback, recursive, addNRemove) var trackChange = function() { var fn = (isFunction(arguments[2])) ? trackProperty : trackObject ; fn.apply(this,arguments); } // track changes made to an object and invoke callback with a single change object containing type, value and array splices var trackObject= function(obj, callback, recursive, addNRemove) { var change = null,lastTimerID = -1; var isArr = isArray(obj); var level,fn = function(prop, action, newValue, oldValue) { var timerID = getTimerID(); if (lastTimerID!==timerID) { // check if timer has changed since last update lastTimerID = timerID; change = { type: 'update' } change['value'] = obj; change['splices'] = null; registerTimeout(function() { callback.call(this,change); change = null; }); } // create splices for array changes if (isArr && obj === this && change !== null) { if (action==='pop'||action==='shift') { newValue = []; oldValue = [oldValue]; } else if (action==='push'||action==='unshift') { newValue = [newValue]; oldValue = []; } else if (action!=='splice') { return; // return here - for reverse and sort operations we don't need to return splices. a simple update will do } if (!change.splices) change.splices = []; change.splices[change.splices.length] = { index: prop, deleteCount: oldValue ? oldValue.length : 0, addedCount: newValue ? newValue.length : 0, added: newValue, deleted: oldValue }; } } level = (recursive==true) ? undefined : 0; watchAll(obj,fn, level, addNRemove); } // track changes made to the property of an object and invoke callback with a single change object containing type, value, oldvalue and splices var trackProperty = function(obj,prop,callback,recursive, addNRemove) { if (obj && prop) { watchOne(obj,prop,function(prop, action, newvalue, oldvalue) { var change = { type: 'update' } change['value'] = newvalue; change['oldvalue'] = oldvalue; if (recursive && isObject(newvalue)||isArray(newvalue)) { trackObject(newvalue,callback,recursive, addNRemove); } callback.call(this,change); },0) if (recursive && isObject(obj[prop])||isArray(obj[prop])) { trackObject(obj[prop],callback,recursive, addNRemove); } } } var defineWatcher = function (obj, prop, watcher, level) { var newWatcher = false; var isArr = isArray(obj); if (!obj.watchers) { defineProp(obj, "watchers", {}); if (isArr) { // watch array functions watchFunctions(obj, function(index,action,newValue, oldValue) { addPendingChange(obj, index, action,newValue, oldValue); if (level !== 0 && newValue && (isObject(newValue) || isArray(newValue))) { var i,n, ln, wAll, watchList = obj.watchers[prop]; if ((wAll = obj.watchers['__watchall__'])) { watchList = watchList ? watchList.concat(wAll) : wAll; } ln = watchList ? watchList.length : 0; for (i = 0; i 0 ? arguments[0] : undefined; } response = original.apply(obj, arguments); if (methodName !== 'slice') { if (methodName === 'pop') { oldValue = response; index = obj.length; } else if (methodName === 'push') { index = obj.length-1; } else if (methodName === 'shift') { oldValue = response; } else if (methodName !== 'unshift' && newValue===undefined) { newValue = response; } callback.call(obj, index, methodName,newValue, oldValue) } return response; }); }; var watchFunctions = function(obj, callback) { if (!isFunction(callback) || !obj || (obj instanceof String) || (!isArray(obj))) { return; } for (var i = methodNames.length, methodName; i--;) { methodName = methodNames[i]; defineArrayMethodWatcher(obj, obj[methodName], methodName, callback); } }; var unwatchOne = function (obj, prop, watcher) { if (prop) { if (obj.watchers && obj.watchers[prop]) { if (watcher === undefined) { delete obj.watchers[prop]; // remove all property watchers } else { for (var i = 0; i < obj.watchers[prop].length; i++) { var w = obj.watchers[prop][i]; if (w == watcher) { obj.watchers[prop].splice(i, 1); } } } } } else { delete obj.watchers; } removeFromLengthSubjects(obj, prop, watcher); removeFromDirtyChecklist(obj, prop); }; // suspend watchers until next update cycle var suspend = function(obj, prop) { if (obj.watchers) { var name = '__wjs_suspend__'+(prop!==undefined ? prop : ''); obj.watchers[name] = true; } } var isSuspended = function(obj, prop) { return obj.watchers && (obj.watchers['__wjs_suspend__'] || obj.watchers['__wjs_suspend__'+prop]); } // resumes preivously suspended watchers var resume = function(obj, prop) { registerTimeout(function() { delete obj.watchers['__wjs_suspend__']; delete obj.watchers['__wjs_suspend__'+prop]; }) } var pendingTimerID = null; var addPendingChange = function(obj,prop, mode, newval, oldval) { pendingChanges[pendingChanges.length] = { obj:obj, prop: prop, mode: mode, newval: newval, oldval: oldval }; if (pendingTimerID===null) { pendingTimerID = setTimeout(applyPendingChanges); } }; var applyPendingChanges = function() { // apply pending changes var change = null; pendingTimerID = null; for(var i=0;i < pendingChanges.length;i++) { change = pendingChanges[i]; callWatchers(change.obj, change.prop, change.mode, change.newval, change.oldval); } if (change) { pendingChanges = []; change = null; } } var loop = function(){ // check for new or deleted props for(var i=0; i 0) { for (var i = 0; i < dirtyChecklist.length; i++) { n = dirtyChecklist[i]; value = n.object[n.prop]; if (!compareValues(n.orig, value)) { n.orig = clone(value); n.callback(value); } } } }; var compareValues = function(a,b) { var i, state = true; if (a!==b) { if (isObject(a)) { for(i in a) { if (!supportDefineProperty && i==='watchers') continue; if (a[i]!==b[i]) { state = false; break; }; } } else { state = false; } } return state; } var pushToLengthSubjects = function(obj, prop, watcher, level){ var actual; if (prop === "$$watchlengthsubjectroot") { actual = clone(obj); } else { actual = clone(obj[prop]); } lengthsubjects.push({ obj: obj, prop: prop, actual: actual, watcher: watcher, level: level }); }; var removeFromLengthSubjects = function(obj, prop, watcher){ for (var i=0; i