From 293294f2401f3dac6d07e9af65b5582ba893a777 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Mon, 9 Feb 2015 08:34:37 -0500 Subject: js: Overhaul UI with FRP! * css/guix.css: New loading spinner. * js/lib/kefir.js: New file. * js/model/packages.js (guix.packages.Packages): Cache result. (guix.packages.Sorter, guix.packages.Pager): Delete. (guix.packages.installPackage): New function. * js/utils.js (K): New variable. (guix.withEmit, guix.withEmitAttr, guix.makeModule): New functions. * js/view/ui.js (guix.ui.paginate, guix.ui.spinUntil): New functions. (guix.ui.spinner): New variable. * js/controller/generations.js: Rewrite. * js/controller/packageInfo.js: Rewrite * js/controller/packages.js: Rewrite. * js/view/packages.js: Rewrite. * js/view/generations.js: Delete. * js/view/packageInfo.js: Delete. * js/routes.js: Use new modules. * guix/web/view/html.scm (javascripts): Update list. --- css/guix.css | 129 +- guix/web/view/html.scm | 9 +- js/controller/generations.js | 49 +- js/controller/packageInfo.js | 59 +- js/controller/packages.js | 246 ++-- js/lib/kefir.js | 2697 ++++++++++++++++++++++++++++++++++++++++++ js/model/packages.js | 80 +- js/routes.js | 6 +- js/utils.js | 30 + js/view/generations.js | 55 - js/view/packageInfo.js | 73 -- js/view/packages.js | 256 ++-- js/view/ui.js | 75 ++ 13 files changed, 3190 insertions(+), 574 deletions(-) create mode 100644 js/lib/kefir.js delete mode 100644 js/view/generations.js delete mode 100644 js/view/packageInfo.js diff --git a/css/guix.css b/css/guix.css index 2848fd5..0dca4c2 100644 --- a/css/guix.css +++ b/css/guix.css @@ -20,105 +20,58 @@ } /* Loading Spinner */ - -@-webkit-keyframes spinner { - 0% { - -webkit-transform: rotate(0deg); - -moz-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -o-transform: rotate(0deg); - transform: rotate(0deg); - } - - 100% { - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - -o-transform: rotate(360deg); - transform: rotate(360deg); - } +/* Copyright (c) 2014 Tobias Ahlin */ +/* https://raw.githubusercontent.com/tobiasahlin/SpinKit/master/LICENSE */ +.spinner { + margin: 100px auto; + width: 50px; + height: 30px; + text-align: center; + font-size: 10px; } -@-moz-keyframes spinner { - 0% { - -webkit-transform: rotate(0deg); - -moz-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -o-transform: rotate(0deg); - transform: rotate(0deg); - } +.spinner > div { + background-color: #333; + height: 60px; + width: 7px; + margin: 0px 3px 0px 0px; + display: inline-block; - 100% { - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - -o-transform: rotate(360deg); - transform: rotate(360deg); - } + -webkit-animation: stretchdelay 1.2s infinite ease-in-out; + animation: stretchdelay 1.2s infinite ease-in-out; } -@-o-keyframes spinner { - 0% { - -webkit-transform: rotate(0deg); - -moz-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -o-transform: rotate(0deg); - transform: rotate(0deg); - } +.spinner .rect2 { + -webkit-animation-delay: -1.1s; + animation-delay: -1.1s; +} - 100% { - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - -o-transform: rotate(360deg); - transform: rotate(360deg); - } +.spinner .rect3 { + -webkit-animation-delay: -1.0s; + animation-delay: -1.0s; } -@keyframes spinner { - 0% { - -webkit-transform: rotate(0deg); - -moz-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -o-transform: rotate(0deg); - transform: rotate(0deg); - } +.spinner .rect4 { + -webkit-animation-delay: -0.9s; + animation-delay: -0.9s; +} - 100% { - -webkit-transform: rotate(360deg); - -moz-transform: rotate(360deg); - -ms-transform: rotate(360deg); - -o-transform: rotate(360deg); - transform: rotate(360deg); - } +.spinner .rect5 { + -webkit-animation-delay: -0.8s; + animation-delay: -0.8s; } -/* :not(:required) hides this rule from IE9 and below */ -.spinner:not(:required) { - -webkit-animation: spinner 1500ms infinite linear; - -moz-animation: spinner 1500ms infinite linear; - -ms-animation: spinner 1500ms infinite linear; - -o-animation: spinner 1500ms infinite linear; - animation: spinner 1500ms infinite linear; - -webkit-border-radius: 0.5em; - -moz-border-radius: 0.5em; - -ms-border-radius: 0.5em; - -o-border-radius: 0.5em; - border-radius: 0.5em; - -webkit-box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0; - -moz-box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0; - box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0; - display: inline-block; - font-size: 18px; - width: 1em; - height: 1em; - margin: 1.5em; - overflow: hidden; - text-indent: 100%; +@-webkit-keyframes stretchdelay { + 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } + 20% { -webkit-transform: scaleY(1.0) } } -.spinner-container { - width: 1em; - margin: 0 auto; - padding: 32px; +@keyframes stretchdelay { + 0%, 40%, 100% { + transform: scaleY(0.4); + -webkit-transform: scaleY(0.4); + } 20% { + transform: scaleY(1.0); + -webkit-transform: scaleY(1.0); + } } diff --git a/guix/web/view/html.scm b/guix/web/view/html.scm index 462055c..170cfce 100644 --- a/guix/web/view/html.scm +++ b/guix/web/view/html.scm @@ -47,17 +47,16 @@ (define javascripts (list (javascript "/js/lib/underscore.js" expat) (javascript "/js/lib/mithril.js" expat) + (javascript "/js/lib/kefir.js" expat) (javascript "/js/utils.js" agpl3+) - (javascript "/js/view/ui.js" agpl3+) - (javascript "/js/view/layout.js" agpl3+) (javascript "/js/model/packages.js" agpl3+) (javascript "/js/model/generations.js" agpl3+) + (javascript "/js/view/ui.js" agpl3+) + (javascript "/js/view/layout.js" agpl3+) + (javascript "/js/view/packages.js" agpl3+) (javascript "/js/controller/packages.js" agpl3+) (javascript "/js/controller/packageInfo.js" agpl3+) (javascript "/js/controller/generations.js" agpl3+) - (javascript "/js/view/packages.js" agpl3+) - (javascript "/js/view/packageInfo.js" agpl3+) - (javascript "/js/view/generations.js" agpl3+) (javascript "/js/routes.js" agpl3+))) (define stylesheets diff --git a/js/controller/generations.js b/js/controller/generations.js index 011dfa2..2b17a59 100644 --- a/js/controller/generations.js +++ b/js/controller/generations.js @@ -15,16 +15,43 @@ // License along with this program. If not, see // . -(function(generations) { - generations.controller = (function() { - function controller() { - this.generations = m.prop([]); +guix.generations.controller = function() { + var generations = K.fromPromise(guix.generations.Generations()); - generations.Generations() - .then(this.generations) - .then(m.redraw); - } + return guix.ui.spinUntil(generations.map(function(generations) { + return [ + guix.ui.headerWithBadge("Generations", generations.length), + m("table.table.table-bordered", [ + m("thead", m("tr", [ + m("th", "#"), + m("th", "Name"), + m("th", "Version"), + m("th", "Output"), + m("th", "Location") + ])), + m("tbody", [ + generations.map(function(generation) { + var entries = generation.manifestEntries; - return controller; - })(); -})(guix.generations); + function renderRow(entry, isFirst) { + return m("tr", [ + isFirst ? m("td", { + rowspan: entries.length + }, m("strong", generation.number)) : null, + m("td", entry.name), + m("td", entry.version), + m("td", entry.output), + m("td", entry.location) + ]); + } + + return [renderRow(entries[0], true)] + .concat(entries.slice(1).map(function (entry) { + return renderRow(entry, false); + })); + }) + ]) + ]) + ]; + })).map(guix.withLayout); +}; diff --git a/js/controller/packageInfo.js b/js/controller/packageInfo.js index ebd6a51..bb6c0b4 100644 --- a/js/controller/packageInfo.js +++ b/js/controller/packageInfo.js @@ -16,10 +16,61 @@ // . (function() { - var packageInfo = guix.packageInfo = {}; + guix.packageInfo = {}; - packageInfo.controller = function() { - this.name = m.route.param("name"); - this.packages = guix.packages.PackagesByName(this.name); + guix.packageInfo.controller = function() { + var name = m.route.param("name"); + var packages = K.fromPromise(guix.packages.PackagesByName(name)); + + // View. + return guix.ui.spinUntil(packages.map(function(packages) { + var packageCount = (function() { + var count = packages.length; + var units = count > 1 ? " versions" : " version"; + + return count.toString().concat(units); + })(); + + function describeInputs(inputs, description) { + return _.isEmpty(inputs) ? [] : [ + m("dt", description), + m("dd", m("ul", inputs.map(function(p) { + return m("li", m("a", { + config: m.route, + href: "/package/".concat(p.name) + }, p.name.concat(" ").concat(p.version))); + }))) + ]; + } + + function describePackage(package) { + var baseDescription = [ + m("dt", "Version"), + m("dd", package.version), + m("dt", "Synopsis"), + m("dd", package.synopsis), + m("dt", "Description"), + m("dd", package.description), + m("dt", "License"), + m("dd", guix.ui.licenseList(package)) + ]; + var inputs = describeInputs(package.inputs, "Inputs"); + var nativeInputs = describeInputs(package.nativeInputs, + "Native Inputs"); + var propagatedInputs = describeInputs(package.propagatedInputs, + "Propagated Inputs"); + return m("li", m("dl", _.flatten([ + baseDescription, + inputs, + nativeInputs, + propagatedInputs + ], true))); + } + + return [ + guix.ui.headerWithBadge(name, packageCount), + m("ul.list-unstyled", packages.map(describePackage)) + ]; + })).map(guix.withLayout); }; })(); diff --git a/js/controller/packages.js b/js/controller/packages.js index 9cac565..6a3ad75 100644 --- a/js/controller/packages.js +++ b/js/controller/packages.js @@ -18,99 +18,181 @@ (function() { var packages = guix.packages; - packages.controller = (function() { + packages.controller = function() { var PAGE_SIZE = 20; - - function Pager(items) { - return new packages.Pager(items, PAGE_SIZE); - } - - function controller() { - var self = this; - - this.searchTerm = m.prop(""); - this.columns = [ - { header: "Name", sortField: "name" }, - { header: "Version", sortField: "version" }, - { header: "Synopsis", sortField: "synopsis" }, - { header: "Home Page", sortField: "homepage" }, { - header: "License", - sortField: function(package) { - if(_.isArray(package.license)) { - // Concatenate all license names together for sorting. - return package.license.reduce(function(memo, l) { - return memo.concat(l.name); - }, ""); - } - - return package.license.name; - } - } - ]; - this.sorter = m.prop(new packages.Sorter("name")); - this.pager = m.prop(Pager([])); - this.phase = m.prop(packages.PHASE_NONE); - this.selectedPackage = m.prop(null); - this.packages = m.prop([]); - - packages.Packages() - .then(this.packages) - .then(function(packages) { - // All packages are visible initially - self.sortAndPage(packages); - }) - .then(m.redraw); - }; - - controller.prototype.packageCount = function() { - return _.chain(this.pager().pages) - .pluck('length') - .reduce(guix.add, 0) - .value(); - }; - - controller.prototype.sortAndPage = function(packages) { - this.pager(Pager(this.sorter().sort(packages))); - }; - - controller.prototype.doSearch = function() { - var regexp = new RegExp(this.searchTerm(), "i"); - var filteredPackages = this.packages().filter(function(package) { + // Throttle search to twice per second maximum. + var SEARCH_THROTTLE = 500; + + var packages = K.fromPromise(guix.packages.Packages()); + var searchTerm = K.emitter(); + var sorterStream = K.emitter(); + var pageIndex = K.emitter(); + var phaseStream = K.emitter(); + var selectedPackageStream = K.emitter(); + + var filteredPackages = K.combine([ + packages, + searchTerm.throttle(SEARCH_THROTTLE).toProperty("") + ], function(packages, search) { + var regexp = new RegExp(search === "" ? ".*" : search, "i"); + + return packages.filter(function(package) { return regexp.test(package.name) || regexp.test(package.synopsis); }); + }); + + var sorter = sorterStream.toProperty({ + field: "name", + reverse: false + }); + + var sortFields = { + license: function(package) { + if(_.isArray(package.license)) { + // Concatenate all license names together for sorting. + return package.license.reduce(function(memo, l) { + return memo.concat(l.name); + }, ""); + } - this.sortAndPage(filteredPackages); + return package.license.name; + } }; - controller.prototype.sortBy = function(field) { - if(this.sorter().field === field) { - // Reverse sort order if the field is the same as before. - this.sorter(this.sorter().reverse()); - } else { - this.sorter(new packages.Sorter(field)); + var sortedPackages = K.combine([ + filteredPackages, + sorter + ], function(packages, sorter) { + var field = sorter.field; + var sorted = _.sortBy(packages, sortFields[field] || field); + + return sorter.reverse ? sorted.reverse() : sorted; + }); + + var pages = sortedPackages.map(function(packages) { + return guix.chunk(packages, PAGE_SIZE); + }).toProperty([[]]); + + var page = K.combine([ + pages, + pageIndex.toProperty(0) + ], function(pages, i) { + return { + index: i, + packages: pages[i] + }; + }); + + var phase = phaseStream.toProperty(guix.packages.PHASE_NONE); + + return guix.ui.spinUntil(K.combine([ + packages, + pages, + page, + searchTerm.toProperty(""), + sorter, + phase, + selectedPackageStream.toProperty(null) + ], function(packages, pages, page, search, sorter, phase, selectedPackage) { + function renderName(package) { + var name = package.name; + + return m("a", { + config: m.route, + href: "/package/".concat(name) + }, name); } - this.doSearch(); - }; + function renderHomepage(package) { + if(package.homepage) { + return m("a", { href: package.homepage }, package.homepage); + } else { + return ""; + } + } - controller.prototype.installSelectedPackage = function() { - var self = this; + function renderHeader(title, field) { + var isCurrentSorter = sorter.field === field; + var sorterClass = (function() { + if(isCurrentSorter) { + return sorter.reverse ? + "sorter sort-descend" : + "sorter sort-ascend"; + } - this.phase(packages.PHASE_DERIVATION); + return "sorter"; + })(); + + return m("th", { + class: sorterClass, + onclick: function() { + if(isCurrentSorter) { + sorterStream.emit({ + field: field, + reverse: !sorter.reverse + }); + } else { + sorterStream.emit({ + field: field, + reverse: false + }); + } + } + }, title); + } - m.request({ - method: "POST", - url: "/packages/" - .concat(this.selectedPackage().name) - .concat("/install") - }).then(function() { - self.phase(packages.PHASE_SUCCESS); - }, function() { - self.phase(packages.PHASE_ERROR); - }); - }; + function renderInstallLink(package) { + return m("a", { + href: "#", + onclick: function() { + selectedPackageStream.emit(package); + phaseStream.emit(guix.packages.PHASE_PROMPT); + } + }, "install"); + } - return controller; - })(); + var pagination = guix.ui.paginate(page.index, pages.length, + 10, pageIndex); + + return [ + guix.ui.headerWithBadge("Packages", packages.length), + // Installation modal + guix.packages.view.installModal(selectedPackage, phase, phaseStream), + // Search box + m("input.form-control", { + type: "text", + placeholder: "Search", + oninput: guix.withEmitAttr("value", searchTerm) + }), + pagination, + // Package table + m("table.table", [ + m("thead", [ + m("tr", [ + renderHeader("Name", "name"), + renderHeader("Version", "version"), + renderHeader("Synopsis", "synopsis"), + renderHeader("Home page", "homepage"), + renderHeader("License", "license"), + m("th", "") + ]) + ]), + m("tbody", [ + page.packages.map(function(package) { + return m("tr", [ + m("td", renderName(package)), + m("td", package.version), + m("td", package.synopsis), + m("td", renderHomepage(package)), + m("td", guix.ui.licenseList(package)), + m("td", renderInstallLink(package)) + ]); + }) + ]) + ]), + pagination + ]; + })).map(guix.withLayout); + }; })(); diff --git a/js/lib/kefir.js b/js/lib/kefir.js new file mode 100644 index 0000000..9fd52a1 --- /dev/null +++ b/js/lib/kefir.js @@ -0,0 +1,2697 @@ +/*! Kefir.js v1.0.0 + * https://github.com/pozadi/kefir + */ +;(function(global){ + "use strict"; + + var Kefir = {}; + + +function and() { + for (var i = 0; i < arguments.length; i++) { + if (!arguments[i]) { + return arguments[i]; + } + } + return arguments[i - 1]; +} + +function or() { + for (var i = 0; i < arguments.length; i++) { + if (arguments[i]) { + return arguments[i]; + } + } + return arguments[i - 1]; +} + +function not(x) { + return !x; +} + +function concat(a, b) { + var result, length, i, j; + if (a.length === 0) { return b } + if (b.length === 0) { return a } + j = 0; + result = new Array(a.length + b.length); + length = a.length; + for (i = 0; i < length; i++, j++) { + result[j] = a[i]; + } + length = b.length; + for (i = 0; i < length; i++, j++) { + result[j] = b[i]; + } + return result; +} + +function find(arr, value) { + var length = arr.length + , i; + for (i = 0; i < length; i++) { + if (arr[i] === value) { return i } + } + return -1; +} + +function findByPred(arr, pred) { + var length = arr.length + , i; + for (i = 0; i < length; i++) { + if (pred(arr[i])) { return i } + } + return -1; +} + +function cloneArray(input) { + var length = input.length + , result = new Array(length) + , i; + for (i = 0; i < length; i++) { + result[i] = input[i]; + } + return result; +} + +function remove(input, index) { + var length = input.length + , result, i, j; + if (index >= 0 && index < length) { + if (length === 1) { + return []; + } else { + result = new Array(length - 1); + for (i = 0, j = 0; i < length; i++) { + if (i !== index) { + result[j] = input[i]; + j++; + } + } + return result; + } + } else { + return input; + } +} + +function removeByPred(input, pred) { + return remove(input, findByPred(input, pred)); +} + +function map(input, fn) { + var length = input.length + , result = new Array(length) + , i; + for (i = 0; i < length; i++) { + result[i] = fn(input[i]); + } + return result; +} + +function forEach(arr, fn) { + var length = arr.length + , i; + for (i = 0; i < length; i++) { fn(arr[i]) } +} + +function fillArray(arr, value) { + var length = arr.length + , i; + for (i = 0; i < length; i++) { + arr[i] = value; + } +} + +function contains(arr, value) { + return find(arr, value) !== -1; +} + +function rest(arr, start, onEmpty) { + if (arr.length > start) { + return Array.prototype.slice.call(arr, start); + } + return onEmpty; +} + +function slide(cur, next, max) { + var length = Math.min(max, cur.length + 1), + offset = cur.length - length + 1, + result = new Array(length), + i; + for (i = offset; i < length; i++) { + result[i - offset] = cur[i]; + } + result[length - 1] = next; + return result; +} + +function isEqualArrays(a, b) { + var length, i; + if (a == null && b == null) { + return true; + } + if (a == null || b == null) { + return false; + } + if (a.length !== b.length) { + return false; + } + for (i = 0, length = a.length; i < length; i++) { + if (a[i] !== b[i]) { + return false; + } + } + return true; +} + +function spread(fn, length) { + switch(length) { + case 0: return function(a) { return fn() }; + case 1: return function(a) { return fn(a[0]) }; + case 2: return function(a) { return fn(a[0], a[1]) }; + case 3: return function(a) { return fn(a[0], a[1], a[2]) }; + case 4: return function(a) { return fn(a[0], a[1], a[2], a[3]) }; + default: return function(a) { return fn.apply(null, a) }; + } +} + +function apply(fn, c, a) { + var aLength = a ? a.length : 0; + if (c == null) { + switch (aLength) { + case 0: return fn(); + case 1: return fn(a[0]); + case 2: return fn(a[0], a[1]); + case 3: return fn(a[0], a[1], a[2]); + case 4: return fn(a[0], a[1], a[2], a[3]); + default: return fn.apply(null, a); + } + } else { + switch (aLength) { + case 0: return fn.call(c); + default: return fn.apply(c, a); + } + } +} + +function get(map, key, notFound) { + if (map && key in map) { + return map[key]; + } else { + return notFound; + } +} + +function own(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + +function createObj(proto) { + var F = function() {}; + F.prototype = proto; + return new F(); +} + +function extend(target /*, mixin1, mixin2...*/) { + var length = arguments.length + , i, prop; + for (i = 1; i < length; i++) { + for (prop in arguments[i]) { + target[prop] = arguments[i][prop]; + } + } + return target; +} + +function inherit(Child, Parent /*, mixin1, mixin2...*/) { + var length = arguments.length + , i; + Child.prototype = createObj(Parent.prototype); + Child.prototype.constructor = Child; + for (i = 2; i < length; i++) { + extend(Child.prototype, arguments[i]); + } + return Child; +} + +var NOTHING = ['']; +var END = 'end'; +var VALUE = 'value'; +var ERROR = 'error'; +var ANY = 'any'; + +function noop() {} + +function id(x) { + return x; +} + +function strictEqual(a, b) { + return a === b; +} + +function defaultDiff(a, b) { + return [a, b] +} + +var now = Date.now ? + function() { return Date.now() } : + function() { return new Date().getTime() }; + +function isFn(fn) { + return typeof fn === 'function'; +} + +function isUndefined(x) { + return typeof x === 'undefined'; +} + +function isArrayLike(xs) { + return isArray(xs) || isArguments(xs); +} + +var isArray = Array.isArray || function(xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +} + +var isArguments = function(xs) { + return Object.prototype.toString.call(xs) === '[object Arguments]'; +} + +// For IE +if (!isArguments(arguments)) { + isArguments = function(obj) { + return !!(obj && own(obj, 'callee')); + } +} + +function withInterval(name, mixin) { + + function AnonymousStream(wait, args) { + Stream.call(this); + this._wait = wait; + this._intervalId = null; + var $ = this; + this._$onTick = function() { $._onTick() } + this._init(args); + } + + inherit(AnonymousStream, Stream, { + + _name: name, + + _init: function(args) {}, + _free: function() {}, + + _onTick: function() {}, + + _onActivation: function() { + this._intervalId = setInterval(this._$onTick, this._wait); + }, + _onDeactivation: function() { + if (this._intervalId !== null) { + clearInterval(this._intervalId); + this._intervalId = null; + } + }, + + _clear: function() { + Stream.prototype._clear.call(this); + this._$onTick = null; + this._free(); + } + + }, mixin); + + Kefir[name] = function(wait) { + return new AnonymousStream(wait, rest(arguments, 1, [])); + } +} + +function withOneSource(name, mixin, options) { + + + options = extend({ + streamMethod: function(StreamClass, PropertyClass) { + return function() { return new StreamClass(this, arguments) } + }, + propertyMethod: function(StreamClass, PropertyClass) { + return function() { return new PropertyClass(this, arguments) } + } + }, options || {}); + + + + mixin = extend({ + _init: function(args) {}, + _free: function() {}, + + _handleValue: function(x, isCurrent) { this._send(VALUE, x, isCurrent) }, + _handleError: function(x, isCurrent) { this._send(ERROR, x, isCurrent) }, + _handleEnd: function(__, isCurrent) { this._send(END, null, isCurrent) }, + + _handleAny: function(event) { + switch (event.type) { + case VALUE: this._handleValue(event.value, event.current); break; + case ERROR: this._handleError(event.value, event.current); break; + case END: this._handleEnd(event.value, event.current); break; + } + }, + + _onActivation: function() { + this._source.onAny(this._$handleAny); + }, + _onDeactivation: function() { + this._source.offAny(this._$handleAny); + } + }, mixin || {}); + + + + function buildClass(BaseClass) { + function AnonymousObservable(source, args) { + BaseClass.call(this); + this._source = source; + this._name = source._name + '.' + name; + this._init(args); + var $ = this; + this._$handleAny = function(event) { $._handleAny(event) } + } + + inherit(AnonymousObservable, BaseClass, { + _clear: function() { + BaseClass.prototype._clear.call(this); + this._source = null; + this._$handleAny = null; + this._free(); + } + }, mixin); + + return AnonymousObservable; + } + + + var AnonymousStream = buildClass(Stream); + var AnonymousProperty = buildClass(Property); + + if (options.streamMethod) { + Stream.prototype[name] = options.streamMethod(AnonymousStream, AnonymousProperty); + } + + if (options.propertyMethod) { + Property.prototype[name] = options.propertyMethod(AnonymousStream, AnonymousProperty); + } + +} + +function withTwoSources(name, mixin /*, options*/) { + + mixin = extend({ + _init: function(args) {}, + _free: function() {}, + + _handlePrimaryValue: function(x, isCurrent) { this._send(VALUE, x, isCurrent) }, + _handlePrimaryError: function(x, isCurrent) { this._send(ERROR, x, isCurrent) }, + _handlePrimaryEnd: function(__, isCurrent) { this._send(END, null, isCurrent) }, + + _handleSecondaryValue: function(x, isCurrent) { this._lastSecondary = x }, + _handleSecondaryError: function(x, isCurrent) { this._send(ERROR, x, isCurrent) }, + _handleSecondaryEnd: function(__, isCurrent) {}, + + _handlePrimaryAny: function(event) { + switch (event.type) { + case VALUE: + this._handlePrimaryValue(event.value, event.current); + break; + case ERROR: + this._handlePrimaryError(event.value, event.current); + break; + case END: + this._handlePrimaryEnd(event.value, event.current); + break; + } + }, + _handleSecondaryAny: function(event) { + switch (event.type) { + case VALUE: + this._handleSecondaryValue(event.value, event.current); + break; + case ERROR: + this._handleSecondaryError(event.value, event.current); + break; + case END: + this._handleSecondaryEnd(event.value, event.current); + this._removeSecondary(); + break; + } + }, + + _removeSecondary: function() { + if (this._secondary !== null) { + this._secondary.offAny(this._$handleSecondaryAny); + this._$handleSecondaryAny = null; + this._secondary = null; + } + }, + + _onActivation: function() { + if (this._secondary !== null) { + this._secondary.onAny(this._$handleSecondaryAny); + } + if (this._alive) { + this._primary.onAny(this._$handlePrimaryAny); + } + }, + _onDeactivation: function() { + if (this._secondary !== null) { + this._secondary.offAny(this._$handleSecondaryAny); + } + this._primary.offAny(this._$handlePrimaryAny); + } + }, mixin || {}); + + + + function buildClass(BaseClass) { + function AnonymousObservable(primary, secondary, args) { + BaseClass.call(this); + this._primary = primary; + this._secondary = secondary; + this._name = primary._name + '.' + name; + this._lastSecondary = NOTHING; + var $ = this; + this._$handleSecondaryAny = function(event) { $._handleSecondaryAny(event) } + this._$handlePrimaryAny = function(event) { $._handlePrimaryAny(event) } + this._init(args); + } + + inherit(AnonymousObservable, BaseClass, { + _clear: function() { + BaseClass.prototype._clear.call(this); + this._primary = null; + this._secondary = null; + this._lastSecondary = null; + this._$handleSecondaryAny = null; + this._$handlePrimaryAny = null; + this._free(); + } + }, mixin); + + return AnonymousObservable; + } + + + var AnonymousStream = buildClass(Stream); + var AnonymousProperty = buildClass(Property); + + Stream.prototype[name] = function(secondary) { + return new AnonymousStream(this, secondary, rest(arguments, 1, [])); + } + + Property.prototype[name] = function(secondary) { + return new AnonymousProperty(this, secondary, rest(arguments, 1, [])); + } + +} + +// Subscribers + +function Subscribers() { + this._items = []; +} + +extend(Subscribers, { + callOne: function(fnData, event) { + if (fnData.type === ANY) { + fnData.fn(event); + } else if (fnData.type === event.type) { + if (fnData.type === VALUE || fnData.type === ERROR) { + fnData.fn(event.value); + } else { + fnData.fn(); + } + } + }, + callOnce: function(type, fn, event) { + if (type === ANY) { + fn(event); + } else if (type === event.type) { + if (type === VALUE || type === ERROR) { + fn(event.value); + } else { + fn(); + } + } + } +}); + + +extend(Subscribers.prototype, { + add: function(type, fn, _key) { + this._items = concat(this._items, [{ + type: type, + fn: fn, + key: _key || null + }]); + }, + remove: function(type, fn, _key) { + var pred = isArray(_key) ? + function(fnData) {return fnData.type === type && isEqualArrays(fnData.key, _key)} : + function(fnData) {return fnData.type === type && fnData.fn === fn}; + this._items = removeByPred(this._items, pred); + }, + callAll: function(event) { + var items = this._items; + for (var i = 0; i < items.length; i++) { + Subscribers.callOne(items[i], event); + } + }, + isEmpty: function() { + return this._items.length === 0; + } +}); + + + + + +// Events + +function Event(type, value, current) { + return {type: type, value: value, current: !!current}; +} + +var CURRENT_END = Event(END, undefined, true); + + + + + +// Observable + +function Observable() { + this._subscribers = new Subscribers(); + this._active = false; + this._alive = true; +} +Kefir.Observable = Observable; + +extend(Observable.prototype, { + + _name: 'observable', + + _onActivation: function() {}, + _onDeactivation: function() {}, + + _setActive: function(active) { + if (this._active !== active) { + this._active = active; + if (active) { + this._onActivation(); + } else { + this._onDeactivation(); + } + } + }, + + _clear: function() { + this._setActive(false); + this._alive = false; + this._subscribers = null; + }, + + _send: function(type, x, isCurrent) { + if (this._alive) { + this._subscribers.callAll(Event(type, x, isCurrent)); + if (type === END) { this._clear() } + } + }, + + _on: function(type, fn, _key) { + if (this._alive) { + this._subscribers.add(type, fn, _key); + this._setActive(true); + } else { + Subscribers.callOnce(type, fn, CURRENT_END); + } + return this; + }, + + _off: function(type, fn, _key) { + if (this._alive) { + this._subscribers.remove(type, fn, _key); + if (this._subscribers.isEmpty()) { + this._setActive(false); + } + } + return this; + }, + + onValue: function(fn, _key) { return this._on(VALUE, fn, _key) }, + onError: function(fn, _key) { return this._on(ERROR, fn, _key) }, + onEnd: function(fn, _key) { return this._on(END, fn, _key) }, + onAny: function(fn, _key) { return this._on(ANY, fn, _key) }, + + offValue: function(fn, _key) { return this._off(VALUE, fn, _key) }, + offError: function(fn, _key) { return this._off(ERROR, fn, _key) }, + offEnd: function(fn, _key) { return this._off(END, fn, _key) }, + offAny: function(fn, _key) { return this._off(ANY, fn, _key) } + +}); + + +// extend() can't handle `toString` in IE8 +Observable.prototype.toString = function() { return '[' + this._name + ']' }; + + + + + + + + + +// Stream + +function Stream() { + Observable.call(this); +} +Kefir.Stream = Stream; + +inherit(Stream, Observable, { + + _name: 'stream' + +}); + + + + + + + +// Property + +function Property() { + Observable.call(this); + this._current = NOTHING; + this._currentError = NOTHING; +} +Kefir.Property = Property; + +inherit(Property, Observable, { + + _name: 'property', + + _send: function(type, x, isCurrent) { + if (this._alive) { + if (!isCurrent) { + this._subscribers.callAll(Event(type, x)); + } + if (type === VALUE) { this._current = x } + if (type === ERROR) { this._currentError = x } + if (type === END) { this._clear() } + } + }, + + _on: function(type, fn, _key) { + if (this._alive) { + this._subscribers.add(type, fn, _key); + this._setActive(true); + } + if (this._current !== NOTHING) { + Subscribers.callOnce(type, fn, Event(VALUE, this._current, true)); + } + if (this._currentError !== NOTHING) { + Subscribers.callOnce(type, fn, Event(ERROR, this._currentError, true)); + } + if (!this._alive) { + Subscribers.callOnce(type, fn, CURRENT_END); + } + return this; + } + +}); + + + + + + +// Log + +Observable.prototype.log = function(name) { + name = name || this.toString(); + this.onAny(function(event) { + var typeStr = '<' + event.type + (event.current ? ':current' : '') + '>'; + if (event.type === VALUE || event.type === ERROR) { + console.log(name, typeStr, event.value); + } else { + console.log(name, typeStr); + } + }, ['__logKey__', this, name]); + return this; +} + +Observable.prototype.offLog = function(name) { + name = name || this.toString(); + this.offAny(null, ['__logKey__', this, name]); + return this; +} + + + +// Kefir.withInterval() + +withInterval('withInterval', { + _init: function(args) { + this._fn = args[0]; + var $ = this; + this._emitter = { + emit: function(x) { $._send(VALUE, x) }, + error: function(x) { $._send(ERROR, x) }, + end: function() { $._send(END) } + } + }, + _free: function() { + this._fn = null; + this._emitter = null; + }, + _onTick: function() { + this._fn(this._emitter); + } +}); + + + + + +// Kefir.fromPoll() + +withInterval('fromPoll', { + _init: function(args) { + this._fn = args[0]; + }, + _free: function() { + this._fn = null; + }, + _onTick: function() { + this._send(VALUE, this._fn()); + } +}); + + + + + +// Kefir.interval() + +withInterval('interval', { + _init: function(args) { + this._x = args[0]; + }, + _free: function() { + this._x = null; + }, + _onTick: function() { + this._send(VALUE, this._x); + } +}); + + + + +// Kefir.sequentially() + +withInterval('sequentially', { + _init: function(args) { + this._xs = cloneArray(args[0]); + if (this._xs.length === 0) { + this._send(END) + } + }, + _free: function() { + this._xs = null; + }, + _onTick: function() { + switch (this._xs.length) { + case 1: + this._send(VALUE, this._xs[0]); + this._send(END); + break; + default: + this._send(VALUE, this._xs.shift()); + } + } +}); + + + + +// Kefir.repeatedly() + +withInterval('repeatedly', { + _init: function(args) { + this._xs = cloneArray(args[0]); + this._i = -1; + }, + _onTick: function() { + if (this._xs.length > 0) { + this._i = (this._i + 1) % this._xs.length; + this._send(VALUE, this._xs[this._i]); + } + } +}); + + + + + +// Kefir.later() + +withInterval('later', { + _init: function(args) { + this._x = args[0]; + }, + _free: function() { + this._x = null; + }, + _onTick: function() { + this._send(VALUE, this._x); + this._send(END); + } +}); + +function _AbstractPool(options) { + Stream.call(this); + + this._queueLim = get(options, 'queueLim', 0); + this._concurLim = get(options, 'concurLim', -1); + this._drop = get(options, 'drop', 'new'); + if (this._concurLim === 0) { + throw new Error('options.concurLim can\'t be 0'); + } + + var $ = this; + this._$handleSubAny = function(event) { $._handleSubAny(event) }; + + this._queue = []; + this._curSources = []; + this._activating = false; +} + +inherit(_AbstractPool, Stream, { + + _name: 'abstractPool', + + _add: function(obj, toObs) { + toObs = toObs || id; + if (this._concurLim === -1 || this._curSources.length < this._concurLim) { + this._addToCur(toObs(obj)); + } else { + if (this._queueLim === -1 || this._queue.length < this._queueLim) { + this._addToQueue(toObs(obj)); + } else if (this._drop === 'old') { + this._removeOldest(); + this._add(toObs(obj)); + } + } + }, + _addAll: function(obss) { + var $ = this; + forEach(obss, function(obs) { $._add(obs) }); + }, + _remove: function(obs) { + if (this._removeCur(obs) === -1) { + this._removeQueue(obs); + } + }, + + _addToQueue: function(obs) { + this._queue = concat(this._queue, [obs]); + }, + _addToCur: function(obs) { + this._curSources = concat(this._curSources, [obs]); + if (this._active) { this._subscribe(obs) } + }, + _subscribe: function(obs) { + var $ = this; + obs.onAny(this._$handleSubAny); + obs.onEnd(function() { $._removeCur(obs) }, [this, obs]); + }, + _unsubscribe: function(obs) { + obs.offAny(this._$handleSubAny); + obs.offEnd(null, [this, obs]); + }, + _handleSubAny: function(event) { + if (event.type === VALUE || event.type === ERROR) { + this._send(event.type, event.value, event.current && this._activating); + } + }, + + _removeQueue: function(obs) { + var index = find(this._queue, obs); + this._queue = remove(this._queue, index); + return index; + }, + _removeCur: function(obs) { + if (this._active) { this._unsubscribe(obs) } + var index = find(this._curSources, obs); + this._curSources = remove(this._curSources, index); + if (index !== -1) { + if (this._queue.length !== 0) { + this._pullQueue(); + } else if (this._curSources.length === 0) { + this._onEmpty(); + } + } + return index; + }, + _removeOldest: function() { + this._removeCur(this._curSources[0]); + }, + + _pullQueue: function() { + if (this._queue.length !== 0) { + this._queue = cloneArray(this._queue); + this._addToCur(this._queue.shift()); + } + }, + + _onActivation: function() { + var sources = this._curSources + , i; + this._activating = true; + for (i = 0; i < sources.length; i++) { this._subscribe(sources[i]) } + this._activating = false; + }, + _onDeactivation: function() { + var sources = this._curSources + , i; + for (i = 0; i < sources.length; i++) { this._unsubscribe(sources[i]) } + }, + + _isEmpty: function() { return this._curSources.length === 0 }, + _onEmpty: function() {}, + + _clear: function() { + Stream.prototype._clear.call(this); + this._queue = null; + this._curSources = null; + this._$handleSubAny = null; + } + +}); + + + + + +// .merge() + +var MergeLike = { + _onEmpty: function() { + if (this._initialised) { this._send(END, null, this._activating) } + } +}; + +function Merge(sources) { + _AbstractPool.call(this); + if (sources.length === 0) { this._send(END) } else { this._addAll(sources) } + this._initialised = true; +} + +inherit(Merge, _AbstractPool, extend({_name: 'merge'}, MergeLike)); + +Kefir.merge = function(obss) { + return new Merge(obss); +} + +Observable.prototype.merge = function(other) { + return Kefir.merge([this, other]); +} + + + + +// .concat() + +function Concat(sources) { + _AbstractPool.call(this, {concurLim: 1, queueLim: -1}); + if (sources.length === 0) { this._send(END) } else { this._addAll(sources) } + this._initialised = true; +} + +inherit(Concat, _AbstractPool, extend({_name: 'concat'}, MergeLike)); + +Kefir.concat = function(obss) { + return new Concat(obss); +} + +Observable.prototype.concat = function(other) { + return Kefir.concat([this, other]); +} + + + + + + +// .pool() + +function Pool() { + _AbstractPool.call(this); +} + +inherit(Pool, _AbstractPool, { + + _name: 'pool', + + plug: function(obs) { + this._add(obs); + return this; + }, + unplug: function(obs) { + this._remove(obs); + return this; + } + +}); + +Kefir.pool = function() { + return new Pool(); +} + + + + + +// .bus() + +function Bus() { + _AbstractPool.call(this); +} + +inherit(Bus, _AbstractPool, { + + _name: 'bus', + + plug: function(obs) { + this._add(obs); + return this; + }, + unplug: function(obs) { + this._remove(obs); + return this; + }, + + emit: function(x) { + this._send(VALUE, x); + return this; + }, + error: function(x) { + this._send(ERROR, x); + return this; + }, + end: function() { + this._send(END); + return this; + } + +}); + +Kefir.bus = function() { + return new Bus(); +} + + + + + +// .flatMap() + +function FlatMap(source, fn, options) { + _AbstractPool.call(this, options); + this._source = source; + this._fn = fn || id; + this._mainEnded = false; + this._lastCurrent = null; + + var $ = this; + this._$handleMainSource = function(event) { $._handleMainSource(event) }; +} + +inherit(FlatMap, _AbstractPool, { + + _onActivation: function() { + _AbstractPool.prototype._onActivation.call(this); + if (this._active) { + this._activating = true; + this._source.onAny(this._$handleMainSource); + this._activating = false; + } + }, + _onDeactivation: function() { + _AbstractPool.prototype._onDeactivation.call(this); + this._source.offAny(this._$handleMainSource); + }, + + _handleMainSource: function(event) { + if (event.type === VALUE) { + if (!event.current || this._lastCurrent !== event.value) { + this._add(event.value, this._fn); + } + this._lastCurrent = event.value; + } + if (event.type === ERROR) { + this._send(ERROR, event.value, event.current); + } + if (event.type === END) { + if (this._isEmpty()) { + this._send(END, null, event.current); + } else { + this._mainEnded = true; + } + } + }, + + _onEmpty: function() { + if (this._mainEnded) { this._send(END) } + }, + + _clear: function() { + _AbstractPool.prototype._clear.call(this); + this._source = null; + this._lastCurrent = null; + this._$handleMainSource = null; + } + +}); + +Observable.prototype.flatMap = function(fn) { + return new FlatMap(this, fn) + .setName(this, 'flatMap'); +} + +Observable.prototype.flatMapLatest = function(fn) { + return new FlatMap(this, fn, {concurLim: 1, drop: 'old'}) + .setName(this, 'flatMapLatest'); +} + +Observable.prototype.flatMapFirst = function(fn) { + return new FlatMap(this, fn, {concurLim: 1}) + .setName(this, 'flatMapFirst'); +} + +Observable.prototype.flatMapConcat = function(fn) { + return new FlatMap(this, fn, {queueLim: -1, concurLim: 1}) + .setName(this, 'flatMapConcat'); +} + +Observable.prototype.flatMapConcurLimit = function(fn, limit) { + var result; + if (limit === 0) { + result = Kefir.never(); + } else { + if (limit < 0) { limit = -1 } + result = new FlatMap(this, fn, {queueLim: -1, concurLim: limit}); + } + return result.setName(this, 'flatMapConcurLimit'); +} + + + + + + +// .zip() + +function Zip(sources, combinator) { + Stream.call(this); + if (sources.length === 0) { + this._send(END); + } else { + this._buffers = map(sources, function(source) { + return isArray(source) ? cloneArray(source) : []; + }); + this._sources = map(sources, function(source) { + return isArray(source) ? Kefir.never() : source; + }); + this._combinator = combinator ? spread(combinator, this._sources.length) : id; + this._aliveCount = 0; + } +} + + +inherit(Zip, Stream, { + + _name: 'zip', + + _onActivation: function() { + var i, length = this._sources.length; + this._drainArrays(); + this._aliveCount = length; + for (i = 0; i < length; i++) { + this._sources[i].onAny(this._bindHandleAny(i), [this, i]); + } + }, + + _onDeactivation: function() { + for (var i = 0; i < this._sources.length; i++) { + this._sources[i].offAny(null, [this, i]); + } + }, + + _emit: function(isCurrent) { + var values = new Array(this._buffers.length); + for (var i = 0; i < this._buffers.length; i++) { + values[i] = this._buffers[i].shift(); + } + this._send(VALUE, this._combinator(values), isCurrent); + }, + + _isFull: function() { + for (var i = 0; i < this._buffers.length; i++) { + if (this._buffers[i].length === 0) { + return false; + } + } + return true; + }, + + _emitIfFull: function(isCurrent) { + if (this._isFull()) { + this._emit(isCurrent); + } + }, + + _drainArrays: function() { + while (this._isFull()) { + this._emit(true); + } + }, + + _bindHandleAny: function(i) { + var $ = this; + return function(event) { $._handleAny(i, event) }; + }, + + _handleAny: function(i, event) { + if (event.type === VALUE) { + this._buffers[i].push(event.value); + this._emitIfFull(event.current); + } + if (event.type === ERROR) { + this._send(ERROR, event.value, event.current); + } + if (event.type === END) { + this._aliveCount--; + if (this._aliveCount === 0) { + this._send(END, null, event.current); + } + } + }, + + _clear: function() { + Stream.prototype._clear.call(this); + this._sources = null; + this._buffers = null; + this._combinator = null; + } + +}); + +Kefir.zip = function(sources, combinator) { + return new Zip(sources, combinator); +} + +Observable.prototype.zip = function(other, combinator) { + return new Zip([this, other], combinator); +} + + + + + + +// .sampledBy() + +function SampledBy(passive, active, combinator) { + Stream.call(this); + if (active.length === 0) { + this._send(END); + } else { + this._passiveCount = passive.length; + this._sources = concat(passive, active); + this._combinator = combinator ? spread(combinator, this._sources.length) : id; + this._aliveCount = 0; + this._currents = new Array(this._sources.length); + fillArray(this._currents, NOTHING); + this._activating = false; + this._emitAfterActivation = false; + this._endAfterActivation = false; + } +} + + +inherit(SampledBy, Stream, { + + _name: 'sampledBy', + + _onActivation: function() { + var length = this._sources.length, + i; + this._aliveCount = length - this._passiveCount; + this._activating = true; + for (i = 0; i < length; i++) { + this._sources[i].onAny(this._bindHandleAny(i), [this, i]); + } + this._activating = false; + if (this._emitAfterActivation) { + this._emitAfterActivation = false; + this._emitIfFull(true); + } + if (this._endAfterActivation) { + this._send(END, null, true); + } + }, + + _onDeactivation: function() { + var length = this._sources.length, + i; + for (i = 0; i < length; i++) { + this._sources[i].offAny(null, [this, i]); + } + }, + + _emitIfFull: function(isCurrent) { + if (!contains(this._currents, NOTHING)) { + var combined = cloneArray(this._currents); + combined = this._combinator(combined); + this._send(VALUE, combined, isCurrent); + } + }, + + _bindHandleAny: function(i) { + var $ = this; + return function(event) { $._handleAny(i, event) }; + }, + + _handleAny: function(i, event) { + if (event.type === VALUE) { + this._currents[i] = event.value; + if (i >= this._passiveCount) { + if (this._activating) { + this._emitAfterActivation = true; + } else { + this._emitIfFull(event.current); + } + } + } + if (event.type === ERROR) { + this._send(ERROR, event.value, event.current); + } + if (event.type === END) { + if (i >= this._passiveCount) { + this._aliveCount--; + if (this._aliveCount === 0) { + if (this._activating) { + this._endAfterActivation = true; + } else { + this._send(END, null, event.current); + } + } + } + } + }, + + _clear: function() { + Stream.prototype._clear.call(this); + this._sources = null; + this._currents = null; + this._combinator = null; + } + +}); + +Kefir.sampledBy = function(passive, active, combinator) { + return new SampledBy(passive, active, combinator); +} + +Observable.prototype.sampledBy = function(other, combinator) { + return Kefir.sampledBy([this], [other], combinator || id); +} + + + + +// .combine() + +Kefir.combine = function(sources, combinator) { + return new SampledBy([], sources, combinator).setName('combine'); +} + +Observable.prototype.combine = function(other, combinator) { + return Kefir.combine([this, other], combinator); +} + +function produceStream(StreamClass, PropertyClass) { + return function() { return new StreamClass(this, arguments) } +} +function produceProperty(StreamClass, PropertyClass) { + return function() { return new PropertyClass(this, arguments) } +} + + + +// .toProperty() + +withOneSource('toProperty', { + _init: function(args) { + if (args.length > 0) { + this._send(VALUE, args[0]); + } + } +}, {propertyMethod: produceProperty, streamMethod: produceProperty}); + + + + + +// .changes() + +withOneSource('changes', { + _handleValue: function(x, isCurrent) { + if (!isCurrent) { + this._send(VALUE, x); + } + }, + _handleError: function(x, isCurrent) { + if (!isCurrent) { + this._send(ERROR, x); + } + } +}, { + streamMethod: function() { + return function() { + return this; + } + }, + propertyMethod: produceStream +}); + + + + +// .withHandler() + +withOneSource('withHandler', { + _init: function(args) { + this._handler = args[0]; + this._forcedCurrent = false; + var $ = this; + this._emitter = { + emit: function(x) { $._send(VALUE, x, $._forcedCurrent) }, + error: function(x) { $._send(ERROR, x, $._forcedCurrent) }, + end: function() { $._send(END, null, $._forcedCurrent) } + } + }, + _free: function() { + this._handler = null; + this._emitter = null; + }, + _handleAny: function(event) { + this._forcedCurrent = event.current; + this._handler(this._emitter, event); + this._forcedCurrent = false; + } +}); + + + + +// .flatten(fn) + +withOneSource('flatten', { + _init: function(args) { + this._fn = args[0] ? args[0] : id; + }, + _free: function() { + this._fn = null; + }, + _handleValue: function(x, isCurrent) { + var xs = this._fn(x); + for (var i = 0; i < xs.length; i++) { + this._send(VALUE, xs[i], isCurrent); + } + } +}); + + + + + + + +// .transduce(transducer) + +function xformForObs(obs) { + return { + step: function(res, input) { + obs._send(VALUE, input, obs._forcedCurrent); + return null; + }, + result: function(res) { + obs._send(END, null, obs._forcedCurrent); + return null; + } + }; +} + +withOneSource('transduce', { + _init: function(args) { + this._xform = args[0](xformForObs(this)); + }, + _free: function() { + this._xform = null; + }, + _handleValue: function(x, isCurrent) { + this._forcedCurrent = isCurrent; + if (this._xform.step(null, x) !== null) { + this._xform.result(null); + } + this._forcedCurrent = false; + }, + _handleEnd: function(__, isCurrent) { + this._forcedCurrent = isCurrent; + this._xform.result(null); + this._forcedCurrent = false; + } +}); + + + + + +var withFnArgMixin = { + _init: function(args) { this._fn = args[0] || id }, + _free: function() { this._fn = null } +}; + + + +// .map(fn) + +withOneSource('map', extend({ + _handleValue: function(x, isCurrent) { + this._send(VALUE, this._fn(x), isCurrent); + } +}, withFnArgMixin)); + + + + +// .mapErrors(fn) + +withOneSource('mapErrors', extend({ + _handleError: function(x, isCurrent) { + this._send(ERROR, this._fn(x), isCurrent); + } +}, withFnArgMixin)); + + + +// .errorsToValues(fn) + +function defaultErrorsToValuesHandler(x) { + return { + convert: true, + value: x + }; +} + +withOneSource('errorsToValues', extend({ + _init: function(args) { + this._fn = args[0] || defaultErrorsToValuesHandler; + }, + _free: function() { + this._fn = null; + }, + _handleError: function(x, isCurrent) { + var result = this._fn(x); + var type = result.convert ? VALUE : ERROR; + var newX = result.convert ? result.value : x; + this._send(type, newX, isCurrent); + } +})); + + + +// .valuesToErrors(fn) + +function defaultValuesToErrorsHandler(x) { + return { + convert: true, + error: x + }; +} + +withOneSource('valuesToErrors', extend({ + _init: function(args) { + this._fn = args[0] || defaultValuesToErrorsHandler; + }, + _free: function() { + this._fn = null; + }, + _handleValue: function(x, isCurrent) { + var result = this._fn(x); + var type = result.convert ? ERROR : VALUE; + var newX = result.convert ? result.error : x; + this._send(type, newX, isCurrent); + } +})); + + + + +// .filter(fn) + +withOneSource('filter', extend({ + _handleValue: function(x, isCurrent) { + if (this._fn(x)) { + this._send(VALUE, x, isCurrent); + } + } +}, withFnArgMixin)); + + + + +// .filterErrors(fn) + +withOneSource('filterErrors', extend({ + _handleError: function(x, isCurrent) { + if (this._fn(x)) { + this._send(ERROR, x, isCurrent); + } + } +}, withFnArgMixin)); + + + + +// .takeWhile(fn) + +withOneSource('takeWhile', extend({ + _handleValue: function(x, isCurrent) { + if (this._fn(x)) { + this._send(VALUE, x, isCurrent); + } else { + this._send(END, null, isCurrent); + } + } +}, withFnArgMixin)); + + + + + +// .take(n) + +withOneSource('take', { + _init: function(args) { + this._n = args[0]; + if (this._n <= 0) { + this._send(END); + } + }, + _handleValue: function(x, isCurrent) { + this._n--; + this._send(VALUE, x, isCurrent); + if (this._n === 0) { + this._send(END, null, isCurrent); + } + } +}); + + + + + +// .skip(n) + +withOneSource('skip', { + _init: function(args) { + this._n = Math.max(0, args[0]); + }, + _handleValue: function(x, isCurrent) { + if (this._n === 0) { + this._send(VALUE, x, isCurrent); + } else { + this._n--; + } + } +}); + + + + +// .skipDuplicates([fn]) + +withOneSource('skipDuplicates', { + _init: function(args) { + this._fn = args[0] || strictEqual; + this._prev = NOTHING; + }, + _free: function() { + this._fn = null; + this._prev = null; + }, + _handleValue: function(x, isCurrent) { + if (this._prev === NOTHING || !this._fn(this._prev, x)) { + this._prev = x; + this._send(VALUE, x, isCurrent); + } + } +}); + + + + + +// .skipWhile(fn) + +withOneSource('skipWhile', { + _init: function(args) { + this._fn = args[0] || id; + this._skip = true; + }, + _free: function() { + this._fn = null; + }, + _handleValue: function(x, isCurrent) { + if (!this._skip) { + this._send(VALUE, x, isCurrent); + return; + } + if (!this._fn(x)) { + this._skip = false; + this._fn = null; + this._send(VALUE, x, isCurrent); + } + } +}); + + + + + +// .diff(fn, seed) + +withOneSource('diff', { + _init: function(args) { + this._fn = args[0] || defaultDiff; + this._prev = args.length > 1 ? args[1] : NOTHING; + }, + _free: function() { + this._prev = null; + this._fn = null; + }, + _handleValue: function(x, isCurrent) { + if (this._prev !== NOTHING) { + this._send(VALUE, this._fn(this._prev, x), isCurrent); + } + this._prev = x; + } +}); + + + + + +// .scan(fn, seed) + +withOneSource('scan', { + _init: function(args) { + this._fn = args[0]; + if (args.length > 1) { + this._send(VALUE, args[1], true); + } + }, + _free: function() { + this._fn = null; + }, + _handleValue: function(x, isCurrent) { + if (this._current !== NOTHING) { + x = this._fn(this._current, x); + } + this._send(VALUE, x, isCurrent); + } +}, {streamMethod: produceProperty}); + + + + + +// .reduce(fn, seed) + +withOneSource('reduce', { + _init: function(args) { + this._fn = args[0]; + this._result = args.length > 1 ? args[1] : NOTHING; + }, + _free: function() { + this._fn = null; + this._result = null; + }, + _handleValue: function(x) { + this._result = (this._result === NOTHING) ? x : this._fn(this._result, x); + }, + _handleEnd: function(__, isCurrent) { + if (this._result !== NOTHING) { + this._send(VALUE, this._result, isCurrent); + } + this._send(END, null, isCurrent); + } +}); + + + + +// .mapEnd(fn) + +withOneSource('mapEnd', { + _init: function(args) { + this._fn = args[0]; + }, + _free: function() { + this._fn = null; + }, + _handleEnd: function(__, isCurrent) { + this._send(VALUE, this._fn(), isCurrent); + this._send(END, null, isCurrent); + } +}); + + + + +// .skipValue() + +withOneSource('skipValues', { + _handleValue: function() {} +}); + + + +// .skipError() + +withOneSource('skipErrors', { + _handleError: function() {} +}); + + + +// .skipEnd() + +withOneSource('skipEnd', { + _handleEnd: function() {} +}); + + + +// .endOnError(fn) + +withOneSource('endOnError', extend({ + _handleError: function(x, isCurrent) { + this._send(ERROR, x, isCurrent); + this._send(END, null, isCurrent); + } +})); + + + +// .slidingWindow(max[, min]) + +withOneSource('slidingWindow', { + _init: function(args) { + this._max = args[0]; + this._min = args[1] || 0; + this._buff = []; + }, + _free: function() { + this._buff = null; + }, + _handleValue: function(x, isCurrent) { + this._buff = slide(this._buff, x, this._max); + if (this._buff.length >= this._min) { + this._send(VALUE, this._buff, isCurrent); + } + } +}); + + + + +// .bufferWhile([predicate], [options]) + +withOneSource('bufferWhile', { + _init: function(args) { + this._fn = args[0] || id; + this._flushOnEnd = get(args[1], 'flushOnEnd', true); + this._buff = []; + }, + _free: function() { + this._buff = null; + }, + _flush: function(isCurrent) { + if (this._buff !== null && this._buff.length !== 0) { + this._send(VALUE, this._buff, isCurrent); + this._buff = []; + } + }, + _handleValue: function(x, isCurrent) { + this._buff.push(x); + if (!this._fn(x)) { + this._flush(isCurrent); + } + }, + _handleEnd: function(x, isCurrent) { + if (this._flushOnEnd) { + this._flush(isCurrent); + } + this._send(END, null, isCurrent); + } +}); + + + + + +// .debounce(wait, {immediate}) + +withOneSource('debounce', { + _init: function(args) { + this._wait = Math.max(0, args[0]); + this._immediate = get(args[1], 'immediate', false); + this._lastAttempt = 0; + this._timeoutId = null; + this._laterValue = null; + this._endLater = false; + var $ = this; + this._$later = function() { $._later() }; + }, + _free: function() { + this._laterValue = null; + this._$later = null; + }, + _handleValue: function(x, isCurrent) { + if (isCurrent) { + this._send(VALUE, x, isCurrent); + } else { + this._lastAttempt = now(); + if (this._immediate && !this._timeoutId) { + this._send(VALUE, x); + } + if (!this._timeoutId) { + this._timeoutId = setTimeout(this._$later, this._wait); + } + if (!this._immediate) { + this._laterValue = x; + } + } + }, + _handleEnd: function(__, isCurrent) { + if (isCurrent) { + this._send(END, null, isCurrent); + } else { + if (this._timeoutId && !this._immediate) { + this._endLater = true; + } else { + this._send(END); + } + } + }, + _later: function() { + var last = now() - this._lastAttempt; + if (last < this._wait && last >= 0) { + this._timeoutId = setTimeout(this._$later, this._wait - last); + } else { + this._timeoutId = null; + if (!this._immediate) { + this._send(VALUE, this._laterValue); + this._laterValue = null; + } + if (this._endLater) { + this._send(END); + } + } + } +}); + + + + + +// .throttle(wait, {leading, trailing}) + +withOneSource('throttle', { + _init: function(args) { + this._wait = Math.max(0, args[0]); + this._leading = get(args[1], 'leading', true); + this._trailing = get(args[1], 'trailing', true); + this._trailingValue = null; + this._timeoutId = null; + this._endLater = false; + this._lastCallTime = 0; + var $ = this; + this._$trailingCall = function() { $._trailingCall() }; + }, + _free: function() { + this._trailingValue = null; + this._$trailingCall = null; + }, + _handleValue: function(x, isCurrent) { + if (isCurrent) { + this._send(VALUE, x, isCurrent); + } else { + var curTime = now(); + if (this._lastCallTime === 0 && !this._leading) { + this._lastCallTime = curTime; + } + var remaining = this._wait - (curTime - this._lastCallTime); + if (remaining <= 0) { + this._cancelTraling(); + this._lastCallTime = curTime; + this._send(VALUE, x); + } else if (this._trailing) { + this._cancelTraling(); + this._trailingValue = x; + this._timeoutId = setTimeout(this._$trailingCall, remaining); + } + } + }, + _handleEnd: function(__, isCurrent) { + if (isCurrent) { + this._send(END, null, isCurrent); + } else { + if (this._timeoutId) { + this._endLater = true; + } else { + this._send(END); + } + } + }, + _cancelTraling: function() { + if (this._timeoutId !== null) { + clearTimeout(this._timeoutId); + this._timeoutId = null; + } + }, + _trailingCall: function() { + this._send(VALUE, this._trailingValue); + this._timeoutId = null; + this._trailingValue = null; + this._lastCallTime = !this._leading ? 0 : now(); + if (this._endLater) { + this._send(END); + } + } +}); + + + + + +// .delay() + +withOneSource('delay', { + _init: function(args) { + this._wait = Math.max(0, args[0]); + this._buff = []; + var $ = this; + this._$shiftBuff = function() { $._send(VALUE, $._buff.shift()) } + }, + _free: function() { + this._buff = null; + this._$shiftBuff = null; + }, + _handleValue: function(x, isCurrent) { + if (isCurrent) { + this._send(VALUE, x, isCurrent); + } else { + this._buff.push(x); + setTimeout(this._$shiftBuff, this._wait); + } + }, + _handleEnd: function(__, isCurrent) { + if (isCurrent) { + this._send(END, null, isCurrent); + } else { + var $ = this; + setTimeout(function() { $._send(END) }, this._wait); + } + } +}); + +// Kefir.fromBinder(fn) + +function FromBinder(fn) { + Stream.call(this); + this._fn = fn; + this._unsubscribe = null; +} + +inherit(FromBinder, Stream, { + + _name: 'fromBinder', + + _onActivation: function() { + var $ = this + , isCurrent = true + , emitter = { + emit: function(x) { $._send(VALUE, x, isCurrent) }, + error: function(x) { $._send(ERROR, x, isCurrent) }, + end: function() { $._send(END, null, isCurrent) } + }; + this._unsubscribe = this._fn(emitter) || null; + + // work around https://github.com/pozadi/kefir/issues/35 + if (!this._active && this._unsubscribe !== null) { + this._unsubscribe(); + this._unsubscribe = null; + } + + isCurrent = false; + }, + _onDeactivation: function() { + if (this._unsubscribe !== null) { + this._unsubscribe(); + this._unsubscribe = null; + } + }, + + _clear: function() { + Stream.prototype._clear.call(this); + this._fn = null; + } + +}) + +Kefir.fromBinder = function(fn) { + return new FromBinder(fn); +} + + + + + + +// Kefir.emitter() + +function Emitter() { + Stream.call(this); +} + +inherit(Emitter, Stream, { + _name: 'emitter', + emit: function(x) { + this._send(VALUE, x); + return this; + }, + error: function(x) { + this._send(ERROR, x); + return this; + }, + end: function() { + this._send(END); + return this; + } +}); + +Kefir.emitter = function() { + return new Emitter(); +} + +Kefir.Emitter = Emitter; + + + + + + + +// Kefir.never() + +var neverObj = new Stream(); +neverObj._send(END); +neverObj._name = 'never'; +Kefir.never = function() { return neverObj } + + + + + +// Kefir.constant(x) + +function Constant(x) { + Property.call(this); + this._send(VALUE, x); + this._send(END); +} + +inherit(Constant, Property, { + _name: 'constant' +}) + +Kefir.constant = function(x) { + return new Constant(x); +} + + + + +// Kefir.constantError(x) + +function ConstantError(x) { + Property.call(this); + this._send(ERROR, x); + this._send(END); +} + +inherit(ConstantError, Property, { + _name: 'constantError' +}) + +Kefir.constantError = function(x) { + return new ConstantError(x); +} + + +// .setName + +Observable.prototype.setName = function(sourceObs, selfName /* or just selfName */) { + this._name = selfName ? sourceObs._name + '.' + selfName : sourceObs; + return this; +} + + + +// .mapTo + +Observable.prototype.mapTo = function(value) { + return this.map(function() { return value }).setName(this, 'mapTo'); +} + + + +// .pluck + +Observable.prototype.pluck = function(propertyName) { + return this.map(function(x) { + return x[propertyName]; + }).setName(this, 'pluck'); +} + + + +// .invoke + +Observable.prototype.invoke = function(methodName /*, arg1, arg2... */) { + var args = rest(arguments, 1); + return this.map(args ? + function(x) { return apply(x[methodName], x, args) } : + function(x) { return x[methodName]() } + ).setName(this, 'invoke'); +} + + + + +// .timestamp + +Observable.prototype.timestamp = function() { + return this.map(function(x) { return {value: x, time: now()} }).setName(this, 'timestamp'); +} + + + + +// .tap + +Observable.prototype.tap = function(fn) { + return this.map(function(x) { + fn(x); + return x; + }).setName(this, 'tap'); +} + + + +// .and + +Kefir.and = function(observables) { + return Kefir.combine(observables, and).setName('and'); +} + +Observable.prototype.and = function(other) { + return this.combine(other, and).setName('and'); +} + + + +// .or + +Kefir.or = function(observables) { + return Kefir.combine(observables, or).setName('or'); +} + +Observable.prototype.or = function(other) { + return this.combine(other, or).setName('or'); +} + + + +// .not + +Observable.prototype.not = function() { + return this.map(not).setName(this, 'not'); +} + + + +// .awaiting + +Observable.prototype.awaiting = function(other) { + return Kefir.merge([ + this.mapTo(true), + other.mapTo(false) + ]).skipDuplicates().toProperty(false).setName(this, 'awaiting'); +} + + + + +// .fromCallback + +Kefir.fromCallback = function(callbackConsumer) { + var called = false; + return Kefir.fromBinder(function(emitter) { + if (!called) { + callbackConsumer(function(x) { + emitter.emit(x); + emitter.end(); + }); + called = true; + } + }).setName('fromCallback'); +} + + + + +// .fromNodeCallback + +Kefir.fromNodeCallback = function(callbackConsumer) { + var called = false; + return Kefir.fromBinder(function(emitter) { + if (!called) { + callbackConsumer(function(error, x) { + if (error) { + emitter.error(error); + } else { + emitter.emit(x); + } + emitter.end(); + }); + called = true; + } + }).setName('fromNodeCallback'); +} + + + + +// .fromPromise + +Kefir.fromPromise = function(promise) { + var called = false; + return Kefir.fromBinder(function(emitter) { + if (!called) { + var onValue = function(x) { + emitter.emit(x); + emitter.end(); + }; + var onError = function(x) { + emitter.error(x); + emitter.end(); + }; + var _promise = promise.then(onValue, onError); + + // prevent promise/A+ libraries like Q to swallow exceptions + if (_promise && isFn(_promise.done)) { + _promise.done(); + } + + called = true; + } + }).toProperty().setName('fromPromise'); +} + + + + + + +// .fromSubUnsub + +Kefir.fromSubUnsub = function(sub, unsub, transformer) { + return Kefir.fromBinder(function(emitter) { + var handler = transformer ? function() { + emitter.emit(apply(transformer, this, arguments)); + } : emitter.emit; + sub(handler); + return function() { unsub(handler) }; + }); +} + + + + +// .fromEvent + +var subUnsubPairs = [ + ['addEventListener', 'removeEventListener'], + ['addListener', 'removeListener'], + ['on', 'off'] +]; + +Kefir.fromEvent = function(target, eventName, transformer) { + var pair, sub, unsub; + + for (var i = 0; i < subUnsubPairs.length; i++) { + pair = subUnsubPairs[i]; + if (isFn(target[pair[0]]) && isFn(target[pair[1]])) { + sub = pair[0]; + unsub = pair[1]; + break; + } + } + + if (sub === undefined) { + throw new Error('target don\'t support any of ' + + 'addEventListener/removeEventListener, addListener/removeListener, on/off method pair'); + } + + return Kefir.fromSubUnsub( + function(handler) { target[sub](eventName, handler) }, + function(handler) { target[unsub](eventName, handler) }, + transformer + ).setName('fromEvent'); +} + +var withTwoSourcesAndBufferMixin = { + _init: function(args) { + this._buff = []; + this._flushOnEnd = get(args[0], 'flushOnEnd', true); + }, + _free: function() { + this._buff = null; + }, + _flush: function(isCurrent) { + if (this._buff !== null && this._buff.length !== 0) { + this._send(VALUE, this._buff, isCurrent); + this._buff = []; + } + }, + + _handlePrimaryEnd: function(__, isCurrent) { + if (this._flushOnEnd) { + this._flush(isCurrent); + } + this._send(END, null, isCurrent); + } +}; + + + +withTwoSources('bufferBy', extend({ + + _onActivation: function() { + this._primary.onAny(this._$handlePrimaryAny); + if (this._alive && this._secondary !== null) { + this._secondary.onAny(this._$handleSecondaryAny); + } + }, + + _handlePrimaryValue: function(x, isCurrent) { + this._buff.push(x); + }, + + _handleSecondaryValue: function(x, isCurrent) { + this._flush(isCurrent); + }, + + _handleSecondaryEnd: function(x, isCurrent) { + if (!this._flushOnEnd) { + this._send(END, null, isCurrent); + } + } + +}, withTwoSourcesAndBufferMixin)); + + + + +withTwoSources('bufferWhileBy', extend({ + + _handlePrimaryValue: function(x, isCurrent) { + this._buff.push(x); + if (this._lastSecondary !== NOTHING && !this._lastSecondary) { + this._flush(isCurrent); + } + }, + + _handleSecondaryEnd: function(x, isCurrent) { + if (!this._flushOnEnd && (this._lastSecondary === NOTHING || this._lastSecondary)) { + this._send(END, null, isCurrent); + } + } + +}, withTwoSourcesAndBufferMixin)); + + + + + +withTwoSources('filterBy', { + + _handlePrimaryValue: function(x, isCurrent) { + if (this._lastSecondary !== NOTHING && this._lastSecondary) { + this._send(VALUE, x, isCurrent); + } + }, + + _handleSecondaryEnd: function(__, isCurrent) { + if (this._lastSecondary === NOTHING || !this._lastSecondary) { + this._send(END, null, isCurrent); + } + } + +}); + + + +withTwoSources('skipUntilBy', { + + _handlePrimaryValue: function(x, isCurrent) { + if (this._lastSecondary !== NOTHING) { + this._send(VALUE, x, isCurrent); + } + }, + + _handleSecondaryEnd: function(__, isCurrent) { + if (this._lastSecondary === NOTHING) { + this._send(END, null, isCurrent); + } + } + +}); + + + +withTwoSources('takeUntilBy', { + + _handleSecondaryValue: function(x, isCurrent) { + this._send(END, null, isCurrent); + } + +}); + + + +withTwoSources('takeWhileBy', { + + _handlePrimaryValue: function(x, isCurrent) { + if (this._lastSecondary !== NOTHING) { + this._send(VALUE, x, isCurrent); + } + }, + + _handleSecondaryValue: function(x, isCurrent) { + this._lastSecondary = x; + if (!this._lastSecondary) { + this._send(END, null, isCurrent); + } + }, + + _handleSecondaryEnd: function(__, isCurrent) { + if (this._lastSecondary === NOTHING) { + this._send(END, null, isCurrent); + } + } + +}); + + + + +withTwoSources('skipWhileBy', { + + _init: function() { + this._hasFalseyFromSecondary = false; + }, + + _handlePrimaryValue: function(x, isCurrent) { + if (this._hasFalseyFromSecondary) { + this._send(VALUE, x, isCurrent); + } + }, + + _handleSecondaryValue: function(x, isCurrent) { + this._hasFalseyFromSecondary = this._hasFalseyFromSecondary || !x; + }, + + _handleSecondaryEnd: function(__, isCurrent) { + if (!this._hasFalseyFromSecondary) { + this._send(END, null, isCurrent); + } + } + +}); + + + if (typeof define === 'function' && define.amd) { + define([], function() { + return Kefir; + }); + global.Kefir = Kefir; + } else if (typeof module === "object" && typeof exports === "object") { + module.exports = Kefir; + Kefir.Kefir = Kefir; + } else { + global.Kefir = Kefir; + } + +}(this)); \ No newline at end of file diff --git a/js/model/packages.js b/js/model/packages.js index 39e6a8d..6c4e00d 100644 --- a/js/model/packages.js +++ b/js/model/packages.js @@ -20,85 +20,31 @@ (function() { var packages = guix.packages = {}; - packages.Packages = function() { + packages.Packages = _.once(function() { return m.request({ method: "GET", url: "packages.json", background: true }); - }; + }); packages.PackagesByName = function(name) { return m.request({ method: "GET", - url: "/package/".concat(name).concat(".json") + url: "/package/".concat(name).concat(".json"), + background: true }); }; - packages.Sorter = (function() { - function Sorter(field, isDescending) { - this.field = field; - this.isDescending = _.isUndefined(isDescending) ? false : isDescending; - }; - - Sorter.prototype.sort = function(array) { - var result = _.sortBy(array, this.field); - - return this.isDescending ? result.reverse() : result; - }; - - Sorter.prototype.reverse = function() { - return new packages.Sorter(this.field, !this.isDescending); - }; - - return Sorter; - })(); - - packages.Pager = (function() { - function Pager(items, pageSize) { - this.pageSize= pageSize; - this.pageIndex = 0; - this.pages = guix.chunk(items, pageSize); - } - - Pager.prototype.currentPage = function() { - return this.pages[this.pageIndex] || []; - }; - - Pager.prototype.pageCount = function() { - return this.pages.length; - }; - - Pager.prototype.isEmpty = function() { - return this.pageCount() === 0; - }; - - Pager.prototype.isFirstPage = function() { - return this.pageIndex === 0; - }; - - Pager.prototype.isLastPage = function() { - return this.pageIndex === this.pages.length - 1; - }; - - Pager.prototype.isCurrentPage = function(i) { - return this.pageIndex === i; - }; - - Pager.prototype.nextPage = function() { - this.pageIndex = Math.min(this.pageCount() - 1, this.pageIndex + 1); - }; - - Pager.prototype.previousPage = function() { - this.pageIndex = Math.max(0, this.pageIndex - 1); - }; - - Pager.prototype.gotoPage = function(page) { - this.pageIndex = guix.clamp(page, 0, this.pageCount() -1); - }; - - return Pager; - })(); + packages.installPackage = function(package) { + return m.request({ + method: "POST", + url: "/packages/" + .concat(package.name) + .concat("/install"), + background: true + }); + }; packages.PHASE_NONE = 0; packages.PHASE_PROMPT = 1; diff --git a/js/routes.js b/js/routes.js index 400f995..5182e04 100644 --- a/js/routes.js +++ b/js/routes.js @@ -16,7 +16,7 @@ // . m.route(document.body, "/", { - "/": guix.packages, - "/generations": guix.generations, - "/package/:name": guix.packageInfo + "/": guix.makeModule(guix.packages.controller), + "/generations": guix.makeModule(guix.generations.controller), + "/package/:name": guix.makeModule(guix.packageInfo.controller) }); diff --git a/js/utils.js b/js/utils.js index cfaa7fd..5359814 100644 --- a/js/utils.js +++ b/js/utils.js @@ -17,6 +17,9 @@ var guix = {}; +// Shorthand for Kefir module. +var K = Kefir; + // Here is a perfect example of why Scheme is better than JavaScript: // + is an operator, not a function. So if I want to, say, compute // the sum of an array of numbers, I need to write a wrapper function. @@ -41,3 +44,30 @@ guix.chunk = function(array, size) { return memo; }, []); }; + +// Mithril + Kefir integration +guix.withEmit = function(emitter) { + return emitter.emit.bind(emitter); +}; + +guix.withEmitAttr = function(attr, emitter) { + return m.withAttr(attr, guix.withEmit(emitter)); +}; + +guix.makeModule = function(controller) { + var view = m.prop([]); + + // Cheat the module system a bit. + return { + controller: function() { + controller().onValue(function(newView) { + m.startComputation(); + view(newView); + m.endComputation(); + }); + }, + view: function() { + return view(); + } + }; +}; diff --git a/js/view/generations.js b/js/view/generations.js deleted file mode 100644 index 73fbce7..0000000 --- a/js/view/generations.js +++ /dev/null @@ -1,55 +0,0 @@ -// guix-web - Web interface for GNU Guix -// Copyright © 2014 David Thompson -// -// This program is free software: you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public License -// as published by the Free Software Foundation, either version 3 of -// the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public -// License along with this program. If not, see -// . - -(function(generations) { - generations.view = function(ctrl) { - return guix.withLayout([ - guix.ui.headerWithBadge("Generations", ctrl.generations().length), - m("table.table.table-bordered", [ - m("thead", m("tr", [ - m("th", "#"), - m("th", "Name"), - m("th", "Version"), - m("th", "Output"), - m("th", "Location") - ])), - m("tbody", [ - ctrl.generations().map(function(generation) { - var entries = generation.manifestEntries; - - function renderRow(entry, isFirst) { - return m("tr", [ - isFirst ? m("td", { - rowspan: entries.length - }, m("strong", generation.number)) : null, - m("td", entry.name), - m("td", entry.version), - m("td", entry.output), - m("td", entry.location) - ]); - } - - return [renderRow(entries[0], true)] - .concat(entries.slice(1).map(function (entry) { - return renderRow(entry, false); - })); - }) - ]) - ]) - ]); - }; -})(guix.generations); diff --git a/js/view/packageInfo.js b/js/view/packageInfo.js deleted file mode 100644 index 687d145..0000000 --- a/js/view/packageInfo.js +++ /dev/null @@ -1,73 +0,0 @@ -// guix-web - Web interface for GNU Guix -// Copyright © 2015 David Thompson -// -// This program is free software: you can redistribute it and/or -// modify it under the terms of the GNU Affero General Public License -// as published by the Free Software Foundation, either version 3 of -// the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, but -// WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public -// License along with this program. If not, see -// . - -(function() { - var packageInfo = guix.packageInfo; - var spinner = m(".spinner-container", m(".spinner")); - - packageInfo.view = function(ctrl) { - var packages = ctrl.packages(); - var packageCount = (function() { - var count = packages.length; - var units = count > 1 ? " versions" : " version"; - - return count.toString().concat(units); - })(); - - function describeInputs(inputs, description) { - return _.isEmpty(inputs) ? [] : [ - m("dt", description), - m("dd", m("ul", inputs.map(function(p) { - return m("li", m("a", { - config: m.route, - href: "/package/".concat(p.name) - }, p.name.concat(" ").concat(p.version))); - }))) - ]; - } - - function describePackage(package) { - var baseDescription = [ - m("dt", "Version"), - m("dd", package.version), - m("dt", "Synopsis"), - m("dd", package.synopsis), - m("dt", "Description"), - m("dd", package.description), - m("dt", "License"), - m("dd", guix.ui.licenseList(package)) - ]; - var inputs = describeInputs(package.inputs, "Inputs"); - var nativeInputs = describeInputs(package.nativeInputs, - "Native Inputs"); - var propagatedInputs = describeInputs(package.propagatedInputs, - "Propagated Inputs"); - return m("li", - m("dl", _.flatten([ - baseDescription, - inputs, - nativeInputs, - propagatedInputs - ], true))); - } - - return guix.withLayout([ - guix.ui.headerWithBadge(ctrl.name, packageCount), - m("ul.list-unstyled", packages.map(describePackage)) - ]); - }; -})(); diff --git a/js/view/packages.js b/js/view/packages.js index fde0fde..245677a 100644 --- a/js/view/packages.js +++ b/js/view/packages.js @@ -16,207 +16,91 @@ // . (function() { - var packages = guix.packages; - var spinner = m(".spinner-container", m(".spinner")); - - packages.view = function(ctrl) { - function renderName(package) { - var name = package.name; - - return m("a", { - config: m.route, - href: "/package/".concat(name) - }, name); - } - - function renderHomepage(package) { - if(package.homepage) { - return m("a", { href: package.homepage }, package.homepage); - } else { - return ""; - } - } - - function renderInstallLink(package) { - return m("a", { - href: "#", - onclick: function() { - ctrl.selectedPackage(package); - ctrl.phase(packages.PHASE_PROMPT); - return false; - } - }, "install"); - } - - function renderPackageTable() { - return m("table.table", [ - m("thead", [ - m("tr", [ - ctrl.columns.map(function(column) { - return m("th", { - class: columnHeaderClass(column), - onclick: function() { - ctrl.sortBy(column.sortField); - } - }, column.header); - }).concat([m("th", "")]) + var view = guix.packages.view = {}; + + view.installModal = function(package, phase, phaseStream) { + function renderPromptModal() { + var body = [ + m("p", "Do you want to install the following packages?"), + m("ul", [ + m("li", [ + package.name, + " ", + package.version ]) - ]), - m("tbody", [ - ctrl.pager().currentPage().map(function(package) { - return m("tr", [ - m("td", renderName(package)), - m("td", package.version), - m("td", package.synopsis), - m("td", renderHomepage(package)), - m("td", guix.ui.licenseList(package)), - m("td", renderInstallLink(package)) - ]); - }) ]) - ]); - } - - function renderPagination() { - function renderPage(text, opts) { - return m("li", { - class: opts.class || "", - onclick: opts.onclick - }, m("a", { href: "#" }, text)); - } - - return m("div", m("ul.pagination", [ - // Back page - renderPage("«", { - class: ctrl.pager().isFirstPage() ? "disabled" : "", + ]; + var buttons = [ + m(".btn.btn-default", { + onclick: guix.withEmit(phaseStream, guix.packages.PHASE_NONE) + }, "Cancel"), + m(".btn.btn-primary", { onclick: function() { - ctrl.pager().previousPage(); - return false; + phaseStream.emit(guix.packages.PHASE_DERIVATION); + guix.packages.installPackage(package).then(function() { + phaseStream.emit(guix.packages.PHASE_SUCCESS); + }, function() { + phaseStream.emit(guix.packages.PHASE_ERROR); + }); } - }) - ].concat(ctrl.pager().pages.map(function(page, i) { - // Jump to page - return renderPage(i + 1, { - class: ctrl.pager().isCurrentPage(i) ? "active" : "", - onclick: function() { - ctrl.pager().gotoPage(i); - return false; - } - }); - })).concat([ - // Forward page - renderPage("»", { - class: ctrl.pager().isLastPage() ? "disabled" : "", - onclick: function() { - ctrl.pager().nextPage(); - return false; - } - }) - ]))); - } + }, "Install"), + ]; - function renderSearchBox() { - return m("input.form-control", { - type: "text", - placeholder: "Search", - oninput: m.withAttr("value", function(value) { - ctrl.searchTerm(value); - ctrl.doSearch(); - }), - value: ctrl.searchTerm() - }); + return guix.ui.modal("Install Packages", body, buttons); } - function columnHeaderClass(column) { - var sorter = ctrl.sorter(); - - if(column.sortField === sorter.field) { - return sorter.isDescending ? "sorter sort-descend" : "sorter sort-ascend"; - } + function renderDerivationModal() { + var body = [ + m("p", [ + "Installing ", + package.name, + " ", + package.version, + "..." + ]), + m(".progress", [ + m(".progress-bar.progress-bar-striped.active", { + role: "progressbar", + style: { width: "100%" } + }) + ]) + ]; + var buttons = m(".btn.btn-danger", { + onclick: guix.withEmit(phaseStream, guix.packages.PHASE_NONE) + }, "Abort"); - return "sorter"; + return guix.ui.modal("Install Packages", body, buttons); } - function renderModal() { - function renderBody() { - switch(ctrl.phase()) { - case packages.PHASE_PROMPT: - return [ - m("p", "Do you want to install the following packages?"), - m("ul", [ - m("li", [ - ctrl.selectedPackage().name, - " ", - ctrl.selectedPackage().version - ]) - ]) - ]; - case packages.PHASE_DERIVATION: - return [ - m("p", [ - "Installing ", - ctrl.selectedPackage().name, - " ", - ctrl.selectedPackage().version, - "..." - ]), - m(".progress", [ - m(".progress-bar.progress-bar-striped.active", { - role: "progressbar", - style: { width: "100%" } - }) - ]) - ]; - case packages.PHASE_SUCCESS: - return m(".alert.alert-success", "Installation complete!"); - case packages.PHASE_ERROR: - return m(".alert.alert-danger", "Installation failed!"); - } + function renderSuccessModal() { + var body = m(".alert.alert-success", "Installation complete!"); + var buttons = m(".btn.btn-primary", { + onclick: guix.withEmit(phaseStream, guix.packages.PHASE_NONE) + }, "Close"); - return null; - } - - function renderButtons() { - switch(ctrl.phase()) { - case packages.PHASE_PROMPT: - return [ - m(".btn.btn-default", "Cancel"), - m(".btn.btn-primary", { - onclick: function() { - ctrl.installSelectedPackage(); - m.redraw(); - } - }, "Install"), - ]; - case packages.PHASE_DERIVATION: - return m(".btn.btn-danger", "Abort"); - case packages.PHASE_SUCCESS: - case packages.PHASE_ERROR: - return m(".btn.btn-primary", { - onclick: function() { - ctrl.phase(packages.PHASE_NONE); - } - }, "Close"); - } + return guix.ui.modal("Install Packages", body, buttons); + } - return null; - } + function renderErrorModal() { + var body = m(".alert.alert-danger", "Installation failed!"); + var buttons = m(".btn.btn-primary", { + onclick: guix.withEmit(phaseStream, guix.packages.PHASE_NONE) + }, "Close"); - if(ctrl.phase() != packages.PHASE_NONE) { - return guix.ui.modal("Install Packages", - renderBody(), - renderButtons()); - } + return guix.ui.modal("Install Packages", body, buttons); + } - return null; + switch(phase) { + case guix.packages.PHASE_PROMPT: + return renderPromptModal(); + case guix.packages.PHASE_DERIVATION: + return renderDerivationModal(); + case guix.packages.PHASE_SUCCESS: + return renderSuccessModal(); + case guix.packages.PHASE_ERROR: + return renderErrorModal(); } - return guix.withLayout(_.isEmpty(ctrl.packages()) ? spinner : [ - guix.ui.headerWithBadge("Packages", ctrl.packageCount()), - renderModal(), - renderSearchBox(), - renderPackageTable(), - renderPagination() - ]); + return null; }; })(); diff --git a/js/view/ui.js b/js/view/ui.js index a528bee..859fae6 100644 --- a/js/view/ui.js +++ b/js/view/ui.js @@ -58,4 +58,79 @@ return ""; } }; + + ui.paginate = function(currentPage, numPages, maxShown, emitter) { + function renderPage(text, attrs) { + attrs = attrs || {}; + return m("li", attrs, m("a", { href: "#" }, text)); + } + + var ellipsis = renderPage("…", { class: "disabled" }); + var start = currentPage - currentPage % maxShown; + var lastPage = numPages - 1; + var firstPrevClass = currentPage === 0 ? "disabled" : ""; + var lastNextClass = currentPage === lastPage ? "disabled" : ""; + var range = _.range(start, Math.min(start + maxShown, numPages)); + + return m("div", m("ul.pagination", [ + // Back page + renderPage("First", { + class: firstPrevClass, + onclick: function() { + emitter.emit(0); + } + }), + // Jump to first page + renderPage("Previous", { + class: firstPrevClass, + onclick: function() { + if(currentPage > 0) { + emitter.emit(currentPage - 1); + } + } + }), + // Display ellipsis if there are hidden pages. + start > 0 ? ellipsis : "" + ].concat(range.map(function(i) { + // Jump to page + var attrs = { + class: i === currentPage ? "active" : "", + onclick: function() { + emitter.emit(i); + } + }; + return renderPage(i + 1, attrs); + })).concat([ + // Display ellipsis if there are hidden pages. + start + maxShown < numPages ? ellipsis : "", + // Forward page + renderPage("Next", { + class: lastNextClass, + onclick: function() { + if(currentPage < lastPage) { + emitter.emit(currentPage + 1); + } + } + }), + // Jump to last page + renderPage("Last", { + class: lastNextClass, + onclick: function() { + emitter.emit(lastPage); + } + }) + ]))); + }; + + ui.spinner = m(".spinner", [ + m(".rect1"), + m(".rect2"), + m(".rect3"), + m(".rect4"), + m(".rect5") + ]); + + ui.spinUntil = function(ob) { + return K.merge([K.constant(ui.spinner), ob]); + }; })(); -- cgit v1.2.3