From d36ce30d2ccfbf45143f6be96e76a6a5c9fbce47 Mon Sep 17 00:00:00 2001 From: Rob Eisenberg Date: Wed, 10 Dec 2014 12:01:28 -0500 Subject: [PATCH] chore(*): set up the project structure and build This commit adds the initial source code, sets up the project structure, creates the gulp build file and configures karma. --- .editorconfig | 14 +++ .gitignore | 5 + .npmignore | 3 + LICENSE | 5 +- README.md | 71 +++++++++++++- config.js | 34 +++++++ dist/amd/compose.js | 111 ++++++++++++++++++++++ dist/amd/if.js | 53 +++++++++++ dist/amd/index.js | 9 ++ dist/amd/repeat.js | 161 ++++++++++++++++++++++++++++++++ dist/amd/selected-item.js | 62 +++++++++++++ dist/amd/show.js | 33 +++++++ dist/commonjs/compose.js | 109 ++++++++++++++++++++++ dist/commonjs/if.js | 51 ++++++++++ dist/commonjs/index.js | 7 ++ dist/commonjs/repeat.js | 159 +++++++++++++++++++++++++++++++ dist/commonjs/selected-item.js | 60 ++++++++++++ dist/commonjs/show.js | 31 +++++++ dist/es6/compose.js | 108 +++++++++++++++++++++ dist/es6/if.js | 49 ++++++++++ dist/es6/index.js | 5 + dist/es6/repeat.js | 165 +++++++++++++++++++++++++++++++++ dist/es6/selected-item.js | 57 ++++++++++++ dist/es6/show.js | 25 +++++ doc/api.json | 1 + gulpfile.js | 100 ++++++++++++++++++++ karma.conf.js | 70 ++++++++++++++ lib/compose.js | 108 +++++++++++++++++++++ lib/if.js | 49 ++++++++++ lib/index.js | 5 + lib/repeat.js | 165 +++++++++++++++++++++++++++++++++ lib/selected-item.js | 57 ++++++++++++ lib/show.js | 25 +++++ package.json | 49 ++++++++++ 34 files changed, 2010 insertions(+), 6 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 config.js create mode 100644 dist/amd/compose.js create mode 100644 dist/amd/if.js create mode 100644 dist/amd/index.js create mode 100644 dist/amd/repeat.js create mode 100644 dist/amd/selected-item.js create mode 100644 dist/amd/show.js create mode 100644 dist/commonjs/compose.js create mode 100644 dist/commonjs/if.js create mode 100644 dist/commonjs/index.js create mode 100644 dist/commonjs/repeat.js create mode 100644 dist/commonjs/selected-item.js create mode 100644 dist/commonjs/show.js create mode 100644 dist/es6/compose.js create mode 100644 dist/es6/if.js create mode 100644 dist/es6/index.js create mode 100644 dist/es6/repeat.js create mode 100644 dist/es6/selected-item.js create mode 100644 dist/es6/show.js create mode 100644 doc/api.json create mode 100644 gulpfile.js create mode 100644 karma.conf.js create mode 100644 lib/compose.js create mode 100644 lib/if.js create mode 100644 lib/index.js create mode 100644 lib/repeat.js create mode 100644 lib/selected-item.js create mode 100644 lib/show.js create mode 100644 package.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1033e2d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# 2 space indentation +[**.*] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..784886a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +jspm_packages +bower_components +.idea +.DS_STORE \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..cafe9b6 --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +jspm_packages +bower_components +.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE index 35c2026..6714c1d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 aurelia +Copyright (c) 2014 The Durandal Project Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,5 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index ebfb257..67c997c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,69 @@ -templating-resources -==================== +# aurelia-templating-resources -A standard set of behaviors, converters and other resources for use with the Aurelia templating library. +This library is part of the [Aurelia](http://www.aurelia.io/) platform and contains a standard set of behaviors, converters and other resources for use with the Aurelia templating library. + +> To keep up to date on [Aurelia](http://www.aurelia.io/), please visit and subscribe to [the official blog](http://blog.durandal.io/). If you have questions, we invite you to join us on [our Gitter Channel](https://gitter.im/Aurelia/Discuss). + +## Dependencies + +* [aurelia-templating](https://github.com/aurelia/templating) +* [aurelia-dependency-injection](https://github.com/aurelia/dependency-injection) +* [aurelia-binding](https://github.com/aurelia/binding) + +## Platform Support + +This library can be used in the **browser** only. + +## Building The Code + +To build the code, follow these steps. + +1. Ensure that [NodeJS](http://nodejs.org/) is installed. This provides the platform on which the build tooling runs. +2. From the project folder, execute the following command: + + ```shell + npm install + ``` +3. Ensure that [Gulp](http://gulpjs.com/) is installed. If you need to install it, use the following command: + + ```shell + npm install -g gulp + ``` +4. To build the code, you can now run: + + ```shell + gulp build + ``` +5. You will find the compiled code in the `dist` folder, available in three module formats: AMD, CommonJS and ES6. + +6. See `gulpfile.js` for other tasks related to generating the docs and linting. + +## Running The Tests + +To run the unit tests, first ensure that you have followed the steps above in order to install all dependencies and successfully build the library. Once you have done that, proceed with these additional steps: + +1. Ensure that the [Karma](http://karma-runner.github.io/) CLI is installed. If you need to install it, use the following command: + + ```shell + npm install -g karma-cli + ``` +2. Ensure that [jspm](http://jspm.io/) is installed. If you need to install it, use the following commnand: + + ```shell + npm install -g jspm + ``` +3. Install the client-side dependencies with jspm: + + ```shell + jspm install + ``` + +4. You can now run the tests with this command: + + ```shell + karma start + ``` + +## Contributing + +We'd love for you to contribute to our source code and to make this project even better than it is today! If this interests you, please begin by reading [our contributing guidelines](https://github.com/DurandalProject/about/blob/master/CONTRIBUTING.md). The contributing document will provide you with all the information you need to get started. Once you have read that, you will need to also [sign our CLA](http://goo.gl/forms/dI8QDDSyKR) before we can accepts a Pull Request from you. More information on the process is including in the [contributor's guide](https://github.com/DurandalProject/about/blob/master/CONTRIBUTING.md). \ No newline at end of file diff --git a/config.js b/config.js new file mode 100644 index 0000000..58ec969 --- /dev/null +++ b/config.js @@ -0,0 +1,34 @@ +System.config({ + "paths": { + "*": "*.js", + "github:*": "jspm_packages/github/*.js" + } +}); + +System.config({ + "map": { + "aurelia-binding": "github:aurelia/binding@0.0.1", + "aurelia-dependency-injection": "github:aurelia/dependency-injection@0.0.3", + "aurelia-templating": "github:aurelia/templating@0.0.1", + "github:aurelia/binding@0.0.1": { + "aurelia-metadata": "github:aurelia/metadata@0.0.5", + "aurelia-task-queue": "github:aurelia/task-queue@0.0.2" + }, + "github:aurelia/dependency-injection@0.0.3": { + "aurelia-metadata": "github:aurelia/metadata@0.0.5" + }, + "github:aurelia/loader@0.0.2": { + "aurelia-path": "github:aurelia/path@0.0.1" + }, + "github:aurelia/templating@0.0.1": { + "aurelia-binding": "github:aurelia/binding@0.0.1", + "aurelia-dependency-injection": "github:aurelia/dependency-injection@0.0.3", + "aurelia-loader": "github:aurelia/loader@0.0.2", + "aurelia-logging": "github:aurelia/logging@0.0.1", + "aurelia-metadata": "github:aurelia/metadata@0.0.5", + "aurelia-path": "github:aurelia/path@0.0.1", + "aurelia-task-queue": "github:aurelia/task-queue@0.0.2" + } + } +}); + diff --git a/dist/amd/compose.js b/dist/amd/compose.js new file mode 100644 index 0000000..9ad22f7 --- /dev/null +++ b/dist/amd/compose.js @@ -0,0 +1,111 @@ +define(["exports", "aurelia-dependency-injection", "aurelia-templating"], function (exports, _aureliaDependencyInjection, _aureliaTemplating) { + "use strict"; + + var Container = _aureliaDependencyInjection.Container; + var CustomElement = _aureliaTemplating.CustomElement; + var ResourceCoordinator = _aureliaTemplating.ResourceCoordinator; + var Property = _aureliaTemplating.Property; + var ViewSlot = _aureliaTemplating.ViewSlot; + var NoView = _aureliaTemplating.NoView; + var UseView = _aureliaTemplating.UseView; + var ViewEngine = _aureliaTemplating.ViewEngine; + var Compose = (function () { + var Compose = function Compose(container, resourceCoordinator, viewEngine, viewSlot) { + this.container = container; + this.resourceCoordinator = resourceCoordinator; + this.viewEngine = viewEngine; + this.viewSlot = viewSlot; + }; + + Compose.annotations = function () { + return [new CustomElement("compose"), new Property("model"), new Property("view"), new Property("viewModel"), new NoView()]; + }; + + Compose.inject = function () { + return [Container, ResourceCoordinator, ViewEngine, ViewSlot]; + }; + + Compose.prototype.bind = function (executionContext) { + this.executionContext = executionContext; + processInstruction(this, { + view: this.view, + viewModel: this.viewModel, + model: this.model + }); + }; + + Compose.prototype.modelChanged = function (newValue, oldValue) { + if (this.viewModel && this.viewModel.activate) { + this.viewModel.activate(newValue); + } + }; + + Compose.prototype.viewChanged = function (newValue, oldValue) { + processInstruction(this, { view: newValue }); + }; + + Compose.prototype.viewModelChanged = function (newValue, oldValue) { + processInstruction(this, { viewModel: newValue }); + }; + + return Compose; + })(); + + exports.Compose = Compose; + + + function swap(composer, behavior) { + behavior.bind(behavior.executionContext); + composer.viewSlot.swap(behavior.view); + + if (composer.current) { + composer.current.unbind(); + } + + composer.current = behavior; + } + + function processBehavior(composer, instruction, behavior) { + if (instruction.model && "activate" in instruction.viewModel) { + var activated = instruction.viewModel.activate(instruction.model) || Promise.resolve(); + activated.then(function () { + return swap(composer, behavior); + }); + } else { + swap(composer, behavior); + } + } + + function processInstruction(composer, instruction) { + var useView, result, options, childContainer; + + if (typeof instruction.viewModel == "string") { + composer.resourceCoordinator.loadAnonymousElement(composer.viewModel, null, instruction.view).then(function (type) { + childContainer = composer.container.createChild(); + options = { suppressBind: true }; + result = type.create(childContainer, options); + instruction.viewModel = result.executionContext; + processBehavior(composer, instruction, result); + }); + } else { + if (instruction.view) { + useView = new UseView(instruction.view); + } + + if (instruction.viewModel) { + CustomElement.anonymous(composer.container, instruction.viewModel, useView).then(function (type) { + childContainer = composer.container.createChild(); + options = { executionContext: instruction.viewModel, suppressBind: true }; + result = type.create(childContainer, options); + processBehavior(composer, instruction, result); + }); + } else if (useView) { + useView.loadViewFactory(composer.viewEngine).then(function (viewFactory) { + childContainer = composer.container.createChild(); + result = viewFactory.create(childContainer, composer.executionContext); + composer.viewSlot.swap(result); + }); + } + } + } +}); \ No newline at end of file diff --git a/dist/amd/if.js b/dist/amd/if.js new file mode 100644 index 0000000..70cac20 --- /dev/null +++ b/dist/amd/if.js @@ -0,0 +1,53 @@ +define(["exports", "aurelia-templating"], function (exports, _aureliaTemplating) { + "use strict"; + + var TemplateController = _aureliaTemplating.TemplateController; + var Property = _aureliaTemplating.Property; + var BoundViewFactory = _aureliaTemplating.BoundViewFactory; + var ViewSlot = _aureliaTemplating.ViewSlot; + var If = (function () { + var If = function If(viewFactory, viewSlot) { + this.viewFactory = viewFactory; + this.viewSlot = viewSlot; + this.showing = false; + }; + + If.annotations = function () { + return [new TemplateController("if"), new Property("value", "valueChanged", "if")]; + }; + + If.inject = function () { + return [BoundViewFactory, ViewSlot]; + }; + + If.prototype.valueChanged = function (newValue) { + if (!newValue) { + if (this.view) { + this.viewSlot.remove(this.view); + this.view.unbind(); + } + + this.showing = false; + return; + } + + if (!this.view) { + this.view = this.viewFactory.create(); + } + + if (!this.showing) { + this.showing = true; + + if (!this.view.bound) { + this.view.bind(); + } + + this.viewSlot.add(this.view); + } + }; + + return If; + })(); + + exports.If = If; +}); \ No newline at end of file diff --git a/dist/amd/index.js b/dist/amd/index.js new file mode 100644 index 0000000..ddcfc6d --- /dev/null +++ b/dist/amd/index.js @@ -0,0 +1,9 @@ +define(["exports", "./compose", "./if", "./repeat", "./show", "./selected-item"], function (exports, _compose, _if, _repeat, _show, _selectedItem) { + "use strict"; + + exports.Compose = _compose.Compose; + exports.If = _if.If; + exports.Repeat = _repeat.Repeat; + exports.Show = _show.Show; + exports.SelectedItem = _selectedItem.SelectedItem; +}); \ No newline at end of file diff --git a/dist/amd/repeat.js b/dist/amd/repeat.js new file mode 100644 index 0000000..02b15be --- /dev/null +++ b/dist/amd/repeat.js @@ -0,0 +1,161 @@ +define(["exports", "aurelia-binding", "aurelia-templating"], function (exports, _aureliaBinding, _aureliaTemplating) { + "use strict"; + + var ObserverLocator = _aureliaBinding.ObserverLocator; + var calcSplices = _aureliaBinding.calcSplices; + var TemplateController = _aureliaTemplating.TemplateController; + var BoundViewFactory = _aureliaTemplating.BoundViewFactory; + var ViewSlot = _aureliaTemplating.ViewSlot; + var Property = _aureliaTemplating.Property; + var Repeat = (function () { + var Repeat = function Repeat(viewFactory, viewSlot, observerLocator) { + this.viewFactory = viewFactory; + this.viewSlot = viewSlot; + this.observerLocator = observerLocator; + this.local = "item"; + }; + + Repeat.annotations = function () { + return [new TemplateController("repeat"), new Property("items", "itemsChanged", "repeat"), new Property("local")]; + }; + + Repeat.inject = function () { + return [BoundViewFactory, ViewSlot, ObserverLocator]; + }; + + Repeat.prototype.bind = function (executionContext) { + var _this = this; + var items = this.items; + + this.executionContext = executionContext; + + if (this.oldItems === items) { + var splices = calcSplices(items, 0, items.length, this.lastBoundItems, 0, this.lastBoundItems.length); + var observer = this.observerLocator.getArrayObserver(items); + + this.handleSplices(items, splices); + this.lastBoundItems = this.oldItems = null; + + this.disposeArraySubscription = observer.subscribe(function (splices) { + _this.handleSplices(items, splices); + }); + } else { + this.processItems(); + } + }; + + Repeat.prototype.unbind = function () { + this.oldItems = this.items; + this.lastBoundItems = this.items.slice(0); + + if (this.disposeArraySubscription) { + this.disposeArraySubscription(); + this.disposeArraySubscription = null; + } + }; + + Repeat.prototype.itemsChanged = function () { + this.processItems(); + }; + + Repeat.prototype.processItems = function () { + var _this2 = this; + var items = this.items, observer = this.observerLocator.getArrayObserver(items), viewSlot = this.viewSlot, viewFactory = this.viewFactory; + + if (this.disposeArraySubscription) { + this.disposeArraySubscription(); + viewSlot.removeAll(); + } + + for (var i = 0, ii = items.length; i < ii; i++) { + var row = this.createFullExecutionContext(items[i], i, ii); + var view = viewFactory.create(row); + viewSlot.add(view); + } + + this.disposeArraySubscription = observer.subscribe(function (splices) { + _this2.handleSplices(items, splices); + }); + }; + + Repeat.prototype.createBaseExecutionContext = function (data) { + var context = {}; + context[this.local] = data; + return context; + }; + + Repeat.prototype.createFullExecutionContext = function (data, index, length) { + var context = this.createBaseExecutionContext(data); + return this.updateExecutionContext(context, index, length); + }; + + Repeat.prototype.updateExecutionContext = function (context, index, length) { + var first = (index === 0), last = (index === length - 1), even = index % 2 === 0; + + context.$parent = this.executionContext; + context.$index = index; + context.$first = first; + context.$last = last; + context.$middle = !(first || last); + context.$odd = !even; + context.$even = even; + + return context; + }; + + Repeat.prototype.handleSplices = function (array, splices) { + var viewLookup = new Map(), removeDelta = 0, arrayLength = array.length, viewSlot = this.viewSlot, viewFactory = this.viewFactory, i, ii, j, jj, splice, removed, addIndex, end, model, view, children, length, row; + + for (i = 0, ii = splices.length; i < ii; i++) { + splice = splices[i]; + removed = splice.removed; + + for (j = 0, jj = removed.length; j < jj; j++) { + model = removed[j]; + view = viewSlot.removeAt(splice.index + removeDelta); + + if (view) { + viewLookup.set(model, view); + } + } + + removeDelta -= splice.addedCount; + } + + for (i = 0, ii = splices.length; i < ii; i++) { + splice = splices[i]; + addIndex = splice.index; + end = splice.index + splice.addedCount; + + for (; addIndex < end; addIndex++) { + model = array[addIndex]; + view = viewLookup.get(model); + + if (view) { + viewLookup["delete"](model); + viewSlot.insert(addIndex, view); + } else { + row = this.createBaseExecutionContext(model); + view = this.viewFactory.create(row); + viewSlot.insert(addIndex, view); + } + } + } + + children = viewSlot.children; + length = children.length; + + for (i = 0; i < length; i++) { + this.updateExecutionContext(children[i].executionContext, i, length); + } + + viewLookup.forEach(function (x) { + return x.unbind(); + }); + }; + + return Repeat; + })(); + + exports.Repeat = Repeat; +}); \ No newline at end of file diff --git a/dist/amd/selected-item.js b/dist/amd/selected-item.js new file mode 100644 index 0000000..0c1c5d2 --- /dev/null +++ b/dist/amd/selected-item.js @@ -0,0 +1,62 @@ +define(["exports", "aurelia-templating"], function (exports, _aureliaTemplating) { + "use strict"; + + var AttachedBehavior = _aureliaTemplating.AttachedBehavior; + var Property = _aureliaTemplating.Property; + var Children = _aureliaTemplating.Children; + var SelectedItem = (function () { + var SelectedItem = function SelectedItem(element) { + this.element = element; + this.options = []; + this.callback = this.selectedIndexChanged.bind(this); + }; + + SelectedItem.annotations = function () { + return [new AttachedBehavior("selected-item"), new Property("value", "valueChanged", "selected-item"), new Children("options", "optionsChanged", "option")]; + }; + + SelectedItem.inject = function () { + return [Element]; + }; + + SelectedItem.prototype.bind = function () { + this.element.addEventListener("change", this.callback, false); + }; + + SelectedItem.prototype.unbind = function () { + this.element.removeEventListener("change", this.callback); + }; + + SelectedItem.prototype.valueChanged = function (newValue) { + this.optionsChanged(); + }; + + SelectedItem.prototype.selectedIndexChanged = function () { + var index = this.element.selectedIndex, option = this.options[index]; + + this.value = option ? option.model : null; + }; + + SelectedItem.prototype.optionsChanged = function (mutations) { + var value = this.value, options = this.options, option, i, ii; + + for (i = 0, ii = options.length; i < ii; ++i) { + option = options[i]; + + if (option.model === value) { + if (this.element.selectedIndex !== i) { + this.element.selectedIndex = i; + } + + return; + } + } + + this.element.selectedIndex = 0; + }; + + return SelectedItem; + })(); + + exports.SelectedItem = SelectedItem; +}); \ No newline at end of file diff --git a/dist/amd/show.js b/dist/amd/show.js new file mode 100644 index 0000000..1671b87 --- /dev/null +++ b/dist/amd/show.js @@ -0,0 +1,33 @@ +define(["exports", "aurelia-templating"], function (exports, _aureliaTemplating) { + "use strict"; + + var AttachedBehavior = _aureliaTemplating.AttachedBehavior; + var Property = _aureliaTemplating.Property; + var Show = (function () { + var Show = function Show(element) { + this.element = element; + this.displayStyle = element.style.display; + }; + + Show.annotations = function () { + return [new AttachedBehavior("show"), new Property("value", "valueChanged", "show")]; + }; + + Show.inject = function () { + return [Element]; + }; + + Show.prototype.valueChanged = function (newValue) { + if (newValue) { + this.element.style.display = this.displayStyle || "block"; + } else { + this.displayStyle = this.element.style.display; + this.element.style.display = "none"; + } + }; + + return Show; + })(); + + exports.Show = Show; +}); \ No newline at end of file diff --git a/dist/commonjs/compose.js b/dist/commonjs/compose.js new file mode 100644 index 0000000..76e679f --- /dev/null +++ b/dist/commonjs/compose.js @@ -0,0 +1,109 @@ +"use strict"; + +var Container = require('aurelia-dependency-injection').Container; +var CustomElement = require('aurelia-templating').CustomElement; +var ResourceCoordinator = require('aurelia-templating').ResourceCoordinator; +var Property = require('aurelia-templating').Property; +var ViewSlot = require('aurelia-templating').ViewSlot; +var NoView = require('aurelia-templating').NoView; +var UseView = require('aurelia-templating').UseView; +var ViewEngine = require('aurelia-templating').ViewEngine; +var Compose = (function () { + var Compose = function Compose(container, resourceCoordinator, viewEngine, viewSlot) { + this.container = container; + this.resourceCoordinator = resourceCoordinator; + this.viewEngine = viewEngine; + this.viewSlot = viewSlot; + }; + + Compose.annotations = function () { + return [new CustomElement("compose"), new Property("model"), new Property("view"), new Property("viewModel"), new NoView()]; + }; + + Compose.inject = function () { + return [Container, ResourceCoordinator, ViewEngine, ViewSlot]; + }; + + Compose.prototype.bind = function (executionContext) { + this.executionContext = executionContext; + processInstruction(this, { + view: this.view, + viewModel: this.viewModel, + model: this.model + }); + }; + + Compose.prototype.modelChanged = function (newValue, oldValue) { + if (this.viewModel && this.viewModel.activate) { + this.viewModel.activate(newValue); + } + }; + + Compose.prototype.viewChanged = function (newValue, oldValue) { + processInstruction(this, { view: newValue }); + }; + + Compose.prototype.viewModelChanged = function (newValue, oldValue) { + processInstruction(this, { viewModel: newValue }); + }; + + return Compose; +})(); + +exports.Compose = Compose; + + +function swap(composer, behavior) { + behavior.bind(behavior.executionContext); + composer.viewSlot.swap(behavior.view); + + if (composer.current) { + composer.current.unbind(); + } + + composer.current = behavior; +} + +function processBehavior(composer, instruction, behavior) { + if (instruction.model && "activate" in instruction.viewModel) { + var activated = instruction.viewModel.activate(instruction.model) || Promise.resolve(); + activated.then(function () { + return swap(composer, behavior); + }); + } else { + swap(composer, behavior); + } +} + +function processInstruction(composer, instruction) { + var useView, result, options, childContainer; + + if (typeof instruction.viewModel == "string") { + composer.resourceCoordinator.loadAnonymousElement(composer.viewModel, null, instruction.view).then(function (type) { + childContainer = composer.container.createChild(); + options = { suppressBind: true }; + result = type.create(childContainer, options); + instruction.viewModel = result.executionContext; + processBehavior(composer, instruction, result); + }); + } else { + if (instruction.view) { + useView = new UseView(instruction.view); + } + + if (instruction.viewModel) { + CustomElement.anonymous(composer.container, instruction.viewModel, useView).then(function (type) { + childContainer = composer.container.createChild(); + options = { executionContext: instruction.viewModel, suppressBind: true }; + result = type.create(childContainer, options); + processBehavior(composer, instruction, result); + }); + } else if (useView) { + useView.loadViewFactory(composer.viewEngine).then(function (viewFactory) { + childContainer = composer.container.createChild(); + result = viewFactory.create(childContainer, composer.executionContext); + composer.viewSlot.swap(result); + }); + } + } +} \ No newline at end of file diff --git a/dist/commonjs/if.js b/dist/commonjs/if.js new file mode 100644 index 0000000..c7d300a --- /dev/null +++ b/dist/commonjs/if.js @@ -0,0 +1,51 @@ +"use strict"; + +var TemplateController = require('aurelia-templating').TemplateController; +var Property = require('aurelia-templating').Property; +var BoundViewFactory = require('aurelia-templating').BoundViewFactory; +var ViewSlot = require('aurelia-templating').ViewSlot; +var If = (function () { + var If = function If(viewFactory, viewSlot) { + this.viewFactory = viewFactory; + this.viewSlot = viewSlot; + this.showing = false; + }; + + If.annotations = function () { + return [new TemplateController("if"), new Property("value", "valueChanged", "if")]; + }; + + If.inject = function () { + return [BoundViewFactory, ViewSlot]; + }; + + If.prototype.valueChanged = function (newValue) { + if (!newValue) { + if (this.view) { + this.viewSlot.remove(this.view); + this.view.unbind(); + } + + this.showing = false; + return; + } + + if (!this.view) { + this.view = this.viewFactory.create(); + } + + if (!this.showing) { + this.showing = true; + + if (!this.view.bound) { + this.view.bind(); + } + + this.viewSlot.add(this.view); + } + }; + + return If; +})(); + +exports.If = If; \ No newline at end of file diff --git a/dist/commonjs/index.js b/dist/commonjs/index.js new file mode 100644 index 0000000..5903788 --- /dev/null +++ b/dist/commonjs/index.js @@ -0,0 +1,7 @@ +"use strict"; + +exports.Compose = require("./compose").Compose; +exports.If = require("./if").If; +exports.Repeat = require("./repeat").Repeat; +exports.Show = require("./show").Show; +exports.SelectedItem = require("./selected-item").SelectedItem; \ No newline at end of file diff --git a/dist/commonjs/repeat.js b/dist/commonjs/repeat.js new file mode 100644 index 0000000..9c0db62 --- /dev/null +++ b/dist/commonjs/repeat.js @@ -0,0 +1,159 @@ +"use strict"; + +var ObserverLocator = require('aurelia-binding').ObserverLocator; +var calcSplices = require('aurelia-binding').calcSplices; +var TemplateController = require('aurelia-templating').TemplateController; +var BoundViewFactory = require('aurelia-templating').BoundViewFactory; +var ViewSlot = require('aurelia-templating').ViewSlot; +var Property = require('aurelia-templating').Property; +var Repeat = (function () { + var Repeat = function Repeat(viewFactory, viewSlot, observerLocator) { + this.viewFactory = viewFactory; + this.viewSlot = viewSlot; + this.observerLocator = observerLocator; + this.local = "item"; + }; + + Repeat.annotations = function () { + return [new TemplateController("repeat"), new Property("items", "itemsChanged", "repeat"), new Property("local")]; + }; + + Repeat.inject = function () { + return [BoundViewFactory, ViewSlot, ObserverLocator]; + }; + + Repeat.prototype.bind = function (executionContext) { + var _this = this; + var items = this.items; + + this.executionContext = executionContext; + + if (this.oldItems === items) { + var splices = calcSplices(items, 0, items.length, this.lastBoundItems, 0, this.lastBoundItems.length); + var observer = this.observerLocator.getArrayObserver(items); + + this.handleSplices(items, splices); + this.lastBoundItems = this.oldItems = null; + + this.disposeArraySubscription = observer.subscribe(function (splices) { + _this.handleSplices(items, splices); + }); + } else { + this.processItems(); + } + }; + + Repeat.prototype.unbind = function () { + this.oldItems = this.items; + this.lastBoundItems = this.items.slice(0); + + if (this.disposeArraySubscription) { + this.disposeArraySubscription(); + this.disposeArraySubscription = null; + } + }; + + Repeat.prototype.itemsChanged = function () { + this.processItems(); + }; + + Repeat.prototype.processItems = function () { + var _this2 = this; + var items = this.items, observer = this.observerLocator.getArrayObserver(items), viewSlot = this.viewSlot, viewFactory = this.viewFactory; + + if (this.disposeArraySubscription) { + this.disposeArraySubscription(); + viewSlot.removeAll(); + } + + for (var i = 0, ii = items.length; i < ii; i++) { + var row = this.createFullExecutionContext(items[i], i, ii); + var view = viewFactory.create(row); + viewSlot.add(view); + } + + this.disposeArraySubscription = observer.subscribe(function (splices) { + _this2.handleSplices(items, splices); + }); + }; + + Repeat.prototype.createBaseExecutionContext = function (data) { + var context = {}; + context[this.local] = data; + return context; + }; + + Repeat.prototype.createFullExecutionContext = function (data, index, length) { + var context = this.createBaseExecutionContext(data); + return this.updateExecutionContext(context, index, length); + }; + + Repeat.prototype.updateExecutionContext = function (context, index, length) { + var first = (index === 0), last = (index === length - 1), even = index % 2 === 0; + + context.$parent = this.executionContext; + context.$index = index; + context.$first = first; + context.$last = last; + context.$middle = !(first || last); + context.$odd = !even; + context.$even = even; + + return context; + }; + + Repeat.prototype.handleSplices = function (array, splices) { + var viewLookup = new Map(), removeDelta = 0, arrayLength = array.length, viewSlot = this.viewSlot, viewFactory = this.viewFactory, i, ii, j, jj, splice, removed, addIndex, end, model, view, children, length, row; + + for (i = 0, ii = splices.length; i < ii; i++) { + splice = splices[i]; + removed = splice.removed; + + for (j = 0, jj = removed.length; j < jj; j++) { + model = removed[j]; + view = viewSlot.removeAt(splice.index + removeDelta); + + if (view) { + viewLookup.set(model, view); + } + } + + removeDelta -= splice.addedCount; + } + + for (i = 0, ii = splices.length; i < ii; i++) { + splice = splices[i]; + addIndex = splice.index; + end = splice.index + splice.addedCount; + + for (; addIndex < end; addIndex++) { + model = array[addIndex]; + view = viewLookup.get(model); + + if (view) { + viewLookup["delete"](model); + viewSlot.insert(addIndex, view); + } else { + row = this.createBaseExecutionContext(model); + view = this.viewFactory.create(row); + viewSlot.insert(addIndex, view); + } + } + } + + children = viewSlot.children; + length = children.length; + + for (i = 0; i < length; i++) { + this.updateExecutionContext(children[i].executionContext, i, length); + } + + viewLookup.forEach(function (x) { + return x.unbind(); + }); + }; + + return Repeat; +})(); + +exports.Repeat = Repeat; \ No newline at end of file diff --git a/dist/commonjs/selected-item.js b/dist/commonjs/selected-item.js new file mode 100644 index 0000000..2df63a9 --- /dev/null +++ b/dist/commonjs/selected-item.js @@ -0,0 +1,60 @@ +"use strict"; + +var AttachedBehavior = require('aurelia-templating').AttachedBehavior; +var Property = require('aurelia-templating').Property; +var Children = require('aurelia-templating').Children; +var SelectedItem = (function () { + var SelectedItem = function SelectedItem(element) { + this.element = element; + this.options = []; + this.callback = this.selectedIndexChanged.bind(this); + }; + + SelectedItem.annotations = function () { + return [new AttachedBehavior("selected-item"), new Property("value", "valueChanged", "selected-item"), new Children("options", "optionsChanged", "option")]; + }; + + SelectedItem.inject = function () { + return [Element]; + }; + + SelectedItem.prototype.bind = function () { + this.element.addEventListener("change", this.callback, false); + }; + + SelectedItem.prototype.unbind = function () { + this.element.removeEventListener("change", this.callback); + }; + + SelectedItem.prototype.valueChanged = function (newValue) { + this.optionsChanged(); + }; + + SelectedItem.prototype.selectedIndexChanged = function () { + var index = this.element.selectedIndex, option = this.options[index]; + + this.value = option ? option.model : null; + }; + + SelectedItem.prototype.optionsChanged = function (mutations) { + var value = this.value, options = this.options, option, i, ii; + + for (i = 0, ii = options.length; i < ii; ++i) { + option = options[i]; + + if (option.model === value) { + if (this.element.selectedIndex !== i) { + this.element.selectedIndex = i; + } + + return; + } + } + + this.element.selectedIndex = 0; + }; + + return SelectedItem; +})(); + +exports.SelectedItem = SelectedItem; \ No newline at end of file diff --git a/dist/commonjs/show.js b/dist/commonjs/show.js new file mode 100644 index 0000000..ca04f25 --- /dev/null +++ b/dist/commonjs/show.js @@ -0,0 +1,31 @@ +"use strict"; + +var AttachedBehavior = require('aurelia-templating').AttachedBehavior; +var Property = require('aurelia-templating').Property; +var Show = (function () { + var Show = function Show(element) { + this.element = element; + this.displayStyle = element.style.display; + }; + + Show.annotations = function () { + return [new AttachedBehavior("show"), new Property("value", "valueChanged", "show")]; + }; + + Show.inject = function () { + return [Element]; + }; + + Show.prototype.valueChanged = function (newValue) { + if (newValue) { + this.element.style.display = this.displayStyle || "block"; + } else { + this.displayStyle = this.element.style.display; + this.element.style.display = "none"; + } + }; + + return Show; +})(); + +exports.Show = Show; \ No newline at end of file diff --git a/dist/es6/compose.js b/dist/es6/compose.js new file mode 100644 index 0000000..e74e080 --- /dev/null +++ b/dist/es6/compose.js @@ -0,0 +1,108 @@ +import {Container} from 'aurelia-dependency-injection'; +import { + CustomElement, + ResourceCoordinator, + Property, + ViewSlot, + NoView, + UseView, + ViewEngine +} from 'aurelia-templating'; + +export class Compose { + static annotations() { + return [ + new CustomElement('compose'), + new Property('model'), + new Property('view'), + new Property('viewModel'), + new NoView() + ]; + } + + static inject(){ return [Container,ResourceCoordinator,ViewEngine,ViewSlot]; } + constructor(container, resourceCoordinator, viewEngine, viewSlot){ + this.container = container; + this.resourceCoordinator = resourceCoordinator; + this.viewEngine = viewEngine; + this.viewSlot = viewSlot; + } + + bind(executionContext){ + this.executionContext = executionContext; + processInstruction(this, { + view:this.view, + viewModel:this.viewModel, + model:this.model + }); + } + + modelChanged(newValue, oldValue){ + if(this.viewModel && this.viewModel.activate){ + this.viewModel.activate(newValue); + } + } + + viewChanged(newValue, oldValue){ + processInstruction(this, { view:newValue }); + } + + viewModelChanged(newValue, oldValue){ + processInstruction(this, { viewModel:newValue }); + } +} + +function swap(composer, behavior){ + behavior.bind(behavior.executionContext); + composer.viewSlot.swap(behavior.view); + + if(composer.current){ + composer.current.unbind(); + } + + composer.current = behavior; +} + +function processBehavior(composer, instruction, behavior){ + if(instruction.model && 'activate' in instruction.viewModel) { + var activated = instruction.viewModel.activate(instruction.model) || Promise.resolve(); + activated.then(() => swap(composer, behavior)); + }else{ + swap(composer, behavior); + } +} + +function processInstruction(composer, instruction){ + var useView, result, options, childContainer; + + if(typeof instruction.viewModel == 'string'){ + //TODO: make instruction.viewModel relative to compose's containing view + composer.resourceCoordinator.loadAnonymousElement(composer.viewModel, null, instruction.view).then(type => { + childContainer= composer.container.createChild(); + options = {suppressBind:true}; + result = type.create(childContainer, options); + instruction.viewModel = result.executionContext; + processBehavior(composer, instruction, result); + }); + }else{ + if(instruction.view) { + //TODO: make instruction.view relative to compose's containing view + useView = new UseView(instruction.view); + } + + if(instruction.viewModel){ + CustomElement.anonymous(composer.container, instruction.viewModel, useView).then(type => { + childContainer = composer.container.createChild(); + options = {executionContext:instruction.viewModel, suppressBind:true}; + result = type.create(childContainer, options); + processBehavior(composer, instruction, result); + }); + } else if (useView){ + useView.loadViewFactory(composer.viewEngine).then(viewFactory => { + childContainer = composer.container.createChild(); + result = viewFactory.create(childContainer, composer.executionContext); + composer.viewSlot.swap(result); + }); + } + } +} \ No newline at end of file diff --git a/dist/es6/if.js b/dist/es6/if.js new file mode 100644 index 0000000..a6ac201 --- /dev/null +++ b/dist/es6/if.js @@ -0,0 +1,49 @@ +import { + TemplateController, + Property, + BoundViewFactory, + ViewSlot +} from 'aurelia-templating'; + + +export class If { + static annotations() { + return [ + new TemplateController('if'), + new Property('value', 'valueChanged', 'if') + ]; + } + + static inject() { return [BoundViewFactory, ViewSlot]; } + constructor(viewFactory, viewSlot){ + this.viewFactory = viewFactory; + this.viewSlot = viewSlot; + this.showing = false; + } + + valueChanged(newValue){ + if (!newValue) { + if(this.view){ + this.viewSlot.remove(this.view); + this.view.unbind(); + } + + this.showing = false; + return; + } + + if(!this.view){ + this.view = this.viewFactory.create(); + } + + if (!this.showing) { + this.showing = true; + + if(!this.view.bound){ + this.view.bind(); + } + + this.viewSlot.add(this.view); + } + } +} \ No newline at end of file diff --git a/dist/es6/index.js b/dist/es6/index.js new file mode 100644 index 0000000..6591641 --- /dev/null +++ b/dist/es6/index.js @@ -0,0 +1,5 @@ +export {Compose} from './compose'; +export {If} from './if'; +export {Repeat} from './repeat'; +export {Show} from './show'; +export {SelectedItem} from './selected-item'; \ No newline at end of file diff --git a/dist/es6/repeat.js b/dist/es6/repeat.js new file mode 100644 index 0000000..b8b482e --- /dev/null +++ b/dist/es6/repeat.js @@ -0,0 +1,165 @@ +import {ObserverLocator, calcSplices} from 'aurelia-binding'; +import { + TemplateController, + BoundViewFactory, + ViewSlot, + Property +} from 'aurelia-templating'; + +export class Repeat { + static annotations(){ + return [ + new TemplateController('repeat'), + new Property('items', 'itemsChanged', 'repeat'), + new Property('local') + ]; + } + + static inject(){ return [BoundViewFactory,ViewSlot,ObserverLocator]; } + constructor(viewFactory, viewSlot, observerLocator){ + this.viewFactory = viewFactory; + this.viewSlot = viewSlot; + this.observerLocator = observerLocator; + this.local = 'item'; + } + + bind(executionContext){ + var items = this.items; + + this.executionContext = executionContext; + + if(this.oldItems === items){ + var splices = calcSplices(items, 0, items.length, this.lastBoundItems, 0, this.lastBoundItems.length); + var observer = this.observerLocator.getArrayObserver(items); + + this.handleSplices(items, splices); + this.lastBoundItems = this.oldItems = null; + + this.disposeArraySubscription = observer.subscribe(splices => { + this.handleSplices(items, splices); + }); + }else{ + this.processItems(); + } + } + + unbind(){ + this.oldItems = this.items; + this.lastBoundItems = this.items.slice(0); + + if(this.disposeArraySubscription){ + this.disposeArraySubscription(); + this.disposeArraySubscription = null; + } + } + + itemsChanged(){ + this.processItems(); + } + + processItems(){ + var items = this.items, + observer = this.observerLocator.getArrayObserver(items), + viewSlot = this.viewSlot, + viewFactory = this.viewFactory; + + if(this.disposeArraySubscription){ + this.disposeArraySubscription(); + viewSlot.removeAll(); + } + + for(var i = 0, ii = items.length; i < ii; i++){ + var row = this.createFullExecutionContext(items[i], i, ii); + var view = viewFactory.create(row); + viewSlot.add(view); + } + + this.disposeArraySubscription = observer.subscribe(splices => { + this.handleSplices(items, splices); + }); + } + + createBaseExecutionContext(data){ + var context = {}; + context[this.local] = data; + return context; + } + + createFullExecutionContext(data, index, length){ + var context = this.createBaseExecutionContext(data); + return this.updateExecutionContext(context, index, length); + } + + updateExecutionContext(context, index, length){ + var first = (index === 0), + last = (index === length - 1), + even = index % 2 === 0; + + context.$parent = this.executionContext; + context.$index = index; + context.$first = first; + context.$last = last; + context.$middle = !(first || last); + context.$odd = !even; + context.$even = even; + + return context; + } + + handleSplices(array, splices){ + var viewLookup = new Map(), + removeDelta = 0, + arrayLength = array.length, + viewSlot = this.viewSlot, + viewFactory = this.viewFactory, + i, ii, j, jj, splice, removed, addIndex, end, model, view, children, length, row; + + //TODO: track which views are moved instead of removed better + //TODO: only update context after highest changed index + + for (i = 0, ii = splices.length; i < ii; i++) { + splice = splices[i]; + removed = splice.removed; + + for (j = 0, jj = removed.length; j < jj; j++) { + model = removed[j]; + view = viewSlot.removeAt(splice.index + removeDelta); + + if (view) { + viewLookup.set(model, view); + } + } + + removeDelta -= splice.addedCount; + } + + for (i = 0, ii = splices.length; i < ii; i++) { + splice = splices[i]; + addIndex = splice.index; + end = splice.index + splice.addedCount; + + for (; addIndex < end; addIndex++) { + model = array[addIndex]; + view = viewLookup.get(model); + + if (view) { + viewLookup.delete(model); + viewSlot.insert(addIndex, view); //TODO: move + } else { + row = this.createBaseExecutionContext(model); + view = this.viewFactory.create(row); + viewSlot.insert(addIndex, view); + } + } + } + + children = viewSlot.children; + length = children.length; + + for(i = 0; i < length; i++){ + this.updateExecutionContext(children[i].executionContext, i, length); + } + + viewLookup.forEach(x => x.unbind()); + } +} \ No newline at end of file diff --git a/dist/es6/selected-item.js b/dist/es6/selected-item.js new file mode 100644 index 0000000..94f8c43 --- /dev/null +++ b/dist/es6/selected-item.js @@ -0,0 +1,57 @@ +import {AttachedBehavior, Property, Children} from 'aurelia-templating'; + +export class SelectedItem { + static annotations(){ + return [ + new AttachedBehavior('selected-item'), + new Property('value', 'valueChanged', 'selected-item'), + new Children('options', 'optionsChanged', 'option') + ]; + } + + static inject() { return [Element]; } + constructor(element){ + this.element = element; + this.options = []; + this.callback = this.selectedIndexChanged.bind(this); + } + + bind(){ + this.element.addEventListener('change', this.callback, false); + } + + unbind(){ + this.element.removeEventListener('change', this.callback); + } + + valueChanged(newValue){ + this.optionsChanged(); + } + + selectedIndexChanged(){ + var index = this.element.selectedIndex, + option = this.options[index]; + + this.value = option ? option.model : null; + } + + optionsChanged(mutations){ + var value = this.value, + options = this.options, + option, i, ii; + + for(i = 0, ii = options.length; i < ii; ++i){ + option = options[i]; + + if(option.model === value){ + if(this.element.selectedIndex !== i){ + this.element.selectedIndex = i; + } + + return; + } + } + + this.element.selectedIndex = 0; + } +} \ No newline at end of file diff --git a/dist/es6/show.js b/dist/es6/show.js new file mode 100644 index 0000000..9bac918 --- /dev/null +++ b/dist/es6/show.js @@ -0,0 +1,25 @@ +import {AttachedBehavior, Property} from 'aurelia-templating'; + +export class Show { + static annotations(){ + return [ + new AttachedBehavior('show'), + new Property('value', 'valueChanged', 'show') + ]; + } + + static inject() { return [Element]; } + constructor(element) { + this.element = element; + this.displayStyle = element.style.display; + } + + valueChanged(newValue){ + if(newValue){ + this.element.style.display = this.displayStyle || 'block'; + }else{ + this.displayStyle = this.element.style.display; + this.element.style.display = 'none'; + } + } +} \ No newline at end of file diff --git a/doc/api.json b/doc/api.json new file mode 100644 index 0000000..b648957 --- /dev/null +++ b/doc/api.json @@ -0,0 +1 @@ +{"project":{},"files":{},"modules":{},"classes":{},"classitems":[],"warnings":[]} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..c621e05 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,100 @@ +var gulp = require('gulp'); +var runSequence = require('run-sequence'); +var clean = require('gulp-clean'); +var to5 = require('gulp-6to5'); +var jshint = require('gulp-jshint'); +var stylish = require('jshint-stylish'); +var yuidoc = require("gulp-yuidoc"); +var changelog = require('conventional-changelog'); +var assign = Object.assign || require('object.assign'); +var pkg = require('./package.json'); +var fs = require('fs'); + +var VERSION = pkg.version; + +var path = { + source:'lib/**/*.js', + output:'dist/', + doc:'./doc' +}; + +var compilerOptions = { + filename: '', + filenameRelative: '', + blacklist: [], + whitelist: [], + modules: '', + sourceMap: true, + sourceMapName: '', + sourceFileName: '', + sourceRoot: '', + moduleRoot: '', + amdModuleIds: false, + runtime: false, + comments: false, + experimental: false +}; + +var jshintConfig = {esnext:true}; + +gulp.task('clean', function() { + return gulp.src([path.output], {read: false}) + .pipe(clean()); +}); + +gulp.task('build-es6', function () { + return gulp.src(path.source) + .pipe(gulp.dest(path.output + 'es6')); +}); + +gulp.task('build-commonjs', function () { + return gulp.src(path.source) + .pipe(to5(assign({}, compilerOptions, {modules:'common'}))) + .pipe(gulp.dest(path.output + 'commonjs')); +}); + +gulp.task('build-amd', function () { + return gulp.src(path.source) + .pipe(to5(assign({}, compilerOptions, {modules:'amd'}))) + .pipe(gulp.dest(path.output + 'amd')); +}); + +gulp.task('lint', function() { + return gulp.src(path.source) + .pipe(jshint(jshintConfig)) + .pipe(jshint.reporter(stylish)); +}); + +gulp.task('doc', function(){ + gulp.src(path.source) + .pipe(yuidoc.parser(null, 'api.json')) + .pipe(gulp.dest(path.doc)); +}); + +gulp.task('changelog', function(callback) { + changelog({ + repository: pkg.repository.url, + version: VERSION, + file: 'CHANGELOG.md' + }, function(err, log) { + fs.writeFileSync(path.doc + '/CHANGELOG.md', log); + }); +}); + +gulp.task('build', function(callback) { + runSequence( + 'clean', + ['build-es6', 'build-commonjs', 'build-amd'], + callback + ); +}); + +gulp.task('prepare-release', function(callback){ + runSequence( + 'build', + 'lint', + 'doc', + 'changelog', + callback + ); +}); \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..dd69fdf --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,70 @@ +// Karma configuration +// Generated on Fri Dec 05 2014 16:49:29 GMT-0500 (EST) + +module.exports = function(config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['jspm', 'jasmine'], + + jspm: { + // Edit this to your needs + loadFiles: ['lib/**/*.js', 'test/**/*.js'] + }, + + + // list of files / patterns to load in the browser + files: [], + + + // list of files to exclude + exclude: [ + ], + + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + 'test/**/*.js': ['6to5'], + 'lib/**/*.js': ['6to5'] + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ['progress'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + browsers: ['Chrome'], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: false + }); +}; diff --git a/lib/compose.js b/lib/compose.js new file mode 100644 index 0000000..e74e080 --- /dev/null +++ b/lib/compose.js @@ -0,0 +1,108 @@ +import {Container} from 'aurelia-dependency-injection'; +import { + CustomElement, + ResourceCoordinator, + Property, + ViewSlot, + NoView, + UseView, + ViewEngine +} from 'aurelia-templating'; + +export class Compose { + static annotations() { + return [ + new CustomElement('compose'), + new Property('model'), + new Property('view'), + new Property('viewModel'), + new NoView() + ]; + } + + static inject(){ return [Container,ResourceCoordinator,ViewEngine,ViewSlot]; } + constructor(container, resourceCoordinator, viewEngine, viewSlot){ + this.container = container; + this.resourceCoordinator = resourceCoordinator; + this.viewEngine = viewEngine; + this.viewSlot = viewSlot; + } + + bind(executionContext){ + this.executionContext = executionContext; + processInstruction(this, { + view:this.view, + viewModel:this.viewModel, + model:this.model + }); + } + + modelChanged(newValue, oldValue){ + if(this.viewModel && this.viewModel.activate){ + this.viewModel.activate(newValue); + } + } + + viewChanged(newValue, oldValue){ + processInstruction(this, { view:newValue }); + } + + viewModelChanged(newValue, oldValue){ + processInstruction(this, { viewModel:newValue }); + } +} + +function swap(composer, behavior){ + behavior.bind(behavior.executionContext); + composer.viewSlot.swap(behavior.view); + + if(composer.current){ + composer.current.unbind(); + } + + composer.current = behavior; +} + +function processBehavior(composer, instruction, behavior){ + if(instruction.model && 'activate' in instruction.viewModel) { + var activated = instruction.viewModel.activate(instruction.model) || Promise.resolve(); + activated.then(() => swap(composer, behavior)); + }else{ + swap(composer, behavior); + } +} + +function processInstruction(composer, instruction){ + var useView, result, options, childContainer; + + if(typeof instruction.viewModel == 'string'){ + //TODO: make instruction.viewModel relative to compose's containing view + composer.resourceCoordinator.loadAnonymousElement(composer.viewModel, null, instruction.view).then(type => { + childContainer= composer.container.createChild(); + options = {suppressBind:true}; + result = type.create(childContainer, options); + instruction.viewModel = result.executionContext; + processBehavior(composer, instruction, result); + }); + }else{ + if(instruction.view) { + //TODO: make instruction.view relative to compose's containing view + useView = new UseView(instruction.view); + } + + if(instruction.viewModel){ + CustomElement.anonymous(composer.container, instruction.viewModel, useView).then(type => { + childContainer = composer.container.createChild(); + options = {executionContext:instruction.viewModel, suppressBind:true}; + result = type.create(childContainer, options); + processBehavior(composer, instruction, result); + }); + } else if (useView){ + useView.loadViewFactory(composer.viewEngine).then(viewFactory => { + childContainer = composer.container.createChild(); + result = viewFactory.create(childContainer, composer.executionContext); + composer.viewSlot.swap(result); + }); + } + } +} \ No newline at end of file diff --git a/lib/if.js b/lib/if.js new file mode 100644 index 0000000..a6ac201 --- /dev/null +++ b/lib/if.js @@ -0,0 +1,49 @@ +import { + TemplateController, + Property, + BoundViewFactory, + ViewSlot +} from 'aurelia-templating'; + + +export class If { + static annotations() { + return [ + new TemplateController('if'), + new Property('value', 'valueChanged', 'if') + ]; + } + + static inject() { return [BoundViewFactory, ViewSlot]; } + constructor(viewFactory, viewSlot){ + this.viewFactory = viewFactory; + this.viewSlot = viewSlot; + this.showing = false; + } + + valueChanged(newValue){ + if (!newValue) { + if(this.view){ + this.viewSlot.remove(this.view); + this.view.unbind(); + } + + this.showing = false; + return; + } + + if(!this.view){ + this.view = this.viewFactory.create(); + } + + if (!this.showing) { + this.showing = true; + + if(!this.view.bound){ + this.view.bind(); + } + + this.viewSlot.add(this.view); + } + } +} \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..6591641 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,5 @@ +export {Compose} from './compose'; +export {If} from './if'; +export {Repeat} from './repeat'; +export {Show} from './show'; +export {SelectedItem} from './selected-item'; \ No newline at end of file diff --git a/lib/repeat.js b/lib/repeat.js new file mode 100644 index 0000000..b8b482e --- /dev/null +++ b/lib/repeat.js @@ -0,0 +1,165 @@ +import {ObserverLocator, calcSplices} from 'aurelia-binding'; +import { + TemplateController, + BoundViewFactory, + ViewSlot, + Property +} from 'aurelia-templating'; + +export class Repeat { + static annotations(){ + return [ + new TemplateController('repeat'), + new Property('items', 'itemsChanged', 'repeat'), + new Property('local') + ]; + } + + static inject(){ return [BoundViewFactory,ViewSlot,ObserverLocator]; } + constructor(viewFactory, viewSlot, observerLocator){ + this.viewFactory = viewFactory; + this.viewSlot = viewSlot; + this.observerLocator = observerLocator; + this.local = 'item'; + } + + bind(executionContext){ + var items = this.items; + + this.executionContext = executionContext; + + if(this.oldItems === items){ + var splices = calcSplices(items, 0, items.length, this.lastBoundItems, 0, this.lastBoundItems.length); + var observer = this.observerLocator.getArrayObserver(items); + + this.handleSplices(items, splices); + this.lastBoundItems = this.oldItems = null; + + this.disposeArraySubscription = observer.subscribe(splices => { + this.handleSplices(items, splices); + }); + }else{ + this.processItems(); + } + } + + unbind(){ + this.oldItems = this.items; + this.lastBoundItems = this.items.slice(0); + + if(this.disposeArraySubscription){ + this.disposeArraySubscription(); + this.disposeArraySubscription = null; + } + } + + itemsChanged(){ + this.processItems(); + } + + processItems(){ + var items = this.items, + observer = this.observerLocator.getArrayObserver(items), + viewSlot = this.viewSlot, + viewFactory = this.viewFactory; + + if(this.disposeArraySubscription){ + this.disposeArraySubscription(); + viewSlot.removeAll(); + } + + for(var i = 0, ii = items.length; i < ii; i++){ + var row = this.createFullExecutionContext(items[i], i, ii); + var view = viewFactory.create(row); + viewSlot.add(view); + } + + this.disposeArraySubscription = observer.subscribe(splices => { + this.handleSplices(items, splices); + }); + } + + createBaseExecutionContext(data){ + var context = {}; + context[this.local] = data; + return context; + } + + createFullExecutionContext(data, index, length){ + var context = this.createBaseExecutionContext(data); + return this.updateExecutionContext(context, index, length); + } + + updateExecutionContext(context, index, length){ + var first = (index === 0), + last = (index === length - 1), + even = index % 2 === 0; + + context.$parent = this.executionContext; + context.$index = index; + context.$first = first; + context.$last = last; + context.$middle = !(first || last); + context.$odd = !even; + context.$even = even; + + return context; + } + + handleSplices(array, splices){ + var viewLookup = new Map(), + removeDelta = 0, + arrayLength = array.length, + viewSlot = this.viewSlot, + viewFactory = this.viewFactory, + i, ii, j, jj, splice, removed, addIndex, end, model, view, children, length, row; + + //TODO: track which views are moved instead of removed better + //TODO: only update context after highest changed index + + for (i = 0, ii = splices.length; i < ii; i++) { + splice = splices[i]; + removed = splice.removed; + + for (j = 0, jj = removed.length; j < jj; j++) { + model = removed[j]; + view = viewSlot.removeAt(splice.index + removeDelta); + + if (view) { + viewLookup.set(model, view); + } + } + + removeDelta -= splice.addedCount; + } + + for (i = 0, ii = splices.length; i < ii; i++) { + splice = splices[i]; + addIndex = splice.index; + end = splice.index + splice.addedCount; + + for (; addIndex < end; addIndex++) { + model = array[addIndex]; + view = viewLookup.get(model); + + if (view) { + viewLookup.delete(model); + viewSlot.insert(addIndex, view); //TODO: move + } else { + row = this.createBaseExecutionContext(model); + view = this.viewFactory.create(row); + viewSlot.insert(addIndex, view); + } + } + } + + children = viewSlot.children; + length = children.length; + + for(i = 0; i < length; i++){ + this.updateExecutionContext(children[i].executionContext, i, length); + } + + viewLookup.forEach(x => x.unbind()); + } +} \ No newline at end of file diff --git a/lib/selected-item.js b/lib/selected-item.js new file mode 100644 index 0000000..94f8c43 --- /dev/null +++ b/lib/selected-item.js @@ -0,0 +1,57 @@ +import {AttachedBehavior, Property, Children} from 'aurelia-templating'; + +export class SelectedItem { + static annotations(){ + return [ + new AttachedBehavior('selected-item'), + new Property('value', 'valueChanged', 'selected-item'), + new Children('options', 'optionsChanged', 'option') + ]; + } + + static inject() { return [Element]; } + constructor(element){ + this.element = element; + this.options = []; + this.callback = this.selectedIndexChanged.bind(this); + } + + bind(){ + this.element.addEventListener('change', this.callback, false); + } + + unbind(){ + this.element.removeEventListener('change', this.callback); + } + + valueChanged(newValue){ + this.optionsChanged(); + } + + selectedIndexChanged(){ + var index = this.element.selectedIndex, + option = this.options[index]; + + this.value = option ? option.model : null; + } + + optionsChanged(mutations){ + var value = this.value, + options = this.options, + option, i, ii; + + for(i = 0, ii = options.length; i < ii; ++i){ + option = options[i]; + + if(option.model === value){ + if(this.element.selectedIndex !== i){ + this.element.selectedIndex = i; + } + + return; + } + } + + this.element.selectedIndex = 0; + } +} \ No newline at end of file diff --git a/lib/show.js b/lib/show.js new file mode 100644 index 0000000..9bac918 --- /dev/null +++ b/lib/show.js @@ -0,0 +1,25 @@ +import {AttachedBehavior, Property} from 'aurelia-templating'; + +export class Show { + static annotations(){ + return [ + new AttachedBehavior('show'), + new Property('value', 'valueChanged', 'show') + ]; + } + + static inject() { return [Element]; } + constructor(element) { + this.element = element; + this.displayStyle = element.style.display; + } + + valueChanged(newValue){ + if(newValue){ + this.element.style.display = this.displayStyle || 'block'; + }else{ + this.displayStyle = this.element.style.display; + this.element.style.display = 'none'; + } + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..a1d6ca9 --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "aurelia-templating-resources", + "version": "0.0.1", + "description": "A standard set of behaviors, converters and other resources for use with the Aurelia templating library.", + "keywords": [ + "aurelia", + "templating", + "html" + ], + "homepage": "http://aurelia.io", + "bugs": { + "url": "https://github.com/aurelia/templating-resources/issues" + }, + "license": "MIT", + "author": "Rob Eisenberg (http://robeisenberg.com/)", + "main": "dist/commonjs/index.js", + "repository": { + "type": "git", + "url": "git://github.com/aurelia/templating-resources" + }, + "jspm": { + "main": "amd/index", + "directories": { + "lib": "dist" + }, + "dependencies": { + "aurelia-binding": "github:aurelia/binding@^0.0.1", + "aurelia-dependency-injection": "github:aurelia/dependency-injection@^0.0.3", + "aurelia-templating": "github:aurelia/templating@^0.0.1" + } + }, + "devDependencies": { + "conventional-changelog": "0.0.11", + "gulp": "^3.8.10", + "gulp-6to5": "^1.0.2", + "gulp-clean": "^0.3.1", + "gulp-jshint": "^1.9.0", + "gulp-yuidoc": "^0.1.2", + "jasmine-core": "^2.1.3", + "jshint-stylish": "^1.0.0", + "karma": "^0.12.28", + "karma-6to5-preprocessor": "^0.1.3", + "karma-chrome-launcher": "^0.1.7", + "karma-jasmine": "^0.3.2", + "karma-jspm": "^1.0.1", + "object.assign": "^1.0.3", + "run-sequence": "^1.0.2" + } +}