rx-winjs
Version:
Library for composing asynchronous and event-based operations in JavaScript extending the Windows WinJS library
619 lines (541 loc) • 26.4 kB
JavaScript
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
(function (root, factory) {
var freeExports = typeof exports == 'object' && exports,
freeModule = typeof module == 'object' && module && module.exports == freeExports && module,
freeGlobal = typeof global == 'object' && global;
if (freeGlobal.global === freeGlobal) {
window = freeGlobal;
}
// Because of build optimizers
if (typeof define === 'function' && define.amd) {
define(['rx', 'winjs', 'exports'], function (Rx, winjs, exports) {
root.Rx = factory(root, exports, Rx, winjs);
return root.Rx;
});
} else if (typeof module === 'object' && module && module.exports === freeExports) {
module.exports = factory(root, module.exports, require('rx'), require('winjs'));
} else {
root.Rx = factory(root, {}, root.Rx, root.WinJS);
}
}(this, function (global, exp, Rx, WinJS, undefined) { var freeExports = typeof exports == 'object' && exports,
freeModule = typeof module == 'object' && module && module.exports == freeExports && module,
freeGlobal = typeof global == 'object' && global;
if (freeGlobal.global === freeGlobal) {
window = freeGlobal;
}
var Rx = window.Rx,
Subject = Rx.Subject,
AsyncSubject = Rx.AsyncSubject,
Observer = Rx.Observer,
observerCreate = Observer.create,
Observable = Rx.Observable,
observableProto = Observable.prototype,
observableCreate = Observable.create,
observableCreateWithDisposable = Observable.createWithDisposable,
CompositeDisposable = Rx.CompositeDisposable,
disposableCreate = Rx.Disposable.create,
disposableEmpty = Rx.Disposable.empty,
Promise = WinJS.Promise,
Binding = WinJS.Binding;
Rx.WinJS = {};
function createEventListener(el, eventName, handler) {
var disposables = new CompositeDisposable();
function createListener(element, en, fn) {
element.addEventListener(en, fn, false);
return disposableCreate(function () {
element.removeEventListener(en, fn, false);
});
};
// Assume collection versus single
if (el && el.length) {
for (var i = 0, len = el.length; i < len; i++) {
disposables.add(createEventListener(el[i], eventName, handler));
}
} else {
disposables.add(createListener(el, eventName, handler));
}
return disposables;
}
/**
* Binds an event to a given element or item. This supports either a single object or a collection.
*
* @param {Element|NodeList} element The element or object to bind the event to.
* @param {String} eventName The name of the event to bind to the given element
* @returns {Observable} An observable sequence which listens to the event on the given element or object
*/
Rx.WinJS.fromEvent = function (element, eventName) {
return observableCreateWithDisposable(function (observer) {
return createEventListener(element, eventName, observer.onNext.bind(observer));
}).publish().refCount();
};
/**
* Converts an existing WinJS Promise object to an observable sequence with an optional observer for progress.
* @param {Function|Observer} [observerOrOnNext] An optional Observer or onNext function to capture progress events.
* @returns {Observable} An observable sequence wrapping an existing WinJS Promise object.
*/
Promise.prototype.toObservable = function (observerOrOnNext) {
var subject = new AsyncSubject();
this.done(
function (next) {
subject.onNext(next);
subject.onCompleted();
},
subject.onError.bind(subject),
function (progress) {
if (typeof observerOrOnNext === 'function') {
observerOrOnNext(progress);
} else if (observerOrOnNext) {
observerOrOnNext.onNext(progress);
}
}
);
return subject.asObservable();
};
/**
* Converts an existing Observable to a WinJS Promise object.
* @returns {WinJS.Promise} A WinJS Promise object which encapsulates the given Observable sequence.
*/
observableProto.toPromise = function () {
var parent = this, subscription, value;
return new Promise(function (c, e) {
subscription = parent.subscribe(function (v) {
value = v;
}, function (exn) {
e(exn);
}, function () {
c(value);
});
}, function () {
if (subscription) { subscription.dispose(); }
});
};
/**
* Converts an existing WinJS Promise object to an observable sequence with an optional observer for progress.
* @param {WinJS.Promise} promise The Promise to convert to an Observable sequence.
* @param {Function|Observer} [observerOrOnNext] An optional Observer or onNext function to capture progress events.
* @returns {Observable} An observable sequence wrapping an existing WinJS Promise object.
*/
Promise.toObservable = function (promise, observerOrOnNext) {
return Promise.prototype.toObservable.call(promise, observerOrOnNext);
}; var originalAs = Binding.as;
function bindObservable(bindable, name) {
return observableCreate(function (observer) {
var handler = function (newValue, oldValue) {
observer.onNext({
name: name,
newValue: newValue,
oldValue: oldValue,
dataObject: bindable
});
};
bindable.bind(name, handler);
return function () {
bindable.unbind(name, handler);
};
});
}
/**
* Returns an observable object. This may be an observable proxy for the specified object, an existing proxy, or
* the specified object itself if it directly supports observability.
* This also adds a `toObservable` method which allows to turn the given binding object into an observable object.
*
* @param {Object} data Object to provide observability for.
*
* @returns {Observable} The observable object.
*/
Binding.as = function (data) {
var bindable = originalAs(data);
bindable.toObservable = function () {
if (arguments.length === 0) {
throw new Error('Must have at least one binding');
}
var observables = [];
for (var i = 0, len = arguments.length; i < len; i++) {
observables.push(bindObservable(bindable, arguments[i]));
}
return Observable.merge(observables);
};
return bindable;
};
if (window.MutationObserver) {
/**
* Creates an observable sequence from a Mutation Observer.
* MutationObserver provides developers a way to react to changes in a DOM.
* @example
* Rx.DOM.fromMutationObserver(document.getElementById('foo'), { attributes: true, childList: true, characterData: true });
*
* @param {Object} target The Node on which to obserave DOM mutations.
* @param {Object} options A MutationObserverInit object, specifies which DOM mutations should be reported.
* @returns {Observable} An observable sequence which contains mutations on the given DOM target.
*/
Rx.WinJS.fromMutationObserver = function (target, options) {
return observableCreate(function (observer) {
var mutationObserver = new MutationObserver(function (mutations) {
observer.onNext(mutations);
});
mutationObserver.observe(target, options);
return function () {
mutationObserver.disconnect();
};
});
};
}
/**
* Creates a WebSocket Subject with a given URL, protocol and an optional observer for the open event.
*
* @example
* var socket = Rx.DOM.fromWebSocket('http://localhost:8080', 'stock-protocol', function(e) { ... });
* var socket = Rx.DOM.fromWebSocket('http://localhost:8080', 'stock-protocol', observer);
*
* @param {String} url The URL of the WebSocket.
* @param {String} protocol The protocol of the WebSocket.
* @param {Function|Observer} [observerOrOnNext] An optional Observer or onNext function to capture the open event.
* @returns {Subject} An observable sequence wrapping a WebSocket.
*/
Rx.WinJS.fromWebSocket = function (url, protocol, observerOrOnNext) {
var socket = new window.WebSocket(url, protocol);
var observable = observableCreate(function (obs) {
if (observerOrOnNext) {
socket.onopen = function (openEvent) {
if (typeof observerOrOnNext === 'function') {
observerOrOnNext(openEvent);
} else if (observerOrOnNext.onNext) {
observerOrOnNext.onNext(openEvent);
}
};
}
socket.onmessage = function (data) {
obs.onNext(data);
};
socket.onerror = function (err) {
obs.onError(err);
};
socket.onclose = function () {
obs.onCompleted();
};
return function () {
socket.close();
};
});
var observer = observerCreate(function (data) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(data);
}
});
return Subject.create(observer, observable);
};
if (window.Worker) {
/**
* Creates a Web Worker with a given URL as a Subject.
*
* @example
* var worker = Rx.DOM.fromWebWorker('worker.js');
*
* @param {String} url The URL of the Web Worker.
* @returns {Subject} A Subject wrapping the Web Worker.
*/
Rx.WinJS.fromWebWorker = function (url) {
var worker = new window.Worker(url);
var observable = observableCreateWithDisposable(function (obs) {
worker.onmessage = function (data) {
obs.onNext(data);
};
worker.onerror = function (err) {
obs.onError(err);
};
return disposableCreate(function () {
worker.close();
});
});
var observer = observerCreate(function (data) {
worker.postMessage(data);
});
return Subject.create(observer, observable);
};
} Rx.WinJS.Geolocation = {
/**
* Obtains the geographic position, in terms of latitude and longitude coordinates, of the device.
* @param {Object} [geolocationOptions] An object literal to specify one or more of the following attributes and desired values:
* - enableHighAccuracy: Specify true to obtain the most accurate position possible, or false to optimize in favor of performance and power consumption.
* - timeout: An Integer value that indicates the time, in milliseconds, allowed for obtaining the position.
* If timeout is Infinity, (the default value) the location request will not time out.
* If timeout is zero (0) or negative, the results depend on the behavior of the location provider.
* - maximumAge: An Integer value indicating the maximum age, in milliseconds, of cached position information.
* If maximumAge is non-zero, and a cached position that is no older than maximumAge is available, the cached position is used instead of obtaining an updated location.
* If maximumAge is zero (0), watchPosition always tries to obtain an updated position, even if a cached position is already available.
* If maximumAge is Infinity, any cached position is used, regardless of its age, and watchPosition only tries to obtain an updated position if no cached position data exists.
* @returns {AsyncSubject} An observable sequence with the geographical location of the device running the client.
*/
getCurrentPosition: function (geolocationOptions) {
var subject = new Rx.AsyncSubject();
window.navigator.geolocation.getCurrentPosition(
function successHandler (loc) {
subject.onNext(loc);
subject.onCompleted();
},
function errorHandler (err) {
subject.onError(err);
},
geolocationOptions);
return subject.asObservable();
},
/**
* Begins listening for updates to the current geographical location of the device running the client.
* @param {Object} [geolocationOptions] An object literal to specify one or more of the following attributes and desired values:
* - enableHighAccuracy: Specify true to obtain the most accurate position possible, or false to optimize in favor of performance and power consumption.
* - timeout: An Integer value that indicates the time, in milliseconds, allowed for obtaining the position.
* If timeout is Infinity, (the default value) the location request will not time out.
* If timeout is zero (0) or negative, the results depend on the behavior of the location provider.
* - maximumAge: An Integer value indicating the maximum age, in milliseconds, of cached position information.
* If maximumAge is non-zero, and a cached position that is no older than maximumAge is available, the cached position is used instead of obtaining an updated location.
* If maximumAge is zero (0), watchPosition always tries to obtain an updated position, even if a cached position is already available.
* If maximumAge is Infinity, any cached position is used, regardless of its age, and watchPosition only tries to obtain an updated position if no cached position data exists.
* @returns {Observable} An observable sequence with the current geographical location of the device running the client.
*/
watchPosition: function (geolocationOptions) {
return observableCreate(function (observer) {
var watchId = window.navigator.geolocation.watchPosition(
function successHandler (loc) {
observer.onNext(loc);
},
function errorHandler (err) {
observer.onError(err);
},
geolocationOptions);
return function () {
window.navigator.geolocation.clearWatch(watchId);
};
}).publish().refCount();
}
};
/**
* Creates a wrapper for listening to WinJS sensor's events.
* @param {Object} sensor The WinJS sensor to attach the event listener.
* @param {String} eventName The event name to listen for.
* @returns {Observable} An observable sequence wrapping the Sensor's given event event.
*/
var fromSensorEvent = Rx.WinJS.fromSensorEvent = function (sensor, eventName) {
return observableCreateWithDisposable(function (observer) {
if (!sensor) {
observer.onError('Sensor not supported');
return disposableEmpty;
}
function handler(eventData) {
observer.onNext(eventData);
}
sensor.addEventListener(sensor, eventName, false);
return disposableCreate(function () {
sensor.removeEventListener(sensor, eventName, false);
});
}).publish().refCount();
};
var accelerometer = Windows.Devices.Sensors.Accelerometer.getDefault();
Rx.WinJS.Accelerometer = {
/**
* Creates a wrapper for the Accelerometer which listens for the readingchanged event.
* @returns {Observable} An observable sequence wrapping the Accelerometer's readingchanged event.
*/
readingChanged: function () {
return fromSensorEvent(accelerometer, 'readingchanged');
},
/**
* Creates a wrapper for the Accelerometer which listens for the shaken event.
* @returns {Observable} An observable sequence wrapping the Accelerometer's shaken event.
*/
shaken: function () {
return fromSensorEvent(accelerometer, 'shaken');
}
}
var compass = Windows.Devices.Sensors.Compass.getDefault();
Rx.WinJS.Compass = {
/**
* Creates a wrapper for the Compass which listens for the readingchanged event.
* @returns {Observable} An observable sequence wrapping the Compass's readingchanged event.
*/
readingChanged: function () {
return fromSensorEvent(compass, 'readingchanged');
}
};
var lightSensor = Windows.Devices.Sensors.LightSensor.getDefault();
Rx.WinJS.LightSensor = {
/**
* Creates a wrapper for the LightSensor which listens for the readingchanged event.
* @returns {Observable} An observable sequence wrapping the LightSensor's readingchanged event.
*/
readingChanged: function () {
return fromSensorEvent(lightSensor, 'readingchanged');
}
};
var orientationSensor = Windows.Devices.Sensors.OrientationSensor.getDefault();
Rx.WinJS.OrientationSensor = {
/**
* Creates a wrapper for the OrientationSensor which listens for the readingchanged event.
* @returns {Observable} An observable sequence wrapping the OrientationSensor's readingchanged event.
*/
readingChanged: function () {
return fromSensorEvent(orientationSensor, 'readingchanged');
}
};
var simpleOrientationSensor = Windows.Devices.Sensors.SimpleOrientationSensor.getDefault();
Rx.WinJS.SimpleOrientationSensor = {
/**
* Creates a wrapper for the SimpleOrientationSensor which listens for the orientationchanged event.
* @returns {Observable} An observable sequence wrapping the SimpleOrientationSensor's orientationchanged event.
*/
orientationChanged: function (reportInterval) {
return fromSensorEvent(simpleOrientationSensor, 'orientationchanged');
}
};
// Get the right animation frame method
var requestAnimFrame, cancelAnimFrame;
if (window.requestAnimationFrame) {
requestAnimFrame = window.requestAnimationFrame;
cancelAnimFrame = window.cancelAnimationFrame;
} else if (window.mozRequestAnimationFrame) {
requestAnimFrame = window.mozRequestAnimationFrame;
cancelAnimFrame = window.mozCancelAnimationFrame;
} else if (window.webkitRequestAnimationFrame) {
requestAnimFrame = window.webkitRequestAnimationFrame;
cancelAnimFrame = window.webkitCancelAnimationFrame;
} else if (window.msRequestAnimationFrame) {
requestAnimFrame = window.msRequestAnimationFrame;
cancelAnimFrame = window.msCancelAnimationFrame;
} else if (window.oRequestAnimationFrame) {
requestAnimFrame = window.oRequestAnimationFrame;
cancelAnimFrame = window.oCancelAnimationFrame;
} else {
requestAnimFrame = function(cb) { window.setTimeout(cb, 1000 / 60); };
cancelAnimFrame = window.clearTimeout;
}
/**
* Gets a scheduler that schedules schedules work on the requestAnimationFrame for immediate actions.
*
* @memberOf Scheduler
*/
Scheduler.requestAnimationFrame = (function () {
var defaultNow = (function () { return !!Date.now ? Date.now : function () { return +new Date; }; }());
function scheduleNow(state, action) {
var scheduler = this,
disposable = new SingleAssignmentDisposable();
var id = requestAnimFrame(function () {
if (!disposable.isDisposed) {
disposable.setDisposable(action(scheduler, state));
}
});
return new CompositeDisposable(disposable, disposableCreate(function () {
cancelAnimFrame(id);
}));
}
function scheduleRelative(state, dueTime, action) {
var scheduler = this,
dt = Scheduler.normalize(dueTime);
if (dt === 0) {
return scheduler.scheduleWithState(state, action);
}
var disposable = new SingleAssignmentDisposable(),
id;
var scheduleFunc = function () {
if (id) { cancelAnimFrame(id); }
if (dt - scheduler.now() <= 0) {
if (!disposable.isDisposed) {
disposable.setDisposable(action(scheduler, state));
}
} else {
id = requestAnimFrame(scheduleFunc);
}
};
id = requestAnimFrame(scheduleFunc);
return new CompositeDisposable(disposable, disposableCreate(function () {
cancelAnimFrame(id);
}));
}
function scheduleAbsolute(state, dueTime, action) {
return this.scheduleWithRelativeAndState(state, dueTime - this.now(), action);
}
return new Scheduler(defaultNow, scheduleNow, scheduleRelative, scheduleAbsolute);
}());
// Check for mutation observer
var BrowserMutationObserver = window.MutationObserver;
if (BrowserMutationObserver) {
/**
* Scheduler that uses a MutationObserver changes as the scheduling mechanism
* @memberOf {Scheduler}
*/
Scheduler.mutationObserver = (function () {
var queue = {}, queueId = 0;
function cloneObj (obj) {
var newObj = {};
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
newObj[prop] = obj[prop];
}
}
return newObj;
}
var observer = new BrowserMutationObserver(function() {
var toProcess = cloneObj(queue);
queue = {};
for (var prop in toProcess) {
if (toProcess.hasOwnProperty(prop)) {
toProcess[prop]();
}
}
});
var element = document.createElement('div');
observer.observe(element, { attributes: true });
// Prevent leaks
window.addEventListener('unload', function () {
observer.disconnect();
observer = null;
}, false);
function scheduleMethod (action) {
var id = queueId++;
queue[id] = action;
element.setAttribute('drainQueue', 'drainQueue');
return id;
}
function cancelMethod (id) {
delete queue[id];
}
function defaultNow () { return new Date().getTime(); }
function scheduleNow(state, action) {
var scheduler = this,
disposable = new SingleAssignmentDisposable();
var id = scheduleMethod(function () {
if (!disposable.isDisposed) {
disposable.setDisposable(action(scheduler, state));
}
});
return disposable;
}
function scheduleRelative(state, dueTime, action) {
var scheduler = this,
dt = Scheduler.normalize(dueTime);
if (dt === 0) {
return scheduler.scheduleWithState(state, action);
}
var disposable = new SingleAssignmentDisposable(),
id;
var scheduleFunc = function () {
if (id) { cancelMethod(id); }
if (dt - scheduler.now() <= 0) {
if (!disposable.isDisposed) {
disposable.setDisposable(action(scheduler, state));
}
} else {
id = scheduleMethod(scheduleFunc);
}
};
id = scheduleMethod(scheduleFunc);
return new CompositeDisposable(disposable, disposableCreate(function () {
cancelMethod(id);
}));
}
function scheduleAbsolute(state, dueTime, action) {
return this.scheduleWithRelativeAndState(state, dueTime - this.now(), action);
}
return new Scheduler(defaultNow, scheduleNow, scheduleRelative, scheduleAbsolute);
}());
}
return Rx;
}));