| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- /**
- * Durandal 2.1.0 Copyright (c) 2012 Blue Spire Consulting, Inc. All Rights Reserved.
- * Available via the MIT license.
- * see: http://durandaljs.com or https://github.com/BlueSpire/Durandal for details.
- */
- /**
- * Enables automatic observability of plain javascript object for ES5 compatible browsers. Also, converts promise properties into observables that are updated when the promise resolves.
- * @module observable
- * @requires system
- * @requires binder
- * @requires knockout
- */
- define(['durandal/system', 'durandal/binder', 'knockout'], function(system, binder, ko) {
- var observableModule,
- toString = Object.prototype.toString,
- nonObservableTypes = ['[object Function]', '[object String]', '[object Boolean]', '[object Number]', '[object Date]', '[object RegExp]'],
- observableArrayMethods = ['remove', 'removeAll', 'destroy', 'destroyAll', 'replace'],
- arrayMethods = ['pop', 'reverse', 'sort', 'shift', 'slice'],
- additiveArrayFunctions = ['push', 'unshift'],
- es5Functions = ['filter', 'map', 'reduce', 'reduceRight', 'forEach', 'every', 'some'],
- arrayProto = Array.prototype,
- observableArrayFunctions = ko.observableArray.fn,
- logConversion = false,
- changeDetectionMethod = undefined,
- skipPromises = false,
- shouldIgnorePropertyName;
- /**
- * You can call observable(obj, propertyName) to get the observable function for the specified property on the object.
- * @class ObservableModule
- */
- if (!('getPropertyDescriptor' in Object)) {
- var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
- var getPrototypeOf = Object.getPrototypeOf;
- Object['getPropertyDescriptor'] = function(o, name) {
- var proto = o, descriptor;
- while(proto && !(descriptor = getOwnPropertyDescriptor(proto, name))) {
- proto = getPrototypeOf(proto);
- }
- return descriptor;
- };
- }
- function defaultShouldIgnorePropertyName(propertyName){
- var first = propertyName[0];
- return first === '_' || first === '$' || (changeDetectionMethod && propertyName === changeDetectionMethod);
- }
- function isNode(obj) {
- return !!(obj && obj.nodeType !== undefined && system.isNumber(obj.nodeType));
- }
- function canConvertType(value) {
- if (!value || isNode(value) || value.ko === ko || value.jquery) {
- return false;
- }
- var type = toString.call(value);
- return nonObservableTypes.indexOf(type) == -1 && !(value === true || value === false);
- }
- function createLookup(obj) {
- var value = {};
- Object.defineProperty(obj, "__observable__", {
- enumerable: false,
- configurable: false,
- writable: false,
- value: value
- });
- return value;
- }
- function makeObservableArray(original, observable, hasChanged) {
- var lookup = original.__observable__, notify = true;
- if(lookup && lookup.__full__){
- return;
- }
- lookup = lookup || createLookup(original);
- lookup.__full__ = true;
- es5Functions.forEach(function (methodName) {
- observable[methodName] = function () {
- return arrayProto[methodName].apply(original, arguments);
- };
- });
- observableArrayMethods.forEach(function(methodName) {
- original[methodName] = function() {
- notify = false;
- var methodCallResult = observableArrayFunctions[methodName].apply(observable, arguments);
- notify = true;
- return methodCallResult;
- };
- });
- arrayMethods.forEach(function(methodName) {
- original[methodName] = function() {
- if(notify){
- observable.valueWillMutate();
- }
- var methodCallResult = arrayProto[methodName].apply(original, arguments);
- if(notify){
- observable.valueHasMutated();
- }
- return methodCallResult;
- };
- });
- additiveArrayFunctions.forEach(function(methodName){
- original[methodName] = function() {
- for (var i = 0, len = arguments.length; i < len; i++) {
- convertObject(arguments[i], hasChanged);
- }
- if(notify){
- observable.valueWillMutate();
- }
- var methodCallResult = arrayProto[methodName].apply(original, arguments);
- if(notify){
- observable.valueHasMutated();
- }
- return methodCallResult;
- };
- });
- original['splice'] = function() {
- for (var i = 2, len = arguments.length; i < len; i++) {
- convertObject(arguments[i], hasChanged);
- }
- if(notify){
- observable.valueWillMutate();
- }
- var methodCallResult = arrayProto['splice'].apply(original, arguments);
- if(notify){
- observable.valueHasMutated();
- }
- return methodCallResult;
- };
- for (var i = 0, len = original.length; i < len; i++) {
- convertObject(original[i], hasChanged);
- }
- }
- /**
- * Converts an entire object into an observable object by re-writing its attributes using ES5 getters and setters. Attributes beginning with '_' or '$' are ignored.
- * @method convertObject
- * @param {object} obj The target object to convert.
- */
- function convertObject(obj, hasChanged) {
- var lookup, value;
- if (changeDetectionMethod) {
- if(obj && obj[changeDetectionMethod]) {
- if (hasChanged) {
- hasChanged = hasChanged.slice(0);
- } else {
- hasChanged = [];
- }
- hasChanged.push(obj[changeDetectionMethod]);
- }
- }
- if(!canConvertType(obj)){
- return;
- }
- lookup = obj.__observable__;
- if(lookup && lookup.__full__){
- return;
- }
- lookup = lookup || createLookup(obj);
- lookup.__full__ = true;
- if (system.isArray(obj)) {
- var observable = ko.observableArray(obj);
- makeObservableArray(obj, observable, hasChanged);
- } else {
- for (var propertyName in obj) {
- if(shouldIgnorePropertyName(propertyName)){
- continue;
- }
- if (!lookup[propertyName]) {
- var descriptor = Object.getPropertyDescriptor(obj, propertyName);
- if (descriptor && (descriptor.get || descriptor.set)) {
- defineProperty(obj, propertyName, {
- get:descriptor.get,
- set:descriptor.set
- });
- } else {
- value = obj[propertyName];
- if(!system.isFunction(value)) {
- convertProperty(obj, propertyName, value, hasChanged);
- }
- }
- }
- }
- }
- if(logConversion) {
- system.log('Converted', obj);
- }
- }
- function innerSetter(observable, newValue, isArray) {
- //if this was originally an observableArray, then always check to see if we need to add/replace the array methods (if newValue was an entirely new array)
- if (isArray) {
- if (!newValue) {
- //don't allow null, force to an empty array
- newValue = [];
- makeObservableArray(newValue, observable);
- }
- else if (!newValue.destroyAll) {
- makeObservableArray(newValue, observable);
- }
- } else {
- convertObject(newValue);
- }
- //call the update to the observable after the array as been updated.
- observable(newValue);
- }
- /**
- * Converts a normal property into an observable property using ES5 getters and setters.
- * @method convertProperty
- * @param {object} obj The target object on which the property to convert lives.
- * @param {string} propertyName The name of the property to convert.
- * @param {object} [original] The original value of the property. If not specified, it will be retrieved from the object.
- * @return {KnockoutObservable} The underlying observable.
- */
- function convertProperty(obj, propertyName, original, hasChanged) {
- var observable,
- isArray,
- lookup = obj.__observable__ || createLookup(obj);
- if(original === undefined){
- original = obj[propertyName];
- }
- if (system.isArray(original)) {
- observable = ko.observableArray(original);
- makeObservableArray(original, observable, hasChanged);
- isArray = true;
- } else if (typeof original == "function") {
- if(ko.isObservable(original)){
- observable = original;
- }else{
- return null;
- }
- } else if(!skipPromises && system.isPromise(original)) {
- observable = ko.observable();
- original.then(function (result) {
- if(system.isArray(result)) {
- var oa = ko.observableArray(result);
- makeObservableArray(result, oa, hasChanged);
- result = oa;
- }
- observable(result);
- });
- } else {
- observable = ko.observable(original);
- convertObject(original, hasChanged);
- }
- if (hasChanged && hasChanged.length > 0) {
- hasChanged.forEach(function (func) {
- if (system.isArray(original)) {
- observable.subscribe(function (arrayChanges) {
- func(obj, propertyName, null, arrayChanges);
- }, null, "arrayChange");
- } else {
- observable.subscribe(function (newValue) {
- func(obj, propertyName, newValue, null);
- });
- }
- });
- }
- Object.defineProperty(obj, propertyName, {
- configurable: true,
- enumerable: true,
- get: observable,
- set: ko.isWriteableObservable(observable) ? (function (newValue) {
- if (newValue && system.isPromise(newValue) && !skipPromises) {
- newValue.then(function (result) {
- innerSetter(observable, result, system.isArray(result));
- });
- } else {
- innerSetter(observable, newValue, isArray);
- }
- }) : undefined
- });
- lookup[propertyName] = observable;
- return observable;
- }
- /**
- * Defines a computed property using ES5 getters and setters.
- * @method defineProperty
- * @param {object} obj The target object on which to create the property.
- * @param {string} propertyName The name of the property to define.
- * @param {function|object} evaluatorOrOptions The Knockout computed function or computed options object.
- * @return {KnockoutObservable} The underlying computed observable.
- */
- function defineProperty(obj, propertyName, evaluatorOrOptions) {
- var computedOptions = { owner: obj, deferEvaluation: true },
- computed;
- if (typeof evaluatorOrOptions === 'function') {
- computedOptions.read = evaluatorOrOptions;
- } else {
- if ('value' in evaluatorOrOptions) {
- system.error('For defineProperty, you must not specify a "value" for the property. You must provide a "get" function.');
- }
- if (typeof evaluatorOrOptions.get !== 'function' && typeof evaluatorOrOptions.read !== 'function') {
- system.error('For defineProperty, the third parameter must be either an evaluator function, or an options object containing a function called "get".');
- }
- computedOptions.read = evaluatorOrOptions.get || evaluatorOrOptions.read;
- computedOptions.write = evaluatorOrOptions.set || evaluatorOrOptions.write;
- }
- computed = ko.computed(computedOptions);
- obj[propertyName] = computed;
- return convertProperty(obj, propertyName, computed);
- }
- observableModule = function(obj, propertyName){
- var lookup, observable, value;
- if (!obj) {
- return null;
- }
- lookup = obj.__observable__;
- if(lookup){
- observable = lookup[propertyName];
- if(observable){
- return observable;
- }
- }
- value = obj[propertyName];
- if(ko.isObservable(value)){
- return value;
- }
- return convertProperty(obj, propertyName, value);
- };
- observableModule.defineProperty = defineProperty;
- observableModule.convertProperty = convertProperty;
- observableModule.convertObject = convertObject;
- /**
- * Installs the plugin into the view model binder's `beforeBind` hook so that objects are automatically converted before being bound.
- * @method install
- */
- observableModule.install = function(options) {
- var original = binder.binding;
- binder.binding = function(obj, view, instruction) {
- if(instruction.applyBindings && !instruction.skipConversion){
- convertObject(obj);
- }
- original(obj, view);
- };
- logConversion = options.logConversion;
- if (options.changeDetection) {
- changeDetectionMethod = options.changeDetection;
- }
- skipPromises = options.skipPromises;
- shouldIgnorePropertyName = options.shouldIgnorePropertyName || defaultShouldIgnorePropertyName;
- };
- return observableModule;
- });
|