API Docs for: 0.0.1
Show:

File: src/js-ext/extra/observers.js

/**
 *
 * Pollyfils for often used functionality for Objects
 *
 * <i>Copyright (c) 2014 ITSA - https://github.com/itsa</i>
 * New BSD License - http://choosealicense.com/licenses/bsd-3-clause/
 *
 * @module js-ext
 * @submodule lib/object.js
 * @class Object
 *
*/

(function (global) {

    "use strict";

    require('polyfill/polyfill-base.js');
    require('polyfill/lib/weakmap.js');
    require('../lib/object.js');
    require('../lib/array.js');

    var NATIVE_OBJECT_OBSERVE = !!Object.observe,
        NATIVE_ARRAY_OBSERVE = !!Array.observe,
        later = require('utils').later,
        _watchers = [],
        _registeredCallbacks = new global.WeakMap(),
        POLL_OBSERVE = 100,
        // Define configurable, writable and non-enumerable props
        // if they don't exist.
        defineProperty = function (object, name, method, force) {
            if (!force && (name in object)) {
                return;
            }
            Object.defineProperty(object, name, {
                configurable: true,
                enumerable: false,
                writable: true,
                value: method
            });
        },
        defineProperties = function (object, map, force) {
            var names = Object.keys(map),
                l = names.length,
                i = -1,
                name;
            while (++i < l) {
                name = names[i];
                defineProperty(object, name, map[name], force);
            }
        },
        watchObject = function(obj, callback) {
            var watcher;
            watcher = {
                obj: obj,
                cb: callback,
                cloneObj: obj.deepClone(true),
                timer: later(function() {
                    if (!obj.sameValue(watcher.cloneObj)) {
                        watcher.cloneObj = obj.deepClone(true);
                        callback(obj);
                    }
                }, POLL_OBSERVE, true)
            };
            _watchers[_watchers.length] = watcher;
        },

        unWatchObject = function(obj, callback) {
            var currentWatcher;
            _watchers.some(function(watcher) {
                if ((watcher.obj===obj) && (watcher.callback===callback)) {
                    currentWatcher = watcher;
                }
                return currentWatcher;
            });
            if (currentWatcher) {
                currentWatcher.timer.cancel();
                _watchers.remove(currentWatcher);
            }
        },

        callbackFn = function(item, callback) {
            return callback.bind(null, item);
        },

        structureChanged = function(callback) {
            var watcher;
            watcher = function(changes) {
                // changes is an array with objects having the following properties:
                // {
                //    name: The name of the property which was changed.
                //    object: The changed object after the change was made.
                //    type: A string indicating the type of change taking place. One of "add", "update", or "delete".
                //    oldValue: Only for "update" and "delete" types. The value before the change.
                // }
                var len = changes.length,
                    i, changedProp, property;
                for (i=0; i<len; i++) {
                    changedProp = changes[i];
                    property = changedProp.object[changedProp.name];
                    if (changedProp.type==='delete') {
                        // clear previous observer
                        if (Object.isObject(property) || Array.isArray(property)) {
                            property.unobserve(callback);
                        }
                    }
                    if (changedProp.type==='add') {
                        // set new observer
                        if (Object.isObject(property) || Array.isArray(property)) {
                            property.observe(callback);
                        }
                    }
                }
            };
            return watcher;
        };

    defineProperties(Object.prototype, {
        /**
         * Observes changes of the instance. On any changes, the callback will be invoked.
         * Uses a polyfill on environments that don't support native Object.observe.
         *
         * The callback comes without arguments (native Object.observe does, but non-native doesn't)
         * so, they cannot be used.
         *
         * Will observer the complete object nested (deep).
         *
         * @for Object
         * @method observe
         * @chainable
         */
        observe: function (callback) {
            var obj = this,
                property, structureChangedCallback, objCallbackHash, cbFn;
            if (typeof callback==='function') {
                if (NATIVE_OBJECT_OBSERVE) {
                    _registeredCallbacks.has(obj) || _registeredCallbacks.set(obj, []);
                    objCallbackHash = _registeredCallbacks.get(obj);

                    cbFn = callbackFn(obj, callback);
                    Object.observe(obj, cbFn);

                    objCallbackHash[objCallbackHash.length] = {
                        cb: callback,
                        cbFn: cbFn
                    };

                    // check all properties if they are an Array or Object:
                    // in those cases, we need extra observers
                    for (property in obj) {
                        if (Object.isObject(obj[property]) || Array.isArray(obj[property])) {
                            obj[property].observe(callback);
                        }
                    }
                    // we also need to watch the object for new/replaced/removed properties ot the type Object/Array:
                    // they also need to be watched/unwatched
                    // to register this, we add an extra observer that looks for the type of the change

                    structureChangedCallback = structureChanged(callback);
                    Object.observe(obj, structureChangedCallback, ['add', 'delete']);

                    objCallbackHash[objCallbackHash.length] = {
                        cb: callback,
                        cbFn: structureChangedCallback
                    };

                }
                else {
                    watchObject(obj, callback);
                }
            }
            return obj;
        },

        /**
         * Un-observes changes that are registered with `observe`.
         * Uses a polyfill on environments that don't support native Object.observe.
         *
         * @method unobserve
         * @chainable
         */
        unobserve: function (callback) {
            var obj = this,
                property, objCallbackHash, len, i, item, structureChangedCallback;
            if (typeof callback==='function') {
                if (NATIVE_OBJECT_OBSERVE) {
                    objCallbackHash = _registeredCallbacks.get(obj);
                    if (objCallbackHash) {
                        len = objCallbackHash.length -1;
                        for (i=len; i>=0; i--) {
                            item = objCallbackHash[i];
                            if (item.cb===callback) {
                                structureChangedCallback = item.cbFn;
                                Object.unobserve(obj, structureChangedCallback);
                            }
                            objCallbackHash.splice(i, 1);
                        }
                        (objCallbackHash.length>0) || _registeredCallbacks.delete(obj);

                        for (property in obj) {
                            if (Object.isObject(obj[property]) || Array.isArray(obj[property])) {
                                obj[property].unobserve(callback);
                            }
                        }
                    }
                }
                else {
                    unWatchObject(obj, callback);
                }
            }
            return obj;
        }
    });

    defineProperties(Array.prototype, {
        /**
         * Observes changes of the instance. On any changes, the callback will be invoked.
         * Uses a polyfill on environments that don't support native Array.observe.
         *
         * The callback comes without arguments (native Array.observe does, but non-native doesn't)
         * so, they cannot be used.
         *
         * Will observer the complete array nested (deep).
         *
         * @for Array
         * @method observe
         * @chainable
         */
        observe: function (callback) {
            var array = this,
                item, i, len, structureChangedCallback, arrayCallbackHash, cbFn;
            if (typeof callback==='function') {
                if (NATIVE_ARRAY_OBSERVE) {
                    _registeredCallbacks.has(array) || _registeredCallbacks.set(array, []);
                    arrayCallbackHash = _registeredCallbacks.get(array);

                    cbFn = callbackFn(array, callback);
                    Array.observe(array, cbFn);

                    arrayCallbackHash[arrayCallbackHash.length] = {
                        cb: callback,
                        cbFn: cbFn
                    };

                    // check all properties if they are an Array or Object:
                    // in those cases, we need extra observers
                    len = array.length;
                    for (i=0; i<len; i++) {
                        item = array[i];
                        if (Object.isObject(item) || Array.isArray(item)) {
                            item.observe(callback);
                        }
                    }
                    // we also need to watch the object for new/replaced/removed properties ot the type Object/Array:
                    // they also need to be watched/unwatched
                    // to register this, we add an extra observer that looks for the type of the change

                    structureChangedCallback = structureChanged(callback);
                    Array.observe(array, structureChangedCallback);

                    arrayCallbackHash[arrayCallbackHash.length] = {
                        cb: callback,
                        cbFn: structureChangedCallback
                    };
                }
                else {
                    watchObject(array, callback);
                }
            }
            return array;
        },

        /**
         * Un-observes changes that are registered with `observe`.
         * Uses a polyfill on environments that don't support native Array.observe.
         *
         * @method unobserve
         * @chainable
         */
        unobserve: function (callback) {
            var array = this,
                item, i, len, arrayCallbackHash, structureChangedCallback;
            if (typeof callback==='function') {
                if (NATIVE_ARRAY_OBSERVE) {
                    arrayCallbackHash = _registeredCallbacks.get(array);
                    if (arrayCallbackHash) {
                        len = arrayCallbackHash.length -1;
                        for (i=len; i>=0; i--) {
                            item = arrayCallbackHash[i];
                            if (item.cb===callback) {
                                structureChangedCallback = item.cbFn;
                                Array.unobserve(array, structureChangedCallback);
                            }
                            arrayCallbackHash.splice(i, 1);
                        }
                        (arrayCallbackHash.length>0) || _registeredCallbacks.delete(array);

                        len = array.length;
                        for (i=0; i<len; i++) {
                            item = array[i];
                            if (Object.isObject(item) || Array.isArray(item)) {
                                item.unobserve(callback);
                            }
                        }
                    }
                }
                else {
                    unWatchObject(array, callback);
                }
            }
            return array;
        }
    });

}(typeof global !== 'undefined' ? global : /* istanbul ignore next */ this));