123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 |
- /*!
- * Knockout ES5 plugin - https://github.com/SteveSanderson/knockout-es5
- * Copyright (c) Steve Sanderson
- * MIT license
- */
- (function(global, undefined) {
- 'use strict';
- var ko;
- // Model tracking
- // --------------
- //
- // This is the central feature of Knockout-ES5. We augment model objects by converting properties
- // into ES5 getter/setter pairs that read/write an underlying Knockout observable. This means you can
- // use plain JavaScript syntax to read/write the property while still getting the full benefits of
- // Knockout's automatic dependency detection and notification triggering.
- //
- // For comparison, here's Knockout ES3-compatible syntax:
- //
- // var firstNameLength = myModel.user().firstName().length; // Read
- // myModel.user().firstName('Bert'); // Write
- //
- // ... versus Knockout-ES5 syntax:
- //
- // var firstNameLength = myModel.user.firstName.length; // Read
- // myModel.user.firstName = 'Bert'; // Write
- // `ko.track(model)` converts each property on the given model object into a getter/setter pair that
- // wraps a Knockout observable. Optionally specify an array of property names to wrap; otherwise we
- // wrap all properties. If any of the properties are already observables, we replace them with
- // ES5 getter/setter pairs that wrap your original observable instances. In the case of readonly
- // ko.computed properties, we simply do not define a setter (so attempted writes will be ignored,
- // which is how ES5 readonly properties normally behave).
- //
- // By design, this does *not* recursively walk child object properties, because making literally
- // everything everywhere independently observable is usually unhelpful. When you do want to track
- // child object properties independently, define your own class for those child objects and put
- // a separate ko.track call into its constructor --- this gives you far more control.
- /**
- * @param {object} obj
- * @param {object|array.<string>} propertyNamesOrSettings
- * @param {boolean} propertyNamesOrSettings.deep Use deep track.
- * @param {array.<string>} propertyNamesOrSettings.fields Array of property names to wrap.
- * todo: @param {array.<string>} propertyNamesOrSettings.exclude Array of exclude property names to wrap.
- * todo: @param {function(string, *):boolean} propertyNamesOrSettings.filter Function to filter property
- * names to wrap. A function that takes ... params
- * @return {object}
- */
- function track(obj, propertyNamesOrSettings) {
- if (!obj || typeof obj !== 'object') {
- throw new Error('When calling ko.track, you must pass an object as the first parameter.');
- }
- var propertyNames;
- if ( isPlainObject(propertyNamesOrSettings) ) {
- // defaults
- propertyNamesOrSettings.deep = propertyNamesOrSettings.deep || false;
- propertyNamesOrSettings.fields = propertyNamesOrSettings.fields || Object.getOwnPropertyNames(obj);
- propertyNamesOrSettings.lazy = propertyNamesOrSettings.lazy || false;
- wrap(obj, propertyNamesOrSettings.fields, propertyNamesOrSettings);
- } else {
- propertyNames = propertyNamesOrSettings || Object.getOwnPropertyNames(obj);
- wrap(obj, propertyNames, {});
- }
- return obj;
- }
- // fix for ie
- var rFunctionName = /^function\s*([^\s(]+)/;
- function getFunctionName( ctor ){
- if (ctor.name) {
- return ctor.name;
- }
- return (ctor.toString().trim().match( rFunctionName ) || [])[1];
- }
- function canTrack(obj) {
- return obj && typeof obj === 'object' && getFunctionName(obj.constructor) === 'Object';
- }
- function createPropertyDescriptor(originalValue, prop, map) {
- var isObservable = ko.isObservable(originalValue);
- var isArray = !isObservable && Array.isArray(originalValue);
- var observable = isObservable ? originalValue
- : isArray ? ko.observableArray(originalValue)
- : ko.observable(originalValue);
- map[prop] = function () { return observable; };
- // add check in case the object is already an observable array
- if (isArray || (isObservable && 'push' in observable)) {
- notifyWhenPresentOrFutureArrayValuesMutate(ko, observable);
- }
- return {
- configurable: true,
- enumerable: true,
- get: observable,
- set: ko.isWriteableObservable(observable) ? observable : undefined
- };
- }
- function createLazyPropertyDescriptor(originalValue, prop, map) {
- if (ko.isObservable(originalValue)) {
- // no need to be lazy if we already have an observable
- return createPropertyDescriptor(originalValue, prop, map);
- }
- var observable;
- function getOrCreateObservable(value, writing) {
- if (observable) {
- return writing ? observable(value) : observable;
- }
- if (Array.isArray(value)) {
- observable = ko.observableArray(value);
- notifyWhenPresentOrFutureArrayValuesMutate(ko, observable);
- return observable;
- }
- return (observable = ko.observable(value));
- }
- map[prop] = function () { return getOrCreateObservable(originalValue); };
- return {
- configurable: true,
- enumerable: true,
- get: function () { return getOrCreateObservable(originalValue)(); },
- set: function (value) { getOrCreateObservable(value, true); }
- };
- }
- function wrap(obj, props, options) {
- if (!props.length) {
- return;
- }
- var allObservablesForObject = getAllObservablesForObject(obj, true);
- var descriptors = {};
- props.forEach(function (prop) {
- // Skip properties that are already tracked
- if (prop in allObservablesForObject) {
- return;
- }
- // Skip properties where descriptor can't be redefined
- if (Object.getOwnPropertyDescriptor(obj, prop).configurable === false){
- return;
- }
- var originalValue = obj[prop];
- descriptors[prop] = (options.lazy ? createLazyPropertyDescriptor : createPropertyDescriptor)
- (originalValue, prop, allObservablesForObject);
- if (options.deep && canTrack(originalValue)) {
- wrap(originalValue, Object.keys(originalValue), options);
- }
- });
- Object.defineProperties(obj, descriptors);
- }
- function isPlainObject( obj ){
- return !!obj && typeof obj === 'object' && obj.constructor === Object;
- }
- // Lazily created by `getAllObservablesForObject` below. Has to be created lazily because the
- // WeakMap factory isn't available until the module has finished loading (may be async).
- var objectToObservableMap;
- // Gets or creates the hidden internal key-value collection of observables corresponding to
- // properties on the model object.
- function getAllObservablesForObject(obj, createIfNotDefined) {
- if (!objectToObservableMap) {
- objectToObservableMap = weakMapFactory();
- }
- var result = objectToObservableMap.get(obj);
- if (!result && createIfNotDefined) {
- result = {};
- objectToObservableMap.set(obj, result);
- }
- return result;
- }
- // Removes the internal references to observables mapped to the specified properties
- // or the entire object reference if no properties are passed in. This allows the
- // observables to be replaced and tracked again.
- function untrack(obj, propertyNames) {
- if (!objectToObservableMap) {
- return;
- }
- if (arguments.length === 1) {
- objectToObservableMap['delete'](obj);
- } else {
- var allObservablesForObject = getAllObservablesForObject(obj, false);
- if (allObservablesForObject) {
- propertyNames.forEach(function(propertyName) {
- delete allObservablesForObject[propertyName];
- });
- }
- }
- }
- // Computed properties
- // -------------------
- //
- // The preceding code is already sufficient to upgrade ko.computed model properties to ES5
- // getter/setter pairs (or in the case of readonly ko.computed properties, just a getter).
- // These then behave like a regular property with a getter function, except they are smarter:
- // your evaluator is only invoked when one of its dependencies changes. The result is cached
- // and used for all evaluations until the next time a dependency changes).
- //
- // However, instead of forcing developers to declare a ko.computed property explicitly, it's
- // nice to offer a utility function that declares a computed getter directly.
- // Implements `ko.defineProperty`
- function defineComputedProperty(obj, propertyName, evaluatorOrOptions) {
- var ko = this,
- computedOptions = { owner: obj, deferEvaluation: true };
- if (typeof evaluatorOrOptions === 'function') {
- computedOptions.read = evaluatorOrOptions;
- } else {
- if ('value' in evaluatorOrOptions) {
- throw new Error('For ko.defineProperty, you must not specify a "value" for the property. ' +
- 'You must provide a "get" function.');
- }
- if (typeof evaluatorOrOptions.get !== 'function') {
- throw new Error('For ko.defineProperty, the third parameter must be either an evaluator function, ' +
- 'or an options object containing a function called "get".');
- }
- computedOptions.read = evaluatorOrOptions.get;
- computedOptions.write = evaluatorOrOptions.set;
- }
- obj[propertyName] = ko.computed(computedOptions);
- track.call(ko, obj, [propertyName]);
- return obj;
- }
- // Array handling
- // --------------
- //
- // Arrays are special, because unlike other property types, they have standard mutator functions
- // (`push`/`pop`/`splice`/etc.) and it's desirable to trigger a change notification whenever one of
- // those mutator functions is invoked.
- //
- // Traditionally, Knockout handles this by putting special versions of `push`/`pop`/etc. on observable
- // arrays that mutate the underlying array and then trigger a notification. That approach doesn't
- // work for Knockout-ES5 because properties now return the underlying arrays, so the mutator runs
- // in the context of the underlying array, not any particular observable:
- //
- // // Operates on the underlying array value
- // myModel.someCollection.push('New value');
- //
- // To solve this, Knockout-ES5 detects array values, and modifies them as follows:
- // 1. Associates a hidden subscribable with each array instance that it encounters
- // 2. Intercepts standard mutators (`push`/`pop`/etc.) and makes them trigger the subscribable
- // Then, for model properties whose values are arrays, the property's underlying observable
- // subscribes to the array subscribable, so it can trigger a change notification after mutation.
- // Given an observable that underlies a model property, watch for any array value that might
- // be assigned as the property value, and hook into its change events
- function notifyWhenPresentOrFutureArrayValuesMutate(ko, observable) {
- var watchingArraySubscription = null;
- ko.computed(function () {
- // Unsubscribe to any earlier array instance
- if (watchingArraySubscription) {
- watchingArraySubscription.dispose();
- watchingArraySubscription = null;
- }
- // Subscribe to the new array instance
- var newArrayInstance = observable();
- if (newArrayInstance instanceof Array) {
- watchingArraySubscription = startWatchingArrayInstance(ko, observable, newArrayInstance);
- }
- });
- }
- // Listens for array mutations, and when they happen, cause the observable to fire notifications.
- // This is used to make model properties of type array fire notifications when the array changes.
- // Returns a subscribable that can later be disposed.
- function startWatchingArrayInstance(ko, observable, arrayInstance) {
- var subscribable = getSubscribableForArray(ko, arrayInstance);
- return subscribable.subscribe(observable);
- }
- // Lazily created by `getSubscribableForArray` below. Has to be created lazily because the
- // WeakMap factory isn't available until the module has finished loading (may be async).
- var arraySubscribablesMap;
- // Gets or creates a subscribable that fires after each array mutation
- function getSubscribableForArray(ko, arrayInstance) {
- if (!arraySubscribablesMap) {
- arraySubscribablesMap = weakMapFactory();
- }
- var subscribable = arraySubscribablesMap.get(arrayInstance);
- if (!subscribable) {
- subscribable = new ko.subscribable();
- arraySubscribablesMap.set(arrayInstance, subscribable);
- var notificationPauseSignal = {};
- wrapStandardArrayMutators(arrayInstance, subscribable, notificationPauseSignal);
- addKnockoutArrayMutators(ko, arrayInstance, subscribable, notificationPauseSignal);
- }
- return subscribable;
- }
- // After each array mutation, fires a notification on the given subscribable
- function wrapStandardArrayMutators(arrayInstance, subscribable, notificationPauseSignal) {
- ['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'].forEach(function(fnName) {
- var origMutator = arrayInstance[fnName];
- arrayInstance[fnName] = function() {
- var result = origMutator.apply(this, arguments);
- if (notificationPauseSignal.pause !== true) {
- subscribable.notifySubscribers(this);
- }
- return result;
- };
- });
- }
- // Adds Knockout's additional array mutation functions to the array
- function addKnockoutArrayMutators(ko, arrayInstance, subscribable, notificationPauseSignal) {
- ['remove', 'removeAll', 'destroy', 'destroyAll', 'replace'].forEach(function(fnName) {
- // Make it a non-enumerable property for consistency with standard Array functions
- Object.defineProperty(arrayInstance, fnName, {
- enumerable: false,
- value: function() {
- var result;
- // These additional array mutators are built using the underlying push/pop/etc.
- // mutators, which are wrapped to trigger notifications. But we don't want to
- // trigger multiple notifications, so pause the push/pop/etc. wrappers and
- // delivery only one notification at the end of the process.
- notificationPauseSignal.pause = true;
- try {
- // Creates a temporary observableArray that can perform the operation.
- result = ko.observableArray.fn[fnName].apply(ko.observableArray(arrayInstance), arguments);
- }
- finally {
- notificationPauseSignal.pause = false;
- }
- subscribable.notifySubscribers(arrayInstance);
- return result;
- }
- });
- });
- }
- // Static utility functions
- // ------------------------
- //
- // Since Knockout-ES5 sets up properties that return values, not observables, you can't
- // trivially subscribe to the underlying observables (e.g., `someProperty.subscribe(...)`),
- // or tell them that object values have mutated, etc. To handle this, we set up some
- // extra utility functions that can return or work with the underlying observables.
- // Returns the underlying observable associated with a model property (or `null` if the
- // model or property doesn't exist, or isn't associated with an observable). This means
- // you can subscribe to the property, e.g.:
- //
- // ko.getObservable(model, 'propertyName')
- // .subscribe(function(newValue) { ... });
- function getObservable(obj, propertyName) {
- if (!obj || typeof obj !== 'object') {
- return null;
- }
- var allObservablesForObject = getAllObservablesForObject(obj, false);
- if (allObservablesForObject && propertyName in allObservablesForObject) {
- return allObservablesForObject[propertyName]();
- }
- return null;
- }
-
- // Returns a boolean indicating whether the property on the object has an underlying
- // observables. This does the check in a way not to create an observable if the
- // object was created with lazily created observables
- function isTracked(obj, propertyName) {
- if (!obj || typeof obj !== 'object') {
- return false;
- }
-
- var allObservablesForObject = getAllObservablesForObject(obj, false);
- return !!allObservablesForObject && propertyName in allObservablesForObject;
- }
- // Causes a property's associated observable to fire a change notification. Useful when
- // the property value is a complex object and you've modified a child property.
- function valueHasMutated(obj, propertyName) {
- var observable = getObservable(obj, propertyName);
- if (observable) {
- observable.valueHasMutated();
- }
- }
- // Module initialisation
- // ---------------------
- //
- // When this script is first evaluated, it works out what kind of module loading scenario
- // it is in (Node.js or a browser `<script>` tag), stashes a reference to its dependencies
- // (currently that's just the WeakMap shim), and then finally attaches itself to whichever
- // instance of Knockout.js it can find.
- // A function that returns a new ES6-compatible WeakMap instance (using ES5 shim if needed).
- // Instantiated by prepareExports, accounting for which module loader is being used.
- var weakMapFactory;
- // Extends a Knockout instance with Knockout-ES5 functionality
- function attachToKo(ko) {
- ko.track = track;
- ko.untrack = untrack;
- ko.getObservable = getObservable;
- ko.valueHasMutated = valueHasMutated;
- ko.defineProperty = defineComputedProperty;
- // todo: test it, maybe added it to ko. directly
- ko.es5 = {
- getAllObservablesForObject: getAllObservablesForObject,
- notifyWhenPresentOrFutureArrayValuesMutate: notifyWhenPresentOrFutureArrayValuesMutate,
- isTracked: isTracked
- };
- }
- // Determines which module loading scenario we're in, grabs dependencies, and attaches to KO
- function prepareExports() {
- if (typeof exports === 'object' && typeof module === 'object') {
- // Node.js case - load KO and WeakMap modules synchronously
- ko = require('knockout');
- var WM = require('../lib/weakmap');
- attachToKo(ko);
- weakMapFactory = function() { return new WM(); };
- module.exports = ko;
- } else if (typeof define === 'function' && define.amd) {
- define(['knockout'], function(koModule) {
- ko = koModule;
- attachToKo(koModule);
- weakMapFactory = function() { return new global.WeakMap(); };
- return koModule;
- });
- } else if ('ko' in global) {
- // Non-module case - attach to the global instance, and assume a global WeakMap constructor
- ko = global.ko;
- attachToKo(global.ko);
- weakMapFactory = function() { return new global.WeakMap(); };
- }
- }
- prepareExports();
- })(this);
|