333 lines · 17.5 KB · JavaScript
RawDownload
// Deep observer plugin for Knockout http://knockoutjs.com/// (c) Ziad Jeeroburkhan// License: MIT (http://www.opensource.org/licenses/mit-license.php); (function (factory) {    // CommonJS    if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') {        factory(require('knockout'));        // AMD    } else if (typeof define === 'function' && define.amd) {        define(['knockout'], factory);        // Normal script tag    } else {        factory(window.ko);    }}(function (ko) {ko.subscribable.fn['watch'] = function (targetOrCallback, options, evaluatorCallback, context) {    /// <summary>    ///     Track and manage changes within the chained observable down to any given level.     /// </summary>    /// <param name="targetOrCallback">    ///      The subscription callback function or an object containing the subscribables to be watched.    /// </param>    /// <param name="options" type="object">    ///     false -> Disables tracking on the chained observable.    ///     { depth: 2 } -> Track all nested subscribables down to the 2nd level(default is 1).<br/>    ///     { depth: -1 } -> Track all nested subscribables.<br/>    ///     { hide: [...] } -> Property or array of properties to be ignored.<br/>    ///     { hideArrays: true } -> Ignore all nested arrays.<br/>    ///     { hideWrappedValues: true } -> Ignore observables wrapped under yet another parent observable.<br/>    ///     { mutable: true } -> Dynamically adapt to changes made to the target structure through any subscribable.<br/>    ///     { watchedOnly: true } -> Watch only subscribables tagged with .watch().<br/>    ///     { beforeWatch: function(parents, child) {...} } -> Function called prior to creating a subscription. Returning false aborts the operation and ignores its children.<br/>    ///     { wrap: true } -> Wrap all fields into observables. This happens on the fly for new array items(or child objects when mutable is set to true).<br/>    ///     { beforeWrap: function(parents, field, value) {...} } -> Function called prior to wrapping a value into an observable. Returning false leaves it as it is.<br/>    ///     { tagFields: true } -> Add the property '_fieldName' under each property for textual identification.<br/>    ///     { tagFields: 'parentsOnly' } -> Same as above except that it is limited to parent properties only.<br/>    ///     { oldValues: 3 } -> Keep the last three values for each subscribable under the property 'oldValues'.<br/>    ///     { async: false } -> Start watching new objects synchronously    ///     { splitArrayChanges: false } -> receive a single notification for array changes as an array of "items" instead of multiple notifications    ///     { seal: true } -> Prevent any subsequent watcher from watching the target again.<br/>    ///     { unloop: true } -> Avoid circular paths through the use of a breadcrumb property '_watcher' set at each node level.<br/>    /// </param>    /// <param name="evaluatorCallback" type="function">    ///     The  callback function called during changes. Any return value is assigned to the chained observable.    /// </param>     var targetType = typeof targetOrCallback;     if (targetType === 'boolean' || targetType === 'undefined') {        // Turn on or off the watcher for the specified target along with any of its children.        ko.watch(this, { enabled: targetOrCallback !== false });    } else if (targetType === 'function' && !ko.isSubscribable(targetOrCallback)) {        // Target the chained subscribable itself if no target subscribable or object was passed.        ko.watch(this, options || {}, targetOrCallback, context || this);    } else {        ko.watch(targetOrCallback, options, evaluatorCallback, context || this);    }     return this;}; ko['watch'] = function (target, options, evaluatorCallback, context) {    /// <summary>    ///     Track and manage changes within a specific target object down to any given level.    /// </summary>    /// <param name="target">    ///     An object or function containing the targeted subscribable(s).    /// </param>    /// <param name="options" type="object">    ///     { depth: 2 } -> Track all nested subscribables down to the 2nd level(default is 1).<br/>    ///     { depth: -1 } -> Track all nested subscribables.<br/>    ///     { hide: [...] } -> Property or array of properties to be ignored.<br/>    ///     { hideArrays: true } -> Ignore all nested arrays.<br/>    ///     { hideWrappedValues: true } -> Ignore observables wrapped under yet another parent observable.<br/>    ///     { mutable: true } -> Dynamically adapt to changes made to the target structure through any subscribable.<br/>    ///     { watchedOnly: true } -> Watch only subscribables tagged with .watch().<br/>    ///     { beforeWatch: function(parents, child) {...} } -> Function called prior to creating a subscription. Returning false aborts the operation and ignores its children.<br/>    ///     { wrap: true } -> Wrap all fields into observables. This happens on the fly for new array items(or child objects when mutable is set to true).<br/>    ///     { beforeWrap: function(parents, field, value) {...} } -> Function called prior to wrapping a value into an observable. Returning false leaves it as it is.<br/>    ///     { tagFields: true } -> Add the property '_fieldName' under each property for textual identification.<br/>    ///     { tagFields: 'parentsOnly' } -> Same as above except that it is limited to parent properties only.<br/>    ///     { oldValues: 3 } -> Keep the last three values for each subscribable under the property 'oldValues'.<br/>    ///     { seal: true } -> Prevent any subsequent watcher from watching the target again.<br/>    ///     { unloop: true } -> Avoid circular paths through the use of a breadcrumb property '_watcher' set at each node level.<br/>    ///     { getter: function(parents, child, property) {...} } -> Function used to retrieve the property value from the given child. False can be returned to ignore the property.<br/>    /// </param>    /// <param name="evaluatorCallback" type="function">    ///     The callback function called during changes.    /// </param>     if (typeof options === 'function') {        context = context || evaluatorCallback;        evaluatorCallback = options;        options = {};    }     context = context || this;     function watchChildren(child, parent, grandParents, unwatch, keepOffParentList, fieldName) {        if (child && options.depth !== 0 && (options.depth === -1 || grandParents.length < (options.depth || 1))) {             // Proceed on watched children only when in watched-only mode.            if (options.watchedOnly && !child.watchable && child != target)                return;             // Setting the target as false prevents it from being watched later on.            if (options.enabled === false || options.enabled === true)                child.watchable = options.enabled;             // Ignore watch-disabled objects.            if (child.watchable === false)                return;             // Prevent subsequent watchers from watching the target when sealed.            if (options.seal === true)                child.watchable = false;             var type = typeof child;             if (type === 'object' || type === 'function') {                // Bypass circular references.                if (child._watcher === context)                    return;                 // Ignore hidden objects. Also applies to any of their children.                if (options.hide)                    if (ko.utils.arrayIndexOf(options.hide, child) > -1)                        return;                 // Merge parents. Using a fresh array so it is not referenced in the next recursion if any.                var parents = [].concat(grandParents, parent && parent !== target ? parent : []);                 if (type === 'function') {                    if (typeof child['notifySubscribers'] == 'function') {                        // Target is a subscribable. Watch it.                        if (evaluatorCallback) {                            if (options.enabled === true && child.watchable === false)                                // Only waking up an existing watcher. Let's not add another.                                return;                             if (unwatch || !options.beforeWatch || options.beforeWatch.call(context, parents, child, fieldName) !== false) {                                var isArray = typeof child.pop === 'function';                                 if (unwatch) {                                    disposeWatcher(child);                                } else {                                    assignWatcher(child, isArray, parents, keepOffParentList);                                }                                 if (isArray) {                                    watchChildren(child(), keepOffParentList ? null : child, parents, unwatch, true);                                    return true;                                } else {                                    if (options.hideWrappedValues !== true)                                        return watchChildren(child(), keepOffParentList ? null : child, parents, unwatch, true);                                }                            }                        }                    }                } else {                    if (Object.prototype.toString.call(child) === '[object Object]') {                        ko.utils.objectForEach(child, function (property, sub) {                            sub = options.getter ? options.getter.call(context, parents, child, property) : sub;                            if (sub) {                                if (options.wrap) {                                    // Wrap simple objects and arrays into observables.                                    var type = Object.prototype.toString.call(sub);                                    if (type !== '[object Function]' && type !== '[object Object]') {                                        if (!options.beforeWrap || options.beforeWrap.call(context, parents, child, sub) !== false) {                                            sub = child[property] = type === '[object Array]'                                                ? ko.observableArray(sub)                                                : ko.observable(sub);                                        }                                    }                                }                                 if (options.unloop)                                    sub._watcher = unwatch ? undefined : context;                                 var hasChildren = watchChildren(sub, keepOffParentList ? null : child, parents, unwatch, null, property);                                 if (options.tagFields && sub._fieldName === undefined)                                    if (hasChildren                                        || (options.tagFields !== 'parentsOnly' && typeof sub === 'function' || typeof sub === 'object'))                                        sub._fieldName = property;                            }                        });                    } else { // '[object Array]'                        if (options.hideArrays !== true)                            for (var i = 0; i < child.length; i++)                                watchChildren(child[i], keepOffParentList ? null : child, parents, unwatch);                    }                     return true;                }            }        }    }     // Subscriptions are stored under either the _subscriptions field for the debug version    // or the F, H or M fields when minified depending on the version used.    // NOTE: we used to use ko.DEBUG to detect the debug versionbut it was removed in 3.4.0+,    //       so we now check the existence of a "subscription" function.    // An easy way to identify the right name is to search for "{change:" in the minified     // code and look at the property name assigned.    var subscriptionsField;    switch (typeof ko.subscription == 'function' || ko.version) {        case true: subscriptionsField = '_subscriptions'; break;        case "3.0.0": subscriptionsField = 'F'; break;        case "3.1.0": subscriptionsField = 'H'; break;        case "3.2.0": subscriptionsField = 'M'; break;        case "3.3.0": subscriptionsField = 'G'; break;        case "3.4.0": subscriptionsField = 'K'; break;        case "3.4.1": subscriptionsField = 'K'; break;        case "3.4.2": subscriptionsField = 'F'; break;        case "3.5.0": subscriptionsField = 'S'; break;        case "3.5.1": subscriptionsField = 'U'; break;        default: throw "Unsupported Knockout version. Only v3.0.0 to v3.5.1 finals are supported when minified. Current version is " + ko.version;    }     function disposeWatcher(child) {        var subsc = child[subscriptionsField];         if (subsc) {            if (subsc.change)                for (var i = subsc.change.length - 1; i >= 0; i--)                    if (subsc.change[i]._watcher === context)                        subsc.change[i].dispose();             if (subsc.beforeChange && (options.mutable || options.oldValues > 0))                // Also clean up any before-change subscriptions used for tracking old values.                for (var i = subsc.beforeChange.length - 1; i >= 0; i--)                    if (subsc.beforeChange[i]._watcher === context)                        subsc.beforeChange[i].dispose();             if (subsc.arrayChange)                for (var i = subsc.arrayChange.length - 1; i >= 0; i--)                    if (subsc.arrayChange[i]._watcher === context)                        subsc.arrayChange[i].dispose();        } else {            throw "Subscriptions field (." + subscriptionsField + ") not defined for observable child " + (child._fieldName || "");        }    }     function assignWatcher(child, isArray, parents, keepOffParentList) {        if (isArray) {            // Child is an observable array. Watch all changes within it.            child.subscribe(function (changes) {                var returnValue;                if (options.splitArrayChanges === false) {                    returnValue = evaluatorCallback.call(context, parents, child, changes);                    if (returnValue !== undefined)                        context(returnValue);                }                ko.utils.arrayForEach(changes, function (item) {                    if (options.splitArrayChanges !== false) {                        var returnValue = evaluatorCallback.call(context, parents, child, item);                        if (returnValue !== undefined)                            context(returnValue);                    }                     if (!item.moved) {                        // Deleted or brand new item. Unwatch or watch it accordingly.                        if (options.async === false) {                            watchChildren(item.value, (keepOffParentList ? null : child), parents, item.status === 'deleted');                        } else {                            setTimeout(function () {                                watchChildren(item.value, (keepOffParentList ? null : child), parents, item.status === 'deleted');                            });                        }                    }                });            }, undefined, 'arrayChange')._watcher = context;         } else {            child.subscribe(function () {                if (child.watchable !== false) {                    var returnValue = evaluatorCallback.call(context, parents, child);                     if (returnValue !== undefined)                        context(returnValue);                     if (options.mutable && typeof child() === 'object') {                        // Watch the new comer.                        if (options.async === false) {                            watchChildren(child(), (keepOffParentList ? null : child), parents, false, true);                        } else {                            setTimeout(function () {                                watchChildren(child(), (keepOffParentList ? null : child), parents, false, true);                            });                        }                    }                }             }, null, 'change')._watcher = context;             if (options.oldValues > 0 || options.mutable) {                child.subscribe(function (oldValue) {                    if (options.oldValues > 0) {                        // Add old value to history list before every update.                        var values = (child['oldValues']                            ? child['oldValues']                            : child['oldValues'] = []);                         values.unshift(oldValue);                         while (values.length > options.oldValues)                            values.pop();                    }                     if (options.mutable && typeof oldValue === 'object')                        // Clean up all subscriptions for the old child object.                        watchChildren(oldValue, (keepOffParentList ? null : child), parents, true, true);                 }, null, 'beforeChange')._watcher = context;            }        }    }     // Use a computed when targeting a non-watchable function.    if (typeof target === 'function' && !ko.isSubscribable(target))        return ko.computed(target, evaluatorCallback, options);     watchChildren(target, null, []);     return {        dispose: function () {            watchChildren(target, null, [], true);        }    };}; }));

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t