Repository: hoch/WAAX
Branch: master
Commit: ca933954877e
Files: 206
Total size: 2.8 MB
Directory structure:
gitextract_ubii12jd/
├── .gitignore
├── LICENSE
├── README.md
├── build/
│ └── mui/
│ ├── bower_components/
│ │ ├── core-component-page/
│ │ │ ├── README.md
│ │ │ ├── bower.json
│ │ │ ├── core-component-page.html
│ │ │ ├── demo.html
│ │ │ └── index.html
│ │ ├── core-icon/
│ │ │ ├── README.md
│ │ │ ├── bower.json
│ │ │ ├── core-icon.css
│ │ │ ├── core-icon.html
│ │ │ ├── demo.html
│ │ │ ├── index.html
│ │ │ └── metadata.html
│ │ ├── core-icons/
│ │ │ ├── README.md
│ │ │ ├── av-icons.html
│ │ │ ├── bower.json
│ │ │ ├── communication-icons.html
│ │ │ ├── core-icons.html
│ │ │ ├── demo.html
│ │ │ ├── device-icons.html
│ │ │ ├── editor-icons.html
│ │ │ ├── hardware-icons.html
│ │ │ ├── image-icons.html
│ │ │ ├── index.html
│ │ │ ├── maps-icons.html
│ │ │ ├── notification-icons.html
│ │ │ ├── png-icons.html
│ │ │ └── social-icons.html
│ │ ├── core-iconset/
│ │ │ ├── README.md
│ │ │ ├── bower.json
│ │ │ ├── core-iconset.html
│ │ │ ├── demo.html
│ │ │ └── index.html
│ │ ├── core-iconset-svg/
│ │ │ ├── README.md
│ │ │ ├── bower.json
│ │ │ ├── core-iconset-svg.html
│ │ │ ├── demo.html
│ │ │ ├── index.html
│ │ │ └── svg-sample-icons.html
│ │ ├── core-meta/
│ │ │ ├── README.md
│ │ │ ├── bower.json
│ │ │ ├── core-meta.html
│ │ │ ├── demo.html
│ │ │ └── index.html
│ │ ├── polymer/
│ │ │ ├── README.md
│ │ │ ├── bower.json
│ │ │ ├── build.log
│ │ │ ├── dist/
│ │ │ │ └── polymer.html
│ │ │ ├── layout.html
│ │ │ ├── polymer.html
│ │ │ └── polymer.js
│ │ └── webcomponentsjs/
│ │ ├── CustomElements.js
│ │ ├── HTMLImports.js
│ │ ├── README.md
│ │ ├── ShadowDOM.js
│ │ ├── bower.json
│ │ ├── build.log
│ │ ├── package.json
│ │ ├── webcomponents-lite.js
│ │ └── webcomponents.js
│ ├── mui-button/
│ │ └── mui-button.html
│ ├── mui-eblock/
│ │ └── mui-eblock.html
│ ├── mui-group/
│ │ └── mui-group.html
│ ├── mui-knob/
│ │ └── mui-knob.html
│ ├── mui-knobh/
│ │ └── mui-knobh.html
│ ├── mui-meter/
│ │ └── mui-meter.html
│ ├── mui-pianoroll/
│ │ └── mui-pianoroll.html
│ ├── mui-rack/
│ │ └── mui-rack.html
│ ├── mui-select/
│ │ └── mui-select.html
│ ├── mui-spectrum/
│ │ └── mui-spectrum.html
│ ├── mui-vkey/
│ │ └── mui-vkey.html
│ ├── mui.html
│ └── mui.js
├── examples/
│ ├── chorus/
│ │ └── index.html
│ ├── cmp1/
│ │ └── index.html
│ ├── converb/
│ │ └── index.html
│ ├── eq4/
│ │ └── index.html
│ ├── examples.js
│ ├── fader/
│ │ └── index.html
│ ├── filterbank/
│ │ └── index.html
│ ├── fmk1/
│ │ └── index.html
│ ├── hellowaax/
│ │ └── index.html
│ ├── impulse/
│ │ └── index.html
│ ├── index.html
│ ├── lab/
│ │ └── index.html
│ ├── mui/
│ │ ├── ex-mui-meter.html
│ │ ├── ex-mui-pianoroll.html
│ │ ├── index.html
│ │ └── showcase.html
│ ├── noise/
│ │ └── index.html
│ ├── simpleosc/
│ │ └── index.html
│ ├── sp1/
│ │ └── index.html
│ ├── stereodelay/
│ │ └── index.html
│ ├── style.css
│ ├── workshop/
│ │ └── index.html
│ └── wxs1/
│ └── index.html
├── gulpfile.js
├── index.html
├── package.json
├── sound/
│ └── LICENSE
├── src/
│ ├── ktrl.js
│ ├── mui/
│ │ ├── bower.json
│ │ ├── bower_components/
│ │ │ ├── core-component-page/
│ │ │ │ ├── .bower.json
│ │ │ │ ├── README.md
│ │ │ │ ├── bower.json
│ │ │ │ ├── core-component-page.html
│ │ │ │ ├── demo.html
│ │ │ │ └── index.html
│ │ │ ├── core-icon/
│ │ │ │ ├── .bower.json
│ │ │ │ ├── README.md
│ │ │ │ ├── bower.json
│ │ │ │ ├── core-icon.css
│ │ │ │ ├── core-icon.html
│ │ │ │ ├── demo.html
│ │ │ │ ├── index.html
│ │ │ │ └── metadata.html
│ │ │ ├── core-icons/
│ │ │ │ ├── .bower.json
│ │ │ │ ├── README.md
│ │ │ │ ├── av-icons.html
│ │ │ │ ├── bower.json
│ │ │ │ ├── communication-icons.html
│ │ │ │ ├── core-icons.html
│ │ │ │ ├── demo.html
│ │ │ │ ├── device-icons.html
│ │ │ │ ├── editor-icons.html
│ │ │ │ ├── hardware-icons.html
│ │ │ │ ├── image-icons.html
│ │ │ │ ├── index.html
│ │ │ │ ├── maps-icons.html
│ │ │ │ ├── notification-icons.html
│ │ │ │ ├── png-icons.html
│ │ │ │ └── social-icons.html
│ │ │ ├── core-iconset/
│ │ │ │ ├── .bower.json
│ │ │ │ ├── README.md
│ │ │ │ ├── bower.json
│ │ │ │ ├── core-iconset.html
│ │ │ │ ├── demo.html
│ │ │ │ └── index.html
│ │ │ ├── core-iconset-svg/
│ │ │ │ ├── .bower.json
│ │ │ │ ├── README.md
│ │ │ │ ├── bower.json
│ │ │ │ ├── core-iconset-svg.html
│ │ │ │ ├── demo.html
│ │ │ │ ├── index.html
│ │ │ │ └── svg-sample-icons.html
│ │ │ ├── core-meta/
│ │ │ │ ├── .bower.json
│ │ │ │ ├── README.md
│ │ │ │ ├── bower.json
│ │ │ │ ├── core-meta.html
│ │ │ │ ├── demo.html
│ │ │ │ └── index.html
│ │ │ ├── polymer/
│ │ │ │ ├── .bower.json
│ │ │ │ ├── README.md
│ │ │ │ ├── bower.json
│ │ │ │ ├── build.log
│ │ │ │ ├── dist/
│ │ │ │ │ └── polymer.html
│ │ │ │ ├── layout.html
│ │ │ │ ├── polymer.html
│ │ │ │ └── polymer.js
│ │ │ └── webcomponentsjs/
│ │ │ ├── .bower.json
│ │ │ ├── CustomElements.js
│ │ │ ├── HTMLImports.js
│ │ │ ├── README.md
│ │ │ ├── ShadowDOM.js
│ │ │ ├── bower.json
│ │ │ ├── build.log
│ │ │ ├── package.json
│ │ │ ├── webcomponents-lite.js
│ │ │ └── webcomponents.js
│ │ ├── mui-button/
│ │ │ └── mui-button.html
│ │ ├── mui-eblock/
│ │ │ └── mui-eblock.html
│ │ ├── mui-group/
│ │ │ └── mui-group.html
│ │ ├── mui-knob/
│ │ │ └── mui-knob.html
│ │ ├── mui-knobh/
│ │ │ └── mui-knobh.html
│ │ ├── mui-meter/
│ │ │ └── mui-meter.html
│ │ ├── mui-pianoroll/
│ │ │ └── mui-pianoroll.html
│ │ ├── mui-rack/
│ │ │ └── mui-rack.html
│ │ ├── mui-select/
│ │ │ └── mui-select.html
│ │ ├── mui-spectrum/
│ │ │ └── mui-spectrum.html
│ │ ├── mui-vkey/
│ │ │ └── mui-vkey.html
│ │ ├── mui.html
│ │ └── mui.js
│ ├── plug_ins/
│ │ ├── CMP1/
│ │ │ └── cmp1.js
│ │ ├── Chorus/
│ │ │ └── chorus.js
│ │ ├── ConVerb/
│ │ │ └── converb.js
│ │ ├── EQ4/
│ │ │ └── eq4.js
│ │ ├── FMK1/
│ │ │ └── fmk1.js
│ │ ├── Fader/
│ │ │ └── fader.js
│ │ ├── FilterBank/
│ │ │ └── filterbank.js
│ │ ├── Impulse/
│ │ │ └── Impulse.js
│ │ ├── Noise/
│ │ │ └── noise.js
│ │ ├── SP1/
│ │ │ └── sp1.js
│ │ ├── SimpleOsc/
│ │ │ └── SimpleOsc.js
│ │ ├── StereoDelay/
│ │ │ └── StereoDelay.js
│ │ └── WXS1/
│ │ └── wxs1.js
│ ├── waax.core.js
│ ├── waax.extension.js
│ ├── waax.js
│ ├── waax.timebase.js
│ └── waax.util.js
└── test/
├── index.html
├── test-core.js
├── test-setup.js
└── test-timebase.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules/
NOTES.md
================================================
FILE: LICENSE
================================================
Copyright 2011-2014 Hongchan Choi.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
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.
================================================
FILE: README.md
================================================
# WAAX
Web Audio API eXtension (1.0.0-alpha3)
> NOTE: WAAX requires Web Audio API and Web Components.
## Introduction
__WAAX__ offers a comprehensive framework for web-based music application. Its goal is to facilitate and support the development of web-based music software.
## Feature Highlights
- **Fast** - Built around native Web Audio API nodes.
- **Less code**
- Web Audio API helpers and utilities
- Succint parameter control
- Transport and event management (i.e. sequencer)
- **Modular and extensible**
- WAPL (Web Audio PLug-in): Plug-in for Web Audio API ecosystem
- MUI (Musical User Interface): GUI for music apps
- **Robust workflow** - Preconfigured with Bower and Gulp
## Prerequisites
The complete WAAX development setup requires the following software. Make sure they are installed with appropriate scope and permission.
- [Git](http://git-scm.com/) - [Installation](http://git-scm.com/downloads)
- [Node.js](http://nodejs.org/) - [Installation](http://nodejs.org/)
- [Gulp](http://gulpjs.com/) - [Installation](https://github.com/gulpjs/gulp/blob/master/docs/getting-started.md)
## Installation and Quick Start
If you have all the above installed, execute the following commands in the terminal to install and launch WAAX.
~~~bash
git clone https://github.com/hoch/WAAX waax
cd waax
npm install
gulp
~~~
Note that WAAX is pre-configured for the optimum development workflow. Type `gulp` in the terminal and then your web browser (Chrome by default) will open the project index page automatically.
## What's Next?
Go to the [project landing page](http://hoch.github.io/WAAX) and see what WAAX can do.
## Change Log
- 1.0.0-alpha3
+ Updated dependencies with latest version: Polymer, Gulp-related utilities.
+ mui-vkey polyphonic release issue solved.
+ WXS-1 monophonic legato behavior fixed.
+ New project landing page and API reference are online.
+ Using MUI package is significantly simplified.
+ `bower_components` are now part of MUI package, not WAAX.
+ License for sound resources is added.
- 1.0.0-alpha2
+ Updated dependencies with latest version: Gulp, Polymer.
+ MUI elements updated for new version of Polymer.
+ Timebase code has been cleaned/refactored.
+ Updated README and the temporary landing page is removed.
+ MUI elements and test files are now compatible with FireFox/Safari.
+ Audio assets are converted to MP3.
- [bug] `WXS1` and `FMK1` plug-ins produce distorted sound in FireFox.
- [bug] Safari does not load the example with `StereoDelay` plug-in.
- 1.0.0-alpha
+ First alpha version before stable release.
+ Dropped deprecated components from repository.
+ New plug-in builder introduced.
- r17 (dev)
+ Last version of dev/experimental revision.
## License and Contact
MIT License. Copyright 2011-2014 [Hongchan Choi](http://www.hoch.io)
================================================
FILE: build/mui/bower_components/core-component-page/README.md
================================================
core-component-page
===================
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-component-page) for more information.
Note: this is the vulcanized version of [`core-component-page-dev`](https://github.com/Polymer/core-component-page-dev) (the source).
================================================
FILE: build/mui/bower_components/core-component-page/bower.json
================================================
{
"name": "core-component-page",
"private": true,
"dependencies": {
"webcomponentsjs": "Polymer/webcomponentsjs#^0.5.0",
"polymer": "Polymer/polymer#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: build/mui/bower_components/core-component-page/core-component-page.html
================================================
{{data.name}} Home Page Version: {{data.version}}
Attributes <{{attribute.type}} >default: {{attribute.default}}
Properties <{{property.type}} >default: {{property.default}}
Events Event details:
<{{param.type}} > {{param.name}}
{{param.description}}
Methods Method parameters:
<{{param.type}} > {{param.name}}
{{param.description}}
{{label}}
{{name}}
undefined
{{moduleName}} demo
================================================
FILE: build/mui/bower_components/core-component-page/demo.html
================================================
================================================
FILE: build/mui/bower_components/core-component-page/index.html
================================================
================================================
FILE: build/mui/bower_components/core-icon/README.md
================================================
core-icon
=========
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-icon) for more information.
================================================
FILE: build/mui/bower_components/core-icon/bower.json
================================================
{
"name": "core-icon",
"private": true,
"dependencies": {
"core-iconset": "Polymer/core-iconset#^0.5.0",
"core-icons": "Polymer/core-icons#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: build/mui/bower_components/core-icon/core-icon.css
================================================
/* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */
html /deep/ core-icon {
display: inline-block;
vertical-align: middle;
background-repeat: no-repeat;
fill: currentcolor;
position: relative;
height: 24px;
width: 24px;
}
================================================
FILE: build/mui/bower_components/core-icon/core-icon.html
================================================
================================================
FILE: build/mui/bower_components/core-icon/demo.html
================================================
core-icon
{{icon}}
Sized icon:
================================================
FILE: build/mui/bower_components/core-icon/index.html
================================================
================================================
FILE: build/mui/bower_components/core-icon/metadata.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/README.md
================================================
core-icons
=========
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-icons) for more information.
================================================
FILE: build/mui/bower_components/core-icons/av-icons.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/bower.json
================================================
{
"name": "core-icons",
"private": true,
"dependencies": {
"core-icon": "Polymer/core-icon#^0.5.0",
"core-iconset-svg": "Polymer/core-iconset-svg#^0.5.0",
"polymer": "Polymer/polymer#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: build/mui/bower_components/core-icons/communication-icons.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/core-icons.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/demo.html
================================================
core-icons
{{iconset.id}}
{{iconset.id === 'icons' ? 'The Default Set' : 'Import ' + iconset.id + '-icons.html'}}
================================================
FILE: build/mui/bower_components/core-icons/device-icons.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/editor-icons.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/hardware-icons.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/image-icons.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/index.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/maps-icons.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/notification-icons.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/png-icons.html
================================================
================================================
FILE: build/mui/bower_components/core-icons/social-icons.html
================================================
+
================================================
FILE: build/mui/bower_components/core-iconset/README.md
================================================
core-iconset
============
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-iconset) for more information.
================================================
FILE: build/mui/bower_components/core-iconset/bower.json
================================================
{
"name": "core-iconset",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#^0.5.0",
"core-meta": "Polymer/core-meta#^0.5.0",
"core-icon": "Polymer/core-icon#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: build/mui/bower_components/core-iconset/core-iconset.html
================================================
================================================
FILE: build/mui/bower_components/core-iconset/demo.html
================================================
core-iconset
================================================
FILE: build/mui/bower_components/core-iconset/index.html
================================================
================================================
FILE: build/mui/bower_components/core-iconset-svg/README.md
================================================
core-iconset-svg
=========
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-iconset-svg) for more information.
================================================
FILE: build/mui/bower_components/core-iconset-svg/bower.json
================================================
{
"name": "core-iconset-svg",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#^0.5.0",
"core-iconset": "Polymer/core-iconset#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: build/mui/bower_components/core-iconset-svg/core-iconset-svg.html
================================================
================================================
FILE: build/mui/bower_components/core-iconset-svg/demo.html
================================================
core-iconset-svg
================================================
FILE: build/mui/bower_components/core-iconset-svg/index.html
================================================
================================================
FILE: build/mui/bower_components/core-iconset-svg/svg-sample-icons.html
================================================
================================================
FILE: build/mui/bower_components/core-meta/README.md
================================================
core-meta
=========
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-meta) for more information.
================================================
FILE: build/mui/bower_components/core-meta/bower.json
================================================
{
"name": "core-meta",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: build/mui/bower_components/core-meta/core-meta.html
================================================
================================================
FILE: build/mui/bower_components/core-meta/demo.html
================================================
core-meta
meta-data
{{label}}
meta-data (type: fruit)
{{label}}
================================================
FILE: build/mui/bower_components/core-meta/index.html
================================================
================================================
FILE: build/mui/bower_components/polymer/README.md
================================================
# Polymer
[](http://build.chromium.org/p/client.polymer/waterfall)
## Brief Overview
For more detailed info goto [http://polymer-project.org/](http://polymer-project.org/).
Polymer is a new type of library for the web, designed to leverage the existing browser infrastructure to provide the encapsulation and extendability currently only available in JS libraries.
Polymer is based on a set of future technologies, including [Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/), [Custom Elements](http://w3c.github.io/webcomponents/spec/custom/) and Model Driven Views. Currently these technologies are implemented as polyfills or shims, but as browsers adopt these features natively, the platform code that drives Polymer evacipates, leaving only the value-adds.
## Tools & Testing
For running tests or building minified files, consult the [tooling information](https://www.polymer-project.org/resources/tooling-strategy.html).
## Releases
[Release (tagged) versions](https://github.com/Polymer/polymer/releases) of Polymer include concatenated and minified sources for your convenience.
[](https://github.com/igrigorik/ga-beacon)
================================================
FILE: build/mui/bower_components/polymer/bower.json
================================================
{
"name": "polymer",
"description": "Polymer is a new type of library for the web, built on top of Web Components, and designed to leverage the evolving web platform on modern browsers.",
"homepage": "http://www.polymer-project.org/",
"keywords": [
"util",
"client",
"browser",
"web components",
"web-components"
],
"author": "Polymer Authors ",
"private": true,
"dependencies": {
"core-component-page": "Polymer/core-component-page#^0.5.0",
"webcomponentsjs": "Polymer/webcomponentsjs#^0.5.0"
},
"devDependencies": {
"tools": "Polymer/tools#master",
"web-component-tester": "Polymer/web-component-tester#^1.4.2"
},
"version": "0.5.2"
}
================================================
FILE: build/mui/bower_components/polymer/build.log
================================================
BUILD LOG
---------
Build Time: 2014-12-11T12:46:30
NODEJS INFORMATION
==================
nodejs: v0.10.33
grunt: 0.4.5
grunt-audit: 1.0.0
grunt-contrib-concat: 0.5.0
grunt-contrib-copy: 0.7.0
grunt-contrib-uglify: 0.6.0
grunt-string-replace: 1.0.0
REPO REVISIONS
==============
polymer-expressions: 197c3a0150e7a13374cfcc72e7066113723a623d
polymer-gestures: 17a6304916521be39409af292e8adf899bae0ce7
polymer: a74e9f36526361dccb6df91be439ff9c3e043f41
BUILD HASHES
============
dist/polymer.js: b9ad4c86af79c748cf4ea722f6d56671079fadf7
dist/polymer.min.js: 2f2021ba9682b0bb702ee7fb68fb6fbfd288eac2
dist/layout.html: 348d358a91712ecc2f8811efa430fcd954b4590c
================================================
FILE: build/mui/bower_components/polymer/dist/polymer.html
================================================
================================================
FILE: build/mui/bower_components/polymer/layout.html
================================================
================================================
FILE: build/mui/bower_components/polymer/polymer.html
================================================
================================================
FILE: build/mui/bower_components/polymer/polymer.js
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
// @version 0.5.1
window.PolymerGestures = {};
(function(scope) {
var HAS_FULL_PATH = false;
// test for full event path support
var pathTest = document.createElement('meta');
if (pathTest.createShadowRoot) {
var sr = pathTest.createShadowRoot();
var s = document.createElement('span');
sr.appendChild(s);
pathTest.addEventListener('testpath', function(ev) {
if (ev.path) {
// if the span is in the event path, then path[0] is the real source for all events
HAS_FULL_PATH = ev.path[0] === s;
}
ev.stopPropagation();
});
var ev = new CustomEvent('testpath', {bubbles: true});
// must add node to DOM to trigger event listener
document.head.appendChild(pathTest);
s.dispatchEvent(ev);
pathTest.parentNode.removeChild(pathTest);
sr = s = null;
}
pathTest = null;
var target = {
shadow: function(inEl) {
if (inEl) {
return inEl.shadowRoot || inEl.webkitShadowRoot;
}
},
canTarget: function(shadow) {
return shadow && Boolean(shadow.elementFromPoint);
},
targetingShadow: function(inEl) {
var s = this.shadow(inEl);
if (this.canTarget(s)) {
return s;
}
},
olderShadow: function(shadow) {
var os = shadow.olderShadowRoot;
if (!os) {
var se = shadow.querySelector('shadow');
if (se) {
os = se.olderShadowRoot;
}
}
return os;
},
allShadows: function(element) {
var shadows = [], s = this.shadow(element);
while(s) {
shadows.push(s);
s = this.olderShadow(s);
}
return shadows;
},
searchRoot: function(inRoot, x, y) {
var t, st, sr, os;
if (inRoot) {
t = inRoot.elementFromPoint(x, y);
if (t) {
// found element, check if it has a ShadowRoot
sr = this.targetingShadow(t);
} else if (inRoot !== document) {
// check for sibling roots
sr = this.olderShadow(inRoot);
}
// search other roots, fall back to light dom element
return this.searchRoot(sr, x, y) || t;
}
},
owner: function(element) {
if (!element) {
return document;
}
var s = element;
// walk up until you hit the shadow root or document
while (s.parentNode) {
s = s.parentNode;
}
// the owner element is expected to be a Document or ShadowRoot
if (s.nodeType != Node.DOCUMENT_NODE && s.nodeType != Node.DOCUMENT_FRAGMENT_NODE) {
s = document;
}
return s;
},
findTarget: function(inEvent) {
if (HAS_FULL_PATH && inEvent.path && inEvent.path.length) {
return inEvent.path[0];
}
var x = inEvent.clientX, y = inEvent.clientY;
// if the listener is in the shadow root, it is much faster to start there
var s = this.owner(inEvent.target);
// if x, y is not in this root, fall back to document search
if (!s.elementFromPoint(x, y)) {
s = document;
}
return this.searchRoot(s, x, y);
},
findTouchAction: function(inEvent) {
var n;
if (HAS_FULL_PATH && inEvent.path && inEvent.path.length) {
var path = inEvent.path;
for (var i = 0; i < path.length; i++) {
n = path[i];
if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) {
return n.getAttribute('touch-action');
}
}
} else {
n = inEvent.target;
while(n) {
if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) {
return n.getAttribute('touch-action');
}
n = n.parentNode || n.host;
}
}
// auto is default
return "auto";
},
LCA: function(a, b) {
if (a === b) {
return a;
}
if (a && !b) {
return a;
}
if (b && !a) {
return b;
}
if (!b && !a) {
return document;
}
// fast case, a is a direct descendant of b or vice versa
if (a.contains && a.contains(b)) {
return a;
}
if (b.contains && b.contains(a)) {
return b;
}
var adepth = this.depth(a);
var bdepth = this.depth(b);
var d = adepth - bdepth;
if (d >= 0) {
a = this.walk(a, d);
} else {
b = this.walk(b, -d);
}
while (a && b && a !== b) {
a = a.parentNode || a.host;
b = b.parentNode || b.host;
}
return a;
},
walk: function(n, u) {
for (var i = 0; n && (i < u); i++) {
n = n.parentNode || n.host;
}
return n;
},
depth: function(n) {
var d = 0;
while(n) {
d++;
n = n.parentNode || n.host;
}
return d;
},
deepContains: function(a, b) {
var common = this.LCA(a, b);
// if a is the common ancestor, it must "deeply" contain b
return common === a;
},
insideNode: function(node, x, y) {
var rect = node.getBoundingClientRect();
return (rect.left <= x) && (x <= rect.right) && (rect.top <= y) && (y <= rect.bottom);
},
path: function(event) {
var p;
if (HAS_FULL_PATH && event.path && event.path.length) {
p = event.path;
} else {
p = [];
var n = this.findTarget(event);
while (n) {
p.push(n);
n = n.parentNode || n.host;
}
}
return p;
}
};
scope.targetFinding = target;
/**
* Given an event, finds the "deepest" node that could have been the original target before ShadowDOM retargetting
*
* @param {Event} Event An event object with clientX and clientY properties
* @return {Element} The probable event origninator
*/
scope.findTarget = target.findTarget.bind(target);
/**
* Determines if the "container" node deeply contains the "containee" node, including situations where the "containee" is contained by one or more ShadowDOM
* roots.
*
* @param {Node} container
* @param {Node} containee
* @return {Boolean}
*/
scope.deepContains = target.deepContains.bind(target);
/**
* Determines if the x/y position is inside the given node.
*
* Example:
*
* function upHandler(event) {
* var innode = PolymerGestures.insideNode(event.target, event.clientX, event.clientY);
* if (innode) {
* // wait for tap?
* } else {
* // tap will never happen
* }
* }
*
* @param {Node} node
* @param {Number} x Screen X position
* @param {Number} y screen Y position
* @return {Boolean}
*/
scope.insideNode = target.insideNode;
})(window.PolymerGestures);
(function() {
function shadowSelector(v) {
return 'html /deep/ ' + selector(v);
}
function selector(v) {
return '[touch-action="' + v + '"]';
}
function rule(v) {
return '{ -ms-touch-action: ' + v + '; touch-action: ' + v + ';}';
}
var attrib2css = [
'none',
'auto',
'pan-x',
'pan-y',
{
rule: 'pan-x pan-y',
selectors: [
'pan-x pan-y',
'pan-y pan-x'
]
},
'manipulation'
];
var styles = '';
// only install stylesheet if the browser has touch action support
var hasTouchAction = typeof document.head.style.touchAction === 'string';
// only add shadow selectors if shadowdom is supported
var hasShadowRoot = !window.ShadowDOMPolyfill && document.head.createShadowRoot;
if (hasTouchAction) {
attrib2css.forEach(function(r) {
if (String(r) === r) {
styles += selector(r) + rule(r) + '\n';
if (hasShadowRoot) {
styles += shadowSelector(r) + rule(r) + '\n';
}
} else {
styles += r.selectors.map(selector) + rule(r.rule) + '\n';
if (hasShadowRoot) {
styles += r.selectors.map(shadowSelector) + rule(r.rule) + '\n';
}
}
});
var el = document.createElement('style');
el.textContent = styles;
document.head.appendChild(el);
}
})();
/**
* This is the constructor for new PointerEvents.
*
* New Pointer Events must be given a type, and an optional dictionary of
* initialization properties.
*
* Due to certain platform requirements, events returned from the constructor
* identify as MouseEvents.
*
* @constructor
* @param {String} inType The type of the event to create.
* @param {Object} [inDict] An optional dictionary of initial event properties.
* @return {Event} A new PointerEvent of type `inType` and initialized with properties from `inDict`.
*/
(function(scope) {
var MOUSE_PROPS = [
'bubbles',
'cancelable',
'view',
'detail',
'screenX',
'screenY',
'clientX',
'clientY',
'ctrlKey',
'altKey',
'shiftKey',
'metaKey',
'button',
'relatedTarget',
'pageX',
'pageY'
];
var MOUSE_DEFAULTS = [
false,
false,
null,
null,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null,
0,
0
];
var NOP_FACTORY = function(){ return function(){}; };
var eventFactory = {
// TODO(dfreedm): this is overridden by tap recognizer, needs review
preventTap: NOP_FACTORY,
makeBaseEvent: function(inType, inDict) {
var e = document.createEvent('Event');
e.initEvent(inType, inDict.bubbles || false, inDict.cancelable || false);
e.preventTap = eventFactory.preventTap(e);
return e;
},
makeGestureEvent: function(inType, inDict) {
inDict = inDict || Object.create(null);
var e = this.makeBaseEvent(inType, inDict);
for (var i = 0, keys = Object.keys(inDict), k; i < keys.length; i++) {
k = keys[i];
e[k] = inDict[k];
}
return e;
},
makePointerEvent: function(inType, inDict) {
inDict = inDict || Object.create(null);
var e = this.makeBaseEvent(inType, inDict);
// define inherited MouseEvent properties
for(var i = 0, p; i < MOUSE_PROPS.length; i++) {
p = MOUSE_PROPS[i];
e[p] = inDict[p] || MOUSE_DEFAULTS[i];
}
e.buttons = inDict.buttons || 0;
// Spec requires that pointers without pressure specified use 0.5 for down
// state and 0 for up state.
var pressure = 0;
if (inDict.pressure) {
pressure = inDict.pressure;
} else {
pressure = e.buttons ? 0.5 : 0;
}
// add x/y properties aliased to clientX/Y
e.x = e.clientX;
e.y = e.clientY;
// define the properties of the PointerEvent interface
e.pointerId = inDict.pointerId || 0;
e.width = inDict.width || 0;
e.height = inDict.height || 0;
e.pressure = pressure;
e.tiltX = inDict.tiltX || 0;
e.tiltY = inDict.tiltY || 0;
e.pointerType = inDict.pointerType || '';
e.hwTimestamp = inDict.hwTimestamp || 0;
e.isPrimary = inDict.isPrimary || false;
e._source = inDict._source || '';
return e;
}
};
scope.eventFactory = eventFactory;
})(window.PolymerGestures);
/**
* This module implements an map of pointer states
*/
(function(scope) {
var USE_MAP = window.Map && window.Map.prototype.forEach;
var POINTERS_FN = function(){ return this.size; };
function PointerMap() {
if (USE_MAP) {
var m = new Map();
m.pointers = POINTERS_FN;
return m;
} else {
this.keys = [];
this.values = [];
}
}
PointerMap.prototype = {
set: function(inId, inEvent) {
var i = this.keys.indexOf(inId);
if (i > -1) {
this.values[i] = inEvent;
} else {
this.keys.push(inId);
this.values.push(inEvent);
}
},
has: function(inId) {
return this.keys.indexOf(inId) > -1;
},
'delete': function(inId) {
var i = this.keys.indexOf(inId);
if (i > -1) {
this.keys.splice(i, 1);
this.values.splice(i, 1);
}
},
get: function(inId) {
var i = this.keys.indexOf(inId);
return this.values[i];
},
clear: function() {
this.keys.length = 0;
this.values.length = 0;
},
// return value, key, map
forEach: function(callback, thisArg) {
this.values.forEach(function(v, i) {
callback.call(thisArg, v, this.keys[i], this);
}, this);
},
pointers: function() {
return this.keys.length;
}
};
scope.PointerMap = PointerMap;
})(window.PolymerGestures);
(function(scope) {
var CLONE_PROPS = [
// MouseEvent
'bubbles',
'cancelable',
'view',
'detail',
'screenX',
'screenY',
'clientX',
'clientY',
'ctrlKey',
'altKey',
'shiftKey',
'metaKey',
'button',
'relatedTarget',
// DOM Level 3
'buttons',
// PointerEvent
'pointerId',
'width',
'height',
'pressure',
'tiltX',
'tiltY',
'pointerType',
'hwTimestamp',
'isPrimary',
// event instance
'type',
'target',
'currentTarget',
'which',
'pageX',
'pageY',
'timeStamp',
// gesture addons
'preventTap',
'tapPrevented',
'_source'
];
var CLONE_DEFAULTS = [
// MouseEvent
false,
false,
null,
null,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null,
// DOM Level 3
0,
// PointerEvent
0,
0,
0,
0,
0,
0,
'',
0,
false,
// event instance
'',
null,
null,
0,
0,
0,
0,
function(){},
false
];
var HAS_SVG_INSTANCE = (typeof SVGElementInstance !== 'undefined');
var eventFactory = scope.eventFactory;
// set of recognizers to run for the currently handled event
var currentGestures;
/**
* This module is for normalizing events. Mouse and Touch events will be
* collected here, and fire PointerEvents that have the same semantics, no
* matter the source.
* Events fired:
* - pointerdown: a pointing is added
* - pointerup: a pointer is removed
* - pointermove: a pointer is moved
* - pointerover: a pointer crosses into an element
* - pointerout: a pointer leaves an element
* - pointercancel: a pointer will no longer generate events
*/
var dispatcher = {
IS_IOS: false,
pointermap: new scope.PointerMap(),
requiredGestures: new scope.PointerMap(),
eventMap: Object.create(null),
// Scope objects for native events.
// This exists for ease of testing.
eventSources: Object.create(null),
eventSourceList: [],
gestures: [],
// map gesture event -> {listeners: int, index: gestures[int]}
dependencyMap: {
// make sure down and up are in the map to trigger "register"
down: {listeners: 0, index: -1},
up: {listeners: 0, index: -1}
},
gestureQueue: [],
/**
* Add a new event source that will generate pointer events.
*
* `inSource` must contain an array of event names named `events`, and
* functions with the names specified in the `events` array.
* @param {string} name A name for the event source
* @param {Object} source A new source of platform events.
*/
registerSource: function(name, source) {
var s = source;
var newEvents = s.events;
if (newEvents) {
newEvents.forEach(function(e) {
if (s[e]) {
this.eventMap[e] = s[e].bind(s);
}
}, this);
this.eventSources[name] = s;
this.eventSourceList.push(s);
}
},
registerGesture: function(name, source) {
var obj = Object.create(null);
obj.listeners = 0;
obj.index = this.gestures.length;
for (var i = 0, g; i < source.exposes.length; i++) {
g = source.exposes[i].toLowerCase();
this.dependencyMap[g] = obj;
}
this.gestures.push(source);
},
register: function(element, initial) {
var l = this.eventSourceList.length;
for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) {
// call eventsource register
es.register.call(es, element, initial);
}
},
unregister: function(element) {
var l = this.eventSourceList.length;
for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) {
// call eventsource register
es.unregister.call(es, element);
}
},
// EVENTS
down: function(inEvent) {
this.requiredGestures.set(inEvent.pointerId, currentGestures);
this.fireEvent('down', inEvent);
},
move: function(inEvent) {
// pipe move events into gesture queue directly
inEvent.type = 'move';
this.fillGestureQueue(inEvent);
},
up: function(inEvent) {
this.fireEvent('up', inEvent);
this.requiredGestures.delete(inEvent.pointerId);
},
cancel: function(inEvent) {
inEvent.tapPrevented = true;
this.fireEvent('up', inEvent);
this.requiredGestures.delete(inEvent.pointerId);
},
addGestureDependency: function(node, currentGestures) {
var gesturesWanted = node._pgEvents;
if (gesturesWanted && currentGestures) {
var gk = Object.keys(gesturesWanted);
for (var i = 0, r, ri, g; i < gk.length; i++) {
// gesture
g = gk[i];
if (gesturesWanted[g] > 0) {
// lookup gesture recognizer
r = this.dependencyMap[g];
// recognizer index
ri = r ? r.index : -1;
currentGestures[ri] = true;
}
}
}
},
// LISTENER LOGIC
eventHandler: function(inEvent) {
// This is used to prevent multiple dispatch of events from
// platform events. This can happen when two elements in different scopes
// are set up to create pointer events, which is relevant to Shadow DOM.
var type = inEvent.type;
// only generate the list of desired events on "down"
if (type === 'touchstart' || type === 'mousedown' || type === 'pointerdown' || type === 'MSPointerDown') {
if (!inEvent._handledByPG) {
currentGestures = {};
}
// in IOS mode, there is only a listener on the document, so this is not re-entrant
if (this.IS_IOS) {
var ev = inEvent;
if (type === 'touchstart') {
var ct = inEvent.changedTouches[0];
// set up a fake event to give to the path builder
ev = {target: inEvent.target, clientX: ct.clientX, clientY: ct.clientY, path: inEvent.path};
}
// use event path if available, otherwise build a path from target finding
var nodes = inEvent.path || scope.targetFinding.path(ev);
for (var i = 0, n; i < nodes.length; i++) {
n = nodes[i];
this.addGestureDependency(n, currentGestures);
}
} else {
this.addGestureDependency(inEvent.currentTarget, currentGestures);
}
}
if (inEvent._handledByPG) {
return;
}
var fn = this.eventMap && this.eventMap[type];
if (fn) {
fn(inEvent);
}
inEvent._handledByPG = true;
},
// set up event listeners
listen: function(target, events) {
for (var i = 0, l = events.length, e; (i < l) && (e = events[i]); i++) {
this.addEvent(target, e);
}
},
// remove event listeners
unlisten: function(target, events) {
for (var i = 0, l = events.length, e; (i < l) && (e = events[i]); i++) {
this.removeEvent(target, e);
}
},
addEvent: function(target, eventName) {
target.addEventListener(eventName, this.boundHandler);
},
removeEvent: function(target, eventName) {
target.removeEventListener(eventName, this.boundHandler);
},
// EVENT CREATION AND TRACKING
/**
* Creates a new Event of type `inType`, based on the information in
* `inEvent`.
*
* @param {string} inType A string representing the type of event to create
* @param {Event} inEvent A platform event with a target
* @return {Event} A PointerEvent of type `inType`
*/
makeEvent: function(inType, inEvent) {
var e = eventFactory.makePointerEvent(inType, inEvent);
e.preventDefault = inEvent.preventDefault;
e.tapPrevented = inEvent.tapPrevented;
e._target = e._target || inEvent.target;
return e;
},
// make and dispatch an event in one call
fireEvent: function(inType, inEvent) {
var e = this.makeEvent(inType, inEvent);
return this.dispatchEvent(e);
},
/**
* Returns a snapshot of inEvent, with writable properties.
*
* @param {Event} inEvent An event that contains properties to copy.
* @return {Object} An object containing shallow copies of `inEvent`'s
* properties.
*/
cloneEvent: function(inEvent) {
var eventCopy = Object.create(null), p;
for (var i = 0; i < CLONE_PROPS.length; i++) {
p = CLONE_PROPS[i];
eventCopy[p] = inEvent[p] || CLONE_DEFAULTS[i];
// Work around SVGInstanceElement shadow tree
// Return the element that is represented by the instance for Safari, Chrome, IE.
// This is the behavior implemented by Firefox.
if (p === 'target' || p === 'relatedTarget') {
if (HAS_SVG_INSTANCE && eventCopy[p] instanceof SVGElementInstance) {
eventCopy[p] = eventCopy[p].correspondingUseElement;
}
}
}
// keep the semantics of preventDefault
eventCopy.preventDefault = function() {
inEvent.preventDefault();
};
return eventCopy;
},
/**
* Dispatches the event to its target.
*
* @param {Event} inEvent The event to be dispatched.
* @return {Boolean} True if an event handler returns true, false otherwise.
*/
dispatchEvent: function(inEvent) {
var t = inEvent._target;
if (t) {
t.dispatchEvent(inEvent);
// clone the event for the gesture system to process
// clone after dispatch to pick up gesture prevention code
var clone = this.cloneEvent(inEvent);
clone.target = t;
this.fillGestureQueue(clone);
}
},
gestureTrigger: function() {
// process the gesture queue
for (var i = 0, e, rg; i < this.gestureQueue.length; i++) {
e = this.gestureQueue[i];
rg = e._requiredGestures;
if (rg) {
for (var j = 0, g, fn; j < this.gestures.length; j++) {
// only run recognizer if an element in the source event's path is listening for those gestures
if (rg[j]) {
g = this.gestures[j];
fn = g[e.type];
if (fn) {
fn.call(g, e);
}
}
}
}
}
this.gestureQueue.length = 0;
},
fillGestureQueue: function(ev) {
// only trigger the gesture queue once
if (!this.gestureQueue.length) {
requestAnimationFrame(this.boundGestureTrigger);
}
ev._requiredGestures = this.requiredGestures.get(ev.pointerId);
this.gestureQueue.push(ev);
}
};
dispatcher.boundHandler = dispatcher.eventHandler.bind(dispatcher);
dispatcher.boundGestureTrigger = dispatcher.gestureTrigger.bind(dispatcher);
scope.dispatcher = dispatcher;
/**
* Listen for `gesture` on `node` with the `handler` function
*
* If `handler` is the first listener for `gesture`, the underlying gesture recognizer is then enabled.
*
* @param {Element} node
* @param {string} gesture
* @return Boolean `gesture` is a valid gesture
*/
scope.activateGesture = function(node, gesture) {
var g = gesture.toLowerCase();
var dep = dispatcher.dependencyMap[g];
if (dep) {
var recognizer = dispatcher.gestures[dep.index];
if (!node._pgListeners) {
dispatcher.register(node);
node._pgListeners = 0;
}
// TODO(dfreedm): re-evaluate bookkeeping to avoid using attributes
if (recognizer) {
var touchAction = recognizer.defaultActions && recognizer.defaultActions[g];
var actionNode;
switch(node.nodeType) {
case Node.ELEMENT_NODE:
actionNode = node;
break;
case Node.DOCUMENT_FRAGMENT_NODE:
actionNode = node.host;
break;
default:
actionNode = null;
break;
}
if (touchAction && actionNode && !actionNode.hasAttribute('touch-action')) {
actionNode.setAttribute('touch-action', touchAction);
}
}
if (!node._pgEvents) {
node._pgEvents = {};
}
node._pgEvents[g] = (node._pgEvents[g] || 0) + 1;
node._pgListeners++;
}
return Boolean(dep);
};
/**
*
* Listen for `gesture` from `node` with `handler` function.
*
* @param {Element} node
* @param {string} gesture
* @param {Function} handler
* @param {Boolean} capture
*/
scope.addEventListener = function(node, gesture, handler, capture) {
if (handler) {
scope.activateGesture(node, gesture);
node.addEventListener(gesture, handler, capture);
}
};
/**
* Tears down the gesture configuration for `node`
*
* If `handler` is the last listener for `gesture`, the underlying gesture recognizer is disabled.
*
* @param {Element} node
* @param {string} gesture
* @return Boolean `gesture` is a valid gesture
*/
scope.deactivateGesture = function(node, gesture) {
var g = gesture.toLowerCase();
var dep = dispatcher.dependencyMap[g];
if (dep) {
if (node._pgListeners > 0) {
node._pgListeners--;
}
if (node._pgListeners === 0) {
dispatcher.unregister(node);
}
if (node._pgEvents) {
if (node._pgEvents[g] > 0) {
node._pgEvents[g]--;
} else {
node._pgEvents[g] = 0;
}
}
}
return Boolean(dep);
};
/**
* Stop listening for `gesture` from `node` with `handler` function.
*
* @param {Element} node
* @param {string} gesture
* @param {Function} handler
* @param {Boolean} capture
*/
scope.removeEventListener = function(node, gesture, handler, capture) {
if (handler) {
scope.deactivateGesture(node, gesture);
node.removeEventListener(gesture, handler, capture);
}
};
})(window.PolymerGestures);
(function(scope) {
var dispatcher = scope.dispatcher;
var pointermap = dispatcher.pointermap;
// radius around touchend that swallows mouse events
var DEDUP_DIST = 25;
var WHICH_TO_BUTTONS = [0, 1, 4, 2];
var CURRENT_BUTTONS = 0;
var FIREFOX_LINUX = /Linux.*Firefox\//i;
var HAS_BUTTONS = (function() {
// firefox on linux returns spec-incorrect values for mouseup.buttons
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent.buttons#See_also
// https://codereview.chromium.org/727593003/#msg16
if (FIREFOX_LINUX.test(navigator.userAgent)) {
return false;
}
try {
return new MouseEvent('test', {buttons: 1}).buttons === 1;
} catch (e) {
return false;
}
})();
// handler block for native mouse events
var mouseEvents = {
POINTER_ID: 1,
POINTER_TYPE: 'mouse',
events: [
'mousedown',
'mousemove',
'mouseup'
],
exposes: [
'down',
'up',
'move'
],
register: function(target) {
dispatcher.listen(target, this.events);
},
unregister: function(target) {
if (target === document) {
return;
}
dispatcher.unlisten(target, this.events);
},
lastTouches: [],
// collide with the global mouse listener
isEventSimulatedFromTouch: function(inEvent) {
var lts = this.lastTouches;
var x = inEvent.clientX, y = inEvent.clientY;
for (var i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) {
// simulated mouse events will be swallowed near a primary touchend
var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y);
if (dx <= DEDUP_DIST && dy <= DEDUP_DIST) {
return true;
}
}
},
prepareEvent: function(inEvent) {
var e = dispatcher.cloneEvent(inEvent);
e.pointerId = this.POINTER_ID;
e.isPrimary = true;
e.pointerType = this.POINTER_TYPE;
e._source = 'mouse';
if (!HAS_BUTTONS) {
var type = inEvent.type;
var bit = WHICH_TO_BUTTONS[inEvent.which] || 0;
if (type === 'mousedown') {
CURRENT_BUTTONS |= bit;
} else if (type === 'mouseup') {
CURRENT_BUTTONS &= ~bit;
}
e.buttons = CURRENT_BUTTONS;
}
return e;
},
mousedown: function(inEvent) {
if (!this.isEventSimulatedFromTouch(inEvent)) {
var p = pointermap.has(this.POINTER_ID);
var e = this.prepareEvent(inEvent);
e.target = scope.findTarget(inEvent);
pointermap.set(this.POINTER_ID, e.target);
dispatcher.down(e);
}
},
mousemove: function(inEvent) {
if (!this.isEventSimulatedFromTouch(inEvent)) {
var target = pointermap.get(this.POINTER_ID);
if (target) {
var e = this.prepareEvent(inEvent);
e.target = target;
// handle case where we missed a mouseup
if ((HAS_BUTTONS ? e.buttons : e.which) === 0) {
if (!HAS_BUTTONS) {
CURRENT_BUTTONS = e.buttons = 0;
}
dispatcher.cancel(e);
this.cleanupMouse(e.buttons);
} else {
dispatcher.move(e);
}
}
}
},
mouseup: function(inEvent) {
if (!this.isEventSimulatedFromTouch(inEvent)) {
var e = this.prepareEvent(inEvent);
e.relatedTarget = scope.findTarget(inEvent);
e.target = pointermap.get(this.POINTER_ID);
dispatcher.up(e);
this.cleanupMouse(e.buttons);
}
},
cleanupMouse: function(buttons) {
if (buttons === 0) {
pointermap.delete(this.POINTER_ID);
}
}
};
scope.mouseEvents = mouseEvents;
})(window.PolymerGestures);
(function(scope) {
var dispatcher = scope.dispatcher;
var allShadows = scope.targetFinding.allShadows.bind(scope.targetFinding);
var pointermap = dispatcher.pointermap;
var touchMap = Array.prototype.map.call.bind(Array.prototype.map);
// This should be long enough to ignore compat mouse events made by touch
var DEDUP_TIMEOUT = 2500;
var DEDUP_DIST = 25;
var CLICK_COUNT_TIMEOUT = 200;
var HYSTERESIS = 20;
var ATTRIB = 'touch-action';
// TODO(dfreedm): disable until http://crbug.com/399765 is resolved
// var HAS_TOUCH_ACTION = ATTRIB in document.head.style;
var HAS_TOUCH_ACTION = false;
// handler block for native touch events
var touchEvents = {
IS_IOS: false,
events: [
'touchstart',
'touchmove',
'touchend',
'touchcancel'
],
exposes: [
'down',
'up',
'move'
],
register: function(target, initial) {
if (this.IS_IOS ? initial : !initial) {
dispatcher.listen(target, this.events);
}
},
unregister: function(target) {
if (!this.IS_IOS) {
dispatcher.unlisten(target, this.events);
}
},
scrollTypes: {
EMITTER: 'none',
XSCROLLER: 'pan-x',
YSCROLLER: 'pan-y',
},
touchActionToScrollType: function(touchAction) {
var t = touchAction;
var st = this.scrollTypes;
if (t === st.EMITTER) {
return 'none';
} else if (t === st.XSCROLLER) {
return 'X';
} else if (t === st.YSCROLLER) {
return 'Y';
} else {
return 'XY';
}
},
POINTER_TYPE: 'touch',
firstTouch: null,
isPrimaryTouch: function(inTouch) {
return this.firstTouch === inTouch.identifier;
},
setPrimaryTouch: function(inTouch) {
// set primary touch if there no pointers, or the only pointer is the mouse
if (pointermap.pointers() === 0 || (pointermap.pointers() === 1 && pointermap.has(1))) {
this.firstTouch = inTouch.identifier;
this.firstXY = {X: inTouch.clientX, Y: inTouch.clientY};
this.firstTarget = inTouch.target;
this.scrolling = null;
this.cancelResetClickCount();
}
},
removePrimaryPointer: function(inPointer) {
if (inPointer.isPrimary) {
this.firstTouch = null;
this.firstXY = null;
this.resetClickCount();
}
},
clickCount: 0,
resetId: null,
resetClickCount: function() {
var fn = function() {
this.clickCount = 0;
this.resetId = null;
}.bind(this);
this.resetId = setTimeout(fn, CLICK_COUNT_TIMEOUT);
},
cancelResetClickCount: function() {
if (this.resetId) {
clearTimeout(this.resetId);
}
},
typeToButtons: function(type) {
var ret = 0;
if (type === 'touchstart' || type === 'touchmove') {
ret = 1;
}
return ret;
},
findTarget: function(touch, id) {
if (this.currentTouchEvent.type === 'touchstart') {
if (this.isPrimaryTouch(touch)) {
var fastPath = {
clientX: touch.clientX,
clientY: touch.clientY,
path: this.currentTouchEvent.path,
target: this.currentTouchEvent.target
};
return scope.findTarget(fastPath);
} else {
return scope.findTarget(touch);
}
}
// reuse target we found in touchstart
return pointermap.get(id);
},
touchToPointer: function(inTouch) {
var cte = this.currentTouchEvent;
var e = dispatcher.cloneEvent(inTouch);
// Spec specifies that pointerId 1 is reserved for Mouse.
// Touch identifiers can start at 0.
// Add 2 to the touch identifier for compatibility.
var id = e.pointerId = inTouch.identifier + 2;
e.target = this.findTarget(inTouch, id);
e.bubbles = true;
e.cancelable = true;
e.detail = this.clickCount;
e.buttons = this.typeToButtons(cte.type);
e.width = inTouch.webkitRadiusX || inTouch.radiusX || 0;
e.height = inTouch.webkitRadiusY || inTouch.radiusY || 0;
e.pressure = inTouch.webkitForce || inTouch.force || 0.5;
e.isPrimary = this.isPrimaryTouch(inTouch);
e.pointerType = this.POINTER_TYPE;
e._source = 'touch';
// forward touch preventDefaults
var self = this;
e.preventDefault = function() {
self.scrolling = false;
self.firstXY = null;
cte.preventDefault();
};
return e;
},
processTouches: function(inEvent, inFunction) {
var tl = inEvent.changedTouches;
this.currentTouchEvent = inEvent;
for (var i = 0, t, p; i < tl.length; i++) {
t = tl[i];
p = this.touchToPointer(t);
if (inEvent.type === 'touchstart') {
pointermap.set(p.pointerId, p.target);
}
if (pointermap.has(p.pointerId)) {
inFunction.call(this, p);
}
if (inEvent.type === 'touchend' || inEvent._cancel) {
this.cleanUpPointer(p);
}
}
},
// For single axis scrollers, determines whether the element should emit
// pointer events or behave as a scroller
shouldScroll: function(inEvent) {
if (this.firstXY) {
var ret;
var touchAction = scope.targetFinding.findTouchAction(inEvent);
var scrollAxis = this.touchActionToScrollType(touchAction);
if (scrollAxis === 'none') {
// this element is a touch-action: none, should never scroll
ret = false;
} else if (scrollAxis === 'XY') {
// this element should always scroll
ret = true;
} else {
var t = inEvent.changedTouches[0];
// check the intended scroll axis, and other axis
var a = scrollAxis;
var oa = scrollAxis === 'Y' ? 'X' : 'Y';
var da = Math.abs(t['client' + a] - this.firstXY[a]);
var doa = Math.abs(t['client' + oa] - this.firstXY[oa]);
// if delta in the scroll axis > delta other axis, scroll instead of
// making events
ret = da >= doa;
}
return ret;
}
},
findTouch: function(inTL, inId) {
for (var i = 0, l = inTL.length, t; i < l && (t = inTL[i]); i++) {
if (t.identifier === inId) {
return true;
}
}
},
// In some instances, a touchstart can happen without a touchend. This
// leaves the pointermap in a broken state.
// Therefore, on every touchstart, we remove the touches that did not fire a
// touchend event.
// To keep state globally consistent, we fire a
// pointercancel for this "abandoned" touch
vacuumTouches: function(inEvent) {
var tl = inEvent.touches;
// pointermap.pointers() should be < tl.length here, as the touchstart has not
// been processed yet.
if (pointermap.pointers() >= tl.length) {
var d = [];
pointermap.forEach(function(value, key) {
// Never remove pointerId == 1, which is mouse.
// Touch identifiers are 2 smaller than their pointerId, which is the
// index in pointermap.
if (key !== 1 && !this.findTouch(tl, key - 2)) {
var p = value;
d.push(p);
}
}, this);
d.forEach(function(p) {
this.cancel(p);
pointermap.delete(p.pointerId);
}, this);
}
},
touchstart: function(inEvent) {
this.vacuumTouches(inEvent);
this.setPrimaryTouch(inEvent.changedTouches[0]);
this.dedupSynthMouse(inEvent);
if (!this.scrolling) {
this.clickCount++;
this.processTouches(inEvent, this.down);
}
},
down: function(inPointer) {
dispatcher.down(inPointer);
},
touchmove: function(inEvent) {
if (HAS_TOUCH_ACTION) {
// touchevent.cancelable == false is sent when the page is scrolling under native Touch Action in Chrome 36
// https://groups.google.com/a/chromium.org/d/msg/input-dev/wHnyukcYBcA/b9kmtwM1jJQJ
if (inEvent.cancelable) {
this.processTouches(inEvent, this.move);
}
} else {
if (!this.scrolling) {
if (this.scrolling === null && this.shouldScroll(inEvent)) {
this.scrolling = true;
} else {
this.scrolling = false;
inEvent.preventDefault();
this.processTouches(inEvent, this.move);
}
} else if (this.firstXY) {
var t = inEvent.changedTouches[0];
var dx = t.clientX - this.firstXY.X;
var dy = t.clientY - this.firstXY.Y;
var dd = Math.sqrt(dx * dx + dy * dy);
if (dd >= HYSTERESIS) {
this.touchcancel(inEvent);
this.scrolling = true;
this.firstXY = null;
}
}
}
},
move: function(inPointer) {
dispatcher.move(inPointer);
},
touchend: function(inEvent) {
this.dedupSynthMouse(inEvent);
this.processTouches(inEvent, this.up);
},
up: function(inPointer) {
inPointer.relatedTarget = scope.findTarget(inPointer);
dispatcher.up(inPointer);
},
cancel: function(inPointer) {
dispatcher.cancel(inPointer);
},
touchcancel: function(inEvent) {
inEvent._cancel = true;
this.processTouches(inEvent, this.cancel);
},
cleanUpPointer: function(inPointer) {
pointermap['delete'](inPointer.pointerId);
this.removePrimaryPointer(inPointer);
},
// prevent synth mouse events from creating pointer events
dedupSynthMouse: function(inEvent) {
var lts = scope.mouseEvents.lastTouches;
var t = inEvent.changedTouches[0];
// only the primary finger will synth mouse events
if (this.isPrimaryTouch(t)) {
// remember x/y of last touch
var lt = {x: t.clientX, y: t.clientY};
lts.push(lt);
var fn = (function(lts, lt){
var i = lts.indexOf(lt);
if (i > -1) {
lts.splice(i, 1);
}
}).bind(null, lts, lt);
setTimeout(fn, DEDUP_TIMEOUT);
}
}
};
// prevent "ghost clicks" that come from elements that were removed in a touch handler
var STOP_PROP_FN = Event.prototype.stopImmediatePropagation || Event.prototype.stopPropagation;
document.addEventListener('click', function(ev) {
var x = ev.clientX, y = ev.clientY;
// check if a click is within DEDUP_DIST px radius of the touchstart
var closeTo = function(touch) {
var dx = Math.abs(x - touch.x), dy = Math.abs(y - touch.y);
return (dx <= DEDUP_DIST && dy <= DEDUP_DIST);
};
// if click coordinates are close to touch coordinates, assume the click came from a touch
var wasTouched = scope.mouseEvents.lastTouches.some(closeTo);
// if the click came from touch, and the touchstart target is not in the path of the click event,
// then the touchstart target was probably removed, and the click should be "busted"
var path = scope.targetFinding.path(ev);
if (wasTouched) {
for (var i = 0; i < path.length; i++) {
if (path[i] === touchEvents.firstTarget) {
return;
}
}
ev.preventDefault();
STOP_PROP_FN.call(ev);
}
}, true);
scope.touchEvents = touchEvents;
})(window.PolymerGestures);
(function(scope) {
var dispatcher = scope.dispatcher;
var pointermap = dispatcher.pointermap;
var HAS_BITMAP_TYPE = window.MSPointerEvent && typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE === 'number';
var msEvents = {
events: [
'MSPointerDown',
'MSPointerMove',
'MSPointerUp',
'MSPointerCancel',
],
register: function(target) {
dispatcher.listen(target, this.events);
},
unregister: function(target) {
if (target === document) {
return;
}
dispatcher.unlisten(target, this.events);
},
POINTER_TYPES: [
'',
'unavailable',
'touch',
'pen',
'mouse'
],
prepareEvent: function(inEvent) {
var e = inEvent;
e = dispatcher.cloneEvent(inEvent);
if (HAS_BITMAP_TYPE) {
e.pointerType = this.POINTER_TYPES[inEvent.pointerType];
}
e._source = 'ms';
return e;
},
cleanup: function(id) {
pointermap['delete'](id);
},
MSPointerDown: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.target = scope.findTarget(inEvent);
pointermap.set(inEvent.pointerId, e.target);
dispatcher.down(e);
},
MSPointerMove: function(inEvent) {
var target = pointermap.get(inEvent.pointerId);
if (target) {
var e = this.prepareEvent(inEvent);
e.target = target;
dispatcher.move(e);
}
},
MSPointerUp: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.relatedTarget = scope.findTarget(inEvent);
e.target = pointermap.get(e.pointerId);
dispatcher.up(e);
this.cleanup(inEvent.pointerId);
},
MSPointerCancel: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.relatedTarget = scope.findTarget(inEvent);
e.target = pointermap.get(e.pointerId);
dispatcher.cancel(e);
this.cleanup(inEvent.pointerId);
}
};
scope.msEvents = msEvents;
})(window.PolymerGestures);
(function(scope) {
var dispatcher = scope.dispatcher;
var pointermap = dispatcher.pointermap;
var pointerEvents = {
events: [
'pointerdown',
'pointermove',
'pointerup',
'pointercancel'
],
prepareEvent: function(inEvent) {
var e = dispatcher.cloneEvent(inEvent);
e._source = 'pointer';
return e;
},
register: function(target) {
dispatcher.listen(target, this.events);
},
unregister: function(target) {
if (target === document) {
return;
}
dispatcher.unlisten(target, this.events);
},
cleanup: function(id) {
pointermap['delete'](id);
},
pointerdown: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.target = scope.findTarget(inEvent);
pointermap.set(e.pointerId, e.target);
dispatcher.down(e);
},
pointermove: function(inEvent) {
var target = pointermap.get(inEvent.pointerId);
if (target) {
var e = this.prepareEvent(inEvent);
e.target = target;
dispatcher.move(e);
}
},
pointerup: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.relatedTarget = scope.findTarget(inEvent);
e.target = pointermap.get(e.pointerId);
dispatcher.up(e);
this.cleanup(inEvent.pointerId);
},
pointercancel: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.relatedTarget = scope.findTarget(inEvent);
e.target = pointermap.get(e.pointerId);
dispatcher.cancel(e);
this.cleanup(inEvent.pointerId);
}
};
scope.pointerEvents = pointerEvents;
})(window.PolymerGestures);
/**
* This module contains the handlers for native platform events.
* From here, the dispatcher is called to create unified pointer events.
* Included are touch events (v1), mouse events, and MSPointerEvents.
*/
(function(scope) {
var dispatcher = scope.dispatcher;
var nav = window.navigator;
if (window.PointerEvent) {
dispatcher.registerSource('pointer', scope.pointerEvents);
} else if (nav.msPointerEnabled) {
dispatcher.registerSource('ms', scope.msEvents);
} else {
dispatcher.registerSource('mouse', scope.mouseEvents);
if (window.ontouchstart !== undefined) {
dispatcher.registerSource('touch', scope.touchEvents);
}
}
// Work around iOS bugs https://bugs.webkit.org/show_bug.cgi?id=135628 and https://bugs.webkit.org/show_bug.cgi?id=136506
var ua = navigator.userAgent;
var IS_IOS = ua.match(/iPad|iPhone|iPod/) && 'ontouchstart' in window;
dispatcher.IS_IOS = IS_IOS;
scope.touchEvents.IS_IOS = IS_IOS;
dispatcher.register(document, true);
})(window.PolymerGestures);
/**
* This event denotes the beginning of a series of tracking events.
*
* @module PointerGestures
* @submodule Events
* @class trackstart
*/
/**
* Pixels moved in the x direction since trackstart.
* @type Number
* @property dx
*/
/**
* Pixes moved in the y direction since trackstart.
* @type Number
* @property dy
*/
/**
* Pixels moved in the x direction since the last track.
* @type Number
* @property ddx
*/
/**
* Pixles moved in the y direction since the last track.
* @type Number
* @property ddy
*/
/**
* The clientX position of the track gesture.
* @type Number
* @property clientX
*/
/**
* The clientY position of the track gesture.
* @type Number
* @property clientY
*/
/**
* The pageX position of the track gesture.
* @type Number
* @property pageX
*/
/**
* The pageY position of the track gesture.
* @type Number
* @property pageY
*/
/**
* The screenX position of the track gesture.
* @type Number
* @property screenX
*/
/**
* The screenY position of the track gesture.
* @type Number
* @property screenY
*/
/**
* The last x axis direction of the pointer.
* @type Number
* @property xDirection
*/
/**
* The last y axis direction of the pointer.
* @type Number
* @property yDirection
*/
/**
* A shared object between all tracking events.
* @type Object
* @property trackInfo
*/
/**
* The element currently under the pointer.
* @type Element
* @property relatedTarget
*/
/**
* The type of pointer that make the track gesture.
* @type String
* @property pointerType
*/
/**
*
* This event fires for all pointer movement being tracked.
*
* @class track
* @extends trackstart
*/
/**
* This event fires when the pointer is no longer being tracked.
*
* @class trackend
* @extends trackstart
*/
(function(scope) {
var dispatcher = scope.dispatcher;
var eventFactory = scope.eventFactory;
var pointermap = new scope.PointerMap();
var track = {
events: [
'down',
'move',
'up',
],
exposes: [
'trackstart',
'track',
'trackx',
'tracky',
'trackend'
],
defaultActions: {
'track': 'none',
'trackx': 'pan-y',
'tracky': 'pan-x'
},
WIGGLE_THRESHOLD: 4,
clampDir: function(inDelta) {
return inDelta > 0 ? 1 : -1;
},
calcPositionDelta: function(inA, inB) {
var x = 0, y = 0;
if (inA && inB) {
x = inB.pageX - inA.pageX;
y = inB.pageY - inA.pageY;
}
return {x: x, y: y};
},
fireTrack: function(inType, inEvent, inTrackingData) {
var t = inTrackingData;
var d = this.calcPositionDelta(t.downEvent, inEvent);
var dd = this.calcPositionDelta(t.lastMoveEvent, inEvent);
if (dd.x) {
t.xDirection = this.clampDir(dd.x);
} else if (inType === 'trackx') {
return;
}
if (dd.y) {
t.yDirection = this.clampDir(dd.y);
} else if (inType === 'tracky') {
return;
}
var gestureProto = {
bubbles: true,
cancelable: true,
trackInfo: t.trackInfo,
relatedTarget: inEvent.relatedTarget,
pointerType: inEvent.pointerType,
pointerId: inEvent.pointerId,
_source: 'track'
};
if (inType !== 'tracky') {
gestureProto.x = inEvent.x;
gestureProto.dx = d.x;
gestureProto.ddx = dd.x;
gestureProto.clientX = inEvent.clientX;
gestureProto.pageX = inEvent.pageX;
gestureProto.screenX = inEvent.screenX;
gestureProto.xDirection = t.xDirection;
}
if (inType !== 'trackx') {
gestureProto.dy = d.y;
gestureProto.ddy = dd.y;
gestureProto.y = inEvent.y;
gestureProto.clientY = inEvent.clientY;
gestureProto.pageY = inEvent.pageY;
gestureProto.screenY = inEvent.screenY;
gestureProto.yDirection = t.yDirection;
}
var e = eventFactory.makeGestureEvent(inType, gestureProto);
t.downTarget.dispatchEvent(e);
},
down: function(inEvent) {
if (inEvent.isPrimary && (inEvent.pointerType === 'mouse' ? inEvent.buttons === 1 : true)) {
var p = {
downEvent: inEvent,
downTarget: inEvent.target,
trackInfo: {},
lastMoveEvent: null,
xDirection: 0,
yDirection: 0,
tracking: false
};
pointermap.set(inEvent.pointerId, p);
}
},
move: function(inEvent) {
var p = pointermap.get(inEvent.pointerId);
if (p) {
if (!p.tracking) {
var d = this.calcPositionDelta(p.downEvent, inEvent);
var move = d.x * d.x + d.y * d.y;
// start tracking only if finger moves more than WIGGLE_THRESHOLD
if (move > this.WIGGLE_THRESHOLD) {
p.tracking = true;
p.lastMoveEvent = p.downEvent;
this.fireTrack('trackstart', inEvent, p);
}
}
if (p.tracking) {
this.fireTrack('track', inEvent, p);
this.fireTrack('trackx', inEvent, p);
this.fireTrack('tracky', inEvent, p);
}
p.lastMoveEvent = inEvent;
}
},
up: function(inEvent) {
var p = pointermap.get(inEvent.pointerId);
if (p) {
if (p.tracking) {
this.fireTrack('trackend', inEvent, p);
}
pointermap.delete(inEvent.pointerId);
}
}
};
dispatcher.registerGesture('track', track);
})(window.PolymerGestures);
/**
* This event is fired when a pointer is held down for 200ms.
*
* @module PointerGestures
* @submodule Events
* @class hold
*/
/**
* Type of pointer that made the holding event.
* @type String
* @property pointerType
*/
/**
* Screen X axis position of the held pointer
* @type Number
* @property clientX
*/
/**
* Screen Y axis position of the held pointer
* @type Number
* @property clientY
*/
/**
* Type of pointer that made the holding event.
* @type String
* @property pointerType
*/
/**
* This event is fired every 200ms while a pointer is held down.
*
* @class holdpulse
* @extends hold
*/
/**
* Milliseconds pointer has been held down.
* @type Number
* @property holdTime
*/
/**
* This event is fired when a held pointer is released or moved.
*
* @class release
*/
(function(scope) {
var dispatcher = scope.dispatcher;
var eventFactory = scope.eventFactory;
var hold = {
// wait at least HOLD_DELAY ms between hold and pulse events
HOLD_DELAY: 200,
// pointer can move WIGGLE_THRESHOLD pixels before not counting as a hold
WIGGLE_THRESHOLD: 16,
events: [
'down',
'move',
'up',
],
exposes: [
'hold',
'holdpulse',
'release'
],
heldPointer: null,
holdJob: null,
pulse: function() {
var hold = Date.now() - this.heldPointer.timeStamp;
var type = this.held ? 'holdpulse' : 'hold';
this.fireHold(type, hold);
this.held = true;
},
cancel: function() {
clearInterval(this.holdJob);
if (this.held) {
this.fireHold('release');
}
this.held = false;
this.heldPointer = null;
this.target = null;
this.holdJob = null;
},
down: function(inEvent) {
if (inEvent.isPrimary && !this.heldPointer) {
this.heldPointer = inEvent;
this.target = inEvent.target;
this.holdJob = setInterval(this.pulse.bind(this), this.HOLD_DELAY);
}
},
up: function(inEvent) {
if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) {
this.cancel();
}
},
move: function(inEvent) {
if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) {
var x = inEvent.clientX - this.heldPointer.clientX;
var y = inEvent.clientY - this.heldPointer.clientY;
if ((x * x + y * y) > this.WIGGLE_THRESHOLD) {
this.cancel();
}
}
},
fireHold: function(inType, inHoldTime) {
var p = {
bubbles: true,
cancelable: true,
pointerType: this.heldPointer.pointerType,
pointerId: this.heldPointer.pointerId,
x: this.heldPointer.clientX,
y: this.heldPointer.clientY,
_source: 'hold'
};
if (inHoldTime) {
p.holdTime = inHoldTime;
}
var e = eventFactory.makeGestureEvent(inType, p);
this.target.dispatchEvent(e);
}
};
dispatcher.registerGesture('hold', hold);
})(window.PolymerGestures);
/**
* This event is fired when a pointer quickly goes down and up, and is used to
* denote activation.
*
* Any gesture event can prevent the tap event from being created by calling
* `event.preventTap`.
*
* Any pointer event can prevent the tap by setting the `tapPrevented` property
* on itself.
*
* @module PointerGestures
* @submodule Events
* @class tap
*/
/**
* X axis position of the tap.
* @property x
* @type Number
*/
/**
* Y axis position of the tap.
* @property y
* @type Number
*/
/**
* Type of the pointer that made the tap.
* @property pointerType
* @type String
*/
(function(scope) {
var dispatcher = scope.dispatcher;
var eventFactory = scope.eventFactory;
var pointermap = new scope.PointerMap();
var tap = {
events: [
'down',
'up'
],
exposes: [
'tap'
],
down: function(inEvent) {
if (inEvent.isPrimary && !inEvent.tapPrevented) {
pointermap.set(inEvent.pointerId, {
target: inEvent.target,
buttons: inEvent.buttons,
x: inEvent.clientX,
y: inEvent.clientY
});
}
},
shouldTap: function(e, downState) {
var tap = true;
if (e.pointerType === 'mouse') {
// only allow left click to tap for mouse
tap = (e.buttons ^ 1) && (downState.buttons & 1);
}
return tap && !e.tapPrevented;
},
up: function(inEvent) {
var start = pointermap.get(inEvent.pointerId);
if (start && this.shouldTap(inEvent, start)) {
// up.relatedTarget is target currently under finger
var t = scope.targetFinding.LCA(start.target, inEvent.relatedTarget);
if (t) {
var e = eventFactory.makeGestureEvent('tap', {
bubbles: true,
cancelable: true,
x: inEvent.clientX,
y: inEvent.clientY,
detail: inEvent.detail,
pointerType: inEvent.pointerType,
pointerId: inEvent.pointerId,
altKey: inEvent.altKey,
ctrlKey: inEvent.ctrlKey,
metaKey: inEvent.metaKey,
shiftKey: inEvent.shiftKey,
_source: 'tap'
});
t.dispatchEvent(e);
}
}
pointermap.delete(inEvent.pointerId);
}
};
// patch eventFactory to remove id from tap's pointermap for preventTap calls
eventFactory.preventTap = function(e) {
return function() {
e.tapPrevented = true;
pointermap.delete(e.pointerId);
};
};
dispatcher.registerGesture('tap', tap);
})(window.PolymerGestures);
/*
* Basic strategy: find the farthest apart points, use as diameter of circle
* react to size change and rotation of the chord
*/
/**
* @module pointer-gestures
* @submodule Events
* @class pinch
*/
/**
* Scale of the pinch zoom gesture
* @property scale
* @type Number
*/
/**
* Center X position of pointers causing pinch
* @property centerX
* @type Number
*/
/**
* Center Y position of pointers causing pinch
* @property centerY
* @type Number
*/
/**
* @module pointer-gestures
* @submodule Events
* @class rotate
*/
/**
* Angle (in degrees) of rotation. Measured from starting positions of pointers.
* @property angle
* @type Number
*/
/**
* Center X position of pointers causing rotation
* @property centerX
* @type Number
*/
/**
* Center Y position of pointers causing rotation
* @property centerY
* @type Number
*/
(function(scope) {
var dispatcher = scope.dispatcher;
var eventFactory = scope.eventFactory;
var pointermap = new scope.PointerMap();
var RAD_TO_DEG = 180 / Math.PI;
var pinch = {
events: [
'down',
'up',
'move',
'cancel'
],
exposes: [
'pinchstart',
'pinch',
'pinchend',
'rotate'
],
defaultActions: {
'pinch': 'none',
'rotate': 'none'
},
reference: {},
down: function(inEvent) {
pointermap.set(inEvent.pointerId, inEvent);
if (pointermap.pointers() == 2) {
var points = this.calcChord();
var angle = this.calcAngle(points);
this.reference = {
angle: angle,
diameter: points.diameter,
target: scope.targetFinding.LCA(points.a.target, points.b.target)
};
this.firePinch('pinchstart', points.diameter, points);
}
},
up: function(inEvent) {
var p = pointermap.get(inEvent.pointerId);
var num = pointermap.pointers();
if (p) {
if (num === 2) {
// fire 'pinchend' before deleting pointer
var points = this.calcChord();
this.firePinch('pinchend', points.diameter, points);
}
pointermap.delete(inEvent.pointerId);
}
},
move: function(inEvent) {
if (pointermap.has(inEvent.pointerId)) {
pointermap.set(inEvent.pointerId, inEvent);
if (pointermap.pointers() > 1) {
this.calcPinchRotate();
}
}
},
cancel: function(inEvent) {
this.up(inEvent);
},
firePinch: function(type, diameter, points) {
var zoom = diameter / this.reference.diameter;
var e = eventFactory.makeGestureEvent(type, {
bubbles: true,
cancelable: true,
scale: zoom,
centerX: points.center.x,
centerY: points.center.y,
_source: 'pinch'
});
this.reference.target.dispatchEvent(e);
},
fireRotate: function(angle, points) {
var diff = Math.round((angle - this.reference.angle) % 360);
var e = eventFactory.makeGestureEvent('rotate', {
bubbles: true,
cancelable: true,
angle: diff,
centerX: points.center.x,
centerY: points.center.y,
_source: 'pinch'
});
this.reference.target.dispatchEvent(e);
},
calcPinchRotate: function() {
var points = this.calcChord();
var diameter = points.diameter;
var angle = this.calcAngle(points);
if (diameter != this.reference.diameter) {
this.firePinch('pinch', diameter, points);
}
if (angle != this.reference.angle) {
this.fireRotate(angle, points);
}
},
calcChord: function() {
var pointers = [];
pointermap.forEach(function(p) {
pointers.push(p);
});
var dist = 0;
// start with at least two pointers
var points = {a: pointers[0], b: pointers[1]};
var x, y, d;
for (var i = 0; i < pointers.length; i++) {
var a = pointers[i];
for (var j = i + 1; j < pointers.length; j++) {
var b = pointers[j];
x = Math.abs(a.clientX - b.clientX);
y = Math.abs(a.clientY - b.clientY);
d = x + y;
if (d > dist) {
dist = d;
points = {a: a, b: b};
}
}
}
x = Math.abs(points.a.clientX + points.b.clientX) / 2;
y = Math.abs(points.a.clientY + points.b.clientY) / 2;
points.center = { x: x, y: y };
points.diameter = dist;
return points;
},
calcAngle: function(points) {
var x = points.a.clientX - points.b.clientX;
var y = points.a.clientY - points.b.clientY;
return (360 + Math.atan2(y, x) * RAD_TO_DEG) % 360;
}
};
dispatcher.registerGesture('pinch', pinch);
})(window.PolymerGestures);
(function (global) {
'use strict';
var Token,
TokenName,
Syntax,
Messages,
source,
index,
length,
delegate,
lookahead,
state;
Token = {
BooleanLiteral: 1,
EOF: 2,
Identifier: 3,
Keyword: 4,
NullLiteral: 5,
NumericLiteral: 6,
Punctuator: 7,
StringLiteral: 8
};
TokenName = {};
TokenName[Token.BooleanLiteral] = 'Boolean';
TokenName[Token.EOF] = '';
TokenName[Token.Identifier] = 'Identifier';
TokenName[Token.Keyword] = 'Keyword';
TokenName[Token.NullLiteral] = 'Null';
TokenName[Token.NumericLiteral] = 'Numeric';
TokenName[Token.Punctuator] = 'Punctuator';
TokenName[Token.StringLiteral] = 'String';
Syntax = {
ArrayExpression: 'ArrayExpression',
BinaryExpression: 'BinaryExpression',
CallExpression: 'CallExpression',
ConditionalExpression: 'ConditionalExpression',
EmptyStatement: 'EmptyStatement',
ExpressionStatement: 'ExpressionStatement',
Identifier: 'Identifier',
Literal: 'Literal',
LabeledStatement: 'LabeledStatement',
LogicalExpression: 'LogicalExpression',
MemberExpression: 'MemberExpression',
ObjectExpression: 'ObjectExpression',
Program: 'Program',
Property: 'Property',
ThisExpression: 'ThisExpression',
UnaryExpression: 'UnaryExpression'
};
// Error messages should be identical to V8.
Messages = {
UnexpectedToken: 'Unexpected token %0',
UnknownLabel: 'Undefined label \'%0\'',
Redeclaration: '%0 \'%1\' has already been declared'
};
// Ensure the condition is true, otherwise throw an error.
// This is only to have a better contract semantic, i.e. another safety net
// to catch a logic error. The condition shall be fulfilled in normal case.
// Do NOT use this to enforce a certain condition on any user input.
function assert(condition, message) {
if (!condition) {
throw new Error('ASSERT: ' + message);
}
}
function isDecimalDigit(ch) {
return (ch >= 48 && ch <= 57); // 0..9
}
// 7.2 White Space
function isWhiteSpace(ch) {
return (ch === 32) || // space
(ch === 9) || // tab
(ch === 0xB) ||
(ch === 0xC) ||
(ch === 0xA0) ||
(ch >= 0x1680 && '\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\uFEFF'.indexOf(String.fromCharCode(ch)) > 0);
}
// 7.3 Line Terminators
function isLineTerminator(ch) {
return (ch === 10) || (ch === 13) || (ch === 0x2028) || (ch === 0x2029);
}
// 7.6 Identifier Names and Identifiers
function isIdentifierStart(ch) {
return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore)
(ch >= 65 && ch <= 90) || // A..Z
(ch >= 97 && ch <= 122); // a..z
}
function isIdentifierPart(ch) {
return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore)
(ch >= 65 && ch <= 90) || // A..Z
(ch >= 97 && ch <= 122) || // a..z
(ch >= 48 && ch <= 57); // 0..9
}
// 7.6.1.1 Keywords
function isKeyword(id) {
return (id === 'this')
}
// 7.4 Comments
function skipWhitespace() {
while (index < length && isWhiteSpace(source.charCodeAt(index))) {
++index;
}
}
function getIdentifier() {
var start, ch;
start = index++;
while (index < length) {
ch = source.charCodeAt(index);
if (isIdentifierPart(ch)) {
++index;
} else {
break;
}
}
return source.slice(start, index);
}
function scanIdentifier() {
var start, id, type;
start = index;
id = getIdentifier();
// There is no keyword or literal with only one character.
// Thus, it must be an identifier.
if (id.length === 1) {
type = Token.Identifier;
} else if (isKeyword(id)) {
type = Token.Keyword;
} else if (id === 'null') {
type = Token.NullLiteral;
} else if (id === 'true' || id === 'false') {
type = Token.BooleanLiteral;
} else {
type = Token.Identifier;
}
return {
type: type,
value: id,
range: [start, index]
};
}
// 7.7 Punctuators
function scanPunctuator() {
var start = index,
code = source.charCodeAt(index),
code2,
ch1 = source[index],
ch2;
switch (code) {
// Check for most common single-character punctuators.
case 46: // . dot
case 40: // ( open bracket
case 41: // ) close bracket
case 59: // ; semicolon
case 44: // , comma
case 123: // { open curly brace
case 125: // } close curly brace
case 91: // [
case 93: // ]
case 58: // :
case 63: // ?
++index;
return {
type: Token.Punctuator,
value: String.fromCharCode(code),
range: [start, index]
};
default:
code2 = source.charCodeAt(index + 1);
// '=' (char #61) marks an assignment or comparison operator.
if (code2 === 61) {
switch (code) {
case 37: // %
case 38: // &
case 42: // *:
case 43: // +
case 45: // -
case 47: // /
case 60: // <
case 62: // >
case 124: // |
index += 2;
return {
type: Token.Punctuator,
value: String.fromCharCode(code) + String.fromCharCode(code2),
range: [start, index]
};
case 33: // !
case 61: // =
index += 2;
// !== and ===
if (source.charCodeAt(index) === 61) {
++index;
}
return {
type: Token.Punctuator,
value: source.slice(start, index),
range: [start, index]
};
default:
break;
}
}
break;
}
// Peek more characters.
ch2 = source[index + 1];
// Other 2-character punctuators: && ||
if (ch1 === ch2 && ('&|'.indexOf(ch1) >= 0)) {
index += 2;
return {
type: Token.Punctuator,
value: ch1 + ch2,
range: [start, index]
};
}
if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) {
++index;
return {
type: Token.Punctuator,
value: ch1,
range: [start, index]
};
}
throwError({}, Messages.UnexpectedToken, 'ILLEGAL');
}
// 7.8.3 Numeric Literals
function scanNumericLiteral() {
var number, start, ch;
ch = source[index];
assert(isDecimalDigit(ch.charCodeAt(0)) || (ch === '.'),
'Numeric literal must start with a decimal digit or a decimal point');
start = index;
number = '';
if (ch !== '.') {
number = source[index++];
ch = source[index];
// Hex number starts with '0x'.
// Octal number starts with '0'.
if (number === '0') {
// decimal number starts with '0' such as '09' is illegal.
if (ch && isDecimalDigit(ch.charCodeAt(0))) {
throwError({}, Messages.UnexpectedToken, 'ILLEGAL');
}
}
while (isDecimalDigit(source.charCodeAt(index))) {
number += source[index++];
}
ch = source[index];
}
if (ch === '.') {
number += source[index++];
while (isDecimalDigit(source.charCodeAt(index))) {
number += source[index++];
}
ch = source[index];
}
if (ch === 'e' || ch === 'E') {
number += source[index++];
ch = source[index];
if (ch === '+' || ch === '-') {
number += source[index++];
}
if (isDecimalDigit(source.charCodeAt(index))) {
while (isDecimalDigit(source.charCodeAt(index))) {
number += source[index++];
}
} else {
throwError({}, Messages.UnexpectedToken, 'ILLEGAL');
}
}
if (isIdentifierStart(source.charCodeAt(index))) {
throwError({}, Messages.UnexpectedToken, 'ILLEGAL');
}
return {
type: Token.NumericLiteral,
value: parseFloat(number),
range: [start, index]
};
}
// 7.8.4 String Literals
function scanStringLiteral() {
var str = '', quote, start, ch, octal = false;
quote = source[index];
assert((quote === '\'' || quote === '"'),
'String literal must starts with a quote');
start = index;
++index;
while (index < length) {
ch = source[index++];
if (ch === quote) {
quote = '';
break;
} else if (ch === '\\') {
ch = source[index++];
if (!ch || !isLineTerminator(ch.charCodeAt(0))) {
switch (ch) {
case 'n':
str += '\n';
break;
case 'r':
str += '\r';
break;
case 't':
str += '\t';
break;
case 'b':
str += '\b';
break;
case 'f':
str += '\f';
break;
case 'v':
str += '\x0B';
break;
default:
str += ch;
break;
}
} else {
if (ch === '\r' && source[index] === '\n') {
++index;
}
}
} else if (isLineTerminator(ch.charCodeAt(0))) {
break;
} else {
str += ch;
}
}
if (quote !== '') {
throwError({}, Messages.UnexpectedToken, 'ILLEGAL');
}
return {
type: Token.StringLiteral,
value: str,
octal: octal,
range: [start, index]
};
}
function isIdentifierName(token) {
return token.type === Token.Identifier ||
token.type === Token.Keyword ||
token.type === Token.BooleanLiteral ||
token.type === Token.NullLiteral;
}
function advance() {
var ch;
skipWhitespace();
if (index >= length) {
return {
type: Token.EOF,
range: [index, index]
};
}
ch = source.charCodeAt(index);
// Very common: ( and ) and ;
if (ch === 40 || ch === 41 || ch === 58) {
return scanPunctuator();
}
// String literal starts with single quote (#39) or double quote (#34).
if (ch === 39 || ch === 34) {
return scanStringLiteral();
}
if (isIdentifierStart(ch)) {
return scanIdentifier();
}
// Dot (.) char #46 can also start a floating-point number, hence the need
// to check the next character.
if (ch === 46) {
if (isDecimalDigit(source.charCodeAt(index + 1))) {
return scanNumericLiteral();
}
return scanPunctuator();
}
if (isDecimalDigit(ch)) {
return scanNumericLiteral();
}
return scanPunctuator();
}
function lex() {
var token;
token = lookahead;
index = token.range[1];
lookahead = advance();
index = token.range[1];
return token;
}
function peek() {
var pos;
pos = index;
lookahead = advance();
index = pos;
}
// Throw an exception
function throwError(token, messageFormat) {
var error,
args = Array.prototype.slice.call(arguments, 2),
msg = messageFormat.replace(
/%(\d)/g,
function (whole, index) {
assert(index < args.length, 'Message reference must be in range');
return args[index];
}
);
error = new Error(msg);
error.index = index;
error.description = msg;
throw error;
}
// Throw an exception because of the token.
function throwUnexpected(token) {
throwError(token, Messages.UnexpectedToken, token.value);
}
// Expect the next token to match the specified punctuator.
// If not, an exception will be thrown.
function expect(value) {
var token = lex();
if (token.type !== Token.Punctuator || token.value !== value) {
throwUnexpected(token);
}
}
// Return true if the next token matches the specified punctuator.
function match(value) {
return lookahead.type === Token.Punctuator && lookahead.value === value;
}
// Return true if the next token matches the specified keyword
function matchKeyword(keyword) {
return lookahead.type === Token.Keyword && lookahead.value === keyword;
}
function consumeSemicolon() {
// Catch the very common case first: immediately a semicolon (char #59).
if (source.charCodeAt(index) === 59) {
lex();
return;
}
skipWhitespace();
if (match(';')) {
lex();
return;
}
if (lookahead.type !== Token.EOF && !match('}')) {
throwUnexpected(lookahead);
}
}
// 11.1.4 Array Initialiser
function parseArrayInitialiser() {
var elements = [];
expect('[');
while (!match(']')) {
if (match(',')) {
lex();
elements.push(null);
} else {
elements.push(parseExpression());
if (!match(']')) {
expect(',');
}
}
}
expect(']');
return delegate.createArrayExpression(elements);
}
// 11.1.5 Object Initialiser
function parseObjectPropertyKey() {
var token;
skipWhitespace();
token = lex();
// Note: This function is called only from parseObjectProperty(), where
// EOF and Punctuator tokens are already filtered out.
if (token.type === Token.StringLiteral || token.type === Token.NumericLiteral) {
return delegate.createLiteral(token);
}
return delegate.createIdentifier(token.value);
}
function parseObjectProperty() {
var token, key;
token = lookahead;
skipWhitespace();
if (token.type === Token.EOF || token.type === Token.Punctuator) {
throwUnexpected(token);
}
key = parseObjectPropertyKey();
expect(':');
return delegate.createProperty('init', key, parseExpression());
}
function parseObjectInitialiser() {
var properties = [];
expect('{');
while (!match('}')) {
properties.push(parseObjectProperty());
if (!match('}')) {
expect(',');
}
}
expect('}');
return delegate.createObjectExpression(properties);
}
// 11.1.6 The Grouping Operator
function parseGroupExpression() {
var expr;
expect('(');
expr = parseExpression();
expect(')');
return expr;
}
// 11.1 Primary Expressions
function parsePrimaryExpression() {
var type, token, expr;
if (match('(')) {
return parseGroupExpression();
}
type = lookahead.type;
if (type === Token.Identifier) {
expr = delegate.createIdentifier(lex().value);
} else if (type === Token.StringLiteral || type === Token.NumericLiteral) {
expr = delegate.createLiteral(lex());
} else if (type === Token.Keyword) {
if (matchKeyword('this')) {
lex();
expr = delegate.createThisExpression();
}
} else if (type === Token.BooleanLiteral) {
token = lex();
token.value = (token.value === 'true');
expr = delegate.createLiteral(token);
} else if (type === Token.NullLiteral) {
token = lex();
token.value = null;
expr = delegate.createLiteral(token);
} else if (match('[')) {
expr = parseArrayInitialiser();
} else if (match('{')) {
expr = parseObjectInitialiser();
}
if (expr) {
return expr;
}
throwUnexpected(lex());
}
// 11.2 Left-Hand-Side Expressions
function parseArguments() {
var args = [];
expect('(');
if (!match(')')) {
while (index < length) {
args.push(parseExpression());
if (match(')')) {
break;
}
expect(',');
}
}
expect(')');
return args;
}
function parseNonComputedProperty() {
var token;
token = lex();
if (!isIdentifierName(token)) {
throwUnexpected(token);
}
return delegate.createIdentifier(token.value);
}
function parseNonComputedMember() {
expect('.');
return parseNonComputedProperty();
}
function parseComputedMember() {
var expr;
expect('[');
expr = parseExpression();
expect(']');
return expr;
}
function parseLeftHandSideExpression() {
var expr, args, property;
expr = parsePrimaryExpression();
while (true) {
if (match('[')) {
property = parseComputedMember();
expr = delegate.createMemberExpression('[', expr, property);
} else if (match('.')) {
property = parseNonComputedMember();
expr = delegate.createMemberExpression('.', expr, property);
} else if (match('(')) {
args = parseArguments();
expr = delegate.createCallExpression(expr, args);
} else {
break;
}
}
return expr;
}
// 11.3 Postfix Expressions
var parsePostfixExpression = parseLeftHandSideExpression;
// 11.4 Unary Operators
function parseUnaryExpression() {
var token, expr;
if (lookahead.type !== Token.Punctuator && lookahead.type !== Token.Keyword) {
expr = parsePostfixExpression();
} else if (match('+') || match('-') || match('!')) {
token = lex();
expr = parseUnaryExpression();
expr = delegate.createUnaryExpression(token.value, expr);
} else if (matchKeyword('delete') || matchKeyword('void') || matchKeyword('typeof')) {
throwError({}, Messages.UnexpectedToken);
} else {
expr = parsePostfixExpression();
}
return expr;
}
function binaryPrecedence(token) {
var prec = 0;
if (token.type !== Token.Punctuator && token.type !== Token.Keyword) {
return 0;
}
switch (token.value) {
case '||':
prec = 1;
break;
case '&&':
prec = 2;
break;
case '==':
case '!=':
case '===':
case '!==':
prec = 6;
break;
case '<':
case '>':
case '<=':
case '>=':
case 'instanceof':
prec = 7;
break;
case 'in':
prec = 7;
break;
case '+':
case '-':
prec = 9;
break;
case '*':
case '/':
case '%':
prec = 11;
break;
default:
break;
}
return prec;
}
// 11.5 Multiplicative Operators
// 11.6 Additive Operators
// 11.7 Bitwise Shift Operators
// 11.8 Relational Operators
// 11.9 Equality Operators
// 11.10 Binary Bitwise Operators
// 11.11 Binary Logical Operators
function parseBinaryExpression() {
var expr, token, prec, stack, right, operator, left, i;
left = parseUnaryExpression();
token = lookahead;
prec = binaryPrecedence(token);
if (prec === 0) {
return left;
}
token.prec = prec;
lex();
right = parseUnaryExpression();
stack = [left, token, right];
while ((prec = binaryPrecedence(lookahead)) > 0) {
// Reduce: make a binary expression from the three topmost entries.
while ((stack.length > 2) && (prec <= stack[stack.length - 2].prec)) {
right = stack.pop();
operator = stack.pop().value;
left = stack.pop();
expr = delegate.createBinaryExpression(operator, left, right);
stack.push(expr);
}
// Shift.
token = lex();
token.prec = prec;
stack.push(token);
expr = parseUnaryExpression();
stack.push(expr);
}
// Final reduce to clean-up the stack.
i = stack.length - 1;
expr = stack[i];
while (i > 1) {
expr = delegate.createBinaryExpression(stack[i - 1].value, stack[i - 2], expr);
i -= 2;
}
return expr;
}
// 11.12 Conditional Operator
function parseConditionalExpression() {
var expr, consequent, alternate;
expr = parseBinaryExpression();
if (match('?')) {
lex();
consequent = parseConditionalExpression();
expect(':');
alternate = parseConditionalExpression();
expr = delegate.createConditionalExpression(expr, consequent, alternate);
}
return expr;
}
// Simplification since we do not support AssignmentExpression.
var parseExpression = parseConditionalExpression;
// Polymer Syntax extensions
// Filter ::
// Identifier
// Identifier "(" ")"
// Identifier "(" FilterArguments ")"
function parseFilter() {
var identifier, args;
identifier = lex();
if (identifier.type !== Token.Identifier) {
throwUnexpected(identifier);
}
args = match('(') ? parseArguments() : [];
return delegate.createFilter(identifier.value, args);
}
// Filters ::
// "|" Filter
// Filters "|" Filter
function parseFilters() {
while (match('|')) {
lex();
parseFilter();
}
}
// TopLevel ::
// LabelledExpressions
// AsExpression
// InExpression
// FilterExpression
// AsExpression ::
// FilterExpression as Identifier
// InExpression ::
// Identifier, Identifier in FilterExpression
// Identifier in FilterExpression
// FilterExpression ::
// Expression
// Expression Filters
function parseTopLevel() {
skipWhitespace();
peek();
var expr = parseExpression();
if (expr) {
if (lookahead.value === ',' || lookahead.value == 'in' &&
expr.type === Syntax.Identifier) {
parseInExpression(expr);
} else {
parseFilters();
if (lookahead.value === 'as') {
parseAsExpression(expr);
} else {
delegate.createTopLevel(expr);
}
}
}
if (lookahead.type !== Token.EOF) {
throwUnexpected(lookahead);
}
}
function parseAsExpression(expr) {
lex(); // as
var identifier = lex().value;
delegate.createAsExpression(expr, identifier);
}
function parseInExpression(identifier) {
var indexName;
if (lookahead.value === ',') {
lex();
if (lookahead.type !== Token.Identifier)
throwUnexpected(lookahead);
indexName = lex().value;
}
lex(); // in
var expr = parseExpression();
parseFilters();
delegate.createInExpression(identifier.name, indexName, expr);
}
function parse(code, inDelegate) {
delegate = inDelegate;
source = code;
index = 0;
length = source.length;
lookahead = null;
state = {
labelSet: {}
};
return parseTopLevel();
}
global.esprima = {
parse: parse
};
})(this);
// Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
// This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
// The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
// The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
// Code distributed by Google as part of the polymer project is also
// subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
(function (global) {
'use strict';
function prepareBinding(expressionText, name, node, filterRegistry) {
var expression;
try {
expression = getExpression(expressionText);
if (expression.scopeIdent &&
(node.nodeType !== Node.ELEMENT_NODE ||
node.tagName !== 'TEMPLATE' ||
(name !== 'bind' && name !== 'repeat'))) {
throw Error('as and in can only be used within ');
}
} catch (ex) {
console.error('Invalid expression syntax: ' + expressionText, ex);
return;
}
return function(model, node, oneTime) {
var binding = expression.getBinding(model, filterRegistry, oneTime);
if (expression.scopeIdent && binding) {
node.polymerExpressionScopeIdent_ = expression.scopeIdent;
if (expression.indexIdent)
node.polymerExpressionIndexIdent_ = expression.indexIdent;
}
return binding;
}
}
// TODO(rafaelw): Implement simple LRU.
var expressionParseCache = Object.create(null);
function getExpression(expressionText) {
var expression = expressionParseCache[expressionText];
if (!expression) {
var delegate = new ASTDelegate();
esprima.parse(expressionText, delegate);
expression = new Expression(delegate);
expressionParseCache[expressionText] = expression;
}
return expression;
}
function Literal(value) {
this.value = value;
this.valueFn_ = undefined;
}
Literal.prototype = {
valueFn: function() {
if (!this.valueFn_) {
var value = this.value;
this.valueFn_ = function() {
return value;
}
}
return this.valueFn_;
}
}
function IdentPath(name) {
this.name = name;
this.path = Path.get(name);
}
IdentPath.prototype = {
valueFn: function() {
if (!this.valueFn_) {
var name = this.name;
var path = this.path;
this.valueFn_ = function(model, observer) {
if (observer)
observer.addPath(model, path);
return path.getValueFrom(model);
}
}
return this.valueFn_;
},
setValue: function(model, newValue) {
if (this.path.length == 1)
model = findScope(model, this.path[0]);
return this.path.setValueFrom(model, newValue);
}
};
function MemberExpression(object, property, accessor) {
this.computed = accessor == '[';
this.dynamicDeps = typeof object == 'function' ||
object.dynamicDeps ||
(this.computed && !(property instanceof Literal));
this.simplePath =
!this.dynamicDeps &&
(property instanceof IdentPath || property instanceof Literal) &&
(object instanceof MemberExpression || object instanceof IdentPath);
this.object = this.simplePath ? object : getFn(object);
this.property = !this.computed || this.simplePath ?
property : getFn(property);
}
MemberExpression.prototype = {
get fullPath() {
if (!this.fullPath_) {
var parts = this.object instanceof MemberExpression ?
this.object.fullPath.slice() : [this.object.name];
parts.push(this.property instanceof IdentPath ?
this.property.name : this.property.value);
this.fullPath_ = Path.get(parts);
}
return this.fullPath_;
},
valueFn: function() {
if (!this.valueFn_) {
var object = this.object;
if (this.simplePath) {
var path = this.fullPath;
this.valueFn_ = function(model, observer) {
if (observer)
observer.addPath(model, path);
return path.getValueFrom(model);
};
} else if (!this.computed) {
var path = Path.get(this.property.name);
this.valueFn_ = function(model, observer, filterRegistry) {
var context = object(model, observer, filterRegistry);
if (observer)
observer.addPath(context, path);
return path.getValueFrom(context);
}
} else {
// Computed property.
var property = this.property;
this.valueFn_ = function(model, observer, filterRegistry) {
var context = object(model, observer, filterRegistry);
var propName = property(model, observer, filterRegistry);
if (observer)
observer.addPath(context, [propName]);
return context ? context[propName] : undefined;
};
}
}
return this.valueFn_;
},
setValue: function(model, newValue) {
if (this.simplePath) {
this.fullPath.setValueFrom(model, newValue);
return newValue;
}
var object = this.object(model);
var propName = this.property instanceof IdentPath ? this.property.name :
this.property(model);
return object[propName] = newValue;
}
};
function Filter(name, args) {
this.name = name;
this.args = [];
for (var i = 0; i < args.length; i++) {
this.args[i] = getFn(args[i]);
}
}
Filter.prototype = {
transform: function(model, observer, filterRegistry, toModelDirection,
initialArgs) {
var fn = filterRegistry[this.name];
var context = model;
if (fn) {
context = undefined;
} else {
fn = context[this.name];
if (!fn) {
console.error('Cannot find function or filter: ' + this.name);
return;
}
}
// If toModelDirection is falsey, then the "normal" (dom-bound) direction
// is used. Otherwise, it looks for a 'toModel' property function on the
// object.
if (toModelDirection) {
fn = fn.toModel;
} else if (typeof fn.toDOM == 'function') {
fn = fn.toDOM;
}
if (typeof fn != 'function') {
console.error('Cannot find function or filter: ' + this.name);
return;
}
var args = initialArgs || [];
for (var i = 0; i < this.args.length; i++) {
args.push(getFn(this.args[i])(model, observer, filterRegistry));
}
return fn.apply(context, args);
}
};
function notImplemented() { throw Error('Not Implemented'); }
var unaryOperators = {
'+': function(v) { return +v; },
'-': function(v) { return -v; },
'!': function(v) { return !v; }
};
var binaryOperators = {
'+': function(l, r) { return l+r; },
'-': function(l, r) { return l-r; },
'*': function(l, r) { return l*r; },
'/': function(l, r) { return l/r; },
'%': function(l, r) { return l%r; },
'<': function(l, r) { return l': function(l, r) { return l>r; },
'<=': function(l, r) { return l<=r; },
'>=': function(l, r) { return l>=r; },
'==': function(l, r) { return l==r; },
'!=': function(l, r) { return l!=r; },
'===': function(l, r) { return l===r; },
'!==': function(l, r) { return l!==r; },
'&&': function(l, r) { return l&&r; },
'||': function(l, r) { return l||r; },
};
function getFn(arg) {
return typeof arg == 'function' ? arg : arg.valueFn();
}
function ASTDelegate() {
this.expression = null;
this.filters = [];
this.deps = {};
this.currentPath = undefined;
this.scopeIdent = undefined;
this.indexIdent = undefined;
this.dynamicDeps = false;
}
ASTDelegate.prototype = {
createUnaryExpression: function(op, argument) {
if (!unaryOperators[op])
throw Error('Disallowed operator: ' + op);
argument = getFn(argument);
return function(model, observer, filterRegistry) {
return unaryOperators[op](argument(model, observer, filterRegistry));
};
},
createBinaryExpression: function(op, left, right) {
if (!binaryOperators[op])
throw Error('Disallowed operator: ' + op);
left = getFn(left);
right = getFn(right);
switch (op) {
case '||':
this.dynamicDeps = true;
return function(model, observer, filterRegistry) {
return left(model, observer, filterRegistry) ||
right(model, observer, filterRegistry);
};
case '&&':
this.dynamicDeps = true;
return function(model, observer, filterRegistry) {
return left(model, observer, filterRegistry) &&
right(model, observer, filterRegistry);
};
}
return function(model, observer, filterRegistry) {
return binaryOperators[op](left(model, observer, filterRegistry),
right(model, observer, filterRegistry));
};
},
createConditionalExpression: function(test, consequent, alternate) {
test = getFn(test);
consequent = getFn(consequent);
alternate = getFn(alternate);
this.dynamicDeps = true;
return function(model, observer, filterRegistry) {
return test(model, observer, filterRegistry) ?
consequent(model, observer, filterRegistry) :
alternate(model, observer, filterRegistry);
}
},
createIdentifier: function(name) {
var ident = new IdentPath(name);
ident.type = 'Identifier';
return ident;
},
createMemberExpression: function(accessor, object, property) {
var ex = new MemberExpression(object, property, accessor);
if (ex.dynamicDeps)
this.dynamicDeps = true;
return ex;
},
createCallExpression: function(expression, args) {
if (!(expression instanceof IdentPath))
throw Error('Only identifier function invocations are allowed');
var filter = new Filter(expression.name, args);
return function(model, observer, filterRegistry) {
return filter.transform(model, observer, filterRegistry, false);
};
},
createLiteral: function(token) {
return new Literal(token.value);
},
createArrayExpression: function(elements) {
for (var i = 0; i < elements.length; i++)
elements[i] = getFn(elements[i]);
return function(model, observer, filterRegistry) {
var arr = []
for (var i = 0; i < elements.length; i++)
arr.push(elements[i](model, observer, filterRegistry));
return arr;
}
},
createProperty: function(kind, key, value) {
return {
key: key instanceof IdentPath ? key.name : key.value,
value: value
};
},
createObjectExpression: function(properties) {
for (var i = 0; i < properties.length; i++)
properties[i].value = getFn(properties[i].value);
return function(model, observer, filterRegistry) {
var obj = {};
for (var i = 0; i < properties.length; i++)
obj[properties[i].key] =
properties[i].value(model, observer, filterRegistry);
return obj;
}
},
createFilter: function(name, args) {
this.filters.push(new Filter(name, args));
},
createAsExpression: function(expression, scopeIdent) {
this.expression = expression;
this.scopeIdent = scopeIdent;
},
createInExpression: function(scopeIdent, indexIdent, expression) {
this.expression = expression;
this.scopeIdent = scopeIdent;
this.indexIdent = indexIdent;
},
createTopLevel: function(expression) {
this.expression = expression;
},
createThisExpression: notImplemented
}
function ConstantObservable(value) {
this.value_ = value;
}
ConstantObservable.prototype = {
open: function() { return this.value_; },
discardChanges: function() { return this.value_; },
deliver: function() {},
close: function() {},
}
function Expression(delegate) {
this.scopeIdent = delegate.scopeIdent;
this.indexIdent = delegate.indexIdent;
if (!delegate.expression)
throw Error('No expression found.');
this.expression = delegate.expression;
getFn(this.expression); // forces enumeration of path dependencies
this.filters = delegate.filters;
this.dynamicDeps = delegate.dynamicDeps;
}
Expression.prototype = {
getBinding: function(model, filterRegistry, oneTime) {
if (oneTime)
return this.getValue(model, undefined, filterRegistry);
var observer = new CompoundObserver();
// captures deps.
var firstValue = this.getValue(model, observer, filterRegistry);
var firstTime = true;
var self = this;
function valueFn() {
// deps cannot have changed on first value retrieval.
if (firstTime) {
firstTime = false;
return firstValue;
}
if (self.dynamicDeps)
observer.startReset();
var value = self.getValue(model,
self.dynamicDeps ? observer : undefined,
filterRegistry);
if (self.dynamicDeps)
observer.finishReset();
return value;
}
function setValueFn(newValue) {
self.setValue(model, newValue, filterRegistry);
return newValue;
}
return new ObserverTransform(observer, valueFn, setValueFn, true);
},
getValue: function(model, observer, filterRegistry) {
var value = getFn(this.expression)(model, observer, filterRegistry);
for (var i = 0; i < this.filters.length; i++) {
value = this.filters[i].transform(model, observer, filterRegistry,
false, [value]);
}
return value;
},
setValue: function(model, newValue, filterRegistry) {
var count = this.filters ? this.filters.length : 0;
while (count-- > 0) {
newValue = this.filters[count].transform(model, undefined,
filterRegistry, true, [newValue]);
}
if (this.expression.setValue)
return this.expression.setValue(model, newValue);
}
}
/**
* Converts a style property name to a css property name. For example:
* "WebkitUserSelect" to "-webkit-user-select"
*/
function convertStylePropertyName(name) {
return String(name).replace(/[A-Z]/g, function(c) {
return '-' + c.toLowerCase();
});
}
var parentScopeName = '@' + Math.random().toString(36).slice(2);
// Single ident paths must bind directly to the appropriate scope object.
// I.e. Pushed values in two-bindings need to be assigned to the actual model
// object.
function findScope(model, prop) {
while (model[parentScopeName] &&
!Object.prototype.hasOwnProperty.call(model, prop)) {
model = model[parentScopeName];
}
return model;
}
function isLiteralExpression(pathString) {
switch (pathString) {
case '':
return false;
case 'false':
case 'null':
case 'true':
return true;
}
if (!isNaN(Number(pathString)))
return true;
return false;
};
function PolymerExpressions() {}
PolymerExpressions.prototype = {
// "built-in" filters
styleObject: function(value) {
var parts = [];
for (var key in value) {
parts.push(convertStylePropertyName(key) + ': ' + value[key]);
}
return parts.join('; ');
},
tokenList: function(value) {
var tokens = [];
for (var key in value) {
if (value[key])
tokens.push(key);
}
return tokens.join(' ');
},
// binding delegate API
prepareInstancePositionChanged: function(template) {
var indexIdent = template.polymerExpressionIndexIdent_;
if (!indexIdent)
return;
return function(templateInstance, index) {
templateInstance.model[indexIdent] = index;
};
},
prepareBinding: function(pathString, name, node) {
var path = Path.get(pathString);
if (!isLiteralExpression(pathString) && path.valid) {
if (path.length == 1) {
return function(model, node, oneTime) {
if (oneTime)
return path.getValueFrom(model);
var scope = findScope(model, path[0]);
return new PathObserver(scope, path);
};
}
return; // bail out early if pathString is simple path.
}
return prepareBinding(pathString, name, node, this);
},
prepareInstanceModel: function(template) {
var scopeName = template.polymerExpressionScopeIdent_;
if (!scopeName)
return;
var parentScope = template.templateInstance ?
template.templateInstance.model :
template.model;
var indexName = template.polymerExpressionIndexIdent_;
return function(model) {
return createScopeObject(parentScope, model, scopeName, indexName);
};
}
};
var createScopeObject = ('__proto__' in {}) ?
function(parentScope, model, scopeName, indexName) {
var scope = {};
scope[scopeName] = model;
scope[indexName] = undefined;
scope[parentScopeName] = parentScope;
scope.__proto__ = parentScope;
return scope;
} :
function(parentScope, model, scopeName, indexName) {
var scope = Object.create(parentScope);
Object.defineProperty(scope, scopeName,
{ value: model, configurable: true, writable: true });
Object.defineProperty(scope, indexName,
{ value: undefined, configurable: true, writable: true });
Object.defineProperty(scope, parentScopeName,
{ value: parentScope, configurable: true, writable: true });
return scope;
};
global.PolymerExpressions = PolymerExpressions;
PolymerExpressions.getExpression = getExpression;
})(this);
Polymer = {
version: '0.5.1'
};
// TODO(sorvell): this ensures Polymer is an object and not a function
// Platform is currently defining it as a function to allow for async loading
// of polymer; once we refine the loading process this likely goes away.
if (typeof window.Polymer === 'function') {
Polymer = {};
}
(function(scope) {
function withDependencies(task, depends) {
depends = depends || [];
if (!depends.map) {
depends = [depends];
}
return task.apply(this, depends.map(marshal));
}
function module(name, dependsOrFactory, moduleFactory) {
var module;
switch (arguments.length) {
case 0:
return;
case 1:
module = null;
break;
case 2:
// dependsOrFactory is `factory` in this case
module = dependsOrFactory.apply(this);
break;
default:
// dependsOrFactory is `depends` in this case
module = withDependencies(moduleFactory, dependsOrFactory);
break;
}
modules[name] = module;
};
function marshal(name) {
return modules[name];
}
var modules = {};
function using(depends, task) {
HTMLImports.whenImportsReady(function() {
withDependencies(task, depends);
});
};
// exports
scope.marshal = marshal;
// `module` confuses commonjs detectors
scope.modularize = module;
scope.using = using;
})(window);
/*
Build only script.
Ensures scripts needed for basic x-platform compatibility
will be run when platform.js is not loaded.
*/
if (!window.WebComponents) {
/*
On supported platforms, platform.js is not needed. To retain compatibility
with the polyfills, we stub out minimal functionality.
*/
if (!window.WebComponents) {
WebComponents = {
flush: function() {},
flags: {log: {}}
};
Platform = WebComponents;
CustomElements = {
useNative: true,
ready: true,
takeRecords: function() {},
instanceof: function(obj, base) {
return obj instanceof base;
}
};
HTMLImports = {
useNative: true
};
addEventListener('HTMLImportsLoaded', function() {
document.dispatchEvent(
new CustomEvent('WebComponentsReady', {bubbles: true})
);
});
// ShadowDOM
ShadowDOMPolyfill = null;
wrap = unwrap = function(n){
return n;
};
}
/*
Create polyfill scope and feature detect native support.
*/
window.HTMLImports = window.HTMLImports || {flags:{}};
(function(scope) {
/**
Basic setup and simple module executer. We collect modules and then execute
the code later, only if it's necessary for polyfilling.
*/
var IMPORT_LINK_TYPE = 'import';
var useNative = Boolean(IMPORT_LINK_TYPE in document.createElement('link'));
/**
Support `currentScript` on all browsers as `document._currentScript.`
NOTE: We cannot polyfill `document.currentScript` because it's not possible
both to override and maintain the ability to capture the native value.
Therefore we choose to expose `_currentScript` both when native imports
and the polyfill are in use.
*/
// NOTE: ShadowDOMPolyfill intrusion.
var hasShadowDOMPolyfill = Boolean(window.ShadowDOMPolyfill);
var wrap = function(node) {
return hasShadowDOMPolyfill ? ShadowDOMPolyfill.wrapIfNeeded(node) : node;
};
var rootDocument = wrap(document);
var currentScriptDescriptor = {
get: function() {
var script = HTMLImports.currentScript || document.currentScript ||
// NOTE: only works when called in synchronously executing code.
// readyState should check if `loading` but IE10 is
// interactive when scripts run so we cheat.
(document.readyState !== 'complete' ?
document.scripts[document.scripts.length - 1] : null);
return wrap(script);
},
configurable: true
};
Object.defineProperty(document, '_currentScript', currentScriptDescriptor);
Object.defineProperty(rootDocument, '_currentScript', currentScriptDescriptor);
/**
Add support for the `HTMLImportsLoaded` event and the `HTMLImports.whenReady`
method. This api is necessary because unlike the native implementation,
script elements do not force imports to resolve. Instead, users should wrap
code in either an `HTMLImportsLoaded` hander or after load time in an
`HTMLImports.whenReady(callback)` call.
NOTE: This module also supports these apis under the native implementation.
Therefore, if this file is loaded, the same code can be used under both
the polyfill and native implementation.
*/
var isIE = /Trident/.test(navigator.userAgent);
// call a callback when all HTMLImports in the document at call time
// (or at least document ready) have loaded.
// 1. ensure the document is in a ready state (has dom), then
// 2. watch for loading of imports and call callback when done
function whenReady(callback, doc) {
doc = doc || rootDocument;
// if document is loading, wait and try again
whenDocumentReady(function() {
watchImportsLoad(callback, doc);
}, doc);
}
// call the callback when the document is in a ready state (has dom)
var requiredReadyState = isIE ? 'complete' : 'interactive';
var READY_EVENT = 'readystatechange';
function isDocumentReady(doc) {
return (doc.readyState === 'complete' ||
doc.readyState === requiredReadyState);
}
// call when we ensure the document is in a ready state
function whenDocumentReady(callback, doc) {
if (!isDocumentReady(doc)) {
var checkReady = function() {
if (doc.readyState === 'complete' ||
doc.readyState === requiredReadyState) {
doc.removeEventListener(READY_EVENT, checkReady);
whenDocumentReady(callback, doc);
}
};
doc.addEventListener(READY_EVENT, checkReady);
} else if (callback) {
callback();
}
}
function markTargetLoaded(event) {
event.target.__loaded = true;
}
// call when we ensure all imports have loaded
function watchImportsLoad(callback, doc) {
var imports = doc.querySelectorAll('link[rel=import]');
var loaded = 0, l = imports.length;
function checkDone(d) {
if ((loaded == l) && callback) {
callback();
}
}
function loadedImport(e) {
markTargetLoaded(e);
loaded++;
checkDone();
}
if (l) {
for (var i=0, imp; (i>> 0 && s !== '';
}
function toNumber(s) {
return +s;
}
function isObject(obj) {
return obj === Object(obj);
}
var numberIsNaN = global.Number.isNaN || function(value) {
return typeof value === 'number' && global.isNaN(value);
}
function areSameValue(left, right) {
if (left === right)
return left !== 0 || 1 / left === 1 / right;
if (numberIsNaN(left) && numberIsNaN(right))
return true;
return left !== left && right !== right;
}
var createObject = ('__proto__' in {}) ?
function(obj) { return obj; } :
function(obj) {
var proto = obj.__proto__;
if (!proto)
return obj;
var newObject = Object.create(proto);
Object.getOwnPropertyNames(obj).forEach(function(name) {
Object.defineProperty(newObject, name,
Object.getOwnPropertyDescriptor(obj, name));
});
return newObject;
};
var identStart = '[\$_a-zA-Z]';
var identPart = '[\$_a-zA-Z0-9]';
var identRegExp = new RegExp('^' + identStart + '+' + identPart + '*' + '$');
function getPathCharType(char) {
if (char === undefined)
return 'eof';
var code = char.charCodeAt(0);
switch(code) {
case 0x5B: // [
case 0x5D: // ]
case 0x2E: // .
case 0x22: // "
case 0x27: // '
case 0x30: // 0
return char;
case 0x5F: // _
case 0x24: // $
return 'ident';
case 0x20: // Space
case 0x09: // Tab
case 0x0A: // Newline
case 0x0D: // Return
case 0xA0: // No-break space
case 0xFEFF: // Byte Order Mark
case 0x2028: // Line Separator
case 0x2029: // Paragraph Separator
return 'ws';
}
// a-z, A-Z
if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A))
return 'ident';
// 1-9
if (0x31 <= code && code <= 0x39)
return 'number';
return 'else';
}
var pathStateMachine = {
'beforePath': {
'ws': ['beforePath'],
'ident': ['inIdent', 'append'],
'[': ['beforeElement'],
'eof': ['afterPath']
},
'inPath': {
'ws': ['inPath'],
'.': ['beforeIdent'],
'[': ['beforeElement'],
'eof': ['afterPath']
},
'beforeIdent': {
'ws': ['beforeIdent'],
'ident': ['inIdent', 'append']
},
'inIdent': {
'ident': ['inIdent', 'append'],
'0': ['inIdent', 'append'],
'number': ['inIdent', 'append'],
'ws': ['inPath', 'push'],
'.': ['beforeIdent', 'push'],
'[': ['beforeElement', 'push'],
'eof': ['afterPath', 'push']
},
'beforeElement': {
'ws': ['beforeElement'],
'0': ['afterZero', 'append'],
'number': ['inIndex', 'append'],
"'": ['inSingleQuote', 'append', ''],
'"': ['inDoubleQuote', 'append', '']
},
'afterZero': {
'ws': ['afterElement', 'push'],
']': ['inPath', 'push']
},
'inIndex': {
'0': ['inIndex', 'append'],
'number': ['inIndex', 'append'],
'ws': ['afterElement'],
']': ['inPath', 'push']
},
'inSingleQuote': {
"'": ['afterElement'],
'eof': ['error'],
'else': ['inSingleQuote', 'append']
},
'inDoubleQuote': {
'"': ['afterElement'],
'eof': ['error'],
'else': ['inDoubleQuote', 'append']
},
'afterElement': {
'ws': ['afterElement'],
']': ['inPath', 'push']
}
}
function noop() {}
function parsePath(path) {
var keys = [];
var index = -1;
var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath';
var actions = {
push: function() {
if (key === undefined)
return;
keys.push(key);
key = undefined;
},
append: function() {
if (key === undefined)
key = newChar
else
key += newChar;
}
};
function maybeUnescapeQuote() {
if (index >= path.length)
return;
var nextChar = path[index + 1];
if ((mode == 'inSingleQuote' && nextChar == "'") ||
(mode == 'inDoubleQuote' && nextChar == '"')) {
index++;
newChar = nextChar;
actions.append();
return true;
}
}
while (mode) {
index++;
c = path[index];
if (c == '\\' && maybeUnescapeQuote(mode))
continue;
type = getPathCharType(c);
typeMap = pathStateMachine[mode];
transition = typeMap[type] || typeMap['else'] || 'error';
if (transition == 'error')
return; // parse error;
mode = transition[0];
action = actions[transition[1]] || noop;
newChar = transition[2] === undefined ? c : transition[2];
action();
if (mode === 'afterPath') {
return keys;
}
}
return; // parse error
}
function isIdent(s) {
return identRegExp.test(s);
}
var constructorIsPrivate = {};
function Path(parts, privateToken) {
if (privateToken !== constructorIsPrivate)
throw Error('Use Path.get to retrieve path objects');
for (var i = 0; i < parts.length; i++) {
this.push(String(parts[i]));
}
if (hasEval && this.length) {
this.getValueFrom = this.compiledGetValueFromFn();
}
}
// TODO(rafaelw): Make simple LRU cache
var pathCache = {};
function getPath(pathString) {
if (pathString instanceof Path)
return pathString;
if (pathString == null || pathString.length == 0)
pathString = '';
if (typeof pathString != 'string') {
if (isIndex(pathString.length)) {
// Constructed with array-like (pre-parsed) keys
return new Path(pathString, constructorIsPrivate);
}
pathString = String(pathString);
}
var path = pathCache[pathString];
if (path)
return path;
var parts = parsePath(pathString);
if (!parts)
return invalidPath;
var path = new Path(parts, constructorIsPrivate);
pathCache[pathString] = path;
return path;
}
Path.get = getPath;
function formatAccessor(key) {
if (isIndex(key)) {
return '[' + key + ']';
} else {
return '["' + key.replace(/"/g, '\\"') + '"]';
}
}
Path.prototype = createObject({
__proto__: [],
valid: true,
toString: function() {
var pathString = '';
for (var i = 0; i < this.length; i++) {
var key = this[i];
if (isIdent(key)) {
pathString += i ? '.' + key : key;
} else {
pathString += formatAccessor(key);
}
}
return pathString;
},
getValueFrom: function(obj, directObserver) {
for (var i = 0; i < this.length; i++) {
if (obj == null)
return;
obj = obj[this[i]];
}
return obj;
},
iterateObjects: function(obj, observe) {
for (var i = 0; i < this.length; i++) {
if (i)
obj = obj[this[i - 1]];
if (!isObject(obj))
return;
observe(obj, this[i]);
}
},
compiledGetValueFromFn: function() {
var str = '';
var pathString = 'obj';
str += 'if (obj != null';
var i = 0;
var key;
for (; i < (this.length - 1); i++) {
key = this[i];
pathString += isIdent(key) ? '.' + key : formatAccessor(key);
str += ' &&\n ' + pathString + ' != null';
}
str += ')\n';
var key = this[i];
pathString += isIdent(key) ? '.' + key : formatAccessor(key);
str += ' return ' + pathString + ';\nelse\n return undefined;';
return new Function('obj', str);
},
setValueFrom: function(obj, value) {
if (!this.length)
return false;
for (var i = 0; i < this.length - 1; i++) {
if (!isObject(obj))
return false;
obj = obj[this[i]];
}
if (!isObject(obj))
return false;
obj[this[i]] = value;
return true;
}
});
var invalidPath = new Path('', constructorIsPrivate);
invalidPath.valid = false;
invalidPath.getValueFrom = invalidPath.setValueFrom = function() {};
var MAX_DIRTY_CHECK_CYCLES = 1000;
function dirtyCheck(observer) {
var cycles = 0;
while (cycles < MAX_DIRTY_CHECK_CYCLES && observer.check_()) {
cycles++;
}
if (testingExposeCycleCount)
global.dirtyCheckCycleCount = cycles;
return cycles > 0;
}
function objectIsEmpty(object) {
for (var prop in object)
return false;
return true;
}
function diffIsEmpty(diff) {
return objectIsEmpty(diff.added) &&
objectIsEmpty(diff.removed) &&
objectIsEmpty(diff.changed);
}
function diffObjectFromOldObject(object, oldObject) {
var added = {};
var removed = {};
var changed = {};
for (var prop in oldObject) {
var newValue = object[prop];
if (newValue !== undefined && newValue === oldObject[prop])
continue;
if (!(prop in object)) {
removed[prop] = undefined;
continue;
}
if (newValue !== oldObject[prop])
changed[prop] = newValue;
}
for (var prop in object) {
if (prop in oldObject)
continue;
added[prop] = object[prop];
}
if (Array.isArray(object) && object.length !== oldObject.length)
changed.length = object.length;
return {
added: added,
removed: removed,
changed: changed
};
}
var eomTasks = [];
function runEOMTasks() {
if (!eomTasks.length)
return false;
for (var i = 0; i < eomTasks.length; i++) {
eomTasks[i]();
}
eomTasks.length = 0;
return true;
}
var runEOM = hasObserve ? (function(){
return function(fn) {
return Promise.resolve().then(fn);
}
})() :
(function() {
return function(fn) {
eomTasks.push(fn);
};
})();
var observedObjectCache = [];
function newObservedObject() {
var observer;
var object;
var discardRecords = false;
var first = true;
function callback(records) {
if (observer && observer.state_ === OPENED && !discardRecords)
observer.check_(records);
}
return {
open: function(obs) {
if (observer)
throw Error('ObservedObject in use');
if (!first)
Object.deliverChangeRecords(callback);
observer = obs;
first = false;
},
observe: function(obj, arrayObserve) {
object = obj;
if (arrayObserve)
Array.observe(object, callback);
else
Object.observe(object, callback);
},
deliver: function(discard) {
discardRecords = discard;
Object.deliverChangeRecords(callback);
discardRecords = false;
},
close: function() {
observer = undefined;
Object.unobserve(object, callback);
observedObjectCache.push(this);
}
};
}
/*
* The observedSet abstraction is a perf optimization which reduces the total
* number of Object.observe observations of a set of objects. The idea is that
* groups of Observers will have some object dependencies in common and this
* observed set ensures that each object in the transitive closure of
* dependencies is only observed once. The observedSet acts as a write barrier
* such that whenever any change comes through, all Observers are checked for
* changed values.
*
* Note that this optimization is explicitly moving work from setup-time to
* change-time.
*
* TODO(rafaelw): Implement "garbage collection". In order to move work off
* the critical path, when Observers are closed, their observed objects are
* not Object.unobserve(d). As a result, it's possible that if the observedSet
* is kept open, but some Observers have been closed, it could cause "leaks"
* (prevent otherwise collectable objects from being collected). At some
* point, we should implement incremental "gc" which keeps a list of
* observedSets which may need clean-up and does small amounts of cleanup on a
* timeout until all is clean.
*/
function getObservedObject(observer, object, arrayObserve) {
var dir = observedObjectCache.pop() || newObservedObject();
dir.open(observer);
dir.observe(object, arrayObserve);
return dir;
}
var observedSetCache = [];
function newObservedSet() {
var observerCount = 0;
var observers = [];
var objects = [];
var rootObj;
var rootObjProps;
function observe(obj, prop) {
if (!obj)
return;
if (obj === rootObj)
rootObjProps[prop] = true;
if (objects.indexOf(obj) < 0) {
objects.push(obj);
Object.observe(obj, callback);
}
observe(Object.getPrototypeOf(obj), prop);
}
function allRootObjNonObservedProps(recs) {
for (var i = 0; i < recs.length; i++) {
var rec = recs[i];
if (rec.object !== rootObj ||
rootObjProps[rec.name] ||
rec.type === 'setPrototype') {
return false;
}
}
return true;
}
function callback(recs) {
if (allRootObjNonObservedProps(recs))
return;
var observer;
for (var i = 0; i < observers.length; i++) {
observer = observers[i];
if (observer.state_ == OPENED) {
observer.iterateObjects_(observe);
}
}
for (var i = 0; i < observers.length; i++) {
observer = observers[i];
if (observer.state_ == OPENED) {
observer.check_();
}
}
}
var record = {
objects: objects,
get rootObject() { return rootObj; },
set rootObject(value) {
rootObj = value;
rootObjProps = {};
},
open: function(obs, object) {
observers.push(obs);
observerCount++;
obs.iterateObjects_(observe);
},
close: function(obs) {
observerCount--;
if (observerCount > 0) {
return;
}
for (var i = 0; i < objects.length; i++) {
Object.unobserve(objects[i], callback);
Observer.unobservedCount++;
}
observers.length = 0;
objects.length = 0;
rootObj = undefined;
rootObjProps = undefined;
observedSetCache.push(this);
if (lastObservedSet === this)
lastObservedSet = null;
},
};
return record;
}
var lastObservedSet;
function getObservedSet(observer, obj) {
if (!lastObservedSet || lastObservedSet.rootObject !== obj) {
lastObservedSet = observedSetCache.pop() || newObservedSet();
lastObservedSet.rootObject = obj;
}
lastObservedSet.open(observer, obj);
return lastObservedSet;
}
var UNOPENED = 0;
var OPENED = 1;
var CLOSED = 2;
var RESETTING = 3;
var nextObserverId = 1;
function Observer() {
this.state_ = UNOPENED;
this.callback_ = undefined;
this.target_ = undefined; // TODO(rafaelw): Should be WeakRef
this.directObserver_ = undefined;
this.value_ = undefined;
this.id_ = nextObserverId++;
}
Observer.prototype = {
open: function(callback, target) {
if (this.state_ != UNOPENED)
throw Error('Observer has already been opened.');
addToAll(this);
this.callback_ = callback;
this.target_ = target;
this.connect_();
this.state_ = OPENED;
return this.value_;
},
close: function() {
if (this.state_ != OPENED)
return;
removeFromAll(this);
this.disconnect_();
this.value_ = undefined;
this.callback_ = undefined;
this.target_ = undefined;
this.state_ = CLOSED;
},
deliver: function() {
if (this.state_ != OPENED)
return;
dirtyCheck(this);
},
report_: function(changes) {
try {
this.callback_.apply(this.target_, changes);
} catch (ex) {
Observer._errorThrownDuringCallback = true;
console.error('Exception caught during observer callback: ' +
(ex.stack || ex));
}
},
discardChanges: function() {
this.check_(undefined, true);
return this.value_;
}
}
var collectObservers = !hasObserve;
var allObservers;
Observer._allObserversCount = 0;
if (collectObservers) {
allObservers = [];
}
function addToAll(observer) {
Observer._allObserversCount++;
if (!collectObservers)
return;
allObservers.push(observer);
}
function removeFromAll(observer) {
Observer._allObserversCount--;
}
var runningMicrotaskCheckpoint = false;
global.Platform = global.Platform || {};
global.Platform.performMicrotaskCheckpoint = function() {
if (runningMicrotaskCheckpoint)
return;
if (!collectObservers)
return;
runningMicrotaskCheckpoint = true;
var cycles = 0;
var anyChanged, toCheck;
do {
cycles++;
toCheck = allObservers;
allObservers = [];
anyChanged = false;
for (var i = 0; i < toCheck.length; i++) {
var observer = toCheck[i];
if (observer.state_ != OPENED)
continue;
if (observer.check_())
anyChanged = true;
allObservers.push(observer);
}
if (runEOMTasks())
anyChanged = true;
} while (cycles < MAX_DIRTY_CHECK_CYCLES && anyChanged);
if (testingExposeCycleCount)
global.dirtyCheckCycleCount = cycles;
runningMicrotaskCheckpoint = false;
};
if (collectObservers) {
global.Platform.clearObservers = function() {
allObservers = [];
};
}
function ObjectObserver(object) {
Observer.call(this);
this.value_ = object;
this.oldObject_ = undefined;
}
ObjectObserver.prototype = createObject({
__proto__: Observer.prototype,
arrayObserve: false,
connect_: function(callback, target) {
if (hasObserve) {
this.directObserver_ = getObservedObject(this, this.value_,
this.arrayObserve);
} else {
this.oldObject_ = this.copyObject(this.value_);
}
},
copyObject: function(object) {
var copy = Array.isArray(object) ? [] : {};
for (var prop in object) {
copy[prop] = object[prop];
};
if (Array.isArray(object))
copy.length = object.length;
return copy;
},
check_: function(changeRecords, skipChanges) {
var diff;
var oldValues;
if (hasObserve) {
if (!changeRecords)
return false;
oldValues = {};
diff = diffObjectFromChangeRecords(this.value_, changeRecords,
oldValues);
} else {
oldValues = this.oldObject_;
diff = diffObjectFromOldObject(this.value_, this.oldObject_);
}
if (diffIsEmpty(diff))
return false;
if (!hasObserve)
this.oldObject_ = this.copyObject(this.value_);
this.report_([
diff.added || {},
diff.removed || {},
diff.changed || {},
function(property) {
return oldValues[property];
}
]);
return true;
},
disconnect_: function() {
if (hasObserve) {
this.directObserver_.close();
this.directObserver_ = undefined;
} else {
this.oldObject_ = undefined;
}
},
deliver: function() {
if (this.state_ != OPENED)
return;
if (hasObserve)
this.directObserver_.deliver(false);
else
dirtyCheck(this);
},
discardChanges: function() {
if (this.directObserver_)
this.directObserver_.deliver(true);
else
this.oldObject_ = this.copyObject(this.value_);
return this.value_;
}
});
function ArrayObserver(array) {
if (!Array.isArray(array))
throw Error('Provided object is not an Array');
ObjectObserver.call(this, array);
}
ArrayObserver.prototype = createObject({
__proto__: ObjectObserver.prototype,
arrayObserve: true,
copyObject: function(arr) {
return arr.slice();
},
check_: function(changeRecords) {
var splices;
if (hasObserve) {
if (!changeRecords)
return false;
splices = projectArraySplices(this.value_, changeRecords);
} else {
splices = calcSplices(this.value_, 0, this.value_.length,
this.oldObject_, 0, this.oldObject_.length);
}
if (!splices || !splices.length)
return false;
if (!hasObserve)
this.oldObject_ = this.copyObject(this.value_);
this.report_([splices]);
return true;
}
});
ArrayObserver.applySplices = function(previous, current, splices) {
splices.forEach(function(splice) {
var spliceArgs = [splice.index, splice.removed.length];
var addIndex = splice.index;
while (addIndex < splice.index + splice.addedCount) {
spliceArgs.push(current[addIndex]);
addIndex++;
}
Array.prototype.splice.apply(previous, spliceArgs);
});
};
function PathObserver(object, path) {
Observer.call(this);
this.object_ = object;
this.path_ = getPath(path);
this.directObserver_ = undefined;
}
PathObserver.prototype = createObject({
__proto__: Observer.prototype,
get path() {
return this.path_;
},
connect_: function() {
if (hasObserve)
this.directObserver_ = getObservedSet(this, this.object_);
this.check_(undefined, true);
},
disconnect_: function() {
this.value_ = undefined;
if (this.directObserver_) {
this.directObserver_.close(this);
this.directObserver_ = undefined;
}
},
iterateObjects_: function(observe) {
this.path_.iterateObjects(this.object_, observe);
},
check_: function(changeRecords, skipChanges) {
var oldValue = this.value_;
this.value_ = this.path_.getValueFrom(this.object_);
if (skipChanges || areSameValue(this.value_, oldValue))
return false;
this.report_([this.value_, oldValue, this]);
return true;
},
setValue: function(newValue) {
if (this.path_)
this.path_.setValueFrom(this.object_, newValue);
}
});
function CompoundObserver(reportChangesOnOpen) {
Observer.call(this);
this.reportChangesOnOpen_ = reportChangesOnOpen;
this.value_ = [];
this.directObserver_ = undefined;
this.observed_ = [];
}
var observerSentinel = {};
CompoundObserver.prototype = createObject({
__proto__: Observer.prototype,
connect_: function() {
if (hasObserve) {
var object;
var needsDirectObserver = false;
for (var i = 0; i < this.observed_.length; i += 2) {
object = this.observed_[i]
if (object !== observerSentinel) {
needsDirectObserver = true;
break;
}
}
if (needsDirectObserver)
this.directObserver_ = getObservedSet(this, object);
}
this.check_(undefined, !this.reportChangesOnOpen_);
},
disconnect_: function() {
for (var i = 0; i < this.observed_.length; i += 2) {
if (this.observed_[i] === observerSentinel)
this.observed_[i + 1].close();
}
this.observed_.length = 0;
this.value_.length = 0;
if (this.directObserver_) {
this.directObserver_.close(this);
this.directObserver_ = undefined;
}
},
addPath: function(object, path) {
if (this.state_ != UNOPENED && this.state_ != RESETTING)
throw Error('Cannot add paths once started.');
var path = getPath(path);
this.observed_.push(object, path);
if (!this.reportChangesOnOpen_)
return;
var index = this.observed_.length / 2 - 1;
this.value_[index] = path.getValueFrom(object);
},
addObserver: function(observer) {
if (this.state_ != UNOPENED && this.state_ != RESETTING)
throw Error('Cannot add observers once started.');
this.observed_.push(observerSentinel, observer);
if (!this.reportChangesOnOpen_)
return;
var index = this.observed_.length / 2 - 1;
this.value_[index] = observer.open(this.deliver, this);
},
startReset: function() {
if (this.state_ != OPENED)
throw Error('Can only reset while open');
this.state_ = RESETTING;
this.disconnect_();
},
finishReset: function() {
if (this.state_ != RESETTING)
throw Error('Can only finishReset after startReset');
this.state_ = OPENED;
this.connect_();
return this.value_;
},
iterateObjects_: function(observe) {
var object;
for (var i = 0; i < this.observed_.length; i += 2) {
object = this.observed_[i]
if (object !== observerSentinel)
this.observed_[i + 1].iterateObjects(object, observe)
}
},
check_: function(changeRecords, skipChanges) {
var oldValues;
for (var i = 0; i < this.observed_.length; i += 2) {
var object = this.observed_[i];
var path = this.observed_[i+1];
var value;
if (object === observerSentinel) {
var observable = path;
value = this.state_ === UNOPENED ?
observable.open(this.deliver, this) :
observable.discardChanges();
} else {
value = path.getValueFrom(object);
}
if (skipChanges) {
this.value_[i / 2] = value;
continue;
}
if (areSameValue(value, this.value_[i / 2]))
continue;
oldValues = oldValues || [];
oldValues[i / 2] = this.value_[i / 2];
this.value_[i / 2] = value;
}
if (!oldValues)
return false;
// TODO(rafaelw): Having observed_ as the third callback arg here is
// pretty lame API. Fix.
this.report_([this.value_, oldValues, this.observed_]);
return true;
}
});
function identFn(value) { return value; }
function ObserverTransform(observable, getValueFn, setValueFn,
dontPassThroughSet) {
this.callback_ = undefined;
this.target_ = undefined;
this.value_ = undefined;
this.observable_ = observable;
this.getValueFn_ = getValueFn || identFn;
this.setValueFn_ = setValueFn || identFn;
// TODO(rafaelw): This is a temporary hack. PolymerExpressions needs this
// at the moment because of a bug in it's dependency tracking.
this.dontPassThroughSet_ = dontPassThroughSet;
}
ObserverTransform.prototype = {
open: function(callback, target) {
this.callback_ = callback;
this.target_ = target;
this.value_ =
this.getValueFn_(this.observable_.open(this.observedCallback_, this));
return this.value_;
},
observedCallback_: function(value) {
value = this.getValueFn_(value);
if (areSameValue(value, this.value_))
return;
var oldValue = this.value_;
this.value_ = value;
this.callback_.call(this.target_, this.value_, oldValue);
},
discardChanges: function() {
this.value_ = this.getValueFn_(this.observable_.discardChanges());
return this.value_;
},
deliver: function() {
return this.observable_.deliver();
},
setValue: function(value) {
value = this.setValueFn_(value);
if (!this.dontPassThroughSet_ && this.observable_.setValue)
return this.observable_.setValue(value);
},
close: function() {
if (this.observable_)
this.observable_.close();
this.callback_ = undefined;
this.target_ = undefined;
this.observable_ = undefined;
this.value_ = undefined;
this.getValueFn_ = undefined;
this.setValueFn_ = undefined;
}
}
var expectedRecordTypes = {
add: true,
update: true,
delete: true
};
function diffObjectFromChangeRecords(object, changeRecords, oldValues) {
var added = {};
var removed = {};
for (var i = 0; i < changeRecords.length; i++) {
var record = changeRecords[i];
if (!expectedRecordTypes[record.type]) {
console.error('Unknown changeRecord type: ' + record.type);
console.error(record);
continue;
}
if (!(record.name in oldValues))
oldValues[record.name] = record.oldValue;
if (record.type == 'update')
continue;
if (record.type == 'add') {
if (record.name in removed)
delete removed[record.name];
else
added[record.name] = true;
continue;
}
// type = 'delete'
if (record.name in added) {
delete added[record.name];
delete oldValues[record.name];
} else {
removed[record.name] = true;
}
}
for (var prop in added)
added[prop] = object[prop];
for (var prop in removed)
removed[prop] = undefined;
var changed = {};
for (var prop in oldValues) {
if (prop in added || prop in removed)
continue;
var newValue = object[prop];
if (oldValues[prop] !== newValue)
changed[prop] = newValue;
}
return {
added: added,
removed: removed,
changed: changed
};
}
function newSplice(index, removed, addedCount) {
return {
index: index,
removed: removed,
addedCount: addedCount
};
}
var EDIT_LEAVE = 0;
var EDIT_UPDATE = 1;
var EDIT_ADD = 2;
var EDIT_DELETE = 3;
function ArraySplice() {}
ArraySplice.prototype = {
// Note: This function is *based* on the computation of the Levenshtein
// "edit" distance. The one change is that "updates" are treated as two
// edits - not one. With Array splices, an update is really a delete
// followed by an add. By retaining this, we optimize for "keeping" the
// maximum array items in the original array. For example:
//
// 'xxxx123' -> '123yyyy'
//
// With 1-edit updates, the shortest path would be just to update all seven
// characters. With 2-edit updates, we delete 4, leave 3, and add 4. This
// leaves the substring '123' intact.
calcEditDistances: function(current, currentStart, currentEnd,
old, oldStart, oldEnd) {
// "Deletion" columns
var rowCount = oldEnd - oldStart + 1;
var columnCount = currentEnd - currentStart + 1;
var distances = new Array(rowCount);
// "Addition" rows. Initialize null column.
for (var i = 0; i < rowCount; i++) {
distances[i] = new Array(columnCount);
distances[i][0] = i;
}
// Initialize null row
for (var j = 0; j < columnCount; j++)
distances[0][j] = j;
for (var i = 1; i < rowCount; i++) {
for (var j = 1; j < columnCount; j++) {
if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1]))
distances[i][j] = distances[i - 1][j - 1];
else {
var north = distances[i - 1][j] + 1;
var west = distances[i][j - 1] + 1;
distances[i][j] = north < west ? north : west;
}
}
}
return distances;
},
// This starts at the final weight, and walks "backward" by finding
// the minimum previous weight recursively until the origin of the weight
// matrix.
spliceOperationsFromEditDistances: function(distances) {
var i = distances.length - 1;
var j = distances[0].length - 1;
var current = distances[i][j];
var edits = [];
while (i > 0 || j > 0) {
if (i == 0) {
edits.push(EDIT_ADD);
j--;
continue;
}
if (j == 0) {
edits.push(EDIT_DELETE);
i--;
continue;
}
var northWest = distances[i - 1][j - 1];
var west = distances[i - 1][j];
var north = distances[i][j - 1];
var min;
if (west < north)
min = west < northWest ? west : northWest;
else
min = north < northWest ? north : northWest;
if (min == northWest) {
if (northWest == current) {
edits.push(EDIT_LEAVE);
} else {
edits.push(EDIT_UPDATE);
current = northWest;
}
i--;
j--;
} else if (min == west) {
edits.push(EDIT_DELETE);
i--;
current = west;
} else {
edits.push(EDIT_ADD);
j--;
current = north;
}
}
edits.reverse();
return edits;
},
/**
* Splice Projection functions:
*
* A splice map is a representation of how a previous array of items
* was transformed into a new array of items. Conceptually it is a list of
* tuples of
*
*
*
* which are kept in ascending index order of. The tuple represents that at
* the |index|, |removed| sequence of items were removed, and counting forward
* from |index|, |addedCount| items were added.
*/
/**
* Lacking individual splice mutation information, the minimal set of
* splices can be synthesized given the previous state and final state of an
* array. The basic approach is to calculate the edit distance matrix and
* choose the shortest path through it.
*
* Complexity: O(l * p)
* l: The length of the current array
* p: The length of the old array
*/
calcSplices: function(current, currentStart, currentEnd,
old, oldStart, oldEnd) {
var prefixCount = 0;
var suffixCount = 0;
var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart);
if (currentStart == 0 && oldStart == 0)
prefixCount = this.sharedPrefix(current, old, minLength);
if (currentEnd == current.length && oldEnd == old.length)
suffixCount = this.sharedSuffix(current, old, minLength - prefixCount);
currentStart += prefixCount;
oldStart += prefixCount;
currentEnd -= suffixCount;
oldEnd -= suffixCount;
if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0)
return [];
if (currentStart == currentEnd) {
var splice = newSplice(currentStart, [], 0);
while (oldStart < oldEnd)
splice.removed.push(old[oldStart++]);
return [ splice ];
} else if (oldStart == oldEnd)
return [ newSplice(currentStart, [], currentEnd - currentStart) ];
var ops = this.spliceOperationsFromEditDistances(
this.calcEditDistances(current, currentStart, currentEnd,
old, oldStart, oldEnd));
var splice = undefined;
var splices = [];
var index = currentStart;
var oldIndex = oldStart;
for (var i = 0; i < ops.length; i++) {
switch(ops[i]) {
case EDIT_LEAVE:
if (splice) {
splices.push(splice);
splice = undefined;
}
index++;
oldIndex++;
break;
case EDIT_UPDATE:
if (!splice)
splice = newSplice(index, [], 0);
splice.addedCount++;
index++;
splice.removed.push(old[oldIndex]);
oldIndex++;
break;
case EDIT_ADD:
if (!splice)
splice = newSplice(index, [], 0);
splice.addedCount++;
index++;
break;
case EDIT_DELETE:
if (!splice)
splice = newSplice(index, [], 0);
splice.removed.push(old[oldIndex]);
oldIndex++;
break;
}
}
if (splice) {
splices.push(splice);
}
return splices;
},
sharedPrefix: function(current, old, searchLength) {
for (var i = 0; i < searchLength; i++)
if (!this.equals(current[i], old[i]))
return i;
return searchLength;
},
sharedSuffix: function(current, old, searchLength) {
var index1 = current.length;
var index2 = old.length;
var count = 0;
while (count < searchLength && this.equals(current[--index1], old[--index2]))
count++;
return count;
},
calculateSplices: function(current, previous) {
return this.calcSplices(current, 0, current.length, previous, 0,
previous.length);
},
equals: function(currentValue, previousValue) {
return currentValue === previousValue;
}
};
var arraySplice = new ArraySplice();
function calcSplices(current, currentStart, currentEnd,
old, oldStart, oldEnd) {
return arraySplice.calcSplices(current, currentStart, currentEnd,
old, oldStart, oldEnd);
}
function intersect(start1, end1, start2, end2) {
// Disjoint
if (end1 < start2 || end2 < start1)
return -1;
// Adjacent
if (end1 == start2 || end2 == start1)
return 0;
// Non-zero intersect, span1 first
if (start1 < start2) {
if (end1 < end2)
return end1 - start2; // Overlap
else
return end2 - start2; // Contained
} else {
// Non-zero intersect, span2 first
if (end2 < end1)
return end2 - start1; // Overlap
else
return end1 - start1; // Contained
}
}
function mergeSplice(splices, index, removed, addedCount) {
var splice = newSplice(index, removed, addedCount);
var inserted = false;
var insertionOffset = 0;
for (var i = 0; i < splices.length; i++) {
var current = splices[i];
current.index += insertionOffset;
if (inserted)
continue;
var intersectCount = intersect(splice.index,
splice.index + splice.removed.length,
current.index,
current.index + current.addedCount);
if (intersectCount >= 0) {
// Merge the two splices
splices.splice(i, 1);
i--;
insertionOffset -= current.addedCount - current.removed.length;
splice.addedCount += current.addedCount - intersectCount;
var deleteCount = splice.removed.length +
current.removed.length - intersectCount;
if (!splice.addedCount && !deleteCount) {
// merged splice is a noop. discard.
inserted = true;
} else {
var removed = current.removed;
if (splice.index < current.index) {
// some prefix of splice.removed is prepended to current.removed.
var prepend = splice.removed.slice(0, current.index - splice.index);
Array.prototype.push.apply(prepend, removed);
removed = prepend;
}
if (splice.index + splice.removed.length > current.index + current.addedCount) {
// some suffix of splice.removed is appended to current.removed.
var append = splice.removed.slice(current.index + current.addedCount - splice.index);
Array.prototype.push.apply(removed, append);
}
splice.removed = removed;
if (current.index < splice.index) {
splice.index = current.index;
}
}
} else if (splice.index < current.index) {
// Insert splice here.
inserted = true;
splices.splice(i, 0, splice);
i++;
var offset = splice.addedCount - splice.removed.length
current.index += offset;
insertionOffset += offset;
}
}
if (!inserted)
splices.push(splice);
}
function createInitialSplices(array, changeRecords) {
var splices = [];
for (var i = 0; i < changeRecords.length; i++) {
var record = changeRecords[i];
switch(record.type) {
case 'splice':
mergeSplice(splices, record.index, record.removed.slice(), record.addedCount);
break;
case 'add':
case 'update':
case 'delete':
if (!isIndex(record.name))
continue;
var index = toNumber(record.name);
if (index < 0)
continue;
mergeSplice(splices, index, [record.oldValue], 1);
break;
default:
console.error('Unexpected record type: ' + JSON.stringify(record));
break;
}
}
return splices;
}
function projectArraySplices(array, changeRecords) {
var splices = [];
createInitialSplices(array, changeRecords).forEach(function(splice) {
if (splice.addedCount == 1 && splice.removed.length == 1) {
if (splice.removed[0] !== array[splice.index])
splices.push(splice);
return
};
splices = splices.concat(calcSplices(array, splice.index, splice.index + splice.addedCount,
splice.removed, 0, splice.removed.length));
});
return splices;
}
// Export the observe-js object for **Node.js**, with
// backwards-compatibility for the old `require()` API. If we're in
// the browser, export as a global object.
var expose = global;
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
expose = exports = module.exports;
}
expose = exports;
}
expose.Observer = Observer;
expose.Observer.runEOM_ = runEOM;
expose.Observer.observerSentinel_ = observerSentinel; // for testing.
expose.Observer.hasObjectObserve = hasObserve;
expose.ArrayObserver = ArrayObserver;
expose.ArrayObserver.calculateSplices = function(current, previous) {
return arraySplice.calculateSplices(current, previous);
};
expose.ArraySplice = ArraySplice;
expose.ObjectObserver = ObjectObserver;
expose.PathObserver = PathObserver;
expose.CompoundObserver = CompoundObserver;
expose.Path = Path;
expose.ObserverTransform = ObserverTransform;
})(typeof global !== 'undefined' && global && typeof module !== 'undefined' && module ? global : this || window);
// Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
// This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
// The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
// The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
// Code distributed by Google as part of the polymer project is also
// subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
(function(global) {
'use strict';
var filter = Array.prototype.filter.call.bind(Array.prototype.filter);
function getTreeScope(node) {
while (node.parentNode) {
node = node.parentNode;
}
return typeof node.getElementById === 'function' ? node : null;
}
Node.prototype.bind = function(name, observable) {
console.error('Unhandled binding to Node: ', this, name, observable);
};
Node.prototype.bindFinished = function() {};
function updateBindings(node, name, binding) {
var bindings = node.bindings_;
if (!bindings)
bindings = node.bindings_ = {};
if (bindings[name])
binding[name].close();
return bindings[name] = binding;
}
function returnBinding(node, name, binding) {
return binding;
}
function sanitizeValue(value) {
return value == null ? '' : value;
}
function updateText(node, value) {
node.data = sanitizeValue(value);
}
function textBinding(node) {
return function(value) {
return updateText(node, value);
};
}
var maybeUpdateBindings = returnBinding;
Object.defineProperty(Platform, 'enableBindingsReflection', {
get: function() {
return maybeUpdateBindings === updateBindings;
},
set: function(enable) {
maybeUpdateBindings = enable ? updateBindings : returnBinding;
return enable;
},
configurable: true
});
Text.prototype.bind = function(name, value, oneTime) {
if (name !== 'textContent')
return Node.prototype.bind.call(this, name, value, oneTime);
if (oneTime)
return updateText(this, value);
var observable = value;
updateText(this, observable.open(textBinding(this)));
return maybeUpdateBindings(this, name, observable);
}
function updateAttribute(el, name, conditional, value) {
if (conditional) {
if (value)
el.setAttribute(name, '');
else
el.removeAttribute(name);
return;
}
el.setAttribute(name, sanitizeValue(value));
}
function attributeBinding(el, name, conditional) {
return function(value) {
updateAttribute(el, name, conditional, value);
};
}
Element.prototype.bind = function(name, value, oneTime) {
var conditional = name[name.length - 1] == '?';
if (conditional) {
this.removeAttribute(name);
name = name.slice(0, -1);
}
if (oneTime)
return updateAttribute(this, name, conditional, value);
var observable = value;
updateAttribute(this, name, conditional,
observable.open(attributeBinding(this, name, conditional)));
return maybeUpdateBindings(this, name, observable);
};
var checkboxEventType;
(function() {
// Attempt to feature-detect which event (change or click) is fired first
// for checkboxes.
var div = document.createElement('div');
var checkbox = div.appendChild(document.createElement('input'));
checkbox.setAttribute('type', 'checkbox');
var first;
var count = 0;
checkbox.addEventListener('click', function(e) {
count++;
first = first || 'click';
});
checkbox.addEventListener('change', function() {
count++;
first = first || 'change';
});
var event = document.createEvent('MouseEvent');
event.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false,
false, false, false, 0, null);
checkbox.dispatchEvent(event);
// WebKit/Blink don't fire the change event if the element is outside the
// document, so assume 'change' for that case.
checkboxEventType = count == 1 ? 'change' : first;
})();
function getEventForInputType(element) {
switch (element.type) {
case 'checkbox':
return checkboxEventType;
case 'radio':
case 'select-multiple':
case 'select-one':
return 'change';
case 'range':
if (/Trident|MSIE/.test(navigator.userAgent))
return 'change';
default:
return 'input';
}
}
function updateInput(input, property, value, santizeFn) {
input[property] = (santizeFn || sanitizeValue)(value);
}
function inputBinding(input, property, santizeFn) {
return function(value) {
return updateInput(input, property, value, santizeFn);
}
}
function noop() {}
function bindInputEvent(input, property, observable, postEventFn) {
var eventType = getEventForInputType(input);
function eventHandler() {
observable.setValue(input[property]);
observable.discardChanges();
(postEventFn || noop)(input);
Platform.performMicrotaskCheckpoint();
}
input.addEventListener(eventType, eventHandler);
return {
close: function() {
input.removeEventListener(eventType, eventHandler);
observable.close();
},
observable_: observable
}
}
function booleanSanitize(value) {
return Boolean(value);
}
// |element| is assumed to be an HTMLInputElement with |type| == 'radio'.
// Returns an array containing all radio buttons other than |element| that
// have the same |name|, either in the form that |element| belongs to or,
// if no form, in the document tree to which |element| belongs.
//
// This implementation is based upon the HTML spec definition of a
// "radio button group":
// http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.html#radio-button-group
//
function getAssociatedRadioButtons(element) {
if (element.form) {
return filter(element.form.elements, function(el) {
return el != element &&
el.tagName == 'INPUT' &&
el.type == 'radio' &&
el.name == element.name;
});
} else {
var treeScope = getTreeScope(element);
if (!treeScope)
return [];
var radios = treeScope.querySelectorAll(
'input[type="radio"][name="' + element.name + '"]');
return filter(radios, function(el) {
return el != element && !el.form;
});
}
}
function checkedPostEvent(input) {
// Only the radio button that is getting checked gets an event. We
// therefore find all the associated radio buttons and update their
// check binding manually.
if (input.tagName === 'INPUT' &&
input.type === 'radio') {
getAssociatedRadioButtons(input).forEach(function(radio) {
var checkedBinding = radio.bindings_.checked;
if (checkedBinding) {
// Set the value directly to avoid an infinite call stack.
checkedBinding.observable_.setValue(false);
}
});
}
}
HTMLInputElement.prototype.bind = function(name, value, oneTime) {
if (name !== 'value' && name !== 'checked')
return HTMLElement.prototype.bind.call(this, name, value, oneTime);
this.removeAttribute(name);
var sanitizeFn = name == 'checked' ? booleanSanitize : sanitizeValue;
var postEventFn = name == 'checked' ? checkedPostEvent : noop;
if (oneTime)
return updateInput(this, name, value, sanitizeFn);
var observable = value;
var binding = bindInputEvent(this, name, observable, postEventFn);
updateInput(this, name,
observable.open(inputBinding(this, name, sanitizeFn)),
sanitizeFn);
// Checkboxes may need to update bindings of other checkboxes.
return updateBindings(this, name, binding);
}
HTMLTextAreaElement.prototype.bind = function(name, value, oneTime) {
if (name !== 'value')
return HTMLElement.prototype.bind.call(this, name, value, oneTime);
this.removeAttribute('value');
if (oneTime)
return updateInput(this, 'value', value);
var observable = value;
var binding = bindInputEvent(this, 'value', observable);
updateInput(this, 'value',
observable.open(inputBinding(this, 'value', sanitizeValue)));
return maybeUpdateBindings(this, name, binding);
}
function updateOption(option, value) {
var parentNode = option.parentNode;;
var select;
var selectBinding;
var oldValue;
if (parentNode instanceof HTMLSelectElement &&
parentNode.bindings_ &&
parentNode.bindings_.value) {
select = parentNode;
selectBinding = select.bindings_.value;
oldValue = select.value;
}
option.value = sanitizeValue(value);
if (select && select.value != oldValue) {
selectBinding.observable_.setValue(select.value);
selectBinding.observable_.discardChanges();
Platform.performMicrotaskCheckpoint();
}
}
function optionBinding(option) {
return function(value) {
updateOption(option, value);
}
}
HTMLOptionElement.prototype.bind = function(name, value, oneTime) {
if (name !== 'value')
return HTMLElement.prototype.bind.call(this, name, value, oneTime);
this.removeAttribute('value');
if (oneTime)
return updateOption(this, value);
var observable = value;
var binding = bindInputEvent(this, 'value', observable);
updateOption(this, observable.open(optionBinding(this)));
return maybeUpdateBindings(this, name, binding);
}
HTMLSelectElement.prototype.bind = function(name, value, oneTime) {
if (name === 'selectedindex')
name = 'selectedIndex';
if (name !== 'selectedIndex' && name !== 'value')
return HTMLElement.prototype.bind.call(this, name, value, oneTime);
this.removeAttribute(name);
if (oneTime)
return updateInput(this, name, value);
var observable = value;
var binding = bindInputEvent(this, name, observable);
updateInput(this, name,
observable.open(inputBinding(this, name)));
// Option update events may need to access select bindings.
return updateBindings(this, name, binding);
}
})(this);
// Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
// This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
// The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
// The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
// Code distributed by Google as part of the polymer project is also
// subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
(function(global) {
'use strict';
function assert(v) {
if (!v)
throw new Error('Assertion failed');
}
var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);
function getFragmentRoot(node) {
var p;
while (p = node.parentNode) {
node = p;
}
return node;
}
function searchRefId(node, id) {
if (!id)
return;
var ref;
var selector = '#' + id;
while (!ref) {
node = getFragmentRoot(node);
if (node.protoContent_)
ref = node.protoContent_.querySelector(selector);
else if (node.getElementById)
ref = node.getElementById(id);
if (ref || !node.templateCreator_)
break
node = node.templateCreator_;
}
return ref;
}
function getInstanceRoot(node) {
while (node.parentNode) {
node = node.parentNode;
}
return node.templateCreator_ ? node : null;
}
var Map;
if (global.Map && typeof global.Map.prototype.forEach === 'function') {
Map = global.Map;
} else {
Map = function() {
this.keys = [];
this.values = [];
};
Map.prototype = {
set: function(key, value) {
var index = this.keys.indexOf(key);
if (index < 0) {
this.keys.push(key);
this.values.push(value);
} else {
this.values[index] = value;
}
},
get: function(key) {
var index = this.keys.indexOf(key);
if (index < 0)
return;
return this.values[index];
},
delete: function(key, value) {
var index = this.keys.indexOf(key);
if (index < 0)
return false;
this.keys.splice(index, 1);
this.values.splice(index, 1);
return true;
},
forEach: function(f, opt_this) {
for (var i = 0; i < this.keys.length; i++)
f.call(opt_this || this, this.values[i], this.keys[i], this);
}
};
}
// JScript does not have __proto__. We wrap all object literals with
// createObject which uses Object.create, Object.defineProperty and
// Object.getOwnPropertyDescriptor to create a new object that does the exact
// same thing. The main downside to this solution is that we have to extract
// all those property descriptors for IE.
var createObject = ('__proto__' in {}) ?
function(obj) { return obj; } :
function(obj) {
var proto = obj.__proto__;
if (!proto)
return obj;
var newObject = Object.create(proto);
Object.getOwnPropertyNames(obj).forEach(function(name) {
Object.defineProperty(newObject, name,
Object.getOwnPropertyDescriptor(obj, name));
});
return newObject;
};
// IE does not support have Document.prototype.contains.
if (typeof document.contains != 'function') {
Document.prototype.contains = function(node) {
if (node === this || node.parentNode === this)
return true;
return this.documentElement.contains(node);
}
}
var BIND = 'bind';
var REPEAT = 'repeat';
var IF = 'if';
var templateAttributeDirectives = {
'template': true,
'repeat': true,
'bind': true,
'ref': true
};
var semanticTemplateElements = {
'THEAD': true,
'TBODY': true,
'TFOOT': true,
'TH': true,
'TR': true,
'TD': true,
'COLGROUP': true,
'COL': true,
'CAPTION': true,
'OPTION': true,
'OPTGROUP': true
};
var hasTemplateElement = typeof HTMLTemplateElement !== 'undefined';
if (hasTemplateElement) {
// TODO(rafaelw): Remove when fix for
// https://codereview.chromium.org/164803002/
// makes it to Chrome release.
(function() {
var t = document.createElement('template');
var d = t.content.ownerDocument;
var html = d.appendChild(d.createElement('html'));
var head = html.appendChild(d.createElement('head'));
var base = d.createElement('base');
base.href = document.baseURI;
head.appendChild(base);
})();
}
var allTemplatesSelectors = 'template, ' +
Object.keys(semanticTemplateElements).map(function(tagName) {
return tagName.toLowerCase() + '[template]';
}).join(', ');
function isSVGTemplate(el) {
return el.tagName == 'template' &&
el.namespaceURI == 'http://www.w3.org/2000/svg';
}
function isHTMLTemplate(el) {
return el.tagName == 'TEMPLATE' &&
el.namespaceURI == 'http://www.w3.org/1999/xhtml';
}
function isAttributeTemplate(el) {
return Boolean(semanticTemplateElements[el.tagName] &&
el.hasAttribute('template'));
}
function isTemplate(el) {
if (el.isTemplate_ === undefined)
el.isTemplate_ = el.tagName == 'TEMPLATE' || isAttributeTemplate(el);
return el.isTemplate_;
}
// FIXME: Observe templates being added/removed from documents
// FIXME: Expose imperative API to decorate and observe templates in
// "disconnected tress" (e.g. ShadowRoot)
document.addEventListener('DOMContentLoaded', function(e) {
bootstrapTemplatesRecursivelyFrom(document);
// FIXME: Is this needed? Seems like it shouldn't be.
Platform.performMicrotaskCheckpoint();
}, false);
function forAllTemplatesFrom(node, fn) {
var subTemplates = node.querySelectorAll(allTemplatesSelectors);
if (isTemplate(node))
fn(node)
forEach(subTemplates, fn);
}
function bootstrapTemplatesRecursivelyFrom(node) {
function bootstrap(template) {
if (!HTMLTemplateElement.decorate(template))
bootstrapTemplatesRecursivelyFrom(template.content);
}
forAllTemplatesFrom(node, bootstrap);
}
if (!hasTemplateElement) {
/**
* This represents a element.
* @constructor
* @extends {HTMLElement}
*/
global.HTMLTemplateElement = function() {
throw TypeError('Illegal constructor');
};
}
var hasProto = '__proto__' in {};
function mixin(to, from) {
Object.getOwnPropertyNames(from).forEach(function(name) {
Object.defineProperty(to, name,
Object.getOwnPropertyDescriptor(from, name));
});
}
// http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner
function getOrCreateTemplateContentsOwner(template) {
var doc = template.ownerDocument
if (!doc.defaultView)
return doc;
var d = doc.templateContentsOwner_;
if (!d) {
// TODO(arv): This should either be a Document or HTMLDocument depending
// on doc.
d = doc.implementation.createHTMLDocument('');
while (d.lastChild) {
d.removeChild(d.lastChild);
}
doc.templateContentsOwner_ = d;
}
return d;
}
function getTemplateStagingDocument(template) {
if (!template.stagingDocument_) {
var owner = template.ownerDocument;
if (!owner.stagingDocument_) {
owner.stagingDocument_ = owner.implementation.createHTMLDocument('');
owner.stagingDocument_.isStagingDocument = true;
// TODO(rafaelw): Remove when fix for
// https://codereview.chromium.org/164803002/
// makes it to Chrome release.
var base = owner.stagingDocument_.createElement('base');
base.href = document.baseURI;
owner.stagingDocument_.head.appendChild(base);
owner.stagingDocument_.stagingDocument_ = owner.stagingDocument_;
}
template.stagingDocument_ = owner.stagingDocument_;
}
return template.stagingDocument_;
}
// For non-template browsers, the parser will disallow in certain
// locations, so we allow "attribute templates" which combine the template
// element with the top-level container node of the content, e.g.
//
// Bar
//
// becomes
//
//
// + #document-fragment
// +
// + Bar
//
function extractTemplateFromAttributeTemplate(el) {
var template = el.ownerDocument.createElement('template');
el.parentNode.insertBefore(template, el);
var attribs = el.attributes;
var count = attribs.length;
while (count-- > 0) {
var attrib = attribs[count];
if (templateAttributeDirectives[attrib.name]) {
if (attrib.name !== 'template')
template.setAttribute(attrib.name, attrib.value);
el.removeAttribute(attrib.name);
}
}
return template;
}
function extractTemplateFromSVGTemplate(el) {
var template = el.ownerDocument.createElement('template');
el.parentNode.insertBefore(template, el);
var attribs = el.attributes;
var count = attribs.length;
while (count-- > 0) {
var attrib = attribs[count];
template.setAttribute(attrib.name, attrib.value);
el.removeAttribute(attrib.name);
}
el.parentNode.removeChild(el);
return template;
}
function liftNonNativeTemplateChildrenIntoContent(template, el, useRoot) {
var content = template.content;
if (useRoot) {
content.appendChild(el);
return;
}
var child;
while (child = el.firstChild) {
content.appendChild(child);
}
}
var templateObserver;
if (typeof MutationObserver == 'function') {
templateObserver = new MutationObserver(function(records) {
for (var i = 0; i < records.length; i++) {
records[i].target.refChanged_();
}
});
}
/**
* Ensures proper API and content model for template elements.
* @param {HTMLTemplateElement} opt_instanceRef The template element which
* |el| template element will return as the value of its ref(), and whose
* content will be used as source when createInstance() is invoked.
*/
HTMLTemplateElement.decorate = function(el, opt_instanceRef) {
if (el.templateIsDecorated_)
return false;
var templateElement = el;
templateElement.templateIsDecorated_ = true;
var isNativeHTMLTemplate = isHTMLTemplate(templateElement) &&
hasTemplateElement;
var bootstrapContents = isNativeHTMLTemplate;
var liftContents = !isNativeHTMLTemplate;
var liftRoot = false;
if (!isNativeHTMLTemplate) {
if (isAttributeTemplate(templateElement)) {
assert(!opt_instanceRef);
templateElement = extractTemplateFromAttributeTemplate(el);
templateElement.templateIsDecorated_ = true;
isNativeHTMLTemplate = hasTemplateElement;
liftRoot = true;
} else if (isSVGTemplate(templateElement)) {
templateElement = extractTemplateFromSVGTemplate(el);
templateElement.templateIsDecorated_ = true;
isNativeHTMLTemplate = hasTemplateElement;
}
}
if (!isNativeHTMLTemplate) {
fixTemplateElementPrototype(templateElement);
var doc = getOrCreateTemplateContentsOwner(templateElement);
templateElement.content_ = doc.createDocumentFragment();
}
if (opt_instanceRef) {
// template is contained within an instance, its direct content must be
// empty
templateElement.instanceRef_ = opt_instanceRef;
} else if (liftContents) {
liftNonNativeTemplateChildrenIntoContent(templateElement,
el,
liftRoot);
} else if (bootstrapContents) {
bootstrapTemplatesRecursivelyFrom(templateElement.content);
}
return true;
};
// TODO(rafaelw): This used to decorate recursively all templates from a given
// node. This happens by default on 'DOMContentLoaded', but may be needed
// in subtrees not descendent from document (e.g. ShadowRoot).
// Review whether this is the right public API.
HTMLTemplateElement.bootstrap = bootstrapTemplatesRecursivelyFrom;
var htmlElement = global.HTMLUnknownElement || HTMLElement;
var contentDescriptor = {
get: function() {
return this.content_;
},
enumerable: true,
configurable: true
};
if (!hasTemplateElement) {
// Gecko is more picky with the prototype than WebKit. Make sure to use the
// same prototype as created in the constructor.
HTMLTemplateElement.prototype = Object.create(htmlElement.prototype);
Object.defineProperty(HTMLTemplateElement.prototype, 'content',
contentDescriptor);
}
function fixTemplateElementPrototype(el) {
if (hasProto)
el.__proto__ = HTMLTemplateElement.prototype;
else
mixin(el, HTMLTemplateElement.prototype);
}
function ensureSetModelScheduled(template) {
if (!template.setModelFn_) {
template.setModelFn_ = function() {
template.setModelFnScheduled_ = false;
var map = getBindings(template,
template.delegate_ && template.delegate_.prepareBinding);
processBindings(template, map, template.model_);
};
}
if (!template.setModelFnScheduled_) {
template.setModelFnScheduled_ = true;
Observer.runEOM_(template.setModelFn_);
}
}
mixin(HTMLTemplateElement.prototype, {
bind: function(name, value, oneTime) {
if (name != 'ref')
return Element.prototype.bind.call(this, name, value, oneTime);
var self = this;
var ref = oneTime ? value : value.open(function(ref) {
self.setAttribute('ref', ref);
self.refChanged_();
});
this.setAttribute('ref', ref);
this.refChanged_();
if (oneTime)
return;
if (!this.bindings_) {
this.bindings_ = { ref: value };
} else {
this.bindings_.ref = value;
}
return value;
},
processBindingDirectives_: function(directives) {
if (this.iterator_)
this.iterator_.closeDeps();
if (!directives.if && !directives.bind && !directives.repeat) {
if (this.iterator_) {
this.iterator_.close();
this.iterator_ = undefined;
}
return;
}
if (!this.iterator_) {
this.iterator_ = new TemplateIterator(this);
}
this.iterator_.updateDependencies(directives, this.model_);
if (templateObserver) {
templateObserver.observe(this, { attributes: true,
attributeFilter: ['ref'] });
}
return this.iterator_;
},
createInstance: function(model, bindingDelegate, delegate_) {
if (bindingDelegate)
delegate_ = this.newDelegate_(bindingDelegate);
else if (!delegate_)
delegate_ = this.delegate_;
if (!this.refContent_)
this.refContent_ = this.ref_.content;
var content = this.refContent_;
if (content.firstChild === null)
return emptyInstance;
var map = getInstanceBindingMap(content, delegate_);
var stagingDocument = getTemplateStagingDocument(this);
var instance = stagingDocument.createDocumentFragment();
instance.templateCreator_ = this;
instance.protoContent_ = content;
instance.bindings_ = [];
instance.terminator_ = null;
var instanceRecord = instance.templateInstance_ = {
firstNode: null,
lastNode: null,
model: model
};
var i = 0;
var collectTerminator = false;
for (var child = content.firstChild; child; child = child.nextSibling) {
// The terminator of the instance is the clone of the last child of the
// content. If the last child is an active template, it may produce
// instances as a result of production, so simply collecting the last
// child of the instance after it has finished producing may be wrong.
if (child.nextSibling === null)
collectTerminator = true;
var clone = cloneAndBindInstance(child, instance, stagingDocument,
map.children[i++],
model,
delegate_,
instance.bindings_);
clone.templateInstance_ = instanceRecord;
if (collectTerminator)
instance.terminator_ = clone;
}
instanceRecord.firstNode = instance.firstChild;
instanceRecord.lastNode = instance.lastChild;
instance.templateCreator_ = undefined;
instance.protoContent_ = undefined;
return instance;
},
get model() {
return this.model_;
},
set model(model) {
this.model_ = model;
ensureSetModelScheduled(this);
},
get bindingDelegate() {
return this.delegate_ && this.delegate_.raw;
},
refChanged_: function() {
if (!this.iterator_ || this.refContent_ === this.ref_.content)
return;
this.refContent_ = undefined;
this.iterator_.valueChanged();
this.iterator_.updateIteratedValue(this.iterator_.getUpdatedValue());
},
clear: function() {
this.model_ = undefined;
this.delegate_ = undefined;
if (this.bindings_ && this.bindings_.ref)
this.bindings_.ref.close()
this.refContent_ = undefined;
if (!this.iterator_)
return;
this.iterator_.valueChanged();
this.iterator_.close()
this.iterator_ = undefined;
},
setDelegate_: function(delegate) {
this.delegate_ = delegate;
this.bindingMap_ = undefined;
if (this.iterator_) {
this.iterator_.instancePositionChangedFn_ = undefined;
this.iterator_.instanceModelFn_ = undefined;
}
},
newDelegate_: function(bindingDelegate) {
if (!bindingDelegate)
return;
function delegateFn(name) {
var fn = bindingDelegate && bindingDelegate[name];
if (typeof fn != 'function')
return;
return function() {
return fn.apply(bindingDelegate, arguments);
};
}
return {
bindingMaps: {},
raw: bindingDelegate,
prepareBinding: delegateFn('prepareBinding'),
prepareInstanceModel: delegateFn('prepareInstanceModel'),
prepareInstancePositionChanged:
delegateFn('prepareInstancePositionChanged')
};
},
set bindingDelegate(bindingDelegate) {
if (this.delegate_) {
throw Error('Template must be cleared before a new bindingDelegate ' +
'can be assigned');
}
this.setDelegate_(this.newDelegate_(bindingDelegate));
},
get ref_() {
var ref = searchRefId(this, this.getAttribute('ref'));
if (!ref)
ref = this.instanceRef_;
if (!ref)
return this;
var nextRef = ref.ref_;
return nextRef ? nextRef : ref;
}
});
// Returns
// a) undefined if there are no mustaches.
// b) [TEXT, (ONE_TIME?, PATH, DELEGATE_FN, TEXT)+] if there is at least one mustache.
function parseMustaches(s, name, node, prepareBindingFn) {
if (!s || !s.length)
return;
var tokens;
var length = s.length;
var startIndex = 0, lastIndex = 0, endIndex = 0;
var onlyOneTime = true;
while (lastIndex < length) {
var startIndex = s.indexOf('{{', lastIndex);
var oneTimeStart = s.indexOf('[[', lastIndex);
var oneTime = false;
var terminator = '}}';
if (oneTimeStart >= 0 &&
(startIndex < 0 || oneTimeStart < startIndex)) {
startIndex = oneTimeStart;
oneTime = true;
terminator = ']]';
}
endIndex = startIndex < 0 ? -1 : s.indexOf(terminator, startIndex + 2);
if (endIndex < 0) {
if (!tokens)
return;
tokens.push(s.slice(lastIndex)); // TEXT
break;
}
tokens = tokens || [];
tokens.push(s.slice(lastIndex, startIndex)); // TEXT
var pathString = s.slice(startIndex + 2, endIndex).trim();
tokens.push(oneTime); // ONE_TIME?
onlyOneTime = onlyOneTime && oneTime;
var delegateFn = prepareBindingFn &&
prepareBindingFn(pathString, name, node);
// Don't try to parse the expression if there's a prepareBinding function
if (delegateFn == null) {
tokens.push(Path.get(pathString)); // PATH
} else {
tokens.push(null);
}
tokens.push(delegateFn); // DELEGATE_FN
lastIndex = endIndex + 2;
}
if (lastIndex === length)
tokens.push(''); // TEXT
tokens.hasOnePath = tokens.length === 5;
tokens.isSimplePath = tokens.hasOnePath &&
tokens[0] == '' &&
tokens[4] == '';
tokens.onlyOneTime = onlyOneTime;
tokens.combinator = function(values) {
var newValue = tokens[0];
for (var i = 1; i < tokens.length; i += 4) {
var value = tokens.hasOnePath ? values : values[(i - 1) / 4];
if (value !== undefined)
newValue += value;
newValue += tokens[i + 3];
}
return newValue;
}
return tokens;
};
function processOneTimeBinding(name, tokens, node, model) {
if (tokens.hasOnePath) {
var delegateFn = tokens[3];
var value = delegateFn ? delegateFn(model, node, true) :
tokens[2].getValueFrom(model);
return tokens.isSimplePath ? value : tokens.combinator(value);
}
var values = [];
for (var i = 1; i < tokens.length; i += 4) {
var delegateFn = tokens[i + 2];
values[(i - 1) / 4] = delegateFn ? delegateFn(model, node) :
tokens[i + 1].getValueFrom(model);
}
return tokens.combinator(values);
}
function processSinglePathBinding(name, tokens, node, model) {
var delegateFn = tokens[3];
var observer = delegateFn ? delegateFn(model, node, false) :
new PathObserver(model, tokens[2]);
return tokens.isSimplePath ? observer :
new ObserverTransform(observer, tokens.combinator);
}
function processBinding(name, tokens, node, model) {
if (tokens.onlyOneTime)
return processOneTimeBinding(name, tokens, node, model);
if (tokens.hasOnePath)
return processSinglePathBinding(name, tokens, node, model);
var observer = new CompoundObserver();
for (var i = 1; i < tokens.length; i += 4) {
var oneTime = tokens[i];
var delegateFn = tokens[i + 2];
if (delegateFn) {
var value = delegateFn(model, node, oneTime);
if (oneTime)
observer.addPath(value)
else
observer.addObserver(value);
continue;
}
var path = tokens[i + 1];
if (oneTime)
observer.addPath(path.getValueFrom(model))
else
observer.addPath(model, path);
}
return new ObserverTransform(observer, tokens.combinator);
}
function processBindings(node, bindings, model, instanceBindings) {
for (var i = 0; i < bindings.length; i += 2) {
var name = bindings[i]
var tokens = bindings[i + 1];
var value = processBinding(name, tokens, node, model);
var binding = node.bind(name, value, tokens.onlyOneTime);
if (binding && instanceBindings)
instanceBindings.push(binding);
}
node.bindFinished();
if (!bindings.isTemplate)
return;
node.model_ = model;
var iter = node.processBindingDirectives_(bindings);
if (instanceBindings && iter)
instanceBindings.push(iter);
}
function parseWithDefault(el, name, prepareBindingFn) {
var v = el.getAttribute(name);
return parseMustaches(v == '' ? '{{}}' : v, name, el, prepareBindingFn);
}
function parseAttributeBindings(element, prepareBindingFn) {
assert(element);
var bindings = [];
var ifFound = false;
var bindFound = false;
for (var i = 0; i < element.attributes.length; i++) {
var attr = element.attributes[i];
var name = attr.name;
var value = attr.value;
// Allow bindings expressed in attributes to be prefixed with underbars.
// We do this to allow correct semantics for browsers that don't implement
// where certain attributes might trigger side-effects -- and
// for IE which sanitizes certain attributes, disallowing mustache
// replacements in their text.
while (name[0] === '_') {
name = name.substring(1);
}
if (isTemplate(element) &&
(name === IF || name === BIND || name === REPEAT)) {
continue;
}
var tokens = parseMustaches(value, name, element,
prepareBindingFn);
if (!tokens)
continue;
bindings.push(name, tokens);
}
if (isTemplate(element)) {
bindings.isTemplate = true;
bindings.if = parseWithDefault(element, IF, prepareBindingFn);
bindings.bind = parseWithDefault(element, BIND, prepareBindingFn);
bindings.repeat = parseWithDefault(element, REPEAT, prepareBindingFn);
if (bindings.if && !bindings.bind && !bindings.repeat)
bindings.bind = parseMustaches('{{}}', BIND, element, prepareBindingFn);
}
return bindings;
}
function getBindings(node, prepareBindingFn) {
if (node.nodeType === Node.ELEMENT_NODE)
return parseAttributeBindings(node, prepareBindingFn);
if (node.nodeType === Node.TEXT_NODE) {
var tokens = parseMustaches(node.data, 'textContent', node,
prepareBindingFn);
if (tokens)
return ['textContent', tokens];
}
return [];
}
function cloneAndBindInstance(node, parent, stagingDocument, bindings, model,
delegate,
instanceBindings,
instanceRecord) {
var clone = parent.appendChild(stagingDocument.importNode(node, false));
var i = 0;
for (var child = node.firstChild; child; child = child.nextSibling) {
cloneAndBindInstance(child, clone, stagingDocument,
bindings.children[i++],
model,
delegate,
instanceBindings);
}
if (bindings.isTemplate) {
HTMLTemplateElement.decorate(clone, node);
if (delegate)
clone.setDelegate_(delegate);
}
processBindings(clone, bindings, model, instanceBindings);
return clone;
}
function createInstanceBindingMap(node, prepareBindingFn) {
var map = getBindings(node, prepareBindingFn);
map.children = {};
var index = 0;
for (var child = node.firstChild; child; child = child.nextSibling) {
map.children[index++] = createInstanceBindingMap(child, prepareBindingFn);
}
return map;
}
var contentUidCounter = 1;
// TODO(rafaelw): Setup a MutationObserver on content which clears the id
// so that bindingMaps regenerate when the template.content changes.
function getContentUid(content) {
var id = content.id_;
if (!id)
id = content.id_ = contentUidCounter++;
return id;
}
// Each delegate is associated with a set of bindingMaps, one for each
// content which may be used by a template. The intent is that each binding
// delegate gets the opportunity to prepare the instance (via the prepare*
// delegate calls) once across all uses.
// TODO(rafaelw): Separate out the parse map from the binding map. In the
// current implementation, if two delegates need a binding map for the same
// content, the second will have to reparse.
function getInstanceBindingMap(content, delegate_) {
var contentId = getContentUid(content);
if (delegate_) {
var map = delegate_.bindingMaps[contentId];
if (!map) {
map = delegate_.bindingMaps[contentId] =
createInstanceBindingMap(content, delegate_.prepareBinding) || [];
}
return map;
}
var map = content.bindingMap_;
if (!map) {
map = content.bindingMap_ =
createInstanceBindingMap(content, undefined) || [];
}
return map;
}
Object.defineProperty(Node.prototype, 'templateInstance', {
get: function() {
var instance = this.templateInstance_;
return instance ? instance :
(this.parentNode ? this.parentNode.templateInstance : undefined);
}
});
var emptyInstance = document.createDocumentFragment();
emptyInstance.bindings_ = [];
emptyInstance.terminator_ = null;
function TemplateIterator(templateElement) {
this.closed = false;
this.templateElement_ = templateElement;
this.instances = [];
this.deps = undefined;
this.iteratedValue = [];
this.presentValue = undefined;
this.arrayObserver = undefined;
}
TemplateIterator.prototype = {
closeDeps: function() {
var deps = this.deps;
if (deps) {
if (deps.ifOneTime === false)
deps.ifValue.close();
if (deps.oneTime === false)
deps.value.close();
}
},
updateDependencies: function(directives, model) {
this.closeDeps();
var deps = this.deps = {};
var template = this.templateElement_;
var ifValue = true;
if (directives.if) {
deps.hasIf = true;
deps.ifOneTime = directives.if.onlyOneTime;
deps.ifValue = processBinding(IF, directives.if, template, model);
ifValue = deps.ifValue;
// oneTime if & predicate is false. nothing else to do.
if (deps.ifOneTime && !ifValue) {
this.valueChanged();
return;
}
if (!deps.ifOneTime)
ifValue = ifValue.open(this.updateIfValue, this);
}
if (directives.repeat) {
deps.repeat = true;
deps.oneTime = directives.repeat.onlyOneTime;
deps.value = processBinding(REPEAT, directives.repeat, template, model);
} else {
deps.repeat = false;
deps.oneTime = directives.bind.onlyOneTime;
deps.value = processBinding(BIND, directives.bind, template, model);
}
var value = deps.value;
if (!deps.oneTime)
value = value.open(this.updateIteratedValue, this);
if (!ifValue) {
this.valueChanged();
return;
}
this.updateValue(value);
},
/**
* Gets the updated value of the bind/repeat. This can potentially call
* user code (if a bindingDelegate is set up) so we try to avoid it if we
* already have the value in hand (from Observer.open).
*/
getUpdatedValue: function() {
var value = this.deps.value;
if (!this.deps.oneTime)
value = value.discardChanges();
return value;
},
updateIfValue: function(ifValue) {
if (!ifValue) {
this.valueChanged();
return;
}
this.updateValue(this.getUpdatedValue());
},
updateIteratedValue: function(value) {
if (this.deps.hasIf) {
var ifValue = this.deps.ifValue;
if (!this.deps.ifOneTime)
ifValue = ifValue.discardChanges();
if (!ifValue) {
this.valueChanged();
return;
}
}
this.updateValue(value);
},
updateValue: function(value) {
if (!this.deps.repeat)
value = [value];
var observe = this.deps.repeat &&
!this.deps.oneTime &&
Array.isArray(value);
this.valueChanged(value, observe);
},
valueChanged: function(value, observeValue) {
if (!Array.isArray(value))
value = [];
if (value === this.iteratedValue)
return;
this.unobserve();
this.presentValue = value;
if (observeValue) {
this.arrayObserver = new ArrayObserver(this.presentValue);
this.arrayObserver.open(this.handleSplices, this);
}
this.handleSplices(ArrayObserver.calculateSplices(this.presentValue,
this.iteratedValue));
},
getLastInstanceNode: function(index) {
if (index == -1)
return this.templateElement_;
var instance = this.instances[index];
var terminator = instance.terminator_;
if (!terminator)
return this.getLastInstanceNode(index - 1);
if (terminator.nodeType !== Node.ELEMENT_NODE ||
this.templateElement_ === terminator) {
return terminator;
}
var subtemplateIterator = terminator.iterator_;
if (!subtemplateIterator)
return terminator;
return subtemplateIterator.getLastTemplateNode();
},
getLastTemplateNode: function() {
return this.getLastInstanceNode(this.instances.length - 1);
},
insertInstanceAt: function(index, fragment) {
var previousInstanceLast = this.getLastInstanceNode(index - 1);
var parent = this.templateElement_.parentNode;
this.instances.splice(index, 0, fragment);
parent.insertBefore(fragment, previousInstanceLast.nextSibling);
},
extractInstanceAt: function(index) {
var previousInstanceLast = this.getLastInstanceNode(index - 1);
var lastNode = this.getLastInstanceNode(index);
var parent = this.templateElement_.parentNode;
var instance = this.instances.splice(index, 1)[0];
while (lastNode !== previousInstanceLast) {
var node = previousInstanceLast.nextSibling;
if (node == lastNode)
lastNode = previousInstanceLast;
instance.appendChild(parent.removeChild(node));
}
return instance;
},
getDelegateFn: function(fn) {
fn = fn && fn(this.templateElement_);
return typeof fn === 'function' ? fn : null;
},
handleSplices: function(splices) {
if (this.closed || !splices.length)
return;
var template = this.templateElement_;
if (!template.parentNode) {
this.close();
return;
}
ArrayObserver.applySplices(this.iteratedValue, this.presentValue,
splices);
var delegate = template.delegate_;
if (this.instanceModelFn_ === undefined) {
this.instanceModelFn_ =
this.getDelegateFn(delegate && delegate.prepareInstanceModel);
}
if (this.instancePositionChangedFn_ === undefined) {
this.instancePositionChangedFn_ =
this.getDelegateFn(delegate &&
delegate.prepareInstancePositionChanged);
}
// Instance Removals
var instanceCache = new Map;
var removeDelta = 0;
for (var i = 0; i < splices.length; i++) {
var splice = splices[i];
var removed = splice.removed;
for (var j = 0; j < removed.length; j++) {
var model = removed[j];
var instance = this.extractInstanceAt(splice.index + removeDelta);
if (instance !== emptyInstance) {
instanceCache.set(model, instance);
}
}
removeDelta -= splice.addedCount;
}
// Instance Insertions
for (var i = 0; i < splices.length; i++) {
var splice = splices[i];
var addIndex = splice.index;
for (; addIndex < splice.index + splice.addedCount; addIndex++) {
var model = this.iteratedValue[addIndex];
var instance = instanceCache.get(model);
if (instance) {
instanceCache.delete(model);
} else {
if (this.instanceModelFn_) {
model = this.instanceModelFn_(model);
}
if (model === undefined) {
instance = emptyInstance;
} else {
instance = template.createInstance(model, undefined, delegate);
}
}
this.insertInstanceAt(addIndex, instance);
}
}
instanceCache.forEach(function(instance) {
this.closeInstanceBindings(instance);
}, this);
if (this.instancePositionChangedFn_)
this.reportInstancesMoved(splices);
},
reportInstanceMoved: function(index) {
var instance = this.instances[index];
if (instance === emptyInstance)
return;
this.instancePositionChangedFn_(instance.templateInstance_, index);
},
reportInstancesMoved: function(splices) {
var index = 0;
var offset = 0;
for (var i = 0; i < splices.length; i++) {
var splice = splices[i];
if (offset != 0) {
while (index < splice.index) {
this.reportInstanceMoved(index);
index++;
}
} else {
index = splice.index;
}
while (index < splice.index + splice.addedCount) {
this.reportInstanceMoved(index);
index++;
}
offset += splice.addedCount - splice.removed.length;
}
if (offset == 0)
return;
var length = this.instances.length;
while (index < length) {
this.reportInstanceMoved(index);
index++;
}
},
closeInstanceBindings: function(instance) {
var bindings = instance.bindings_;
for (var i = 0; i < bindings.length; i++) {
bindings[i].close();
}
},
unobserve: function() {
if (!this.arrayObserver)
return;
this.arrayObserver.close();
this.arrayObserver = undefined;
},
close: function() {
if (this.closed)
return;
this.unobserve();
for (var i = 0; i < this.instances.length; i++) {
this.closeInstanceBindings(this.instances[i]);
}
this.instances.length = 0;
this.closeDeps();
this.templateElement_.iterator_ = undefined;
this.closed = true;
}
};
// Polyfill-specific API.
HTMLTemplateElement.forAllTemplatesFrom_ = forAllTemplatesFrom;
})(this);
(function(scope) {
'use strict';
// feature detect for URL constructor
var hasWorkingUrl = false;
if (!scope.forceJURL) {
try {
var u = new URL('b', 'http://a');
hasWorkingUrl = u.href === 'http://a/b';
} catch(e) {}
}
if (hasWorkingUrl)
return;
var relative = Object.create(null);
relative['ftp'] = 21;
relative['file'] = 0;
relative['gopher'] = 70;
relative['http'] = 80;
relative['https'] = 443;
relative['ws'] = 80;
relative['wss'] = 443;
var relativePathDotMapping = Object.create(null);
relativePathDotMapping['%2e'] = '.';
relativePathDotMapping['.%2e'] = '..';
relativePathDotMapping['%2e.'] = '..';
relativePathDotMapping['%2e%2e'] = '..';
function isRelativeScheme(scheme) {
return relative[scheme] !== undefined;
}
function invalid() {
clear.call(this);
this._isInvalid = true;
}
function IDNAToASCII(h) {
if ('' == h) {
invalid.call(this)
}
// XXX
return h.toLowerCase()
}
function percentEscape(c) {
var unicode = c.charCodeAt(0);
if (unicode > 0x20 &&
unicode < 0x7F &&
// " # < > ? `
[0x22, 0x23, 0x3C, 0x3E, 0x3F, 0x60].indexOf(unicode) == -1
) {
return c;
}
return encodeURIComponent(c);
}
function percentEscapeQuery(c) {
// XXX This actually needs to encode c using encoding and then
// convert the bytes one-by-one.
var unicode = c.charCodeAt(0);
if (unicode > 0x20 &&
unicode < 0x7F &&
// " # < > ` (do not escape '?')
[0x22, 0x23, 0x3C, 0x3E, 0x60].indexOf(unicode) == -1
) {
return c;
}
return encodeURIComponent(c);
}
var EOF = undefined,
ALPHA = /[a-zA-Z]/,
ALPHANUMERIC = /[a-zA-Z0-9\+\-\.]/;
function parse(input, stateOverride, base) {
function err(message) {
errors.push(message)
}
var state = stateOverride || 'scheme start',
cursor = 0,
buffer = '',
seenAt = false,
seenBracket = false,
errors = [];
loop: while ((input[cursor - 1] != EOF || cursor == 0) && !this._isInvalid) {
var c = input[cursor];
switch (state) {
case 'scheme start':
if (c && ALPHA.test(c)) {
buffer += c.toLowerCase(); // ASCII-safe
state = 'scheme';
} else if (!stateOverride) {
buffer = '';
state = 'no scheme';
continue;
} else {
err('Invalid scheme.');
break loop;
}
break;
case 'scheme':
if (c && ALPHANUMERIC.test(c)) {
buffer += c.toLowerCase(); // ASCII-safe
} else if (':' == c) {
this._scheme = buffer;
buffer = '';
if (stateOverride) {
break loop;
}
if (isRelativeScheme(this._scheme)) {
this._isRelative = true;
}
if ('file' == this._scheme) {
state = 'relative';
} else if (this._isRelative && base && base._scheme == this._scheme) {
state = 'relative or authority';
} else if (this._isRelative) {
state = 'authority first slash';
} else {
state = 'scheme data';
}
} else if (!stateOverride) {
buffer = '';
cursor = 0;
state = 'no scheme';
continue;
} else if (EOF == c) {
break loop;
} else {
err('Code point not allowed in scheme: ' + c)
break loop;
}
break;
case 'scheme data':
if ('?' == c) {
query = '?';
state = 'query';
} else if ('#' == c) {
this._fragment = '#';
state = 'fragment';
} else {
// XXX error handling
if (EOF != c && '\t' != c && '\n' != c && '\r' != c) {
this._schemeData += percentEscape(c);
}
}
break;
case 'no scheme':
if (!base || !(isRelativeScheme(base._scheme))) {
err('Missing scheme.');
invalid.call(this);
} else {
state = 'relative';
continue;
}
break;
case 'relative or authority':
if ('/' == c && '/' == input[cursor+1]) {
state = 'authority ignore slashes';
} else {
err('Expected /, got: ' + c);
state = 'relative';
continue
}
break;
case 'relative':
this._isRelative = true;
if ('file' != this._scheme)
this._scheme = base._scheme;
if (EOF == c) {
this._host = base._host;
this._port = base._port;
this._path = base._path.slice();
this._query = base._query;
break loop;
} else if ('/' == c || '\\' == c) {
if ('\\' == c)
err('\\ is an invalid code point.');
state = 'relative slash';
} else if ('?' == c) {
this._host = base._host;
this._port = base._port;
this._path = base._path.slice();
this._query = '?';
state = 'query';
} else if ('#' == c) {
this._host = base._host;
this._port = base._port;
this._path = base._path.slice();
this._query = base._query;
this._fragment = '#';
state = 'fragment';
} else {
var nextC = input[cursor+1]
var nextNextC = input[cursor+2]
if (
'file' != this._scheme || !ALPHA.test(c) ||
(nextC != ':' && nextC != '|') ||
(EOF != nextNextC && '/' != nextNextC && '\\' != nextNextC && '?' != nextNextC && '#' != nextNextC)) {
this._host = base._host;
this._port = base._port;
this._path = base._path.slice();
this._path.pop();
}
state = 'relative path';
continue;
}
break;
case 'relative slash':
if ('/' == c || '\\' == c) {
if ('\\' == c) {
err('\\ is an invalid code point.');
}
if ('file' == this._scheme) {
state = 'file host';
} else {
state = 'authority ignore slashes';
}
} else {
if ('file' != this._scheme) {
this._host = base._host;
this._port = base._port;
}
state = 'relative path';
continue;
}
break;
case 'authority first slash':
if ('/' == c) {
state = 'authority second slash';
} else {
err("Expected '/', got: " + c);
state = 'authority ignore slashes';
continue;
}
break;
case 'authority second slash':
state = 'authority ignore slashes';
if ('/' != c) {
err("Expected '/', got: " + c);
continue;
}
break;
case 'authority ignore slashes':
if ('/' != c && '\\' != c) {
state = 'authority';
continue;
} else {
err('Expected authority, got: ' + c);
}
break;
case 'authority':
if ('@' == c) {
if (seenAt) {
err('@ already seen.');
buffer += '%40';
}
seenAt = true;
for (var i = 0; i < buffer.length; i++) {
var cp = buffer[i];
if ('\t' == cp || '\n' == cp || '\r' == cp) {
err('Invalid whitespace in authority.');
continue;
}
// XXX check URL code points
if (':' == cp && null === this._password) {
this._password = '';
continue;
}
var tempC = percentEscape(cp);
(null !== this._password) ? this._password += tempC : this._username += tempC;
}
buffer = '';
} else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) {
cursor -= buffer.length;
buffer = '';
state = 'host';
continue;
} else {
buffer += c;
}
break;
case 'file host':
if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) {
if (buffer.length == 2 && ALPHA.test(buffer[0]) && (buffer[1] == ':' || buffer[1] == '|')) {
state = 'relative path';
} else if (buffer.length == 0) {
state = 'relative path start';
} else {
this._host = IDNAToASCII.call(this, buffer);
buffer = '';
state = 'relative path start';
}
continue;
} else if ('\t' == c || '\n' == c || '\r' == c) {
err('Invalid whitespace in file host.');
} else {
buffer += c;
}
break;
case 'host':
case 'hostname':
if (':' == c && !seenBracket) {
// XXX host parsing
this._host = IDNAToASCII.call(this, buffer);
buffer = '';
state = 'port';
if ('hostname' == stateOverride) {
break loop;
}
} else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) {
this._host = IDNAToASCII.call(this, buffer);
buffer = '';
state = 'relative path start';
if (stateOverride) {
break loop;
}
continue;
} else if ('\t' != c && '\n' != c && '\r' != c) {
if ('[' == c) {
seenBracket = true;
} else if (']' == c) {
seenBracket = false;
}
buffer += c;
} else {
err('Invalid code point in host/hostname: ' + c);
}
break;
case 'port':
if (/[0-9]/.test(c)) {
buffer += c;
} else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c || stateOverride) {
if ('' != buffer) {
var temp = parseInt(buffer, 10);
if (temp != relative[this._scheme]) {
this._port = temp + '';
}
buffer = '';
}
if (stateOverride) {
break loop;
}
state = 'relative path start';
continue;
} else if ('\t' == c || '\n' == c || '\r' == c) {
err('Invalid code point in port: ' + c);
} else {
invalid.call(this);
}
break;
case 'relative path start':
if ('\\' == c)
err("'\\' not allowed in path.");
state = 'relative path';
if ('/' != c && '\\' != c) {
continue;
}
break;
case 'relative path':
if (EOF == c || '/' == c || '\\' == c || (!stateOverride && ('?' == c || '#' == c))) {
if ('\\' == c) {
err('\\ not allowed in relative path.');
}
var tmp;
if (tmp = relativePathDotMapping[buffer.toLowerCase()]) {
buffer = tmp;
}
if ('..' == buffer) {
this._path.pop();
if ('/' != c && '\\' != c) {
this._path.push('');
}
} else if ('.' == buffer && '/' != c && '\\' != c) {
this._path.push('');
} else if ('.' != buffer) {
if ('file' == this._scheme && this._path.length == 0 && buffer.length == 2 && ALPHA.test(buffer[0]) && buffer[1] == '|') {
buffer = buffer[0] + ':';
}
this._path.push(buffer);
}
buffer = '';
if ('?' == c) {
this._query = '?';
state = 'query';
} else if ('#' == c) {
this._fragment = '#';
state = 'fragment';
}
} else if ('\t' != c && '\n' != c && '\r' != c) {
buffer += percentEscape(c);
}
break;
case 'query':
if (!stateOverride && '#' == c) {
this._fragment = '#';
state = 'fragment';
} else if (EOF != c && '\t' != c && '\n' != c && '\r' != c) {
this._query += percentEscapeQuery(c);
}
break;
case 'fragment':
if (EOF != c && '\t' != c && '\n' != c && '\r' != c) {
this._fragment += c;
}
break;
}
cursor++;
}
}
function clear() {
this._scheme = '';
this._schemeData = '';
this._username = '';
this._password = null;
this._host = '';
this._port = '';
this._path = [];
this._query = '';
this._fragment = '';
this._isInvalid = false;
this._isRelative = false;
}
// Does not process domain names or IP addresses.
// Does not handle encoding for the query parameter.
function jURL(url, base /* , encoding */) {
if (base !== undefined && !(base instanceof jURL))
base = new jURL(String(base));
this._url = url;
clear.call(this);
var input = url.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, '');
// encoding = encoding || 'utf-8'
parse.call(this, input, null, base);
}
jURL.prototype = {
get href() {
if (this._isInvalid)
return this._url;
var authority = '';
if ('' != this._username || null != this._password) {
authority = this._username +
(null != this._password ? ':' + this._password : '') + '@';
}
return this.protocol +
(this._isRelative ? '//' + authority + this.host : '') +
this.pathname + this._query + this._fragment;
},
set href(href) {
clear.call(this);
parse.call(this, href);
},
get protocol() {
return this._scheme + ':';
},
set protocol(protocol) {
if (this._isInvalid)
return;
parse.call(this, protocol + ':', 'scheme start');
},
get host() {
return this._isInvalid ? '' : this._port ?
this._host + ':' + this._port : this._host;
},
set host(host) {
if (this._isInvalid || !this._isRelative)
return;
parse.call(this, host, 'host');
},
get hostname() {
return this._host;
},
set hostname(hostname) {
if (this._isInvalid || !this._isRelative)
return;
parse.call(this, hostname, 'hostname');
},
get port() {
return this._port;
},
set port(port) {
if (this._isInvalid || !this._isRelative)
return;
parse.call(this, port, 'port');
},
get pathname() {
return this._isInvalid ? '' : this._isRelative ?
'/' + this._path.join('/') : this._schemeData;
},
set pathname(pathname) {
if (this._isInvalid || !this._isRelative)
return;
this._path = [];
parse.call(this, pathname, 'relative path start');
},
get search() {
return this._isInvalid || !this._query || '?' == this._query ?
'' : this._query;
},
set search(search) {
if (this._isInvalid || !this._isRelative)
return;
this._query = '?';
if ('?' == search[0])
search = search.slice(1);
parse.call(this, search, 'query');
},
get hash() {
return this._isInvalid || !this._fragment || '#' == this._fragment ?
'' : this._fragment;
},
set hash(hash) {
if (this._isInvalid)
return;
this._fragment = '#';
if ('#' == hash[0])
hash = hash.slice(1);
parse.call(this, hash, 'fragment');
},
get origin() {
var host;
if (this._isInvalid || !this._scheme) {
return '';
}
// javascript: Gecko returns String(""), WebKit/Blink String("null")
// Gecko throws error for "data://"
// data: Gecko returns "", Blink returns "data://", WebKit returns "null"
// Gecko returns String("") for file: mailto:
// WebKit/Blink returns String("SCHEME://") for file: mailto:
switch (this._scheme) {
case 'data':
case 'file':
case 'javascript':
case 'mailto':
return 'null';
}
host = this.host;
if (!host) {
return '';
}
return this._scheme + '://' + host;
}
};
// Copy over the static methods
var OriginalURL = scope.URL;
if (OriginalURL) {
jURL.createObjectURL = function(blob) {
// IE extension allows a second optional options argument.
// http://msdn.microsoft.com/en-us/library/ie/hh772302(v=vs.85).aspx
return OriginalURL.createObjectURL.apply(OriginalURL, arguments);
};
jURL.revokeObjectURL = function(url) {
OriginalURL.revokeObjectURL(url);
};
}
scope.URL = jURL;
})(this);
(function(scope) {
var iterations = 0;
var callbacks = [];
var twiddle = document.createTextNode('');
function endOfMicrotask(callback) {
twiddle.textContent = iterations++;
callbacks.push(callback);
}
function atEndOfMicrotask() {
while (callbacks.length) {
callbacks.shift()();
}
}
new (window.MutationObserver || JsMutationObserver)(atEndOfMicrotask)
.observe(twiddle, {characterData: true})
;
// exports
scope.endOfMicrotask = endOfMicrotask;
// bc
Platform.endOfMicrotask = endOfMicrotask;
})(Polymer);
(function(scope) {
/**
* @class Polymer
*/
// imports
var endOfMicrotask = scope.endOfMicrotask;
// logging
var log = window.WebComponents ? WebComponents.flags.log : {};
// inject style sheet
var style = document.createElement('style');
style.textContent = 'template {display: none !important;} /* injected by platform.js */';
var head = document.querySelector('head');
head.insertBefore(style, head.firstChild);
/**
* Force any pending data changes to be observed before
* the next task. Data changes are processed asynchronously but are guaranteed
* to be processed, for example, before painting. This method should rarely be
* needed. It does nothing when Object.observe is available;
* when Object.observe is not available, Polymer automatically flushes data
* changes approximately every 1/10 second.
* Therefore, `flush` should only be used when a data mutation should be
* observed sooner than this.
*
* @method flush
*/
// flush (with logging)
var flushing;
function flush() {
if (!flushing) {
flushing = true;
endOfMicrotask(function() {
flushing = false;
log.data && console.group('flush');
Platform.performMicrotaskCheckpoint();
log.data && console.groupEnd();
});
}
};
// polling dirty checker
// flush periodically if platform does not have object observe.
if (!Observer.hasObjectObserve) {
var FLUSH_POLL_INTERVAL = 125;
window.addEventListener('WebComponentsReady', function() {
flush();
// watch document visiblity to toggle dirty-checking
var visibilityHandler = function() {
// only flush if the page is visibile
if (document.visibilityState === 'hidden') {
if (scope.flushPoll) {
clearInterval(scope.flushPoll);
}
} else {
scope.flushPoll = setInterval(flush, FLUSH_POLL_INTERVAL);
}
};
if (typeof document.visibilityState === 'string') {
document.addEventListener('visibilitychange', visibilityHandler);
}
visibilityHandler();
});
} else {
// make flush a no-op when we have Object.observe
flush = function() {};
}
if (window.CustomElements && !CustomElements.useNative) {
var originalImportNode = Document.prototype.importNode;
Document.prototype.importNode = function(node, deep) {
var imported = originalImportNode.call(this, node, deep);
CustomElements.upgradeAll(imported);
return imported;
};
}
// exports
scope.flush = flush;
// bc
Platform.flush = flush;
})(window.Polymer);
(function(scope) {
var urlResolver = {
resolveDom: function(root, url) {
url = url || baseUrl(root);
this.resolveAttributes(root, url);
this.resolveStyles(root, url);
// handle template.content
var templates = root.querySelectorAll('template');
if (templates) {
for (var i = 0, l = templates.length, t; (i < l) && (t = templates[i]); i++) {
if (t.content) {
this.resolveDom(t.content, url);
}
}
}
},
resolveTemplate: function(template) {
this.resolveDom(template.content, baseUrl(template));
},
resolveStyles: function(root, url) {
var styles = root.querySelectorAll('style');
if (styles) {
for (var i = 0, l = styles.length, s; (i < l) && (s = styles[i]); i++) {
this.resolveStyle(s, url);
}
}
},
resolveStyle: function(style, url) {
url = url || baseUrl(style);
style.textContent = this.resolveCssText(style.textContent, url);
},
resolveCssText: function(cssText, baseUrl, keepAbsolute) {
cssText = replaceUrlsInCssText(cssText, baseUrl, keepAbsolute, CSS_URL_REGEXP);
return replaceUrlsInCssText(cssText, baseUrl, keepAbsolute, CSS_IMPORT_REGEXP);
},
resolveAttributes: function(root, url) {
if (root.hasAttributes && root.hasAttributes()) {
this.resolveElementAttributes(root, url);
}
// search for attributes that host urls
var nodes = root && root.querySelectorAll(URL_ATTRS_SELECTOR);
if (nodes) {
for (var i = 0, l = nodes.length, n; (i < l) && (n = nodes[i]); i++) {
this.resolveElementAttributes(n, url);
}
}
},
resolveElementAttributes: function(node, url) {
url = url || baseUrl(node);
URL_ATTRS.forEach(function(v) {
var attr = node.attributes[v];
var value = attr && attr.value;
var replacement;
if (value && value.search(URL_TEMPLATE_SEARCH) < 0) {
if (v === 'style') {
replacement = replaceUrlsInCssText(value, url, false, CSS_URL_REGEXP);
} else {
replacement = resolveRelativeUrl(url, value);
}
attr.value = replacement;
}
});
}
};
var CSS_URL_REGEXP = /(url\()([^)]*)(\))/g;
var CSS_IMPORT_REGEXP = /(@import[\s]+(?!url\())([^;]*)(;)/g;
var URL_ATTRS = ['href', 'src', 'action', 'style', 'url'];
var URL_ATTRS_SELECTOR = '[' + URL_ATTRS.join('],[') + ']';
var URL_TEMPLATE_SEARCH = '{{.*}}';
var URL_HASH = '#';
function baseUrl(node) {
var u = new URL(node.ownerDocument.baseURI);
u.search = '';
u.hash = '';
return u;
}
function replaceUrlsInCssText(cssText, baseUrl, keepAbsolute, regexp) {
return cssText.replace(regexp, function(m, pre, url, post) {
var urlPath = url.replace(/["']/g, '');
urlPath = resolveRelativeUrl(baseUrl, urlPath, keepAbsolute);
return pre + '\'' + urlPath + '\'' + post;
});
}
function resolveRelativeUrl(baseUrl, url, keepAbsolute) {
// do not resolve '/' absolute urls
if (url && url[0] === '/') {
return url;
}
var u = new URL(url, baseUrl);
return keepAbsolute ? u.href : makeDocumentRelPath(u.href);
}
function makeDocumentRelPath(url) {
var root = baseUrl(document.documentElement);
var u = new URL(url, root);
if (u.host === root.host && u.port === root.port &&
u.protocol === root.protocol) {
return makeRelPath(root, u);
} else {
return url;
}
}
// make a relative path from source to target
function makeRelPath(sourceUrl, targetUrl) {
var source = sourceUrl.pathname;
var target = targetUrl.pathname;
var s = source.split('/');
var t = target.split('/');
while (s.length && s[0] === t[0]){
s.shift();
t.shift();
}
for (var i = 0, l = s.length - 1; i < l; i++) {
t.unshift('..');
}
// empty '#' is discarded but we need to preserve it.
var hash = (targetUrl.href.slice(-1) === URL_HASH) ? URL_HASH : targetUrl.hash;
return t.join('/') + targetUrl.search + hash;
}
// exports
scope.urlResolver = urlResolver;
})(Polymer);
(function(scope) {
var endOfMicrotask = Polymer.endOfMicrotask;
// Generic url loader
function Loader(regex) {
this.cache = Object.create(null);
this.map = Object.create(null);
this.requests = 0;
this.regex = regex;
}
Loader.prototype = {
// TODO(dfreedm): there may be a better factoring here
// extract absolute urls from the text (full of relative urls)
extractUrls: function(text, base) {
var matches = [];
var matched, u;
while ((matched = this.regex.exec(text))) {
u = new URL(matched[1], base);
matches.push({matched: matched[0], url: u.href});
}
return matches;
},
// take a text blob, a root url, and a callback and load all the urls found within the text
// returns a map of absolute url to text
process: function(text, root, callback) {
var matches = this.extractUrls(text, root);
// every call to process returns all the text this loader has ever received
var done = callback.bind(null, this.map);
this.fetch(matches, done);
},
// build a mapping of url -> text from matches
fetch: function(matches, callback) {
var inflight = matches.length;
// return early if there is no fetching to be done
if (!inflight) {
return callback();
}
// wait for all subrequests to return
var done = function() {
if (--inflight === 0) {
callback();
}
};
// start fetching all subrequests
var m, req, url;
for (var i = 0; i < inflight; i++) {
m = matches[i];
url = m.url;
req = this.cache[url];
// if this url has already been requested, skip requesting it again
if (!req) {
req = this.xhr(url);
req.match = m;
this.cache[url] = req;
}
// wait for the request to process its subrequests
req.wait(done);
}
},
handleXhr: function(request) {
var match = request.match;
var url = match.url;
// handle errors with an empty string
var response = request.response || request.responseText || '';
this.map[url] = response;
this.fetch(this.extractUrls(response, url), request.resolve);
},
xhr: function(url) {
this.requests++;
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.send();
request.onerror = request.onload = this.handleXhr.bind(this, request);
// queue of tasks to run after XHR returns
request.pending = [];
request.resolve = function() {
var pending = request.pending;
for(var i = 0; i < pending.length; i++) {
pending[i]();
}
request.pending = null;
};
// if we have already resolved, pending is null, async call the callback
request.wait = function(fn) {
if (request.pending) {
request.pending.push(fn);
} else {
endOfMicrotask(fn);
}
};
return request;
}
};
scope.Loader = Loader;
})(Polymer);
(function(scope) {
var urlResolver = scope.urlResolver;
var Loader = scope.Loader;
function StyleResolver() {
this.loader = new Loader(this.regex);
}
StyleResolver.prototype = {
regex: /@import\s+(?:url)?["'\(]*([^'"\)]*)['"\)]*;/g,
// Recursively replace @imports with the text at that url
resolve: function(text, url, callback) {
var done = function(map) {
callback(this.flatten(text, url, map));
}.bind(this);
this.loader.process(text, url, done);
},
// resolve the textContent of a style node
resolveNode: function(style, url, callback) {
var text = style.textContent;
var done = function(text) {
style.textContent = text;
callback(style);
};
this.resolve(text, url, done);
},
// flatten all the @imports to text
flatten: function(text, base, map) {
var matches = this.loader.extractUrls(text, base);
var match, url, intermediate;
for (var i = 0; i < matches.length; i++) {
match = matches[i];
url = match.url;
// resolve any css text to be relative to the importer, keep absolute url
intermediate = urlResolver.resolveCssText(map[url], url, true);
// flatten intermediate @imports
intermediate = this.flatten(intermediate, base, map);
text = text.replace(match.matched, intermediate);
}
return text;
},
loadStyles: function(styles, base, callback) {
var loaded=0, l = styles.length;
// called in the context of the style
function loadedStyle(style) {
loaded++;
if (loaded === l && callback) {
callback();
}
}
for (var i=0, s; (i
HTMLElement.getPrototypeForTag = function(tag) {
var prototype = !tag ? HTMLElement.prototype : registry[tag];
// TODO(sjmiles): creating is likely to have wasteful side-effects
return prototype || Object.getPrototypeOf(document.createElement(tag));
};
// we have to flag propagation stoppage for the event dispatcher
var originalStopPropagation = Event.prototype.stopPropagation;
Event.prototype.stopPropagation = function() {
this.cancelBubble = true;
originalStopPropagation.apply(this, arguments);
};
// polyfill DOMTokenList
// * add/remove: allow these methods to take multiple classNames
// * toggle: add a 2nd argument which forces the given state rather
// than toggling.
var add = DOMTokenList.prototype.add;
var remove = DOMTokenList.prototype.remove;
DOMTokenList.prototype.add = function() {
for (var i = 0; i < arguments.length; i++) {
add.call(this, arguments[i]);
}
};
DOMTokenList.prototype.remove = function() {
for (var i = 0; i < arguments.length; i++) {
remove.call(this, arguments[i]);
}
};
DOMTokenList.prototype.toggle = function(name, bool) {
if (arguments.length == 1) {
bool = !this.contains(name);
}
bool ? this.add(name) : this.remove(name);
};
DOMTokenList.prototype.switch = function(oldName, newName) {
oldName && this.remove(oldName);
newName && this.add(newName);
};
// add array() to NodeList, NamedNodeMap, HTMLCollection
var ArraySlice = function() {
return Array.prototype.slice.call(this);
};
var namedNodeMap = (window.NamedNodeMap || window.MozNamedAttrMap || {});
NodeList.prototype.array = ArraySlice;
namedNodeMap.prototype.array = ArraySlice;
HTMLCollection.prototype.array = ArraySlice;
// utility
function createDOM(inTagOrNode, inHTML, inAttrs) {
var dom = typeof inTagOrNode == 'string' ?
document.createElement(inTagOrNode) : inTagOrNode.cloneNode(true);
dom.innerHTML = inHTML;
if (inAttrs) {
for (var n in inAttrs) {
dom.setAttribute(n, inAttrs[n]);
}
}
return dom;
}
// exports
scope.createDOM = createDOM;
})(Polymer);
(function(scope) {
// super
// `arrayOfArgs` is an optional array of args like one might pass
// to `Function.apply`
// TODO(sjmiles):
// $super must be installed on an instance or prototype chain
// as `super`, and invoked via `this`, e.g.
// `this.super();`
// will not work if function objects are not unique, for example,
// when using mixins.
// The memoization strategy assumes each function exists on only one
// prototype chain i.e. we use the function object for memoizing)
// perhaps we can bookkeep on the prototype itself instead
function $super(arrayOfArgs) {
// since we are thunking a method call, performance is important here:
// memoize all lookups, once memoized the fast path calls no other
// functions
//
// find the caller (cannot be `strict` because of 'caller')
var caller = $super.caller;
// memoized 'name of method'
var nom = caller.nom;
// memoized next implementation prototype
var _super = caller._super;
if (!_super) {
if (!nom) {
nom = caller.nom = nameInThis.call(this, caller);
}
if (!nom) {
console.warn('called super() on a method not installed declaratively (has no .nom property)');
}
// super prototype is either cached or we have to find it
// by searching __proto__ (at the 'top')
// invariant: because we cache _super on fn below, we never reach
// here from inside a series of calls to super(), so it's ok to
// start searching from the prototype of 'this' (at the 'top')
// we must never memoize a null super for this reason
_super = memoizeSuper(caller, nom, getPrototypeOf(this));
}
// our super function
var fn = _super[nom];
if (fn) {
// memoize information so 'fn' can call 'super'
if (!fn._super) {
// must not memoize null, or we lose our invariant above
memoizeSuper(fn, nom, _super);
}
// invoke the inherited method
// if 'fn' is not function valued, this will throw
return fn.apply(this, arrayOfArgs || []);
}
}
function nameInThis(value) {
var p = this.__proto__;
while (p && p !== HTMLElement.prototype) {
// TODO(sjmiles): getOwnPropertyNames is absurdly expensive
var n$ = Object.getOwnPropertyNames(p);
for (var i=0, l=n$.length, n; icancelAsync to cancel the
* asynchronous call.
*
* @method async
* @param {Function|String} method
* @param {any|Array} args
* @param {number} timeout
*/
async: function(method, args, timeout) {
// when polyfilling Object.observe, ensure changes
// propagate before executing the async method
Polymer.flush();
// second argument to `apply` must be an array
args = (args && args.length) ? args : [args];
// function to invoke
var fn = function() {
(this[method] || method).apply(this, args);
}.bind(this);
// execute `fn` sooner or later
var handle = timeout ? setTimeout(fn, timeout) :
requestAnimationFrame(fn);
// NOTE: switch on inverting handle to determine which time is used.
return timeout ? handle : ~handle;
},
/**
* Cancels a pending callback that was scheduled via
* async .
*
* @method cancelAsync
* @param {handle} handle Handle of the `async` to cancel.
*/
cancelAsync: function(handle) {
if (handle < 0) {
cancelAnimationFrame(~handle);
} else {
clearTimeout(handle);
}
},
/**
* Fire an event.
*
* @method fire
* @returns {Object} event
* @param {string} type An event name.
* @param {any} detail
* @param {Node} onNode Target node.
* @param {Boolean} bubbles Set false to prevent bubbling, defaults to true
* @param {Boolean} cancelable Set false to prevent cancellation, defaults to true
*/
fire: function(type, detail, onNode, bubbles, cancelable) {
var node = onNode || this;
var detail = detail === null || detail === undefined ? {} : detail;
var event = new CustomEvent(type, {
bubbles: bubbles !== undefined ? bubbles : true,
cancelable: cancelable !== undefined ? cancelable : true,
detail: detail
});
node.dispatchEvent(event);
return event;
},
/**
* Fire an event asynchronously.
*
* @method asyncFire
* @param {string} type An event name.
* @param detail
* @param {Node} toNode Target node.
*/
asyncFire: function(/*inType, inDetail*/) {
this.async("fire", arguments);
},
/**
* Remove class from old, add class to anew, if they exist.
*
* @param classFollows
* @param anew A node.
* @param old A node
* @param className
*/
classFollows: function(anew, old, className) {
if (old) {
old.classList.remove(className);
}
if (anew) {
anew.classList.add(className);
}
},
/**
* Inject HTML which contains markup bound to this element into
* a target element (replacing target element content).
*
* @param String html to inject
* @param Element target element
*/
injectBoundHTML: function(html, element) {
var template = document.createElement('template');
template.innerHTML = html;
var fragment = this.instanceTemplate(template);
if (element) {
element.textContent = '';
element.appendChild(fragment);
}
return fragment;
}
};
// no-operation function for handy stubs
var nop = function() {};
// null-object for handy stubs
var nob = {};
// deprecated
utils.asyncMethod = utils.async;
// exports
scope.api.instance.utils = utils;
scope.nop = nop;
scope.nob = nob;
})(Polymer);
(function(scope) {
// imports
var log = window.WebComponents ? WebComponents.flags.log : {};
var EVENT_PREFIX = 'on-';
// instance events api
var events = {
// read-only
EVENT_PREFIX: EVENT_PREFIX,
// event listeners on host
addHostListeners: function() {
var events = this.eventDelegates;
log.events && (Object.keys(events).length > 0) && console.log('[%s] addHostListeners:', this.localName, events);
// NOTE: host events look like bindings but really are not;
// (1) we don't want the attribute to be set and (2) we want to support
// multiple event listeners ('host' and 'instance') and Node.bind
// by default supports 1 thing being bound.
for (var type in events) {
var methodName = events[type];
PolymerGestures.addEventListener(this, type, this.element.getEventHandler(this, this, methodName));
}
},
// call 'method' or function method on 'obj' with 'args', if the method exists
dispatchMethod: function(obj, method, args) {
if (obj) {
log.events && console.group('[%s] dispatch [%s]', obj.localName, method);
var fn = typeof method === 'function' ? method : obj[method];
if (fn) {
fn[args ? 'apply' : 'call'](obj, args);
}
log.events && console.groupEnd();
// NOTE: dirty check right after calling method to ensure
// changes apply quickly; in a very complicated app using high
// frequency events, this can be a perf concern; in this case,
// imperative handlers can be used to avoid flushing.
Polymer.flush();
}
}
};
// exports
scope.api.instance.events = events;
/**
* @class Polymer
*/
/**
* Add a gesture aware event handler to the given `node`. Can be used
* in place of `element.addEventListener` and ensures gestures will function
* as expected on mobile platforms. Please note that Polymer's declarative
* event handlers include this functionality by default.
*
* @method addEventListener
* @param {Node} node node on which to listen
* @param {String} eventType name of the event
* @param {Function} handlerFn event handler function
* @param {Boolean} capture set to true to invoke event capturing
* @type Function
*/
// alias PolymerGestures event listener logic
scope.addEventListener = function(node, eventType, handlerFn, capture) {
PolymerGestures.addEventListener(wrap(node), eventType, handlerFn, capture);
};
/**
* Remove a gesture aware event handler on the given `node`. To remove an
* event listener, the exact same arguments are required that were passed
* to `Polymer.addEventListener`.
*
* @method removeEventListener
* @param {Node} node node on which to listen
* @param {String} eventType name of the event
* @param {Function} handlerFn event handler function
* @param {Boolean} capture set to true to invoke event capturing
* @type Function
*/
scope.removeEventListener = function(node, eventType, handlerFn, capture) {
PolymerGestures.removeEventListener(wrap(node), eventType, handlerFn, capture);
};
})(Polymer);
(function(scope) {
// instance api for attributes
var attributes = {
// copy attributes defined in the element declaration to the instance
// e.g. tabIndex is copied
// to the element instance here.
copyInstanceAttributes: function () {
var a$ = this._instanceAttributes;
for (var k in a$) {
if (!this.hasAttribute(k)) {
this.setAttribute(k, a$[k]);
}
}
},
// for each attribute on this, deserialize value to property as needed
takeAttributes: function() {
// if we have no publish lookup table, we have no attributes to take
// TODO(sjmiles): ad hoc
if (this._publishLC) {
for (var i=0, a$=this.attributes, l=a$.length, a; (a=a$[i]) && i= 0) {
return;
}
// get original value
var currentValue = this[name];
// deserialize Boolean or Number values from attribute
var value = this.deserializeValue(value, currentValue);
// only act if the value has changed
if (value !== currentValue) {
// install new value (has side-effects)
this[name] = value;
}
}
},
// return the published property matching name, or undefined
propertyForAttribute: function(name) {
var match = this._publishLC && this._publishLC[name];
return match;
},
// convert representation of `stringValue` based on type of `currentValue`
deserializeValue: function(stringValue, currentValue) {
return scope.deserializeValue(stringValue, currentValue);
},
// convert to a string value based on the type of `inferredType`
serializeValue: function(value, inferredType) {
if (inferredType === 'boolean') {
return value ? '' : undefined;
} else if (inferredType !== 'object' && inferredType !== 'function'
&& value !== undefined) {
return value;
}
},
// serializes `name` property value and updates the corresponding attribute
// note that reflection is opt-in.
reflectPropertyToAttribute: function(name) {
var inferredType = typeof this[name];
// try to intelligently serialize property value
var serializedValue = this.serializeValue(this[name], inferredType);
// boolean properties must reflect as boolean attributes
if (serializedValue !== undefined) {
this.setAttribute(name, serializedValue);
// TODO(sorvell): we should remove attr for all properties
// that have undefined serialization; however, we will need to
// refine the attr reflection system to achieve this; pica, for example,
// relies on having inferredType object properties not removed as
// attrs.
} else if (inferredType === 'boolean') {
this.removeAttribute(name);
}
}
};
// exports
scope.api.instance.attributes = attributes;
})(Polymer);
(function(scope) {
/**
* @class polymer-base
*/
// imports
var log = window.WebComponents ? WebComponents.flags.log : {};
// magic words
var OBSERVE_SUFFIX = 'Changed';
// element api
var empty = [];
var updateRecord = {
object: undefined,
type: 'update',
name: undefined,
oldValue: undefined
};
var numberIsNaN = Number.isNaN || function(value) {
return typeof value === 'number' && isNaN(value);
};
function areSameValue(left, right) {
if (left === right)
return left !== 0 || 1 / left === 1 / right;
if (numberIsNaN(left) && numberIsNaN(right))
return true;
return left !== left && right !== right;
}
// capture A's value if B's value is null or undefined,
// otherwise use B's value
function resolveBindingValue(oldValue, value) {
if (value === undefined && oldValue === null) {
return value;
}
return (value === null || value === undefined) ? oldValue : value;
}
var properties = {
// creates a CompoundObserver to observe property changes
// NOTE, this is only done there are any properties in the `observe` object
createPropertyObserver: function() {
var n$ = this._observeNames;
if (n$ && n$.length) {
var o = this._propertyObserver = new CompoundObserver(true);
this.registerObserver(o);
// TODO(sorvell): may not be kosher to access the value here (this[n]);
// previously we looked at the descriptor on the prototype
// this doesn't work for inheritance and not for accessors without
// a value property
for (var i=0, l=n$.length, n; (i work)
HTMLTemplateElement.decorate(template);
// ensure a default bindingDelegate
var syntax = this.syntax || (!template.bindingDelegate &&
this.element.syntax);
var dom = template.createInstance(this, syntax);
var observers = dom.bindings_;
for (var i = 0; i < observers.length; i++) {
this.registerObserver(observers[i]);
}
return dom;
},
// Called by TemplateBinding/NodeBind to setup a binding to the given
// property. It's overridden here to support property bindings
// in addition to attribute bindings that are supported by default.
bind: function(name, observable, oneTime) {
var property = this.propertyForAttribute(name);
if (!property) {
// TODO(sjmiles): this mixin method must use the special form
// of `super` installed by `mixinMethod` in declaration/prototype.js
return this.mixinSuper(arguments);
} else {
// use n-way Polymer binding
var observer = this.bindProperty(property, observable, oneTime);
// NOTE: reflecting binding information is typically required only for
// tooling. It has a performance cost so it's opt-in in Node.bind.
if (Platform.enableBindingsReflection && observer) {
observer.path = observable.path_;
this._recordBinding(property, observer);
}
if (this.reflect[property]) {
this.reflectPropertyToAttribute(property);
}
return observer;
}
},
_recordBinding: function(name, observer) {
this.bindings_ = this.bindings_ || {};
this.bindings_[name] = observer;
},
// Called by TemplateBinding when all bindings on an element have been
// executed. This signals that all element inputs have been gathered
// and it's safe to ready the element, create shadow-root and start
// data-observation.
bindFinished: function() {
this.makeElementReady();
},
// called at detached time to signal that an element's bindings should be
// cleaned up. This is done asynchronously so that users have the chance
// to call `cancelUnbindAll` to prevent unbinding.
asyncUnbindAll: function() {
if (!this._unbound) {
log.unbind && console.log('[%s] asyncUnbindAll', this.localName);
this._unbindAllJob = this.job(this._unbindAllJob, this.unbindAll, 0);
}
},
/**
* This method should rarely be used and only if
* `cancelUnbindAll` has been called to
* prevent element unbinding. In this case, the element's bindings will
* not be automatically cleaned up and it cannot be garbage collected
* by the system. If memory pressure is a concern or a
* large amount of elements need to be managed in this way, `unbindAll`
* can be called to deactivate the element's bindings and allow its
* memory to be reclaimed.
*
* @method unbindAll
*/
unbindAll: function() {
if (!this._unbound) {
this.closeObservers();
this.closeNamedObservers();
this._unbound = true;
}
},
/**
* Call in `detached` to prevent the element from unbinding when it is
* detached from the dom. The element is unbound as a cleanup step that
* allows its memory to be reclaimed.
* If `cancelUnbindAll` is used, consider calling
* `unbindAll` when the element is no longer
* needed. This will allow its memory to be reclaimed.
*
* @method cancelUnbindAll
*/
cancelUnbindAll: function() {
if (this._unbound) {
log.unbind && console.warn('[%s] already unbound, cannot cancel unbindAll', this.localName);
return;
}
log.unbind && console.log('[%s] cancelUnbindAll', this.localName);
if (this._unbindAllJob) {
this._unbindAllJob = this._unbindAllJob.stop();
}
}
};
function unbindNodeTree(node) {
forNodeTree(node, _nodeUnbindAll);
}
function _nodeUnbindAll(node) {
node.unbindAll();
}
function forNodeTree(node, callback) {
if (node) {
callback(node);
for (var child = node.firstChild; child; child = child.nextSibling) {
forNodeTree(child, callback);
}
}
}
var mustachePattern = /\{\{([^{}]*)}}/;
// exports
scope.bindPattern = mustachePattern;
scope.api.instance.mdv = mdv;
})(Polymer);
(function(scope) {
/**
* Common prototype for all Polymer Elements.
*
* @class polymer-base
* @homepage polymer.github.io
*/
var base = {
/**
* Tags this object as the canonical Base prototype.
*
* @property PolymerBase
* @type boolean
* @default true
*/
PolymerBase: true,
/**
* Debounce signals.
*
* Call `job` to defer a named signal, and all subsequent matching signals,
* until a wait time has elapsed with no new signal.
*
* debouncedClickAction: function(e) {
* // processClick only when it's been 100ms since the last click
* this.job('click', function() {
* this.processClick;
* }, 100);
* }
*
* @method job
* @param String {String} job A string identifier for the job to debounce.
* @param Function {Function} callback A function that is called (with `this` context) when the wait time elapses.
* @param Number {Number} wait Time in milliseconds (ms) after the last signal that must elapse before invoking `callback`
* @type Handle
*/
job: function(job, callback, wait) {
if (typeof job === 'string') {
var n = '___' + job;
this[n] = Polymer.job.call(this, this[n], callback, wait);
} else {
// TODO(sjmiles): suggest we deprecate this call signature
return Polymer.job.call(this, job, callback, wait);
}
},
/**
* Invoke a superclass method.
*
* Use `super()` to invoke the most recently overridden call to the
* currently executing function.
*
* To pass arguments through, use the literal `arguments` as the parameter
* to `super()`.
*
* nextPageAction: function(e) {
* // invoke the superclass version of `nextPageAction`
* this.super(arguments);
* }
*
* To pass custom arguments, arrange them in an array.
*
* appendSerialNo: function(value, serial) {
* // prefix the superclass serial number with our lot # before
* // invoking the superlcass
* return this.super([value, this.lotNo + serial])
* }
*
* @method super
* @type Any
* @param {args) An array of arguments to use when calling the superclass method, or null.
*/
super: Polymer.super,
/**
* Lifecycle method called when the element is instantiated.
*
* Override `created` to perform custom create-time tasks. No need to call
* super-class `created` unless you are extending another Polymer element.
* Created is called before the element creates `shadowRoot` or prepares
* data-observation.
*
* @method created
* @type void
*/
created: function() {
},
/**
* Lifecycle method called when the element has populated it's `shadowRoot`,
* prepared data-observation, and made itself ready for API interaction.
*
* @method ready
* @type void
*/
ready: function() {
},
/**
* Low-level lifecycle method called as part of standard Custom Elements
* operation. Polymer implements this method to provide basic default
* functionality. For custom create-time tasks, implement `created`
* instead, which is called immediately after `createdCallback`.
*
* @method createdCallback
*/
createdCallback: function() {
if (this.templateInstance && this.templateInstance.model) {
console.warn('Attributes on ' + this.localName + ' were data bound ' +
'prior to Polymer upgrading the element. This may result in ' +
'incorrect binding types.');
}
this.created();
this.prepareElement();
if (!this.ownerDocument.isStagingDocument) {
this.makeElementReady();
}
},
// system entry point, do not override
prepareElement: function() {
if (this._elementPrepared) {
console.warn('Element already prepared', this.localName);
return;
}
this._elementPrepared = true;
// storage for shadowRoots info
this.shadowRoots = {};
// install property observers
this.createPropertyObserver();
this.openPropertyObserver();
// install boilerplate attributes
this.copyInstanceAttributes();
// process input attributes
this.takeAttributes();
// add event listeners
this.addHostListeners();
},
// system entry point, do not override
makeElementReady: function() {
if (this._readied) {
return;
}
this._readied = true;
this.createComputedProperties();
this.parseDeclarations(this.__proto__);
// NOTE: Support use of the `unresolved` attribute to help polyfill
// custom elements' `:unresolved` feature.
this.removeAttribute('unresolved');
// user entry point
this.ready();
},
/**
* Low-level lifecycle method called as part of standard Custom Elements
* operation. Polymer implements this method to provide basic default
* functionality. For custom tasks in your element, implement `attributeChanged`
* instead, which is called immediately after `attributeChangedCallback`.
*
* @method attributeChangedCallback
*/
attributeChangedCallback: function(name, oldValue) {
// TODO(sjmiles): adhoc filter
if (name !== 'class' && name !== 'style') {
this.attributeToProperty(name, this.getAttribute(name));
}
if (this.attributeChanged) {
this.attributeChanged.apply(this, arguments);
}
},
/**
* Low-level lifecycle method called as part of standard Custom Elements
* operation. Polymer implements this method to provide basic default
* functionality. For custom create-time tasks, implement `attached`
* instead, which is called immediately after `attachedCallback`.
*
* @method attachedCallback
*/
attachedCallback: function() {
// when the element is attached, prevent it from unbinding.
this.cancelUnbindAll();
// invoke user action
if (this.attached) {
this.attached();
}
if (!this.hasBeenAttached) {
this.hasBeenAttached = true;
if (this.domReady) {
this.async('domReady');
}
}
},
/**
* Implement to access custom elements in dom descendants, ancestors,
* or siblings. Because custom elements upgrade in document order,
* elements accessed in `ready` or `attached` may not be upgraded. When
* `domReady` is called, all registered custom elements are guaranteed
* to have been upgraded.
*
* @method domReady
*/
/**
* Low-level lifecycle method called as part of standard Custom Elements
* operation. Polymer implements this method to provide basic default
* functionality. For custom create-time tasks, implement `detached`
* instead, which is called immediately after `detachedCallback`.
*
* @method detachedCallback
*/
detachedCallback: function() {
if (!this.preventDispose) {
this.asyncUnbindAll();
}
// invoke user action
if (this.detached) {
this.detached();
}
// TODO(sorvell): bc
if (this.leftView) {
this.leftView();
}
},
/**
* Walks the prototype-chain of this element and allows specific
* classes a chance to process static declarations.
*
* In particular, each polymer-element has it's own `template`.
* `parseDeclarations` is used to accumulate all element `template`s
* from an inheritance chain.
*
* `parseDeclaration` static methods implemented in the chain are called
* recursively, oldest first, with the `` associated
* with the current prototype passed as an argument.
*
* An element may override this method to customize shadow-root generation.
*
* @method parseDeclarations
*/
parseDeclarations: function(p) {
if (p && p.element) {
this.parseDeclarations(p.__proto__);
p.parseDeclaration.call(this, p.element);
}
},
/**
* Perform init-time actions based on static information in the
* `` instance argument.
*
* For example, the standard implementation locates the template associated
* with the given `` and stamps it into a shadow-root to
* implement shadow inheritance.
*
* An element may override this method for custom behavior.
*
* @method parseDeclaration
*/
parseDeclaration: function(elementElement) {
var template = this.fetchTemplate(elementElement);
if (template) {
var root = this.shadowFromTemplate(template);
this.shadowRoots[elementElement.name] = root;
}
},
/**
* Given a ``, find an associated template (if any) to be
* used for shadow-root generation.
*
* An element may override this method for custom behavior.
*
* @method fetchTemplate
*/
fetchTemplate: function(elementElement) {
return elementElement.querySelector('template');
},
/**
* Create a shadow-root in this host and stamp `template` as it's
* content.
*
* An element may override this method for custom behavior.
*
* @method shadowFromTemplate
*/
shadowFromTemplate: function(template) {
if (template) {
// make a shadow root
var root = this.createShadowRoot();
// stamp template
// which includes parsing and applying MDV bindings before being
// inserted (to avoid {{}} in attribute values)
// e.g. to prevent from generating a 404.
var dom = this.instanceTemplate(template);
// append to shadow dom
root.appendChild(dom);
// perform post-construction initialization tasks on shadow root
this.shadowRootReady(root, template);
// return the created shadow root
return root;
}
},
// utility function that stamps a into light-dom
lightFromTemplate: function(template, refNode) {
if (template) {
// TODO(sorvell): mark this element as an eventController so that
// event listeners on bound nodes inside it will be called on it.
// Note, the expectation here is that events on all descendants
// should be handled by this element.
this.eventController = this;
// stamp template
// which includes parsing and applying MDV bindings before being
// inserted (to avoid {{}} in attribute values)
// e.g. to prevent from generating a 404.
var dom = this.instanceTemplate(template);
// append to shadow dom
if (refNode) {
this.insertBefore(dom, refNode);
} else {
this.appendChild(dom);
}
// perform post-construction initialization tasks on ahem, light root
this.shadowRootReady(this);
// return the created shadow root
return dom;
}
},
shadowRootReady: function(root) {
// locate nodes with id and store references to them in this.$ hash
this.marshalNodeReferences(root);
},
// locate nodes with id and store references to them in this.$ hash
marshalNodeReferences: function(root) {
// establish $ instance variable
var $ = this.$ = this.$ || {};
// populate $ from nodes with ID from the LOCAL tree
if (root) {
var n$ = root.querySelectorAll("[id]");
for (var i=0, l=n$.length, n; (ipolymer-base in it's prototype chain.
*
* @method isBase
* @param Object {Object} object Object to test.
* @type Boolean
*/
function isBase(object) {
return object.hasOwnProperty('PolymerBase')
}
// name a base constructor for dev tools
/**
* The Polymer base-class constructor.
*
* @property Base
* @type Function
*/
function PolymerBase() {};
PolymerBase.prototype = base;
base.constructor = PolymerBase;
// exports
scope.Base = PolymerBase;
scope.isBase = isBase;
scope.api.instance.base = base;
})(Polymer);
(function(scope) {
// imports
var log = window.WebComponents ? WebComponents.flags.log : {};
var hasShadowDOMPolyfill = window.ShadowDOMPolyfill;
// magic words
var STYLE_SCOPE_ATTRIBUTE = 'element';
var STYLE_CONTROLLER_SCOPE = 'controller';
var styles = {
STYLE_SCOPE_ATTRIBUTE: STYLE_SCOPE_ATTRIBUTE,
/**
* Installs external stylesheets and
================================================
FILE: build/mui/mui-eblock/mui-eblock.html
================================================
================================================
FILE: build/mui/mui-group/mui-group.html
================================================
================================================
FILE: build/mui/mui-knob/mui-knob.html
================================================
{{label}}
================================================
FILE: build/mui/mui-knobh/mui-knobh.html
================================================
================================================
FILE: build/mui/mui-meter/mui-meter.html
================================================
================================================
FILE: build/mui/mui-pianoroll/mui-pianoroll.html
================================================
================================================
FILE: build/mui/mui-rack/mui-rack.html
================================================
================================================
FILE: build/mui/mui-select/mui-select.html
================================================
{{ label }}
▾
{{ key }}
================================================
FILE: build/mui/mui-spectrum/mui-spectrum.html
================================================
================================================
FILE: build/mui/mui-vkey/mui-vkey.html
================================================
================================================
FILE: build/mui/mui.html
================================================
================================================
FILE: build/mui/mui.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
window.MUI = {};
/**
* Mouse responder. 2D coordinate detection and event handler.
* @class
* @param {String} senderID Specified Sender ID.
* @param {Object} targetElement Target DOM element.
* @param {Function} MUICallback Event-handling callback.
*/
function MouseResponder(senderID, targetElement, MUICallback) {
this.senderId = senderID;
this.container = targetElement;
this.callback = MUICallback;
// bound function references
this.ondragged = this.dragged.bind(this);
this.onreleased = this.released.bind(this);
// timestamp
this._prevTS = 0;
// init with onclick
this.onclicked(targetElement);
}
MouseResponder.prototype = {
getEventData: function (event) {
var r = this.container.getBoundingClientRect();
return {
x: event.clientX - r.left,
y: event.clientY - r.top,
ctrlKey: event.ctrlKey,
altKey: event.altKey,
shiftKey: event.shiftKey,
metaKey: event.metaKey
};
},
onclicked: function (target) {
target.addEventListener('mousedown', function (event) {
event.preventDefault();
this._prevTS = event.timeStamp;
var p = this.getEventData(event);
this.callback(this.senderId, 'clicked', p);
window.addEventListener('mousemove', this.ondragged, false);
window.addEventListener('mouseup', this.onreleased, false);
}.bind(this), false);
},
dragged: function (event) {
event.preventDefault();
if (event.timeStamp - this._prevTS < 16.7) {
return;
}
this._prevTS = event.timeStamp;
var p = this.getEventData(event);
this.callback(this.senderId, 'dragged', p);
},
released: function (event) {
event.preventDefault();
var p = this.getEventData(event);
this.callback(this.senderId, 'released', p);
window.removeEventListener('mousemove', this.ondragged, false);
window.removeEventListener('mouseup', this.onreleased, false);
}
};
/**
* Keyboard responder, the keyboard event handler.
* @class
* @param {String} senderID Specified Sender ID.
* @param {Object} targetElement Target DOM element.
* @param {Function} MUICallback Event-handling callback.
*/
function KeyResponder(senderID, targetElement, MUICallback) {
this.senderId = senderID;
this.container = targetElement;
this.callback = MUICallback;
// bound function references
this.onkeypress = this.keypressed.bind(this);
this.onblur = this.finished.bind(this);
// init with onclick
this.onfocus(targetElement);
}
KeyResponder.prototype = {
onfocus: function () {
this.container.addEventListener('mousedown', function (event) {
this.callback(this.senderId, 'clicked', null);
this.container.addEventListener('keypress', this.onkeypress, false);
this.container.addEventListener('blur', this.onblur, false);
}.bind(this), false);
},
keypressed: function (event) {
this.callback(this.senderId, 'keypressed', event.keyCode);
},
finished: function (event) {
this.callback(this.senderId, 'finished', null);
this.container.removeEventListener('keypress', this.onkeypress, false);
this.container.removeEventListener('blur', this.onblur, false);
}
};
MUI.$ = function (elementId) {
return document.getElementById(elementId);
};
MUI.start = function (onreadyFn) {
// check up depedency: platform
if (WX.isObject(window.Platform)) {
// start function when polymer is ready
window.addEventListener('polymer-ready', onreadyFn);
} else {
WX.Log.error('FATAL: WebComponentPolyfill/Polymer is not loaded.');
}
};
MUI.isPointInArea = function (point, area) {
return (area.x <= point.x && point.x <= area.x + area.w) &&
(area.y <= point.y && point.y <= area.y + area.h);
};
MUI.buildControls = function (plugin, targetId) {
var targetEl = document.getElementById(targetId);
targetEl.label = plugin.info.name;
for (var param in plugin.params) {
var p = plugin.params[param];
switch (p.type) {
case 'Generic':
var knob = document.createElement('mui-knob');
knob.link(plugin, param);
targetEl.appendChild(knob);
break;
case 'Itemized':
var select = document.createElement('mui-select');
select.link(plugin, param);
targetEl.appendChild(select);
break;
case 'Boolean':
var button = document.createElement('mui-button');
button.type = 'toggle';
button.link(plugin, param);
targetEl.appendChild(button);
break;
}
}
};
MUI.removeChildren = function (targetId) {
var targetEl = document.getElementById(targetId);
while (targetEl.firstChild) {
targetEl.removeChild(targetEl.firstChild);
}
};
MUI.MouseResponder = function (senderID, targetElement, MUICallback) {
return new MouseResponder(senderID, targetElement, MUICallback);
};
MUI.KeyResponder = function (senderID, targetElement, MUICallback) {
return new KeyResponder(senderID, targetElement, MUICallback);
};
================================================
FILE: examples/chorus/index.html
================================================
Chorus | WAAX Examples
Chorus
================================================
FILE: examples/cmp1/index.html
================================================
CMP1 | WAAX Examples
CMP1
================================================
FILE: examples/converb/index.html
================================================
ConVerb | WAAX Examples
ConVerb
================================================
FILE: examples/eq4/index.html
================================================
EQ4 | WAAX Examples
EQ4
================================================
FILE: examples/examples.js
================================================
var wxver = document.querySelector('#wx-version');
wxver.textContent = WX.getVersion();
================================================
FILE: examples/fader/index.html
================================================
Fader | WAAX Examples
Fader
================================================
FILE: examples/filterbank/index.html
================================================
FilterBank | WAAX Examples
FilterBank
================================================
FILE: examples/fmk1/index.html
================================================
FMK1 | WAAX Examples
FMK1
================================================
FILE: examples/hellowaax/index.html
================================================
Hello WAAX! | WAAX Examples
================================================
FILE: examples/impulse/index.html
================================================
Impulse | WAAX Examples
Impulse
================================================
FILE: examples/index.html
================================================
Examples | WAAX
Examples
Basics
Hello WAAX!
The first example.
MUI Elements
Basic demonstration of MUI elements.
Plug-in Workshop
Interactive WAAX plug-in playground.
Plug-ins
Chorus
Chorus effect based on Jon Dattorro's design.
CMP1
Compressor based on Web Audio API native implementation.
ConVerb
Convolution reverberator with easy control.
EQ4
4-band parametring equalizer.
Fader
Channel fader support decibel control and stereo panning.
FilterBank
Harmonized 8-band filterbank.
FMK1
Single operator FM synth keys.
Impulse
Impulse train as an oscillator.
Noise
Robust noise generator for white and pink noise.
SimpleOsc
Simple oscillator with vibrator and tremolo.
SP1
Single timbre/polyphonic sample player.
StereoDelay
Basic stereo delay for pingpong effect and more.
WXS1
Monophonic dual oscillator subtractive synthesizer.
================================================
FILE: examples/lab/index.html
================================================
Lab | WAAX Examples
Lab
================================================
FILE: examples/mui/ex-mui-meter.html
================================================
MUI | WAAX Examples
MUI Elements
================================================
FILE: examples/mui/ex-mui-pianoroll.html
================================================
mui-pianoroll | Web Audio API eXtension
================================================
FILE: examples/mui/index.html
================================================
MUI | WAAX Examples
MUI Elements
mui-button
mui-knob
mui-knobh
mui-select
mui-eblock
mui-rack | mui-group
mui-vkey
mui-spectrum
mui-pianoroll
================================================
FILE: examples/mui/showcase.html
================================================
MUI | WAAX
NOTE : WAAX and MUI require
Chrome because they are built on top of
Web Audio API, Web MIDI API and Web Components . Check
CanIUse.com for the browser support.
×
================================================
FILE: examples/noise/index.html
================================================
CMP1 | WAAX Examples
Noise
================================================
FILE: examples/simpleosc/index.html
================================================
CMP1 | WAAX Examples
SimpleOsc
================================================
FILE: examples/sp1/index.html
================================================
SP1 | WAAX Examples
SP1
================================================
FILE: examples/stereodelay/index.html
================================================
StereoDelay | WAAX Examples
StereoDelay
================================================
FILE: examples/style.css
================================================
body {
font-family: "Roboto Condensed";
color: #37474F;
}
h1 {
font-size: 2.4rem;
margin: 2rem 0 1rem 0;
}
h2 {
font-size: 2.0rem;
margin: 1.8rem 0 0 0;
}
.wx-container {
width: 800px;
margin: 40px 20px;
}
.wx-header {
padding-bottom: 0.25rem;
border-bottom: 1px solid #607D8B;
}
.wx-title {
font-size: 1.2rem;
font-weight: 400;
letter-spacing: 0.25rem;
}
.wx-info {
color:#B0BEC5;
font-size: 0.75rem;
font-weight: 300;
}
.wx-menu {
font-size: 1.0rem;
font-weight: 300;
display: inline-block;
float: right;
line-height: 30px;
}
.wx-menu a:hover {
color: #0277BD;
}
.wx-menu a {
color: #03A9F4;
text-decoration: none;
}
.wx-toc dl {
margin-left: 0.75rem;
}
.wx-toc dt {
font-size: 1.4rem;
font-weight: 300;
margin-bottom: 0.125rem;
}
.wx-toc dd {
color: #546E7A;
font-family: "Roboto";
font-size: 0.925rem;
margin: 0 0 1.2rem 0;
}
.wx-toc a,
.wx-toc a:visited,
.wx-toc a:active {
color: #03A9F4;
text-decoration: none;
}
.wx-toc a:hover {
color: #0277BD;
text-decoration: underline;
}
h2 {
margin: 2.5rem 0 0.5rem 0;
padding: 0;
}
================================================
FILE: examples/workshop/index.html
================================================
Plug-in Workshop | WAAX Examples
Plug-in Workshop
================================================
FILE: examples/wxs1/index.html
================================================
WXS1 | WAAX Examples
WXS1
================================================
FILE: gulpfile.js
================================================
/**
* WAAX project gulp task file (1.0.0-alpha3)
*
* gulp # build everything and serve at 127.0.0.1:3000
* gulp clean # cleans dist, build path
* gulp core # minifies and concats core JS files to build/
* gulp plugins # minifies plug-in JS files to build/plug_ins
* gulp mui # copies MUI elements files to build/
* gulp serve # starts dev server 127.0.0.1:3000 and opens Chrome
* gulp build # build and minify core, plug-ins
*/
var gulp = require('gulp'),
plugins = require('gulp-load-plugins')(),
browserSync = require('browser-sync'),
runSequence = require('run-sequence'),
del = require('del');
var wrap = require('gulp-wrap');
var reload = browserSync.reload;
var WX_CORE = [
'src/waax.js',
'src/waax.extension.js',
'src/waax.util.js',
'src/waax.core.js',
'src/waax.timebase.js',
'src/mui/mui.js'
];
var WX_PLUGINS = [
'src/plug_ins/**/*.js'
];
// Clean: Empty the build directory before a complete build.
gulp.task('clean', del.bind(null, [
'build/**/*',
'!build'
]));
// Everything: Build core and plug-ins.
gulp.task('core', function () {
return gulp.src(WX_CORE)
.pipe(plugins.uglify({ mangle: false }))
.pipe(plugins.concat('waaxcore.min.js'))
.pipe(wrap('(function () {\n"use strict";\n<%= contents %>\n})();'))
.pipe(gulp.dest('build'))
.pipe(plugins.size({ title: 'core' }));
});
gulp.task('plugins', function () {
return gulp.src(WX_PLUGINS)
.pipe(plugins.uglify({ mangle: false }))
.pipe(plugins.concat('plugins.min.js'))
.pipe(gulp.dest('build'))
.pipe(plugins.size({ title: 'plugins' }));
});
gulp.task('everything', ['core', 'plugins'], function () {
gulp.src(['build/waaxcore.min.js', 'build/plugins.min.js'])
.pipe(plugins.concat('waax.min.js'))
.pipe(gulp.dest('build'))
.pipe(plugins.size({ title: 'everything' }));
});
// MUI: Build MUI elements into build/mui/ path.
gulp.task('mui', function () {
return gulp.src([
'src/mui/**/*',
'!src/mui/bower.json'
])
.pipe(gulp.dest('build/mui'))
.pipe(plugins.size({ title: 'mui' }));
});
// Serve: Start a dev server at 127.0.0.1:3000.
gulp.task('serve', function () {
browserSync({
notify: false,
server: {
baseDir: './'
},
browser: 'google chrome'
// browser: 'google chrome canary'
});
gulp.watch(['src/*.js', '!src/ktrl.js'], ['everything', reload]);
gulp.watch(['src/plug_ins/**/*.js'], ['everything', reload]);
gulp.watch(['src/mui/**/*.html'], ['mui', reload]);
gulp.watch(['examples/**/*'], reload);
gulp.watch(['test/**/*.html', 'test/**/*.js'], reload);
});
// Build: Clean and build everything in build/ path.
gulp.task('build', function (cb) {
runSequence('clean', ['everything', 'mui'], cb);
});
// Default: Build and serve.
gulp.task('default', function (cb) {
runSequence('build', 'serve', cb);
});
================================================
FILE: index.html
================================================
Index | WAAX
================================================
FILE: package.json
================================================
{
"name": "WAAX",
"version": "1.0.0-alpha3",
"description": "Web Audio API extenstion. JavaScript web music framework built on top of Web Audio API and WebComponents/Polymer.",
"author": "Hongchan Choi ",
"devDependencies": {
"browser-sync": "~1.8.2",
"chai": "^1.10.0",
"del": "~1.1.1",
"gulp": "~3.8.10",
"gulp-concat": "~2.4.3",
"gulp-flatten": "~0.0.4",
"gulp-load-plugins": "~0.8.0",
"gulp-size": "~1.1.0",
"gulp-uglify": "~1.0.2",
"gulp-wrap": "0.11.0",
"mocha": "~2.1.0",
"run-sequence": "~1.0.2"
},
"repository": {
"type": "git",
"url": "git://github.com/hoch/WAAX.git"
},
"bugs": {
"url": "https://github.com/hoch/WAAX/issues"
},
"license": "MIT"
}
================================================
FILE: sound/LICENSE
================================================
LICENSE
=======
HochKit Drum Samples ('hochkit/')
---------------------------------
: Drum samples made by myself. (CC BY-NC 3.0).
- fx-001.mp3
- fx-002.mp3
- hh-001.mp3
- hh-002.mp3
- hh-003.mp3
- hh-004.mp3
- kd-001.mp3
- kd-002.mp3
- kd-003.mp3
- kd-004.mp3
- kd-005.mp3
- kd-006.mp3
- kd-007.mp3
- kd-008.mp3
- kd-009.mp3
- kd-010.mp3
- kd-011.mp3
- kd-012.mp3
- oh-001.mp3
- oh-002.mp3
- oh-003.mp3
- perc-001.mp3
- perc-002.mp3
- perc-003.mp3
- perc-004.mp3
- perc-005.mp3
- sd-001.mp3
- sd-002.mp3
- sd-003.mp3
- sd-004.mp3
- sd-005.mp3
- sd-006.mp3
- sd-007.mp3
- sd-008.mp3
- sd-009.mp3
- sd-010.mp3
- sd-011.mp3
- sd-012.mp3
Impulse Responses ('ir/')
-------------------------
: Selectively choosen IRs from Echochamber.ch (no license info)
: http://www.echochamber.ch/index.php/tipps-freebies/impulseresponses.html
- 960-BigEmptyChurch.mp3
- 960-BriteStage.mp3
- 960-LargeBrightRoom.mp3
- 960-LargePlate.mp3
- H3000-MetalVerb.mp3
- H3000-ReverseGate.mp3
- SPX990-ElecSNRPlate.mp3
- SPX990-Reflections.mp3
- UAD140-AcousticAmbience.mp3
- UAD140-MasterPlate.mp3
Loops ('loops/')
----------------
: Strever's Rhythmguitar Loops Pack 1 by Strever953 (CC 0)
: https://www.freesound.org/people/Strever953/packs/9201/
- cgtr-120-cmaj.mp3
- cgtr-120-dmin.mp3
- cgtr-120-gmaj.mp3
================================================
FILE: src/ktrl.js
================================================
/*
Copyright 2013, Google Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* safety check first
*/
(function() {
if (typeof navigator.requestMIDIAccess === "undefined") {
throw new Error("[Ktrl] your browser does not support Web MIDI API. Halt.");
}
})();
/**
* @namespace Ktrl
* @version r1
* @author Hongchan Choi (hoch, hongchan@google.com)
*/
window.Ktrl = (function() {
// available MIDI sources (inputs) and targets (outputs)
var sources = [];
var targets = [];
// target ID counter
var targetId = 0;
// on ready event function
var onready = null;
// system-ready flag
var status = false;
// version
var version = "r1";
/**
* @class [internal] MIDI source (input port abstraction)
* @param {MIDIInput} midiinput MIDI input from MIDIAccess instance
*/
function MIDISource (midiinput) {
this.input = midiinput;
this.targets = [];
var me = this;
this.input.onmidimessage = function (e) {
var c = me.targets.length;
while (c--) {
me.targets[c].ondata(e);
}
};
}
// class MIDISource prototype
MIDISource.prototype = {
constructor: MIDISource,
/**
* removes a target from the source
* @param {object} target target object to be removed
*/
removeTarget: function (target) {
// traverse array looking for the target and remove when found
var me = this;
this.targets.map(function (t) {
if (t === target) {
var idx = me.targets.indexOf(target);
me.targets.splice(idx, 1);
}
});
},
/**
* adds a target to this source (creating a connection)
* @param {object} target target object to be added
*/
addTarget: function (target) {
// if target already exists, ignore
for(var i = 0; i < this.targets.length; ++i) {
if (this.targets[i] === target) {
post("duplicate target.");
return;
}
}
this.targets.push(target);
}
};
/**
* @class [internal] MIDI target (MIDI receiving end abstraction)
*/
function MIDITarget (label) {
this.id = targetId++;
this.label = label || "Untitled";
this.active = false;
this.process = function () {};
var me = this;
this.ondata = function(e) {
if (me.active) {
me.process(e);
}
};
targets.push(this);
}
// class MIDItarget prototype
MIDITarget.prototype = {
constructor: MIDITarget,
/**
* defines message handler for "on data" event
* @param {function} fn MIDI data handler
*/
onData: function (fn) {
this.process = fn;
},
/**
* activates the incoming data processing
*/
activate: function () {
this.active = true;
},
/**
* disables the incoming data processing
*/
disable: function () {
this.active = false;
},
/**
* get target's ID number
* @return {int} unique target ID
*/
getID: function () {
return this.id;
}
};
/**
* [helper, factory] creates a new MIDI target
* @return {object} MIDI target object
*/
function createTarget (label) {
return new MIDITarget(label);
}
/**
* routes all sources to a target
* @param {object} target MIDI target
* @return {boolean} result
*/
function routeAllToTarget (target) {
// NOTE: this won't work after closure compilation
// if (target.constructor.name !== "MIDITarget") {
// post("invalid argument. (must use MIDITarget)");
// return false;
// }
// connect all sources to the target
sources.map(function (s) {
s.addTarget(target);
});
return true;
}
/**
* routes a specific source to a target
* @param {int} sourceId MIDI source ID (see Ktrl.report() for available ID)
* @param {object} target MIDI target
* @return {boolean} result
*/
function routeSourceToTarget (sourceId, target) {
// check id first
if (sourceId < sources.length) {
// remove target from all sources
sources.map(function (s) {
s.removeTarget(target);
});
// connect a source to target
sources[sourceId].addTarget(target);
return true;
} else {
post("invalid source id or target.");
return false;
}
}
/**
* disconnect a target from all sources (while not removing)
* @param {MIDITarget} target a target to be disconnected
* @return {boolean} result
*/
function disconnectTarget (target) {
// NOTE: this won't work after closure compilation
// if (target.constructor.name !== "MIDITarget") {
// post("invalid argument. (must use MIDITarget)");
// return false;
// }
sources.map(function (s) {
s.removeTarget(target);
});
return true;
}
/**
* disconnect and remove a target from the system
* @param {MIDITarget} target a target to be disconnected
* @return {boolean} result
*/
function removeTarget (target) {
// disconnect first
if (Ktrl.disconnectTarget(target)) {
// remove target from system wide target pool
targets.map(function (t) {
if (t === target) {
var idx = targets.indexOf(target);
targets.splice(idx, 1);
}
});
return true;
} else {
return false;
}
}
/**
* [helper] defines onReady function
* @param {function} fn user-defined function
*/
function ready (fn) {
if (typeof fn !== 'function') {
post("invalid handler function.");
} else {
onready = fn;
}
}
/**
* [helper] reports available input ports
*/
function report () {
var counter = 0;
post("listing available MIDI Input Ports...");
sources.map(function (s) {
console.log(s.input.type, counter++, "\t", s.input.name, "\t", s.input.manufacturer);
});
post("listing available MIDI targets...");
targets.map(function (t) {
console.log("id " + t.id, "\t", t.label, "\t", t.active);
});
}
/**
* [helper] post message with the library tag
* @param {string} msg log message
*/
function post (msg) {
console.log("[ktrl] " + msg);
}
/**
* [helper] parses MIDI message
* @param {array} midimsg 3-bytes MIDI message
* @return {object} parsed MIDI message (see below for property names)
*
* ["noteoff", "noteon"]: { type, channel, pitch, velocity }
* ["polypressure"]: { type, channel, pitch, pressure }
* ["controlchange"]: { type, channel, control, value }
* ["programchange"]: { type, channel, program }
* ["channelpressure"]: { type, channel, pressure }
* ["pitchwheel"]: { type, channel, wheel }
*/
parse = function (midimsg) {
var data = midimsg.data;
var type = data[0] >> 4;
var channel = (data[0] & 0x0F) + 1;
var parsedData;
switch (type) {
case 8:
parsedData = {
type: "noteoff",
channel: channel,
pitch: data[1],
velocity: data[2]
};
break;
case 9:
parsedData = {
type: "noteon",
channel: channel,
pitch: data[1],
velocity: data[2]
};
break;
case 10:
parsedData = {
type: "polypressure",
channel: channel,
pitch: data[1],
pressure: data[2]
};
break;
case 11:
parsedData = {
type: "controlchange",
channel: channel,
control: data[1],
value: data[2]
};
break;
case 12:
parsedData = {
type: "programchange",
channel: channel,
program: data[1]
};
break;
case 13:
parsedData = {
type: "channelpressure",
channel: channel,
pressure: data[1]
};
break;
case 14:
parsedData = {
type: "pitchwheel",
channel: channel,
wheel: (data[1] << 8 | data[2])
};
break;
}
return parsedData;
};
// ENTRY POINT: scan input port and boot up
navigator.requestMIDIAccess().then(function (midiAccess) {
// check input ports
if (midiAccess.inputs().length === 0) {
post("no input ports available");
return;
}
// creating MIDI sources
for(var i = 0; i < midiAccess.inputs().length; ++i) {
sources[i] = new MIDISource(midiAccess.inputs()[i]);
}
post("Ktrl (" + version + ") is ready.");
status = true;
if (typeof onready === 'function') {
onready();
} else {
post("onReady is not specified.");
}
}, function (msg) {
post("failed to get MIDI access: " + msg);
status = false;
return;
});
// exposes handles
return {
createTarget: createTarget,
removeTarget: removeTarget,
disconnectTarget: disconnectTarget,
routeAllToTarget: routeAllToTarget,
routeSourceToTarget: routeSourceToTarget,
ready: ready,
parse: parse,
report: report
};
})();
================================================
FILE: src/mui/bower.json
================================================
{
"name": "MUI",
"version": "1.0.0-alpha3",
"homepage": "https://github.com/hoch/WAAX",
"license": "MIT",
"ignore": [
"**/.*",
"bower_components"
],
"dependencies": {
"polymer": "Polymer/polymer#~0.5.1",
"core-icon": "Polymer/core-icon#~0.5.2"
}
}
================================================
FILE: src/mui/bower_components/core-component-page/.bower.json
================================================
{
"name": "core-component-page",
"private": true,
"dependencies": {
"webcomponentsjs": "Polymer/webcomponentsjs#^0.5.0",
"polymer": "Polymer/polymer#^0.5.0"
},
"version": "0.5.2",
"homepage": "https://github.com/Polymer/core-component-page",
"_release": "0.5.2",
"_resolution": {
"type": "version",
"tag": "0.5.2",
"commit": "443d8dcbcb1b203ed88c6af64c98f76e6434ba53"
},
"_source": "git://github.com/Polymer/core-component-page.git",
"_target": "^0.5.0",
"_originalSource": "Polymer/core-component-page"
}
================================================
FILE: src/mui/bower_components/core-component-page/README.md
================================================
core-component-page
===================
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-component-page) for more information.
Note: this is the vulcanized version of [`core-component-page-dev`](https://github.com/Polymer/core-component-page-dev) (the source).
================================================
FILE: src/mui/bower_components/core-component-page/bower.json
================================================
{
"name": "core-component-page",
"private": true,
"dependencies": {
"webcomponentsjs": "Polymer/webcomponentsjs#^0.5.0",
"polymer": "Polymer/polymer#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: src/mui/bower_components/core-component-page/core-component-page.html
================================================
{{data.name}} Home Page Version: {{data.version}}
Attributes <{{attribute.type}} >default: {{attribute.default}}
Properties <{{property.type}} >default: {{property.default}}
Events Event details:
<{{param.type}} > {{param.name}}
{{param.description}}
Methods Method parameters:
<{{param.type}} > {{param.name}}
{{param.description}}
{{label}}
{{name}}
undefined
{{moduleName}} demo
================================================
FILE: src/mui/bower_components/core-component-page/demo.html
================================================
================================================
FILE: src/mui/bower_components/core-component-page/index.html
================================================
================================================
FILE: src/mui/bower_components/core-icon/.bower.json
================================================
{
"name": "core-icon",
"private": true,
"dependencies": {
"core-iconset": "Polymer/core-iconset#^0.5.0",
"core-icons": "Polymer/core-icons#^0.5.0"
},
"version": "0.5.2",
"homepage": "https://github.com/Polymer/core-icon",
"_release": "0.5.2",
"_resolution": {
"type": "version",
"tag": "0.5.2",
"commit": "4b6ec20167ad5c176c403ee4ca2387f73dd11532"
},
"_source": "git://github.com/Polymer/core-icon.git",
"_target": "~0.5.2",
"_originalSource": "Polymer/core-icon"
}
================================================
FILE: src/mui/bower_components/core-icon/README.md
================================================
core-icon
=========
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-icon) for more information.
================================================
FILE: src/mui/bower_components/core-icon/bower.json
================================================
{
"name": "core-icon",
"private": true,
"dependencies": {
"core-iconset": "Polymer/core-iconset#^0.5.0",
"core-icons": "Polymer/core-icons#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: src/mui/bower_components/core-icon/core-icon.css
================================================
/* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */
html /deep/ core-icon {
display: inline-block;
vertical-align: middle;
background-repeat: no-repeat;
fill: currentcolor;
position: relative;
height: 24px;
width: 24px;
}
================================================
FILE: src/mui/bower_components/core-icon/core-icon.html
================================================
================================================
FILE: src/mui/bower_components/core-icon/demo.html
================================================
core-icon
{{icon}}
Sized icon:
================================================
FILE: src/mui/bower_components/core-icon/index.html
================================================
================================================
FILE: src/mui/bower_components/core-icon/metadata.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/.bower.json
================================================
{
"name": "core-icons",
"private": true,
"dependencies": {
"core-icon": "Polymer/core-icon#^0.5.0",
"core-iconset-svg": "Polymer/core-iconset-svg#^0.5.0",
"polymer": "Polymer/polymer#^0.5.0"
},
"version": "0.5.2",
"homepage": "https://github.com/Polymer/core-icons",
"_release": "0.5.2",
"_resolution": {
"type": "version",
"tag": "0.5.2",
"commit": "d08341261f7b386fb331b1dd798fcd71727e7c85"
},
"_source": "git://github.com/Polymer/core-icons.git",
"_target": "^0.5.0",
"_originalSource": "Polymer/core-icons"
}
================================================
FILE: src/mui/bower_components/core-icons/README.md
================================================
core-icons
=========
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-icons) for more information.
================================================
FILE: src/mui/bower_components/core-icons/av-icons.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/bower.json
================================================
{
"name": "core-icons",
"private": true,
"dependencies": {
"core-icon": "Polymer/core-icon#^0.5.0",
"core-iconset-svg": "Polymer/core-iconset-svg#^0.5.0",
"polymer": "Polymer/polymer#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: src/mui/bower_components/core-icons/communication-icons.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/core-icons.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/demo.html
================================================
core-icons
{{iconset.id}}
{{iconset.id === 'icons' ? 'The Default Set' : 'Import ' + iconset.id + '-icons.html'}}
================================================
FILE: src/mui/bower_components/core-icons/device-icons.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/editor-icons.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/hardware-icons.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/image-icons.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/index.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/maps-icons.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/notification-icons.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/png-icons.html
================================================
================================================
FILE: src/mui/bower_components/core-icons/social-icons.html
================================================
+
================================================
FILE: src/mui/bower_components/core-iconset/.bower.json
================================================
{
"name": "core-iconset",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#^0.5.0",
"core-meta": "Polymer/core-meta#^0.5.0",
"core-icon": "Polymer/core-icon#^0.5.0"
},
"version": "0.5.2",
"homepage": "https://github.com/Polymer/core-iconset",
"_release": "0.5.2",
"_resolution": {
"type": "version",
"tag": "0.5.2",
"commit": "fabcd6967770984f460930ec1059b950b4126828"
},
"_source": "git://github.com/Polymer/core-iconset.git",
"_target": "^0.5.0",
"_originalSource": "Polymer/core-iconset"
}
================================================
FILE: src/mui/bower_components/core-iconset/README.md
================================================
core-iconset
============
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-iconset) for more information.
================================================
FILE: src/mui/bower_components/core-iconset/bower.json
================================================
{
"name": "core-iconset",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#^0.5.0",
"core-meta": "Polymer/core-meta#^0.5.0",
"core-icon": "Polymer/core-icon#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: src/mui/bower_components/core-iconset/core-iconset.html
================================================
================================================
FILE: src/mui/bower_components/core-iconset/demo.html
================================================
core-iconset
================================================
FILE: src/mui/bower_components/core-iconset/index.html
================================================
================================================
FILE: src/mui/bower_components/core-iconset-svg/.bower.json
================================================
{
"name": "core-iconset-svg",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#^0.5.0",
"core-iconset": "Polymer/core-iconset#^0.5.0"
},
"version": "0.5.2",
"homepage": "https://github.com/Polymer/core-iconset-svg",
"_release": "0.5.2",
"_resolution": {
"type": "version",
"tag": "0.5.2",
"commit": "998ef1ee71f358cd0e95e6085945c0182d86c224"
},
"_source": "git://github.com/Polymer/core-iconset-svg.git",
"_target": "^0.5.0",
"_originalSource": "Polymer/core-iconset-svg"
}
================================================
FILE: src/mui/bower_components/core-iconset-svg/README.md
================================================
core-iconset-svg
=========
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-iconset-svg) for more information.
================================================
FILE: src/mui/bower_components/core-iconset-svg/bower.json
================================================
{
"name": "core-iconset-svg",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#^0.5.0",
"core-iconset": "Polymer/core-iconset#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: src/mui/bower_components/core-iconset-svg/core-iconset-svg.html
================================================
================================================
FILE: src/mui/bower_components/core-iconset-svg/demo.html
================================================
core-iconset-svg
================================================
FILE: src/mui/bower_components/core-iconset-svg/index.html
================================================
================================================
FILE: src/mui/bower_components/core-iconset-svg/svg-sample-icons.html
================================================
================================================
FILE: src/mui/bower_components/core-meta/.bower.json
================================================
{
"name": "core-meta",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#^0.5.0"
},
"version": "0.5.2",
"homepage": "https://github.com/Polymer/core-meta",
"_release": "0.5.2",
"_resolution": {
"type": "version",
"tag": "0.5.2",
"commit": "d16e6c42ed1a4a2b3ada1d978859c85bdcedb79f"
},
"_source": "git://github.com/Polymer/core-meta.git",
"_target": "^0.5.0",
"_originalSource": "Polymer/core-meta"
}
================================================
FILE: src/mui/bower_components/core-meta/README.md
================================================
core-meta
=========
See the [component page](http://polymer-project.org/docs/elements/core-elements.html#core-meta) for more information.
================================================
FILE: src/mui/bower_components/core-meta/bower.json
================================================
{
"name": "core-meta",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#^0.5.0"
},
"version": "0.5.2"
}
================================================
FILE: src/mui/bower_components/core-meta/core-meta.html
================================================
================================================
FILE: src/mui/bower_components/core-meta/demo.html
================================================
core-meta
meta-data
{{label}}
meta-data (type: fruit)
{{label}}
================================================
FILE: src/mui/bower_components/core-meta/index.html
================================================
================================================
FILE: src/mui/bower_components/polymer/.bower.json
================================================
{
"name": "polymer",
"description": "Polymer is a new type of library for the web, built on top of Web Components, and designed to leverage the evolving web platform on modern browsers.",
"homepage": "http://www.polymer-project.org/",
"keywords": [
"util",
"client",
"browser",
"web components",
"web-components"
],
"author": "Polymer Authors ",
"private": true,
"dependencies": {
"core-component-page": "Polymer/core-component-page#^0.5.0",
"webcomponentsjs": "Polymer/webcomponentsjs#^0.5.0"
},
"devDependencies": {
"tools": "Polymer/tools#master",
"web-component-tester": "Polymer/web-component-tester#^1.4.2"
},
"version": "0.5.2",
"_release": "0.5.2",
"_resolution": {
"type": "version",
"tag": "0.5.2",
"commit": "2a9ca53fa79ca09907e818f1adf806765409ec9c"
},
"_source": "git://github.com/Polymer/polymer.git",
"_target": "~0.5.1",
"_originalSource": "Polymer/polymer"
}
================================================
FILE: src/mui/bower_components/polymer/README.md
================================================
# Polymer
[](http://build.chromium.org/p/client.polymer/waterfall)
## Brief Overview
For more detailed info goto [http://polymer-project.org/](http://polymer-project.org/).
Polymer is a new type of library for the web, designed to leverage the existing browser infrastructure to provide the encapsulation and extendability currently only available in JS libraries.
Polymer is based on a set of future technologies, including [Shadow DOM](http://w3c.github.io/webcomponents/spec/shadow/), [Custom Elements](http://w3c.github.io/webcomponents/spec/custom/) and Model Driven Views. Currently these technologies are implemented as polyfills or shims, but as browsers adopt these features natively, the platform code that drives Polymer evacipates, leaving only the value-adds.
## Tools & Testing
For running tests or building minified files, consult the [tooling information](https://www.polymer-project.org/resources/tooling-strategy.html).
## Releases
[Release (tagged) versions](https://github.com/Polymer/polymer/releases) of Polymer include concatenated and minified sources for your convenience.
[](https://github.com/igrigorik/ga-beacon)
================================================
FILE: src/mui/bower_components/polymer/bower.json
================================================
{
"name": "polymer",
"description": "Polymer is a new type of library for the web, built on top of Web Components, and designed to leverage the evolving web platform on modern browsers.",
"homepage": "http://www.polymer-project.org/",
"keywords": [
"util",
"client",
"browser",
"web components",
"web-components"
],
"author": "Polymer Authors ",
"private": true,
"dependencies": {
"core-component-page": "Polymer/core-component-page#^0.5.0",
"webcomponentsjs": "Polymer/webcomponentsjs#^0.5.0"
},
"devDependencies": {
"tools": "Polymer/tools#master",
"web-component-tester": "Polymer/web-component-tester#^1.4.2"
},
"version": "0.5.2"
}
================================================
FILE: src/mui/bower_components/polymer/build.log
================================================
BUILD LOG
---------
Build Time: 2014-12-11T12:46:30
NODEJS INFORMATION
==================
nodejs: v0.10.33
grunt: 0.4.5
grunt-audit: 1.0.0
grunt-contrib-concat: 0.5.0
grunt-contrib-copy: 0.7.0
grunt-contrib-uglify: 0.6.0
grunt-string-replace: 1.0.0
REPO REVISIONS
==============
polymer-expressions: 197c3a0150e7a13374cfcc72e7066113723a623d
polymer-gestures: 17a6304916521be39409af292e8adf899bae0ce7
polymer: a74e9f36526361dccb6df91be439ff9c3e043f41
BUILD HASHES
============
dist/polymer.js: b9ad4c86af79c748cf4ea722f6d56671079fadf7
dist/polymer.min.js: 2f2021ba9682b0bb702ee7fb68fb6fbfd288eac2
dist/layout.html: 348d358a91712ecc2f8811efa430fcd954b4590c
================================================
FILE: src/mui/bower_components/polymer/dist/polymer.html
================================================
================================================
FILE: src/mui/bower_components/polymer/layout.html
================================================
================================================
FILE: src/mui/bower_components/polymer/polymer.html
================================================
================================================
FILE: src/mui/bower_components/polymer/polymer.js
================================================
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
*/
// @version 0.5.1
window.PolymerGestures = {};
(function(scope) {
var HAS_FULL_PATH = false;
// test for full event path support
var pathTest = document.createElement('meta');
if (pathTest.createShadowRoot) {
var sr = pathTest.createShadowRoot();
var s = document.createElement('span');
sr.appendChild(s);
pathTest.addEventListener('testpath', function(ev) {
if (ev.path) {
// if the span is in the event path, then path[0] is the real source for all events
HAS_FULL_PATH = ev.path[0] === s;
}
ev.stopPropagation();
});
var ev = new CustomEvent('testpath', {bubbles: true});
// must add node to DOM to trigger event listener
document.head.appendChild(pathTest);
s.dispatchEvent(ev);
pathTest.parentNode.removeChild(pathTest);
sr = s = null;
}
pathTest = null;
var target = {
shadow: function(inEl) {
if (inEl) {
return inEl.shadowRoot || inEl.webkitShadowRoot;
}
},
canTarget: function(shadow) {
return shadow && Boolean(shadow.elementFromPoint);
},
targetingShadow: function(inEl) {
var s = this.shadow(inEl);
if (this.canTarget(s)) {
return s;
}
},
olderShadow: function(shadow) {
var os = shadow.olderShadowRoot;
if (!os) {
var se = shadow.querySelector('shadow');
if (se) {
os = se.olderShadowRoot;
}
}
return os;
},
allShadows: function(element) {
var shadows = [], s = this.shadow(element);
while(s) {
shadows.push(s);
s = this.olderShadow(s);
}
return shadows;
},
searchRoot: function(inRoot, x, y) {
var t, st, sr, os;
if (inRoot) {
t = inRoot.elementFromPoint(x, y);
if (t) {
// found element, check if it has a ShadowRoot
sr = this.targetingShadow(t);
} else if (inRoot !== document) {
// check for sibling roots
sr = this.olderShadow(inRoot);
}
// search other roots, fall back to light dom element
return this.searchRoot(sr, x, y) || t;
}
},
owner: function(element) {
if (!element) {
return document;
}
var s = element;
// walk up until you hit the shadow root or document
while (s.parentNode) {
s = s.parentNode;
}
// the owner element is expected to be a Document or ShadowRoot
if (s.nodeType != Node.DOCUMENT_NODE && s.nodeType != Node.DOCUMENT_FRAGMENT_NODE) {
s = document;
}
return s;
},
findTarget: function(inEvent) {
if (HAS_FULL_PATH && inEvent.path && inEvent.path.length) {
return inEvent.path[0];
}
var x = inEvent.clientX, y = inEvent.clientY;
// if the listener is in the shadow root, it is much faster to start there
var s = this.owner(inEvent.target);
// if x, y is not in this root, fall back to document search
if (!s.elementFromPoint(x, y)) {
s = document;
}
return this.searchRoot(s, x, y);
},
findTouchAction: function(inEvent) {
var n;
if (HAS_FULL_PATH && inEvent.path && inEvent.path.length) {
var path = inEvent.path;
for (var i = 0; i < path.length; i++) {
n = path[i];
if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) {
return n.getAttribute('touch-action');
}
}
} else {
n = inEvent.target;
while(n) {
if (n.nodeType === Node.ELEMENT_NODE && n.hasAttribute('touch-action')) {
return n.getAttribute('touch-action');
}
n = n.parentNode || n.host;
}
}
// auto is default
return "auto";
},
LCA: function(a, b) {
if (a === b) {
return a;
}
if (a && !b) {
return a;
}
if (b && !a) {
return b;
}
if (!b && !a) {
return document;
}
// fast case, a is a direct descendant of b or vice versa
if (a.contains && a.contains(b)) {
return a;
}
if (b.contains && b.contains(a)) {
return b;
}
var adepth = this.depth(a);
var bdepth = this.depth(b);
var d = adepth - bdepth;
if (d >= 0) {
a = this.walk(a, d);
} else {
b = this.walk(b, -d);
}
while (a && b && a !== b) {
a = a.parentNode || a.host;
b = b.parentNode || b.host;
}
return a;
},
walk: function(n, u) {
for (var i = 0; n && (i < u); i++) {
n = n.parentNode || n.host;
}
return n;
},
depth: function(n) {
var d = 0;
while(n) {
d++;
n = n.parentNode || n.host;
}
return d;
},
deepContains: function(a, b) {
var common = this.LCA(a, b);
// if a is the common ancestor, it must "deeply" contain b
return common === a;
},
insideNode: function(node, x, y) {
var rect = node.getBoundingClientRect();
return (rect.left <= x) && (x <= rect.right) && (rect.top <= y) && (y <= rect.bottom);
},
path: function(event) {
var p;
if (HAS_FULL_PATH && event.path && event.path.length) {
p = event.path;
} else {
p = [];
var n = this.findTarget(event);
while (n) {
p.push(n);
n = n.parentNode || n.host;
}
}
return p;
}
};
scope.targetFinding = target;
/**
* Given an event, finds the "deepest" node that could have been the original target before ShadowDOM retargetting
*
* @param {Event} Event An event object with clientX and clientY properties
* @return {Element} The probable event origninator
*/
scope.findTarget = target.findTarget.bind(target);
/**
* Determines if the "container" node deeply contains the "containee" node, including situations where the "containee" is contained by one or more ShadowDOM
* roots.
*
* @param {Node} container
* @param {Node} containee
* @return {Boolean}
*/
scope.deepContains = target.deepContains.bind(target);
/**
* Determines if the x/y position is inside the given node.
*
* Example:
*
* function upHandler(event) {
* var innode = PolymerGestures.insideNode(event.target, event.clientX, event.clientY);
* if (innode) {
* // wait for tap?
* } else {
* // tap will never happen
* }
* }
*
* @param {Node} node
* @param {Number} x Screen X position
* @param {Number} y screen Y position
* @return {Boolean}
*/
scope.insideNode = target.insideNode;
})(window.PolymerGestures);
(function() {
function shadowSelector(v) {
return 'html /deep/ ' + selector(v);
}
function selector(v) {
return '[touch-action="' + v + '"]';
}
function rule(v) {
return '{ -ms-touch-action: ' + v + '; touch-action: ' + v + ';}';
}
var attrib2css = [
'none',
'auto',
'pan-x',
'pan-y',
{
rule: 'pan-x pan-y',
selectors: [
'pan-x pan-y',
'pan-y pan-x'
]
},
'manipulation'
];
var styles = '';
// only install stylesheet if the browser has touch action support
var hasTouchAction = typeof document.head.style.touchAction === 'string';
// only add shadow selectors if shadowdom is supported
var hasShadowRoot = !window.ShadowDOMPolyfill && document.head.createShadowRoot;
if (hasTouchAction) {
attrib2css.forEach(function(r) {
if (String(r) === r) {
styles += selector(r) + rule(r) + '\n';
if (hasShadowRoot) {
styles += shadowSelector(r) + rule(r) + '\n';
}
} else {
styles += r.selectors.map(selector) + rule(r.rule) + '\n';
if (hasShadowRoot) {
styles += r.selectors.map(shadowSelector) + rule(r.rule) + '\n';
}
}
});
var el = document.createElement('style');
el.textContent = styles;
document.head.appendChild(el);
}
})();
/**
* This is the constructor for new PointerEvents.
*
* New Pointer Events must be given a type, and an optional dictionary of
* initialization properties.
*
* Due to certain platform requirements, events returned from the constructor
* identify as MouseEvents.
*
* @constructor
* @param {String} inType The type of the event to create.
* @param {Object} [inDict] An optional dictionary of initial event properties.
* @return {Event} A new PointerEvent of type `inType` and initialized with properties from `inDict`.
*/
(function(scope) {
var MOUSE_PROPS = [
'bubbles',
'cancelable',
'view',
'detail',
'screenX',
'screenY',
'clientX',
'clientY',
'ctrlKey',
'altKey',
'shiftKey',
'metaKey',
'button',
'relatedTarget',
'pageX',
'pageY'
];
var MOUSE_DEFAULTS = [
false,
false,
null,
null,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null,
0,
0
];
var NOP_FACTORY = function(){ return function(){}; };
var eventFactory = {
// TODO(dfreedm): this is overridden by tap recognizer, needs review
preventTap: NOP_FACTORY,
makeBaseEvent: function(inType, inDict) {
var e = document.createEvent('Event');
e.initEvent(inType, inDict.bubbles || false, inDict.cancelable || false);
e.preventTap = eventFactory.preventTap(e);
return e;
},
makeGestureEvent: function(inType, inDict) {
inDict = inDict || Object.create(null);
var e = this.makeBaseEvent(inType, inDict);
for (var i = 0, keys = Object.keys(inDict), k; i < keys.length; i++) {
k = keys[i];
e[k] = inDict[k];
}
return e;
},
makePointerEvent: function(inType, inDict) {
inDict = inDict || Object.create(null);
var e = this.makeBaseEvent(inType, inDict);
// define inherited MouseEvent properties
for(var i = 0, p; i < MOUSE_PROPS.length; i++) {
p = MOUSE_PROPS[i];
e[p] = inDict[p] || MOUSE_DEFAULTS[i];
}
e.buttons = inDict.buttons || 0;
// Spec requires that pointers without pressure specified use 0.5 for down
// state and 0 for up state.
var pressure = 0;
if (inDict.pressure) {
pressure = inDict.pressure;
} else {
pressure = e.buttons ? 0.5 : 0;
}
// add x/y properties aliased to clientX/Y
e.x = e.clientX;
e.y = e.clientY;
// define the properties of the PointerEvent interface
e.pointerId = inDict.pointerId || 0;
e.width = inDict.width || 0;
e.height = inDict.height || 0;
e.pressure = pressure;
e.tiltX = inDict.tiltX || 0;
e.tiltY = inDict.tiltY || 0;
e.pointerType = inDict.pointerType || '';
e.hwTimestamp = inDict.hwTimestamp || 0;
e.isPrimary = inDict.isPrimary || false;
e._source = inDict._source || '';
return e;
}
};
scope.eventFactory = eventFactory;
})(window.PolymerGestures);
/**
* This module implements an map of pointer states
*/
(function(scope) {
var USE_MAP = window.Map && window.Map.prototype.forEach;
var POINTERS_FN = function(){ return this.size; };
function PointerMap() {
if (USE_MAP) {
var m = new Map();
m.pointers = POINTERS_FN;
return m;
} else {
this.keys = [];
this.values = [];
}
}
PointerMap.prototype = {
set: function(inId, inEvent) {
var i = this.keys.indexOf(inId);
if (i > -1) {
this.values[i] = inEvent;
} else {
this.keys.push(inId);
this.values.push(inEvent);
}
},
has: function(inId) {
return this.keys.indexOf(inId) > -1;
},
'delete': function(inId) {
var i = this.keys.indexOf(inId);
if (i > -1) {
this.keys.splice(i, 1);
this.values.splice(i, 1);
}
},
get: function(inId) {
var i = this.keys.indexOf(inId);
return this.values[i];
},
clear: function() {
this.keys.length = 0;
this.values.length = 0;
},
// return value, key, map
forEach: function(callback, thisArg) {
this.values.forEach(function(v, i) {
callback.call(thisArg, v, this.keys[i], this);
}, this);
},
pointers: function() {
return this.keys.length;
}
};
scope.PointerMap = PointerMap;
})(window.PolymerGestures);
(function(scope) {
var CLONE_PROPS = [
// MouseEvent
'bubbles',
'cancelable',
'view',
'detail',
'screenX',
'screenY',
'clientX',
'clientY',
'ctrlKey',
'altKey',
'shiftKey',
'metaKey',
'button',
'relatedTarget',
// DOM Level 3
'buttons',
// PointerEvent
'pointerId',
'width',
'height',
'pressure',
'tiltX',
'tiltY',
'pointerType',
'hwTimestamp',
'isPrimary',
// event instance
'type',
'target',
'currentTarget',
'which',
'pageX',
'pageY',
'timeStamp',
// gesture addons
'preventTap',
'tapPrevented',
'_source'
];
var CLONE_DEFAULTS = [
// MouseEvent
false,
false,
null,
null,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null,
// DOM Level 3
0,
// PointerEvent
0,
0,
0,
0,
0,
0,
'',
0,
false,
// event instance
'',
null,
null,
0,
0,
0,
0,
function(){},
false
];
var HAS_SVG_INSTANCE = (typeof SVGElementInstance !== 'undefined');
var eventFactory = scope.eventFactory;
// set of recognizers to run for the currently handled event
var currentGestures;
/**
* This module is for normalizing events. Mouse and Touch events will be
* collected here, and fire PointerEvents that have the same semantics, no
* matter the source.
* Events fired:
* - pointerdown: a pointing is added
* - pointerup: a pointer is removed
* - pointermove: a pointer is moved
* - pointerover: a pointer crosses into an element
* - pointerout: a pointer leaves an element
* - pointercancel: a pointer will no longer generate events
*/
var dispatcher = {
IS_IOS: false,
pointermap: new scope.PointerMap(),
requiredGestures: new scope.PointerMap(),
eventMap: Object.create(null),
// Scope objects for native events.
// This exists for ease of testing.
eventSources: Object.create(null),
eventSourceList: [],
gestures: [],
// map gesture event -> {listeners: int, index: gestures[int]}
dependencyMap: {
// make sure down and up are in the map to trigger "register"
down: {listeners: 0, index: -1},
up: {listeners: 0, index: -1}
},
gestureQueue: [],
/**
* Add a new event source that will generate pointer events.
*
* `inSource` must contain an array of event names named `events`, and
* functions with the names specified in the `events` array.
* @param {string} name A name for the event source
* @param {Object} source A new source of platform events.
*/
registerSource: function(name, source) {
var s = source;
var newEvents = s.events;
if (newEvents) {
newEvents.forEach(function(e) {
if (s[e]) {
this.eventMap[e] = s[e].bind(s);
}
}, this);
this.eventSources[name] = s;
this.eventSourceList.push(s);
}
},
registerGesture: function(name, source) {
var obj = Object.create(null);
obj.listeners = 0;
obj.index = this.gestures.length;
for (var i = 0, g; i < source.exposes.length; i++) {
g = source.exposes[i].toLowerCase();
this.dependencyMap[g] = obj;
}
this.gestures.push(source);
},
register: function(element, initial) {
var l = this.eventSourceList.length;
for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) {
// call eventsource register
es.register.call(es, element, initial);
}
},
unregister: function(element) {
var l = this.eventSourceList.length;
for (var i = 0, es; (i < l) && (es = this.eventSourceList[i]); i++) {
// call eventsource register
es.unregister.call(es, element);
}
},
// EVENTS
down: function(inEvent) {
this.requiredGestures.set(inEvent.pointerId, currentGestures);
this.fireEvent('down', inEvent);
},
move: function(inEvent) {
// pipe move events into gesture queue directly
inEvent.type = 'move';
this.fillGestureQueue(inEvent);
},
up: function(inEvent) {
this.fireEvent('up', inEvent);
this.requiredGestures.delete(inEvent.pointerId);
},
cancel: function(inEvent) {
inEvent.tapPrevented = true;
this.fireEvent('up', inEvent);
this.requiredGestures.delete(inEvent.pointerId);
},
addGestureDependency: function(node, currentGestures) {
var gesturesWanted = node._pgEvents;
if (gesturesWanted && currentGestures) {
var gk = Object.keys(gesturesWanted);
for (var i = 0, r, ri, g; i < gk.length; i++) {
// gesture
g = gk[i];
if (gesturesWanted[g] > 0) {
// lookup gesture recognizer
r = this.dependencyMap[g];
// recognizer index
ri = r ? r.index : -1;
currentGestures[ri] = true;
}
}
}
},
// LISTENER LOGIC
eventHandler: function(inEvent) {
// This is used to prevent multiple dispatch of events from
// platform events. This can happen when two elements in different scopes
// are set up to create pointer events, which is relevant to Shadow DOM.
var type = inEvent.type;
// only generate the list of desired events on "down"
if (type === 'touchstart' || type === 'mousedown' || type === 'pointerdown' || type === 'MSPointerDown') {
if (!inEvent._handledByPG) {
currentGestures = {};
}
// in IOS mode, there is only a listener on the document, so this is not re-entrant
if (this.IS_IOS) {
var ev = inEvent;
if (type === 'touchstart') {
var ct = inEvent.changedTouches[0];
// set up a fake event to give to the path builder
ev = {target: inEvent.target, clientX: ct.clientX, clientY: ct.clientY, path: inEvent.path};
}
// use event path if available, otherwise build a path from target finding
var nodes = inEvent.path || scope.targetFinding.path(ev);
for (var i = 0, n; i < nodes.length; i++) {
n = nodes[i];
this.addGestureDependency(n, currentGestures);
}
} else {
this.addGestureDependency(inEvent.currentTarget, currentGestures);
}
}
if (inEvent._handledByPG) {
return;
}
var fn = this.eventMap && this.eventMap[type];
if (fn) {
fn(inEvent);
}
inEvent._handledByPG = true;
},
// set up event listeners
listen: function(target, events) {
for (var i = 0, l = events.length, e; (i < l) && (e = events[i]); i++) {
this.addEvent(target, e);
}
},
// remove event listeners
unlisten: function(target, events) {
for (var i = 0, l = events.length, e; (i < l) && (e = events[i]); i++) {
this.removeEvent(target, e);
}
},
addEvent: function(target, eventName) {
target.addEventListener(eventName, this.boundHandler);
},
removeEvent: function(target, eventName) {
target.removeEventListener(eventName, this.boundHandler);
},
// EVENT CREATION AND TRACKING
/**
* Creates a new Event of type `inType`, based on the information in
* `inEvent`.
*
* @param {string} inType A string representing the type of event to create
* @param {Event} inEvent A platform event with a target
* @return {Event} A PointerEvent of type `inType`
*/
makeEvent: function(inType, inEvent) {
var e = eventFactory.makePointerEvent(inType, inEvent);
e.preventDefault = inEvent.preventDefault;
e.tapPrevented = inEvent.tapPrevented;
e._target = e._target || inEvent.target;
return e;
},
// make and dispatch an event in one call
fireEvent: function(inType, inEvent) {
var e = this.makeEvent(inType, inEvent);
return this.dispatchEvent(e);
},
/**
* Returns a snapshot of inEvent, with writable properties.
*
* @param {Event} inEvent An event that contains properties to copy.
* @return {Object} An object containing shallow copies of `inEvent`'s
* properties.
*/
cloneEvent: function(inEvent) {
var eventCopy = Object.create(null), p;
for (var i = 0; i < CLONE_PROPS.length; i++) {
p = CLONE_PROPS[i];
eventCopy[p] = inEvent[p] || CLONE_DEFAULTS[i];
// Work around SVGInstanceElement shadow tree
// Return the element that is represented by the instance for Safari, Chrome, IE.
// This is the behavior implemented by Firefox.
if (p === 'target' || p === 'relatedTarget') {
if (HAS_SVG_INSTANCE && eventCopy[p] instanceof SVGElementInstance) {
eventCopy[p] = eventCopy[p].correspondingUseElement;
}
}
}
// keep the semantics of preventDefault
eventCopy.preventDefault = function() {
inEvent.preventDefault();
};
return eventCopy;
},
/**
* Dispatches the event to its target.
*
* @param {Event} inEvent The event to be dispatched.
* @return {Boolean} True if an event handler returns true, false otherwise.
*/
dispatchEvent: function(inEvent) {
var t = inEvent._target;
if (t) {
t.dispatchEvent(inEvent);
// clone the event for the gesture system to process
// clone after dispatch to pick up gesture prevention code
var clone = this.cloneEvent(inEvent);
clone.target = t;
this.fillGestureQueue(clone);
}
},
gestureTrigger: function() {
// process the gesture queue
for (var i = 0, e, rg; i < this.gestureQueue.length; i++) {
e = this.gestureQueue[i];
rg = e._requiredGestures;
if (rg) {
for (var j = 0, g, fn; j < this.gestures.length; j++) {
// only run recognizer if an element in the source event's path is listening for those gestures
if (rg[j]) {
g = this.gestures[j];
fn = g[e.type];
if (fn) {
fn.call(g, e);
}
}
}
}
}
this.gestureQueue.length = 0;
},
fillGestureQueue: function(ev) {
// only trigger the gesture queue once
if (!this.gestureQueue.length) {
requestAnimationFrame(this.boundGestureTrigger);
}
ev._requiredGestures = this.requiredGestures.get(ev.pointerId);
this.gestureQueue.push(ev);
}
};
dispatcher.boundHandler = dispatcher.eventHandler.bind(dispatcher);
dispatcher.boundGestureTrigger = dispatcher.gestureTrigger.bind(dispatcher);
scope.dispatcher = dispatcher;
/**
* Listen for `gesture` on `node` with the `handler` function
*
* If `handler` is the first listener for `gesture`, the underlying gesture recognizer is then enabled.
*
* @param {Element} node
* @param {string} gesture
* @return Boolean `gesture` is a valid gesture
*/
scope.activateGesture = function(node, gesture) {
var g = gesture.toLowerCase();
var dep = dispatcher.dependencyMap[g];
if (dep) {
var recognizer = dispatcher.gestures[dep.index];
if (!node._pgListeners) {
dispatcher.register(node);
node._pgListeners = 0;
}
// TODO(dfreedm): re-evaluate bookkeeping to avoid using attributes
if (recognizer) {
var touchAction = recognizer.defaultActions && recognizer.defaultActions[g];
var actionNode;
switch(node.nodeType) {
case Node.ELEMENT_NODE:
actionNode = node;
break;
case Node.DOCUMENT_FRAGMENT_NODE:
actionNode = node.host;
break;
default:
actionNode = null;
break;
}
if (touchAction && actionNode && !actionNode.hasAttribute('touch-action')) {
actionNode.setAttribute('touch-action', touchAction);
}
}
if (!node._pgEvents) {
node._pgEvents = {};
}
node._pgEvents[g] = (node._pgEvents[g] || 0) + 1;
node._pgListeners++;
}
return Boolean(dep);
};
/**
*
* Listen for `gesture` from `node` with `handler` function.
*
* @param {Element} node
* @param {string} gesture
* @param {Function} handler
* @param {Boolean} capture
*/
scope.addEventListener = function(node, gesture, handler, capture) {
if (handler) {
scope.activateGesture(node, gesture);
node.addEventListener(gesture, handler, capture);
}
};
/**
* Tears down the gesture configuration for `node`
*
* If `handler` is the last listener for `gesture`, the underlying gesture recognizer is disabled.
*
* @param {Element} node
* @param {string} gesture
* @return Boolean `gesture` is a valid gesture
*/
scope.deactivateGesture = function(node, gesture) {
var g = gesture.toLowerCase();
var dep = dispatcher.dependencyMap[g];
if (dep) {
if (node._pgListeners > 0) {
node._pgListeners--;
}
if (node._pgListeners === 0) {
dispatcher.unregister(node);
}
if (node._pgEvents) {
if (node._pgEvents[g] > 0) {
node._pgEvents[g]--;
} else {
node._pgEvents[g] = 0;
}
}
}
return Boolean(dep);
};
/**
* Stop listening for `gesture` from `node` with `handler` function.
*
* @param {Element} node
* @param {string} gesture
* @param {Function} handler
* @param {Boolean} capture
*/
scope.removeEventListener = function(node, gesture, handler, capture) {
if (handler) {
scope.deactivateGesture(node, gesture);
node.removeEventListener(gesture, handler, capture);
}
};
})(window.PolymerGestures);
(function(scope) {
var dispatcher = scope.dispatcher;
var pointermap = dispatcher.pointermap;
// radius around touchend that swallows mouse events
var DEDUP_DIST = 25;
var WHICH_TO_BUTTONS = [0, 1, 4, 2];
var CURRENT_BUTTONS = 0;
var FIREFOX_LINUX = /Linux.*Firefox\//i;
var HAS_BUTTONS = (function() {
// firefox on linux returns spec-incorrect values for mouseup.buttons
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent.buttons#See_also
// https://codereview.chromium.org/727593003/#msg16
if (FIREFOX_LINUX.test(navigator.userAgent)) {
return false;
}
try {
return new MouseEvent('test', {buttons: 1}).buttons === 1;
} catch (e) {
return false;
}
})();
// handler block for native mouse events
var mouseEvents = {
POINTER_ID: 1,
POINTER_TYPE: 'mouse',
events: [
'mousedown',
'mousemove',
'mouseup'
],
exposes: [
'down',
'up',
'move'
],
register: function(target) {
dispatcher.listen(target, this.events);
},
unregister: function(target) {
if (target === document) {
return;
}
dispatcher.unlisten(target, this.events);
},
lastTouches: [],
// collide with the global mouse listener
isEventSimulatedFromTouch: function(inEvent) {
var lts = this.lastTouches;
var x = inEvent.clientX, y = inEvent.clientY;
for (var i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) {
// simulated mouse events will be swallowed near a primary touchend
var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y);
if (dx <= DEDUP_DIST && dy <= DEDUP_DIST) {
return true;
}
}
},
prepareEvent: function(inEvent) {
var e = dispatcher.cloneEvent(inEvent);
e.pointerId = this.POINTER_ID;
e.isPrimary = true;
e.pointerType = this.POINTER_TYPE;
e._source = 'mouse';
if (!HAS_BUTTONS) {
var type = inEvent.type;
var bit = WHICH_TO_BUTTONS[inEvent.which] || 0;
if (type === 'mousedown') {
CURRENT_BUTTONS |= bit;
} else if (type === 'mouseup') {
CURRENT_BUTTONS &= ~bit;
}
e.buttons = CURRENT_BUTTONS;
}
return e;
},
mousedown: function(inEvent) {
if (!this.isEventSimulatedFromTouch(inEvent)) {
var p = pointermap.has(this.POINTER_ID);
var e = this.prepareEvent(inEvent);
e.target = scope.findTarget(inEvent);
pointermap.set(this.POINTER_ID, e.target);
dispatcher.down(e);
}
},
mousemove: function(inEvent) {
if (!this.isEventSimulatedFromTouch(inEvent)) {
var target = pointermap.get(this.POINTER_ID);
if (target) {
var e = this.prepareEvent(inEvent);
e.target = target;
// handle case where we missed a mouseup
if ((HAS_BUTTONS ? e.buttons : e.which) === 0) {
if (!HAS_BUTTONS) {
CURRENT_BUTTONS = e.buttons = 0;
}
dispatcher.cancel(e);
this.cleanupMouse(e.buttons);
} else {
dispatcher.move(e);
}
}
}
},
mouseup: function(inEvent) {
if (!this.isEventSimulatedFromTouch(inEvent)) {
var e = this.prepareEvent(inEvent);
e.relatedTarget = scope.findTarget(inEvent);
e.target = pointermap.get(this.POINTER_ID);
dispatcher.up(e);
this.cleanupMouse(e.buttons);
}
},
cleanupMouse: function(buttons) {
if (buttons === 0) {
pointermap.delete(this.POINTER_ID);
}
}
};
scope.mouseEvents = mouseEvents;
})(window.PolymerGestures);
(function(scope) {
var dispatcher = scope.dispatcher;
var allShadows = scope.targetFinding.allShadows.bind(scope.targetFinding);
var pointermap = dispatcher.pointermap;
var touchMap = Array.prototype.map.call.bind(Array.prototype.map);
// This should be long enough to ignore compat mouse events made by touch
var DEDUP_TIMEOUT = 2500;
var DEDUP_DIST = 25;
var CLICK_COUNT_TIMEOUT = 200;
var HYSTERESIS = 20;
var ATTRIB = 'touch-action';
// TODO(dfreedm): disable until http://crbug.com/399765 is resolved
// var HAS_TOUCH_ACTION = ATTRIB in document.head.style;
var HAS_TOUCH_ACTION = false;
// handler block for native touch events
var touchEvents = {
IS_IOS: false,
events: [
'touchstart',
'touchmove',
'touchend',
'touchcancel'
],
exposes: [
'down',
'up',
'move'
],
register: function(target, initial) {
if (this.IS_IOS ? initial : !initial) {
dispatcher.listen(target, this.events);
}
},
unregister: function(target) {
if (!this.IS_IOS) {
dispatcher.unlisten(target, this.events);
}
},
scrollTypes: {
EMITTER: 'none',
XSCROLLER: 'pan-x',
YSCROLLER: 'pan-y',
},
touchActionToScrollType: function(touchAction) {
var t = touchAction;
var st = this.scrollTypes;
if (t === st.EMITTER) {
return 'none';
} else if (t === st.XSCROLLER) {
return 'X';
} else if (t === st.YSCROLLER) {
return 'Y';
} else {
return 'XY';
}
},
POINTER_TYPE: 'touch',
firstTouch: null,
isPrimaryTouch: function(inTouch) {
return this.firstTouch === inTouch.identifier;
},
setPrimaryTouch: function(inTouch) {
// set primary touch if there no pointers, or the only pointer is the mouse
if (pointermap.pointers() === 0 || (pointermap.pointers() === 1 && pointermap.has(1))) {
this.firstTouch = inTouch.identifier;
this.firstXY = {X: inTouch.clientX, Y: inTouch.clientY};
this.firstTarget = inTouch.target;
this.scrolling = null;
this.cancelResetClickCount();
}
},
removePrimaryPointer: function(inPointer) {
if (inPointer.isPrimary) {
this.firstTouch = null;
this.firstXY = null;
this.resetClickCount();
}
},
clickCount: 0,
resetId: null,
resetClickCount: function() {
var fn = function() {
this.clickCount = 0;
this.resetId = null;
}.bind(this);
this.resetId = setTimeout(fn, CLICK_COUNT_TIMEOUT);
},
cancelResetClickCount: function() {
if (this.resetId) {
clearTimeout(this.resetId);
}
},
typeToButtons: function(type) {
var ret = 0;
if (type === 'touchstart' || type === 'touchmove') {
ret = 1;
}
return ret;
},
findTarget: function(touch, id) {
if (this.currentTouchEvent.type === 'touchstart') {
if (this.isPrimaryTouch(touch)) {
var fastPath = {
clientX: touch.clientX,
clientY: touch.clientY,
path: this.currentTouchEvent.path,
target: this.currentTouchEvent.target
};
return scope.findTarget(fastPath);
} else {
return scope.findTarget(touch);
}
}
// reuse target we found in touchstart
return pointermap.get(id);
},
touchToPointer: function(inTouch) {
var cte = this.currentTouchEvent;
var e = dispatcher.cloneEvent(inTouch);
// Spec specifies that pointerId 1 is reserved for Mouse.
// Touch identifiers can start at 0.
// Add 2 to the touch identifier for compatibility.
var id = e.pointerId = inTouch.identifier + 2;
e.target = this.findTarget(inTouch, id);
e.bubbles = true;
e.cancelable = true;
e.detail = this.clickCount;
e.buttons = this.typeToButtons(cte.type);
e.width = inTouch.webkitRadiusX || inTouch.radiusX || 0;
e.height = inTouch.webkitRadiusY || inTouch.radiusY || 0;
e.pressure = inTouch.webkitForce || inTouch.force || 0.5;
e.isPrimary = this.isPrimaryTouch(inTouch);
e.pointerType = this.POINTER_TYPE;
e._source = 'touch';
// forward touch preventDefaults
var self = this;
e.preventDefault = function() {
self.scrolling = false;
self.firstXY = null;
cte.preventDefault();
};
return e;
},
processTouches: function(inEvent, inFunction) {
var tl = inEvent.changedTouches;
this.currentTouchEvent = inEvent;
for (var i = 0, t, p; i < tl.length; i++) {
t = tl[i];
p = this.touchToPointer(t);
if (inEvent.type === 'touchstart') {
pointermap.set(p.pointerId, p.target);
}
if (pointermap.has(p.pointerId)) {
inFunction.call(this, p);
}
if (inEvent.type === 'touchend' || inEvent._cancel) {
this.cleanUpPointer(p);
}
}
},
// For single axis scrollers, determines whether the element should emit
// pointer events or behave as a scroller
shouldScroll: function(inEvent) {
if (this.firstXY) {
var ret;
var touchAction = scope.targetFinding.findTouchAction(inEvent);
var scrollAxis = this.touchActionToScrollType(touchAction);
if (scrollAxis === 'none') {
// this element is a touch-action: none, should never scroll
ret = false;
} else if (scrollAxis === 'XY') {
// this element should always scroll
ret = true;
} else {
var t = inEvent.changedTouches[0];
// check the intended scroll axis, and other axis
var a = scrollAxis;
var oa = scrollAxis === 'Y' ? 'X' : 'Y';
var da = Math.abs(t['client' + a] - this.firstXY[a]);
var doa = Math.abs(t['client' + oa] - this.firstXY[oa]);
// if delta in the scroll axis > delta other axis, scroll instead of
// making events
ret = da >= doa;
}
return ret;
}
},
findTouch: function(inTL, inId) {
for (var i = 0, l = inTL.length, t; i < l && (t = inTL[i]); i++) {
if (t.identifier === inId) {
return true;
}
}
},
// In some instances, a touchstart can happen without a touchend. This
// leaves the pointermap in a broken state.
// Therefore, on every touchstart, we remove the touches that did not fire a
// touchend event.
// To keep state globally consistent, we fire a
// pointercancel for this "abandoned" touch
vacuumTouches: function(inEvent) {
var tl = inEvent.touches;
// pointermap.pointers() should be < tl.length here, as the touchstart has not
// been processed yet.
if (pointermap.pointers() >= tl.length) {
var d = [];
pointermap.forEach(function(value, key) {
// Never remove pointerId == 1, which is mouse.
// Touch identifiers are 2 smaller than their pointerId, which is the
// index in pointermap.
if (key !== 1 && !this.findTouch(tl, key - 2)) {
var p = value;
d.push(p);
}
}, this);
d.forEach(function(p) {
this.cancel(p);
pointermap.delete(p.pointerId);
}, this);
}
},
touchstart: function(inEvent) {
this.vacuumTouches(inEvent);
this.setPrimaryTouch(inEvent.changedTouches[0]);
this.dedupSynthMouse(inEvent);
if (!this.scrolling) {
this.clickCount++;
this.processTouches(inEvent, this.down);
}
},
down: function(inPointer) {
dispatcher.down(inPointer);
},
touchmove: function(inEvent) {
if (HAS_TOUCH_ACTION) {
// touchevent.cancelable == false is sent when the page is scrolling under native Touch Action in Chrome 36
// https://groups.google.com/a/chromium.org/d/msg/input-dev/wHnyukcYBcA/b9kmtwM1jJQJ
if (inEvent.cancelable) {
this.processTouches(inEvent, this.move);
}
} else {
if (!this.scrolling) {
if (this.scrolling === null && this.shouldScroll(inEvent)) {
this.scrolling = true;
} else {
this.scrolling = false;
inEvent.preventDefault();
this.processTouches(inEvent, this.move);
}
} else if (this.firstXY) {
var t = inEvent.changedTouches[0];
var dx = t.clientX - this.firstXY.X;
var dy = t.clientY - this.firstXY.Y;
var dd = Math.sqrt(dx * dx + dy * dy);
if (dd >= HYSTERESIS) {
this.touchcancel(inEvent);
this.scrolling = true;
this.firstXY = null;
}
}
}
},
move: function(inPointer) {
dispatcher.move(inPointer);
},
touchend: function(inEvent) {
this.dedupSynthMouse(inEvent);
this.processTouches(inEvent, this.up);
},
up: function(inPointer) {
inPointer.relatedTarget = scope.findTarget(inPointer);
dispatcher.up(inPointer);
},
cancel: function(inPointer) {
dispatcher.cancel(inPointer);
},
touchcancel: function(inEvent) {
inEvent._cancel = true;
this.processTouches(inEvent, this.cancel);
},
cleanUpPointer: function(inPointer) {
pointermap['delete'](inPointer.pointerId);
this.removePrimaryPointer(inPointer);
},
// prevent synth mouse events from creating pointer events
dedupSynthMouse: function(inEvent) {
var lts = scope.mouseEvents.lastTouches;
var t = inEvent.changedTouches[0];
// only the primary finger will synth mouse events
if (this.isPrimaryTouch(t)) {
// remember x/y of last touch
var lt = {x: t.clientX, y: t.clientY};
lts.push(lt);
var fn = (function(lts, lt){
var i = lts.indexOf(lt);
if (i > -1) {
lts.splice(i, 1);
}
}).bind(null, lts, lt);
setTimeout(fn, DEDUP_TIMEOUT);
}
}
};
// prevent "ghost clicks" that come from elements that were removed in a touch handler
var STOP_PROP_FN = Event.prototype.stopImmediatePropagation || Event.prototype.stopPropagation;
document.addEventListener('click', function(ev) {
var x = ev.clientX, y = ev.clientY;
// check if a click is within DEDUP_DIST px radius of the touchstart
var closeTo = function(touch) {
var dx = Math.abs(x - touch.x), dy = Math.abs(y - touch.y);
return (dx <= DEDUP_DIST && dy <= DEDUP_DIST);
};
// if click coordinates are close to touch coordinates, assume the click came from a touch
var wasTouched = scope.mouseEvents.lastTouches.some(closeTo);
// if the click came from touch, and the touchstart target is not in the path of the click event,
// then the touchstart target was probably removed, and the click should be "busted"
var path = scope.targetFinding.path(ev);
if (wasTouched) {
for (var i = 0; i < path.length; i++) {
if (path[i] === touchEvents.firstTarget) {
return;
}
}
ev.preventDefault();
STOP_PROP_FN.call(ev);
}
}, true);
scope.touchEvents = touchEvents;
})(window.PolymerGestures);
(function(scope) {
var dispatcher = scope.dispatcher;
var pointermap = dispatcher.pointermap;
var HAS_BITMAP_TYPE = window.MSPointerEvent && typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE === 'number';
var msEvents = {
events: [
'MSPointerDown',
'MSPointerMove',
'MSPointerUp',
'MSPointerCancel',
],
register: function(target) {
dispatcher.listen(target, this.events);
},
unregister: function(target) {
if (target === document) {
return;
}
dispatcher.unlisten(target, this.events);
},
POINTER_TYPES: [
'',
'unavailable',
'touch',
'pen',
'mouse'
],
prepareEvent: function(inEvent) {
var e = inEvent;
e = dispatcher.cloneEvent(inEvent);
if (HAS_BITMAP_TYPE) {
e.pointerType = this.POINTER_TYPES[inEvent.pointerType];
}
e._source = 'ms';
return e;
},
cleanup: function(id) {
pointermap['delete'](id);
},
MSPointerDown: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.target = scope.findTarget(inEvent);
pointermap.set(inEvent.pointerId, e.target);
dispatcher.down(e);
},
MSPointerMove: function(inEvent) {
var target = pointermap.get(inEvent.pointerId);
if (target) {
var e = this.prepareEvent(inEvent);
e.target = target;
dispatcher.move(e);
}
},
MSPointerUp: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.relatedTarget = scope.findTarget(inEvent);
e.target = pointermap.get(e.pointerId);
dispatcher.up(e);
this.cleanup(inEvent.pointerId);
},
MSPointerCancel: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.relatedTarget = scope.findTarget(inEvent);
e.target = pointermap.get(e.pointerId);
dispatcher.cancel(e);
this.cleanup(inEvent.pointerId);
}
};
scope.msEvents = msEvents;
})(window.PolymerGestures);
(function(scope) {
var dispatcher = scope.dispatcher;
var pointermap = dispatcher.pointermap;
var pointerEvents = {
events: [
'pointerdown',
'pointermove',
'pointerup',
'pointercancel'
],
prepareEvent: function(inEvent) {
var e = dispatcher.cloneEvent(inEvent);
e._source = 'pointer';
return e;
},
register: function(target) {
dispatcher.listen(target, this.events);
},
unregister: function(target) {
if (target === document) {
return;
}
dispatcher.unlisten(target, this.events);
},
cleanup: function(id) {
pointermap['delete'](id);
},
pointerdown: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.target = scope.findTarget(inEvent);
pointermap.set(e.pointerId, e.target);
dispatcher.down(e);
},
pointermove: function(inEvent) {
var target = pointermap.get(inEvent.pointerId);
if (target) {
var e = this.prepareEvent(inEvent);
e.target = target;
dispatcher.move(e);
}
},
pointerup: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.relatedTarget = scope.findTarget(inEvent);
e.target = pointermap.get(e.pointerId);
dispatcher.up(e);
this.cleanup(inEvent.pointerId);
},
pointercancel: function(inEvent) {
var e = this.prepareEvent(inEvent);
e.relatedTarget = scope.findTarget(inEvent);
e.target = pointermap.get(e.pointerId);
dispatcher.cancel(e);
this.cleanup(inEvent.pointerId);
}
};
scope.pointerEvents = pointerEvents;
})(window.PolymerGestures);
/**
* This module contains the handlers for native platform events.
* From here, the dispatcher is called to create unified pointer events.
* Included are touch events (v1), mouse events, and MSPointerEvents.
*/
(function(scope) {
var dispatcher = scope.dispatcher;
var nav = window.navigator;
if (window.PointerEvent) {
dispatcher.registerSource('pointer', scope.pointerEvents);
} else if (nav.msPointerEnabled) {
dispatcher.registerSource('ms', scope.msEvents);
} else {
dispatcher.registerSource('mouse', scope.mouseEvents);
if (window.ontouchstart !== undefined) {
dispatcher.registerSource('touch', scope.touchEvents);
}
}
// Work around iOS bugs https://bugs.webkit.org/show_bug.cgi?id=135628 and https://bugs.webkit.org/show_bug.cgi?id=136506
var ua = navigator.userAgent;
var IS_IOS = ua.match(/iPad|iPhone|iPod/) && 'ontouchstart' in window;
dispatcher.IS_IOS = IS_IOS;
scope.touchEvents.IS_IOS = IS_IOS;
dispatcher.register(document, true);
})(window.PolymerGestures);
/**
* This event denotes the beginning of a series of tracking events.
*
* @module PointerGestures
* @submodule Events
* @class trackstart
*/
/**
* Pixels moved in the x direction since trackstart.
* @type Number
* @property dx
*/
/**
* Pixes moved in the y direction since trackstart.
* @type Number
* @property dy
*/
/**
* Pixels moved in the x direction since the last track.
* @type Number
* @property ddx
*/
/**
* Pixles moved in the y direction since the last track.
* @type Number
* @property ddy
*/
/**
* The clientX position of the track gesture.
* @type Number
* @property clientX
*/
/**
* The clientY position of the track gesture.
* @type Number
* @property clientY
*/
/**
* The pageX position of the track gesture.
* @type Number
* @property pageX
*/
/**
* The pageY position of the track gesture.
* @type Number
* @property pageY
*/
/**
* The screenX position of the track gesture.
* @type Number
* @property screenX
*/
/**
* The screenY position of the track gesture.
* @type Number
* @property screenY
*/
/**
* The last x axis direction of the pointer.
* @type Number
* @property xDirection
*/
/**
* The last y axis direction of the pointer.
* @type Number
* @property yDirection
*/
/**
* A shared object between all tracking events.
* @type Object
* @property trackInfo
*/
/**
* The element currently under the pointer.
* @type Element
* @property relatedTarget
*/
/**
* The type of pointer that make the track gesture.
* @type String
* @property pointerType
*/
/**
*
* This event fires for all pointer movement being tracked.
*
* @class track
* @extends trackstart
*/
/**
* This event fires when the pointer is no longer being tracked.
*
* @class trackend
* @extends trackstart
*/
(function(scope) {
var dispatcher = scope.dispatcher;
var eventFactory = scope.eventFactory;
var pointermap = new scope.PointerMap();
var track = {
events: [
'down',
'move',
'up',
],
exposes: [
'trackstart',
'track',
'trackx',
'tracky',
'trackend'
],
defaultActions: {
'track': 'none',
'trackx': 'pan-y',
'tracky': 'pan-x'
},
WIGGLE_THRESHOLD: 4,
clampDir: function(inDelta) {
return inDelta > 0 ? 1 : -1;
},
calcPositionDelta: function(inA, inB) {
var x = 0, y = 0;
if (inA && inB) {
x = inB.pageX - inA.pageX;
y = inB.pageY - inA.pageY;
}
return {x: x, y: y};
},
fireTrack: function(inType, inEvent, inTrackingData) {
var t = inTrackingData;
var d = this.calcPositionDelta(t.downEvent, inEvent);
var dd = this.calcPositionDelta(t.lastMoveEvent, inEvent);
if (dd.x) {
t.xDirection = this.clampDir(dd.x);
} else if (inType === 'trackx') {
return;
}
if (dd.y) {
t.yDirection = this.clampDir(dd.y);
} else if (inType === 'tracky') {
return;
}
var gestureProto = {
bubbles: true,
cancelable: true,
trackInfo: t.trackInfo,
relatedTarget: inEvent.relatedTarget,
pointerType: inEvent.pointerType,
pointerId: inEvent.pointerId,
_source: 'track'
};
if (inType !== 'tracky') {
gestureProto.x = inEvent.x;
gestureProto.dx = d.x;
gestureProto.ddx = dd.x;
gestureProto.clientX = inEvent.clientX;
gestureProto.pageX = inEvent.pageX;
gestureProto.screenX = inEvent.screenX;
gestureProto.xDirection = t.xDirection;
}
if (inType !== 'trackx') {
gestureProto.dy = d.y;
gestureProto.ddy = dd.y;
gestureProto.y = inEvent.y;
gestureProto.clientY = inEvent.clientY;
gestureProto.pageY = inEvent.pageY;
gestureProto.screenY = inEvent.screenY;
gestureProto.yDirection = t.yDirection;
}
var e = eventFactory.makeGestureEvent(inType, gestureProto);
t.downTarget.dispatchEvent(e);
},
down: function(inEvent) {
if (inEvent.isPrimary && (inEvent.pointerType === 'mouse' ? inEvent.buttons === 1 : true)) {
var p = {
downEvent: inEvent,
downTarget: inEvent.target,
trackInfo: {},
lastMoveEvent: null,
xDirection: 0,
yDirection: 0,
tracking: false
};
pointermap.set(inEvent.pointerId, p);
}
},
move: function(inEvent) {
var p = pointermap.get(inEvent.pointerId);
if (p) {
if (!p.tracking) {
var d = this.calcPositionDelta(p.downEvent, inEvent);
var move = d.x * d.x + d.y * d.y;
// start tracking only if finger moves more than WIGGLE_THRESHOLD
if (move > this.WIGGLE_THRESHOLD) {
p.tracking = true;
p.lastMoveEvent = p.downEvent;
this.fireTrack('trackstart', inEvent, p);
}
}
if (p.tracking) {
this.fireTrack('track', inEvent, p);
this.fireTrack('trackx', inEvent, p);
this.fireTrack('tracky', inEvent, p);
}
p.lastMoveEvent = inEvent;
}
},
up: function(inEvent) {
var p = pointermap.get(inEvent.pointerId);
if (p) {
if (p.tracking) {
this.fireTrack('trackend', inEvent, p);
}
pointermap.delete(inEvent.pointerId);
}
}
};
dispatcher.registerGesture('track', track);
})(window.PolymerGestures);
/**
* This event is fired when a pointer is held down for 200ms.
*
* @module PointerGestures
* @submodule Events
* @class hold
*/
/**
* Type of pointer that made the holding event.
* @type String
* @property pointerType
*/
/**
* Screen X axis position of the held pointer
* @type Number
* @property clientX
*/
/**
* Screen Y axis position of the held pointer
* @type Number
* @property clientY
*/
/**
* Type of pointer that made the holding event.
* @type String
* @property pointerType
*/
/**
* This event is fired every 200ms while a pointer is held down.
*
* @class holdpulse
* @extends hold
*/
/**
* Milliseconds pointer has been held down.
* @type Number
* @property holdTime
*/
/**
* This event is fired when a held pointer is released or moved.
*
* @class release
*/
(function(scope) {
var dispatcher = scope.dispatcher;
var eventFactory = scope.eventFactory;
var hold = {
// wait at least HOLD_DELAY ms between hold and pulse events
HOLD_DELAY: 200,
// pointer can move WIGGLE_THRESHOLD pixels before not counting as a hold
WIGGLE_THRESHOLD: 16,
events: [
'down',
'move',
'up',
],
exposes: [
'hold',
'holdpulse',
'release'
],
heldPointer: null,
holdJob: null,
pulse: function() {
var hold = Date.now() - this.heldPointer.timeStamp;
var type = this.held ? 'holdpulse' : 'hold';
this.fireHold(type, hold);
this.held = true;
},
cancel: function() {
clearInterval(this.holdJob);
if (this.held) {
this.fireHold('release');
}
this.held = false;
this.heldPointer = null;
this.target = null;
this.holdJob = null;
},
down: function(inEvent) {
if (inEvent.isPrimary && !this.heldPointer) {
this.heldPointer = inEvent;
this.target = inEvent.target;
this.holdJob = setInterval(this.pulse.bind(this), this.HOLD_DELAY);
}
},
up: function(inEvent) {
if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) {
this.cancel();
}
},
move: function(inEvent) {
if (this.heldPointer && this.heldPointer.pointerId === inEvent.pointerId) {
var x = inEvent.clientX - this.heldPointer.clientX;
var y = inEvent.clientY - this.heldPointer.clientY;
if ((x * x + y * y) > this.WIGGLE_THRESHOLD) {
this.cancel();
}
}
},
fireHold: function(inType, inHoldTime) {
var p = {
bubbles: true,
cancelable: true,
pointerType: this.heldPointer.pointerType,
pointerId: this.heldPointer.pointerId,
x: this.heldPointer.clientX,
y: this.heldPointer.clientY,
_source: 'hold'
};
if (inHoldTime) {
p.holdTime = inHoldTime;
}
var e = eventFactory.makeGestureEvent(inType, p);
this.target.dispatchEvent(e);
}
};
dispatcher.registerGesture('hold', hold);
})(window.PolymerGestures);
/**
* This event is fired when a pointer quickly goes down and up, and is used to
* denote activation.
*
* Any gesture event can prevent the tap event from being created by calling
* `event.preventTap`.
*
* Any pointer event can prevent the tap by setting the `tapPrevented` property
* on itself.
*
* @module PointerGestures
* @submodule Events
* @class tap
*/
/**
* X axis position of the tap.
* @property x
* @type Number
*/
/**
* Y axis position of the tap.
* @property y
* @type Number
*/
/**
* Type of the pointer that made the tap.
* @property pointerType
* @type String
*/
(function(scope) {
var dispatcher = scope.dispatcher;
var eventFactory = scope.eventFactory;
var pointermap = new scope.PointerMap();
var tap = {
events: [
'down',
'up'
],
exposes: [
'tap'
],
down: function(inEvent) {
if (inEvent.isPrimary && !inEvent.tapPrevented) {
pointermap.set(inEvent.pointerId, {
target: inEvent.target,
buttons: inEvent.buttons,
x: inEvent.clientX,
y: inEvent.clientY
});
}
},
shouldTap: function(e, downState) {
var tap = true;
if (e.pointerType === 'mouse') {
// only allow left click to tap for mouse
tap = (e.buttons ^ 1) && (downState.buttons & 1);
}
return tap && !e.tapPrevented;
},
up: function(inEvent) {
var start = pointermap.get(inEvent.pointerId);
if (start && this.shouldTap(inEvent, start)) {
// up.relatedTarget is target currently under finger
var t = scope.targetFinding.LCA(start.target, inEvent.relatedTarget);
if (t) {
var e = eventFactory.makeGestureEvent('tap', {
bubbles: true,
cancelable: true,
x: inEvent.clientX,
y: inEvent.clientY,
detail: inEvent.detail,
pointerType: inEvent.pointerType,
pointerId: inEvent.pointerId,
altKey: inEvent.altKey,
ctrlKey: inEvent.ctrlKey,
metaKey: inEvent.metaKey,
shiftKey: inEvent.shiftKey,
_source: 'tap'
});
t.dispatchEvent(e);
}
}
pointermap.delete(inEvent.pointerId);
}
};
// patch eventFactory to remove id from tap's pointermap for preventTap calls
eventFactory.preventTap = function(e) {
return function() {
e.tapPrevented = true;
pointermap.delete(e.pointerId);
};
};
dispatcher.registerGesture('tap', tap);
})(window.PolymerGestures);
/*
* Basic strategy: find the farthest apart points, use as diameter of circle
* react to size change and rotation of the chord
*/
/**
* @module pointer-gestures
* @submodule Events
* @class pinch
*/
/**
* Scale of the pinch zoom gesture
* @property scale
* @type Number
*/
/**
* Center X position of pointers causing pinch
* @property centerX
* @type Number
*/
/**
* Center Y position of pointers causing pinch
* @property centerY
* @type Number
*/
/**
* @module pointer-gestures
* @submodule Events
* @class rotate
*/
/**
* Angle (in degrees) of rotation. Measured from starting positions of pointers.
* @property angle
* @type Number
*/
/**
* Center X position of pointers causing rotation
* @property centerX
* @type Number
*/
/**
* Center Y position of pointers causing rotation
* @property centerY
* @type Number
*/
(function(scope) {
var dispatcher = scope.dispatcher;
var eventFactory = scope.eventFactory;
var pointermap = new scope.PointerMap();
var RAD_TO_DEG = 180 / Math.PI;
var pinch = {
events: [
'down',
'up',
'move',
'cancel'
],
exposes: [
'pinchstart',
'pinch',
'pinchend',
'rotate'
],
defaultActions: {
'pinch': 'none',
'rotate': 'none'
},
reference: {},
down: function(inEvent) {
pointermap.set(inEvent.pointerId, inEvent);
if (pointermap.pointers() == 2) {
var points = this.calcChord();
var angle = this.calcAngle(points);
this.reference = {
angle: angle,
diameter: points.diameter,
target: scope.targetFinding.LCA(points.a.target, points.b.target)
};
this.firePinch('pinchstart', points.diameter, points);
}
},
up: function(inEvent) {
var p = pointermap.get(inEvent.pointerId);
var num = pointermap.pointers();
if (p) {
if (num === 2) {
// fire 'pinchend' before deleting pointer
var points = this.calcChord();
this.firePinch('pinchend', points.diameter, points);
}
pointermap.delete(inEvent.pointerId);
}
},
move: function(inEvent) {
if (pointermap.has(inEvent.pointerId)) {
pointermap.set(inEvent.pointerId, inEvent);
if (pointermap.pointers() > 1) {
this.calcPinchRotate();
}
}
},
cancel: function(inEvent) {
this.up(inEvent);
},
firePinch: function(type, diameter, points) {
var zoom = diameter / this.reference.diameter;
var e = eventFactory.makeGestureEvent(type, {
bubbles: true,
cancelable: true,
scale: zoom,
centerX: points.center.x,
centerY: points.center.y,
_source: 'pinch'
});
this.reference.target.dispatchEvent(e);
},
fireRotate: function(angle, points) {
var diff = Math.round((angle - this.reference.angle) % 360);
var e = eventFactory.makeGestureEvent('rotate', {
bubbles: true,
cancelable: true,
angle: diff,
centerX: points.center.x,
centerY: points.center.y,
_source: 'pinch'
});
this.reference.target.dispatchEvent(e);
},
calcPinchRotate: function() {
var points = this.calcChord();
var diameter = points.diameter;
var angle = this.calcAngle(points);
if (diameter != this.reference.diameter) {
this.firePinch('pinch', diameter, points);
}
if (angle != this.reference.angle) {
this.fireRotate(angle, points);
}
},
calcChord: function() {
var pointers = [];
pointermap.forEach(function(p) {
pointers.push(p);
});
var dist = 0;
// start with at least two pointers
var points = {a: pointers[0], b: pointers[1]};
var x, y, d;
for (var i = 0; i < pointers.length; i++) {
var a = pointers[i];
for (var j = i + 1; j < pointers.length; j++) {
var b = pointers[j];
x = Math.abs(a.clientX - b.clientX);
y = Math.abs(a.clientY - b.clientY);
d = x + y;
if (d > dist) {
dist = d;
points = {a: a, b: b};
}
}
}
x = Math.abs(points.a.clientX + points.b.clientX) / 2;
y = Math.abs(points.a.clientY + points.b.clientY) / 2;
points.center = { x: x, y: y };
points.diameter = dist;
return points;
},
calcAngle: function(points) {
var x = points.a.clientX - points.b.clientX;
var y = points.a.clientY - points.b.clientY;
return (360 + Math.atan2(y, x) * RAD_TO_DEG) % 360;
}
};
dispatcher.registerGesture('pinch', pinch);
})(window.PolymerGestures);
(function (global) {
'use strict';
var Token,
TokenName,
Syntax,
Messages,
source,
index,
length,
delegate,
lookahead,
state;
Token = {
BooleanLiteral: 1,
EOF: 2,
Identifier: 3,
Keyword: 4,
NullLiteral: 5,
NumericLiteral: 6,
Punctuator: 7,
StringLiteral: 8
};
TokenName = {};
TokenName[Token.BooleanLiteral] = 'Boolean';
TokenName[Token.EOF] = '';
TokenName[Token.Identifier] = 'Identifier';
TokenName[Token.Keyword] = 'Keyword';
TokenName[Token.NullLiteral] = 'Null';
TokenName[Token.NumericLiteral] = 'Numeric';
TokenName[Token.Punctuator] = 'Punctuator';
TokenName[Token.StringLiteral] = 'String';
Syntax = {
ArrayExpression: 'ArrayExpression',
BinaryExpression: 'BinaryExpression',
CallExpression: 'CallExpression',
ConditionalExpression: 'ConditionalExpression',
EmptyStatement: 'EmptyStatement',
ExpressionStatement: 'ExpressionStatement',
Identifier: 'Identifier',
Literal: 'Literal',
LabeledStatement: 'LabeledStatement',
LogicalExpression: 'LogicalExpression',
MemberExpression: 'MemberExpression',
ObjectExpression: 'ObjectExpression',
Program: 'Program',
Property: 'Property',
ThisExpression: 'ThisExpression',
UnaryExpression: 'UnaryExpression'
};
// Error messages should be identical to V8.
Messages = {
UnexpectedToken: 'Unexpected token %0',
UnknownLabel: 'Undefined label \'%0\'',
Redeclaration: '%0 \'%1\' has already been declared'
};
// Ensure the condition is true, otherwise throw an error.
// This is only to have a better contract semantic, i.e. another safety net
// to catch a logic error. The condition shall be fulfilled in normal case.
// Do NOT use this to enforce a certain condition on any user input.
function assert(condition, message) {
if (!condition) {
throw new Error('ASSERT: ' + message);
}
}
function isDecimalDigit(ch) {
return (ch >= 48 && ch <= 57); // 0..9
}
// 7.2 White Space
function isWhiteSpace(ch) {
return (ch === 32) || // space
(ch === 9) || // tab
(ch === 0xB) ||
(ch === 0xC) ||
(ch === 0xA0) ||
(ch >= 0x1680 && '\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\uFEFF'.indexOf(String.fromCharCode(ch)) > 0);
}
// 7.3 Line Terminators
function isLineTerminator(ch) {
return (ch === 10) || (ch === 13) || (ch === 0x2028) || (ch === 0x2029);
}
// 7.6 Identifier Names and Identifiers
function isIdentifierStart(ch) {
return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore)
(ch >= 65 && ch <= 90) || // A..Z
(ch >= 97 && ch <= 122); // a..z
}
function isIdentifierPart(ch) {
return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore)
(ch >= 65 && ch <= 90) || // A..Z
(ch >= 97 && ch <= 122) || // a..z
(ch >= 48 && ch <= 57); // 0..9
}
// 7.6.1.1 Keywords
function isKeyword(id) {
return (id === 'this')
}
// 7.4 Comments
function skipWhitespace() {
while (index < length && isWhiteSpace(source.charCodeAt(index))) {
++index;
}
}
function getIdentifier() {
var start, ch;
start = index++;
while (index < length) {
ch = source.charCodeAt(index);
if (isIdentifierPart(ch)) {
++index;
} else {
break;
}
}
return source.slice(start, index);
}
function scanIdentifier() {
var start, id, type;
start = index;
id = getIdentifier();
// There is no keyword or literal with only one character.
// Thus, it must be an identifier.
if (id.length === 1) {
type = Token.Identifier;
} else if (isKeyword(id)) {
type = Token.Keyword;
} else if (id === 'null') {
type = Token.NullLiteral;
} else if (id === 'true' || id === 'false') {
type = Token.BooleanLiteral;
} else {
type = Token.Identifier;
}
return {
type: type,
value: id,
range: [start, index]
};
}
// 7.7 Punctuators
function scanPunctuator() {
var start = index,
code = source.charCodeAt(index),
code2,
ch1 = source[index],
ch2;
switch (code) {
// Check for most common single-character punctuators.
case 46: // . dot
case 40: // ( open bracket
case 41: // ) close bracket
case 59: // ; semicolon
case 44: // , comma
case 123: // { open curly brace
case 125: // } close curly brace
case 91: // [
case 93: // ]
case 58: // :
case 63: // ?
++index;
return {
type: Token.Punctuator,
value: String.fromCharCode(code),
range: [start, index]
};
default:
code2 = source.charCodeAt(index + 1);
// '=' (char #61) marks an assignment or comparison operator.
if (code2 === 61) {
switch (code) {
case 37: // %
case 38: // &
case 42: // *:
case 43: // +
case 45: // -
case 47: // /
case 60: // <
case 62: // >
case 124: // |
index += 2;
return {
type: Token.Punctuator,
value: String.fromCharCode(code) + String.fromCharCode(code2),
range: [start, index]
};
case 33: // !
case 61: // =
index += 2;
// !== and ===
if (source.charCodeAt(index) === 61) {
++index;
}
return {
type: Token.Punctuator,
value: source.slice(start, index),
range: [start, index]
};
default:
break;
}
}
break;
}
// Peek more characters.
ch2 = source[index + 1];
// Other 2-character punctuators: && ||
if (ch1 === ch2 && ('&|'.indexOf(ch1) >= 0)) {
index += 2;
return {
type: Token.Punctuator,
value: ch1 + ch2,
range: [start, index]
};
}
if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) {
++index;
return {
type: Token.Punctuator,
value: ch1,
range: [start, index]
};
}
throwError({}, Messages.UnexpectedToken, 'ILLEGAL');
}
// 7.8.3 Numeric Literals
function scanNumericLiteral() {
var number, start, ch;
ch = source[index];
assert(isDecimalDigit(ch.charCodeAt(0)) || (ch === '.'),
'Numeric literal must start with a decimal digit or a decimal point');
start = index;
number = '';
if (ch !== '.') {
number = source[index++];
ch = source[index];
// Hex number starts with '0x'.
// Octal number starts with '0'.
if (number === '0') {
// decimal number starts with '0' such as '09' is illegal.
if (ch && isDecimalDigit(ch.charCodeAt(0))) {
throwError({}, Messages.UnexpectedToken, 'ILLEGAL');
}
}
while (isDecimalDigit(source.charCodeAt(index))) {
number += source[index++];
}
ch = source[index];
}
if (ch === '.') {
number += source[index++];
while (isDecimalDigit(source.charCodeAt(index))) {
number += source[index++];
}
ch = source[index];
}
if (ch === 'e' || ch === 'E') {
number += source[index++];
ch = source[index];
if (ch === '+' || ch === '-') {
number += source[index++];
}
if (isDecimalDigit(source.charCodeAt(index))) {
while (isDecimalDigit(source.charCodeAt(index))) {
number += source[index++];
}
} else {
throwError({}, Messages.UnexpectedToken, 'ILLEGAL');
}
}
if (isIdentifierStart(source.charCodeAt(index))) {
throwError({}, Messages.UnexpectedToken, 'ILLEGAL');
}
return {
type: Token.NumericLiteral,
value: parseFloat(number),
range: [start, index]
};
}
// 7.8.4 String Literals
function scanStringLiteral() {
var str = '', quote, start, ch, octal = false;
quote = source[index];
assert((quote === '\'' || quote === '"'),
'String literal must starts with a quote');
start = index;
++index;
while (index < length) {
ch = source[index++];
if (ch === quote) {
quote = '';
break;
} else if (ch === '\\') {
ch = source[index++];
if (!ch || !isLineTerminator(ch.charCodeAt(0))) {
switch (ch) {
case 'n':
str += '\n';
break;
case 'r':
str += '\r';
break;
case 't':
str += '\t';
break;
case 'b':
str += '\b';
break;
case 'f':
str += '\f';
break;
case 'v':
str += '\x0B';
break;
default:
str += ch;
break;
}
} else {
if (ch === '\r' && source[index] === '\n') {
++index;
}
}
} else if (isLineTerminator(ch.charCodeAt(0))) {
break;
} else {
str += ch;
}
}
if (quote !== '') {
throwError({}, Messages.UnexpectedToken, 'ILLEGAL');
}
return {
type: Token.StringLiteral,
value: str,
octal: octal,
range: [start, index]
};
}
function isIdentifierName(token) {
return token.type === Token.Identifier ||
token.type === Token.Keyword ||
token.type === Token.BooleanLiteral ||
token.type === Token.NullLiteral;
}
function advance() {
var ch;
skipWhitespace();
if (index >= length) {
return {
type: Token.EOF,
range: [index, index]
};
}
ch = source.charCodeAt(index);
// Very common: ( and ) and ;
if (ch === 40 || ch === 41 || ch === 58) {
return scanPunctuator();
}
// String literal starts with single quote (#39) or double quote (#34).
if (ch === 39 || ch === 34) {
return scanStringLiteral();
}
if (isIdentifierStart(ch)) {
return scanIdentifier();
}
// Dot (.) char #46 can also start a floating-point number, hence the need
// to check the next character.
if (ch === 46) {
if (isDecimalDigit(source.charCodeAt(index + 1))) {
return scanNumericLiteral();
}
return scanPunctuator();
}
if (isDecimalDigit(ch)) {
return scanNumericLiteral();
}
return scanPunctuator();
}
function lex() {
var token;
token = lookahead;
index = token.range[1];
lookahead = advance();
index = token.range[1];
return token;
}
function peek() {
var pos;
pos = index;
lookahead = advance();
index = pos;
}
// Throw an exception
function throwError(token, messageFormat) {
var error,
args = Array.prototype.slice.call(arguments, 2),
msg = messageFormat.replace(
/%(\d)/g,
function (whole, index) {
assert(index < args.length, 'Message reference must be in range');
return args[index];
}
);
error = new Error(msg);
error.index = index;
error.description = msg;
throw error;
}
// Throw an exception because of the token.
function throwUnexpected(token) {
throwError(token, Messages.UnexpectedToken, token.value);
}
// Expect the next token to match the specified punctuator.
// If not, an exception will be thrown.
function expect(value) {
var token = lex();
if (token.type !== Token.Punctuator || token.value !== value) {
throwUnexpected(token);
}
}
// Return true if the next token matches the specified punctuator.
function match(value) {
return lookahead.type === Token.Punctuator && lookahead.value === value;
}
// Return true if the next token matches the specified keyword
function matchKeyword(keyword) {
return lookahead.type === Token.Keyword && lookahead.value === keyword;
}
function consumeSemicolon() {
// Catch the very common case first: immediately a semicolon (char #59).
if (source.charCodeAt(index) === 59) {
lex();
return;
}
skipWhitespace();
if (match(';')) {
lex();
return;
}
if (lookahead.type !== Token.EOF && !match('}')) {
throwUnexpected(lookahead);
}
}
// 11.1.4 Array Initialiser
function parseArrayInitialiser() {
var elements = [];
expect('[');
while (!match(']')) {
if (match(',')) {
lex();
elements.push(null);
} else {
elements.push(parseExpression());
if (!match(']')) {
expect(',');
}
}
}
expect(']');
return delegate.createArrayExpression(elements);
}
// 11.1.5 Object Initialiser
function parseObjectPropertyKey() {
var token;
skipWhitespace();
token = lex();
// Note: This function is called only from parseObjectProperty(), where
// EOF and Punctuator tokens are already filtered out.
if (token.type === Token.StringLiteral || token.type === Token.NumericLiteral) {
return delegate.createLiteral(token);
}
return delegate.createIdentifier(token.value);
}
function parseObjectProperty() {
var token, key;
token = lookahead;
skipWhitespace();
if (token.type === Token.EOF || token.type === Token.Punctuator) {
throwUnexpected(token);
}
key = parseObjectPropertyKey();
expect(':');
return delegate.createProperty('init', key, parseExpression());
}
function parseObjectInitialiser() {
var properties = [];
expect('{');
while (!match('}')) {
properties.push(parseObjectProperty());
if (!match('}')) {
expect(',');
}
}
expect('}');
return delegate.createObjectExpression(properties);
}
// 11.1.6 The Grouping Operator
function parseGroupExpression() {
var expr;
expect('(');
expr = parseExpression();
expect(')');
return expr;
}
// 11.1 Primary Expressions
function parsePrimaryExpression() {
var type, token, expr;
if (match('(')) {
return parseGroupExpression();
}
type = lookahead.type;
if (type === Token.Identifier) {
expr = delegate.createIdentifier(lex().value);
} else if (type === Token.StringLiteral || type === Token.NumericLiteral) {
expr = delegate.createLiteral(lex());
} else if (type === Token.Keyword) {
if (matchKeyword('this')) {
lex();
expr = delegate.createThisExpression();
}
} else if (type === Token.BooleanLiteral) {
token = lex();
token.value = (token.value === 'true');
expr = delegate.createLiteral(token);
} else if (type === Token.NullLiteral) {
token = lex();
token.value = null;
expr = delegate.createLiteral(token);
} else if (match('[')) {
expr = parseArrayInitialiser();
} else if (match('{')) {
expr = parseObjectInitialiser();
}
if (expr) {
return expr;
}
throwUnexpected(lex());
}
// 11.2 Left-Hand-Side Expressions
function parseArguments() {
var args = [];
expect('(');
if (!match(')')) {
while (index < length) {
args.push(parseExpression());
if (match(')')) {
break;
}
expect(',');
}
}
expect(')');
return args;
}
function parseNonComputedProperty() {
var token;
token = lex();
if (!isIdentifierName(token)) {
throwUnexpected(token);
}
return delegate.createIdentifier(token.value);
}
function parseNonComputedMember() {
expect('.');
return parseNonComputedProperty();
}
function parseComputedMember() {
var expr;
expect('[');
expr = parseExpression();
expect(']');
return expr;
}
function parseLeftHandSideExpression() {
var expr, args, property;
expr = parsePrimaryExpression();
while (true) {
if (match('[')) {
property = parseComputedMember();
expr = delegate.createMemberExpression('[', expr, property);
} else if (match('.')) {
property = parseNonComputedMember();
expr = delegate.createMemberExpression('.', expr, property);
} else if (match('(')) {
args = parseArguments();
expr = delegate.createCallExpression(expr, args);
} else {
break;
}
}
return expr;
}
// 11.3 Postfix Expressions
var parsePostfixExpression = parseLeftHandSideExpression;
// 11.4 Unary Operators
function parseUnaryExpression() {
var token, expr;
if (lookahead.type !== Token.Punctuator && lookahead.type !== Token.Keyword) {
expr = parsePostfixExpression();
} else if (match('+') || match('-') || match('!')) {
token = lex();
expr = parseUnaryExpression();
expr = delegate.createUnaryExpression(token.value, expr);
} else if (matchKeyword('delete') || matchKeyword('void') || matchKeyword('typeof')) {
throwError({}, Messages.UnexpectedToken);
} else {
expr = parsePostfixExpression();
}
return expr;
}
function binaryPrecedence(token) {
var prec = 0;
if (token.type !== Token.Punctuator && token.type !== Token.Keyword) {
return 0;
}
switch (token.value) {
case '||':
prec = 1;
break;
case '&&':
prec = 2;
break;
case '==':
case '!=':
case '===':
case '!==':
prec = 6;
break;
case '<':
case '>':
case '<=':
case '>=':
case 'instanceof':
prec = 7;
break;
case 'in':
prec = 7;
break;
case '+':
case '-':
prec = 9;
break;
case '*':
case '/':
case '%':
prec = 11;
break;
default:
break;
}
return prec;
}
// 11.5 Multiplicative Operators
// 11.6 Additive Operators
// 11.7 Bitwise Shift Operators
// 11.8 Relational Operators
// 11.9 Equality Operators
// 11.10 Binary Bitwise Operators
// 11.11 Binary Logical Operators
function parseBinaryExpression() {
var expr, token, prec, stack, right, operator, left, i;
left = parseUnaryExpression();
token = lookahead;
prec = binaryPrecedence(token);
if (prec === 0) {
return left;
}
token.prec = prec;
lex();
right = parseUnaryExpression();
stack = [left, token, right];
while ((prec = binaryPrecedence(lookahead)) > 0) {
// Reduce: make a binary expression from the three topmost entries.
while ((stack.length > 2) && (prec <= stack[stack.length - 2].prec)) {
right = stack.pop();
operator = stack.pop().value;
left = stack.pop();
expr = delegate.createBinaryExpression(operator, left, right);
stack.push(expr);
}
// Shift.
token = lex();
token.prec = prec;
stack.push(token);
expr = parseUnaryExpression();
stack.push(expr);
}
// Final reduce to clean-up the stack.
i = stack.length - 1;
expr = stack[i];
while (i > 1) {
expr = delegate.createBinaryExpression(stack[i - 1].value, stack[i - 2], expr);
i -= 2;
}
return expr;
}
// 11.12 Conditional Operator
function parseConditionalExpression() {
var expr, consequent, alternate;
expr = parseBinaryExpression();
if (match('?')) {
lex();
consequent = parseConditionalExpression();
expect(':');
alternate = parseConditionalExpression();
expr = delegate.createConditionalExpression(expr, consequent, alternate);
}
return expr;
}
// Simplification since we do not support AssignmentExpression.
var parseExpression = parseConditionalExpression;
// Polymer Syntax extensions
// Filter ::
// Identifier
// Identifier "(" ")"
// Identifier "(" FilterArguments ")"
function parseFilter() {
var identifier, args;
identifier = lex();
if (identifier.type !== Token.Identifier) {
throwUnexpected(identifier);
}
args = match('(') ? parseArguments() : [];
return delegate.createFilter(identifier.value, args);
}
// Filters ::
// "|" Filter
// Filters "|" Filter
function parseFilters() {
while (match('|')) {
lex();
parseFilter();
}
}
// TopLevel ::
// LabelledExpressions
// AsExpression
// InExpression
// FilterExpression
// AsExpression ::
// FilterExpression as Identifier
// InExpression ::
// Identifier, Identifier in FilterExpression
// Identifier in FilterExpression
// FilterExpression ::
// Expression
// Expression Filters
function parseTopLevel() {
skipWhitespace();
peek();
var expr = parseExpression();
if (expr) {
if (lookahead.value === ',' || lookahead.value == 'in' &&
expr.type === Syntax.Identifier) {
parseInExpression(expr);
} else {
parseFilters();
if (lookahead.value === 'as') {
parseAsExpression(expr);
} else {
delegate.createTopLevel(expr);
}
}
}
if (lookahead.type !== Token.EOF) {
throwUnexpected(lookahead);
}
}
function parseAsExpression(expr) {
lex(); // as
var identifier = lex().value;
delegate.createAsExpression(expr, identifier);
}
function parseInExpression(identifier) {
var indexName;
if (lookahead.value === ',') {
lex();
if (lookahead.type !== Token.Identifier)
throwUnexpected(lookahead);
indexName = lex().value;
}
lex(); // in
var expr = parseExpression();
parseFilters();
delegate.createInExpression(identifier.name, indexName, expr);
}
function parse(code, inDelegate) {
delegate = inDelegate;
source = code;
index = 0;
length = source.length;
lookahead = null;
state = {
labelSet: {}
};
return parseTopLevel();
}
global.esprima = {
parse: parse
};
})(this);
// Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
// This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
// The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
// The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
// Code distributed by Google as part of the polymer project is also
// subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
(function (global) {
'use strict';
function prepareBinding(expressionText, name, node, filterRegistry) {
var expression;
try {
expression = getExpression(expressionText);
if (expression.scopeIdent &&
(node.nodeType !== Node.ELEMENT_NODE ||
node.tagName !== 'TEMPLATE' ||
(name !== 'bind' && name !== 'repeat'))) {
throw Error('as and in can only be used within ');
}
} catch (ex) {
console.error('Invalid expression syntax: ' + expressionText, ex);
return;
}
return function(model, node, oneTime) {
var binding = expression.getBinding(model, filterRegistry, oneTime);
if (expression.scopeIdent && binding) {
node.polymerExpressionScopeIdent_ = expression.scopeIdent;
if (expression.indexIdent)
node.polymerExpressionIndexIdent_ = expression.indexIdent;
}
return binding;
}
}
// TODO(rafaelw): Implement simple LRU.
var expressionParseCache = Object.create(null);
function getExpression(expressionText) {
var expression = expressionParseCache[expressionText];
if (!expression) {
var delegate = new ASTDelegate();
esprima.parse(expressionText, delegate);
expression = new Expression(delegate);
expressionParseCache[expressionText] = expression;
}
return expression;
}
function Literal(value) {
this.value = value;
this.valueFn_ = undefined;
}
Literal.prototype = {
valueFn: function() {
if (!this.valueFn_) {
var value = this.value;
this.valueFn_ = function() {
return value;
}
}
return this.valueFn_;
}
}
function IdentPath(name) {
this.name = name;
this.path = Path.get(name);
}
IdentPath.prototype = {
valueFn: function() {
if (!this.valueFn_) {
var name = this.name;
var path = this.path;
this.valueFn_ = function(model, observer) {
if (observer)
observer.addPath(model, path);
return path.getValueFrom(model);
}
}
return this.valueFn_;
},
setValue: function(model, newValue) {
if (this.path.length == 1)
model = findScope(model, this.path[0]);
return this.path.setValueFrom(model, newValue);
}
};
function MemberExpression(object, property, accessor) {
this.computed = accessor == '[';
this.dynamicDeps = typeof object == 'function' ||
object.dynamicDeps ||
(this.computed && !(property instanceof Literal));
this.simplePath =
!this.dynamicDeps &&
(property instanceof IdentPath || property instanceof Literal) &&
(object instanceof MemberExpression || object instanceof IdentPath);
this.object = this.simplePath ? object : getFn(object);
this.property = !this.computed || this.simplePath ?
property : getFn(property);
}
MemberExpression.prototype = {
get fullPath() {
if (!this.fullPath_) {
var parts = this.object instanceof MemberExpression ?
this.object.fullPath.slice() : [this.object.name];
parts.push(this.property instanceof IdentPath ?
this.property.name : this.property.value);
this.fullPath_ = Path.get(parts);
}
return this.fullPath_;
},
valueFn: function() {
if (!this.valueFn_) {
var object = this.object;
if (this.simplePath) {
var path = this.fullPath;
this.valueFn_ = function(model, observer) {
if (observer)
observer.addPath(model, path);
return path.getValueFrom(model);
};
} else if (!this.computed) {
var path = Path.get(this.property.name);
this.valueFn_ = function(model, observer, filterRegistry) {
var context = object(model, observer, filterRegistry);
if (observer)
observer.addPath(context, path);
return path.getValueFrom(context);
}
} else {
// Computed property.
var property = this.property;
this.valueFn_ = function(model, observer, filterRegistry) {
var context = object(model, observer, filterRegistry);
var propName = property(model, observer, filterRegistry);
if (observer)
observer.addPath(context, [propName]);
return context ? context[propName] : undefined;
};
}
}
return this.valueFn_;
},
setValue: function(model, newValue) {
if (this.simplePath) {
this.fullPath.setValueFrom(model, newValue);
return newValue;
}
var object = this.object(model);
var propName = this.property instanceof IdentPath ? this.property.name :
this.property(model);
return object[propName] = newValue;
}
};
function Filter(name, args) {
this.name = name;
this.args = [];
for (var i = 0; i < args.length; i++) {
this.args[i] = getFn(args[i]);
}
}
Filter.prototype = {
transform: function(model, observer, filterRegistry, toModelDirection,
initialArgs) {
var fn = filterRegistry[this.name];
var context = model;
if (fn) {
context = undefined;
} else {
fn = context[this.name];
if (!fn) {
console.error('Cannot find function or filter: ' + this.name);
return;
}
}
// If toModelDirection is falsey, then the "normal" (dom-bound) direction
// is used. Otherwise, it looks for a 'toModel' property function on the
// object.
if (toModelDirection) {
fn = fn.toModel;
} else if (typeof fn.toDOM == 'function') {
fn = fn.toDOM;
}
if (typeof fn != 'function') {
console.error('Cannot find function or filter: ' + this.name);
return;
}
var args = initialArgs || [];
for (var i = 0; i < this.args.length; i++) {
args.push(getFn(this.args[i])(model, observer, filterRegistry));
}
return fn.apply(context, args);
}
};
function notImplemented() { throw Error('Not Implemented'); }
var unaryOperators = {
'+': function(v) { return +v; },
'-': function(v) { return -v; },
'!': function(v) { return !v; }
};
var binaryOperators = {
'+': function(l, r) { return l+r; },
'-': function(l, r) { return l-r; },
'*': function(l, r) { return l*r; },
'/': function(l, r) { return l/r; },
'%': function(l, r) { return l%r; },
'<': function(l, r) { return l': function(l, r) { return l>r; },
'<=': function(l, r) { return l<=r; },
'>=': function(l, r) { return l>=r; },
'==': function(l, r) { return l==r; },
'!=': function(l, r) { return l!=r; },
'===': function(l, r) { return l===r; },
'!==': function(l, r) { return l!==r; },
'&&': function(l, r) { return l&&r; },
'||': function(l, r) { return l||r; },
};
function getFn(arg) {
return typeof arg == 'function' ? arg : arg.valueFn();
}
function ASTDelegate() {
this.expression = null;
this.filters = [];
this.deps = {};
this.currentPath = undefined;
this.scopeIdent = undefined;
this.indexIdent = undefined;
this.dynamicDeps = false;
}
ASTDelegate.prototype = {
createUnaryExpression: function(op, argument) {
if (!unaryOperators[op])
throw Error('Disallowed operator: ' + op);
argument = getFn(argument);
return function(model, observer, filterRegistry) {
return unaryOperators[op](argument(model, observer, filterRegistry));
};
},
createBinaryExpression: function(op, left, right) {
if (!binaryOperators[op])
throw Error('Disallowed operator: ' + op);
left = getFn(left);
right = getFn(right);
switch (op) {
case '||':
this.dynamicDeps = true;
return function(model, observer, filterRegistry) {
return left(model, observer, filterRegistry) ||
right(model, observer, filterRegistry);
};
case '&&':
this.dynamicDeps = true;
return function(model, observer, filterRegistry) {
return left(model, observer, filterRegistry) &&
right(model, observer, filterRegistry);
};
}
return function(model, observer, filterRegistry) {
return binaryOperators[op](left(model, observer, filterRegistry),
right(model, observer, filterRegistry));
};
},
createConditionalExpression: function(test, consequent, alternate) {
test = getFn(test);
consequent = getFn(consequent);
alternate = getFn(alternate);
this.dynamicDeps = true;
return function(model, observer, filterRegistry) {
return test(model, observer, filterRegistry) ?
consequent(model, observer, filterRegistry) :
alternate(model, observer, filterRegistry);
}
},
createIdentifier: function(name) {
var ident = new IdentPath(name);
ident.type = 'Identifier';
return ident;
},
createMemberExpression: function(accessor, object, property) {
var ex = new MemberExpression(object, property, accessor);
if (ex.dynamicDeps)
this.dynamicDeps = true;
return ex;
},
createCallExpression: function(expression, args) {
if (!(expression instanceof IdentPath))
throw Error('Only identifier function invocations are allowed');
var filter = new Filter(expression.name, args);
return function(model, observer, filterRegistry) {
return filter.transform(model, observer, filterRegistry, false);
};
},
createLiteral: function(token) {
return new Literal(token.value);
},
createArrayExpression: function(elements) {
for (var i = 0; i < elements.length; i++)
elements[i] = getFn(elements[i]);
return function(model, observer, filterRegistry) {
var arr = []
for (var i = 0; i < elements.length; i++)
arr.push(elements[i](model, observer, filterRegistry));
return arr;
}
},
createProperty: function(kind, key, value) {
return {
key: key instanceof IdentPath ? key.name : key.value,
value: value
};
},
createObjectExpression: function(properties) {
for (var i = 0; i < properties.length; i++)
properties[i].value = getFn(properties[i].value);
return function(model, observer, filterRegistry) {
var obj = {};
for (var i = 0; i < properties.length; i++)
obj[properties[i].key] =
properties[i].value(model, observer, filterRegistry);
return obj;
}
},
createFilter: function(name, args) {
this.filters.push(new Filter(name, args));
},
createAsExpression: function(expression, scopeIdent) {
this.expression = expression;
this.scopeIdent = scopeIdent;
},
createInExpression: function(scopeIdent, indexIdent, expression) {
this.expression = expression;
this.scopeIdent = scopeIdent;
this.indexIdent = indexIdent;
},
createTopLevel: function(expression) {
this.expression = expression;
},
createThisExpression: notImplemented
}
function ConstantObservable(value) {
this.value_ = value;
}
ConstantObservable.prototype = {
open: function() { return this.value_; },
discardChanges: function() { return this.value_; },
deliver: function() {},
close: function() {},
}
function Expression(delegate) {
this.scopeIdent = delegate.scopeIdent;
this.indexIdent = delegate.indexIdent;
if (!delegate.expression)
throw Error('No expression found.');
this.expression = delegate.expression;
getFn(this.expression); // forces enumeration of path dependencies
this.filters = delegate.filters;
this.dynamicDeps = delegate.dynamicDeps;
}
Expression.prototype = {
getBinding: function(model, filterRegistry, oneTime) {
if (oneTime)
return this.getValue(model, undefined, filterRegistry);
var observer = new CompoundObserver();
// captures deps.
var firstValue = this.getValue(model, observer, filterRegistry);
var firstTime = true;
var self = this;
function valueFn() {
// deps cannot have changed on first value retrieval.
if (firstTime) {
firstTime = false;
return firstValue;
}
if (self.dynamicDeps)
observer.startReset();
var value = self.getValue(model,
self.dynamicDeps ? observer : undefined,
filterRegistry);
if (self.dynamicDeps)
observer.finishReset();
return value;
}
function setValueFn(newValue) {
self.setValue(model, newValue, filterRegistry);
return newValue;
}
return new ObserverTransform(observer, valueFn, setValueFn, true);
},
getValue: function(model, observer, filterRegistry) {
var value = getFn(this.expression)(model, observer, filterRegistry);
for (var i = 0; i < this.filters.length; i++) {
value = this.filters[i].transform(model, observer, filterRegistry,
false, [value]);
}
return value;
},
setValue: function(model, newValue, filterRegistry) {
var count = this.filters ? this.filters.length : 0;
while (count-- > 0) {
newValue = this.filters[count].transform(model, undefined,
filterRegistry, true, [newValue]);
}
if (this.expression.setValue)
return this.expression.setValue(model, newValue);
}
}
/**
* Converts a style property name to a css property name. For example:
* "WebkitUserSelect" to "-webkit-user-select"
*/
function convertStylePropertyName(name) {
return String(name).replace(/[A-Z]/g, function(c) {
return '-' + c.toLowerCase();
});
}
var parentScopeName = '@' + Math.random().toString(36).slice(2);
// Single ident paths must bind directly to the appropriate scope object.
// I.e. Pushed values in two-bindings need to be assigned to the actual model
// object.
function findScope(model, prop) {
while (model[parentScopeName] &&
!Object.prototype.hasOwnProperty.call(model, prop)) {
model = model[parentScopeName];
}
return model;
}
function isLiteralExpression(pathString) {
switch (pathString) {
case '':
return false;
case 'false':
case 'null':
case 'true':
return true;
}
if (!isNaN(Number(pathString)))
return true;
return false;
};
function PolymerExpressions() {}
PolymerExpressions.prototype = {
// "built-in" filters
styleObject: function(value) {
var parts = [];
for (var key in value) {
parts.push(convertStylePropertyName(key) + ': ' + value[key]);
}
return parts.join('; ');
},
tokenList: function(value) {
var tokens = [];
for (var key in value) {
if (value[key])
tokens.push(key);
}
return tokens.join(' ');
},
// binding delegate API
prepareInstancePositionChanged: function(template) {
var indexIdent = template.polymerExpressionIndexIdent_;
if (!indexIdent)
return;
return function(templateInstance, index) {
templateInstance.model[indexIdent] = index;
};
},
prepareBinding: function(pathString, name, node) {
var path = Path.get(pathString);
if (!isLiteralExpression(pathString) && path.valid) {
if (path.length == 1) {
return function(model, node, oneTime) {
if (oneTime)
return path.getValueFrom(model);
var scope = findScope(model, path[0]);
return new PathObserver(scope, path);
};
}
return; // bail out early if pathString is simple path.
}
return prepareBinding(pathString, name, node, this);
},
prepareInstanceModel: function(template) {
var scopeName = template.polymerExpressionScopeIdent_;
if (!scopeName)
return;
var parentScope = template.templateInstance ?
template.templateInstance.model :
template.model;
var indexName = template.polymerExpressionIndexIdent_;
return function(model) {
return createScopeObject(parentScope, model, scopeName, indexName);
};
}
};
var createScopeObject = ('__proto__' in {}) ?
function(parentScope, model, scopeName, indexName) {
var scope = {};
scope[scopeName] = model;
scope[indexName] = undefined;
scope[parentScopeName] = parentScope;
scope.__proto__ = parentScope;
return scope;
} :
function(parentScope, model, scopeName, indexName) {
var scope = Object.create(parentScope);
Object.defineProperty(scope, scopeName,
{ value: model, configurable: true, writable: true });
Object.defineProperty(scope, indexName,
{ value: undefined, configurable: true, writable: true });
Object.defineProperty(scope, parentScopeName,
{ value: parentScope, configurable: true, writable: true });
return scope;
};
global.PolymerExpressions = PolymerExpressions;
PolymerExpressions.getExpression = getExpression;
})(this);
Polymer = {
version: '0.5.1'
};
// TODO(sorvell): this ensures Polymer is an object and not a function
// Platform is currently defining it as a function to allow for async loading
// of polymer; once we refine the loading process this likely goes away.
if (typeof window.Polymer === 'function') {
Polymer = {};
}
(function(scope) {
function withDependencies(task, depends) {
depends = depends || [];
if (!depends.map) {
depends = [depends];
}
return task.apply(this, depends.map(marshal));
}
function module(name, dependsOrFactory, moduleFactory) {
var module;
switch (arguments.length) {
case 0:
return;
case 1:
module = null;
break;
case 2:
// dependsOrFactory is `factory` in this case
module = dependsOrFactory.apply(this);
break;
default:
// dependsOrFactory is `depends` in this case
module = withDependencies(moduleFactory, dependsOrFactory);
break;
}
modules[name] = module;
};
function marshal(name) {
return modules[name];
}
var modules = {};
function using(depends, task) {
HTMLImports.whenImportsReady(function() {
withDependencies(task, depends);
});
};
// exports
scope.marshal = marshal;
// `module` confuses commonjs detectors
scope.modularize = module;
scope.using = using;
})(window);
/*
Build only script.
Ensures scripts needed for basic x-platform compatibility
will be run when platform.js is not loaded.
*/
if (!window.WebComponents) {
/*
On supported platforms, platform.js is not needed. To retain compatibility
with the polyfills, we stub out minimal functionality.
*/
if (!window.WebComponents) {
WebComponents = {
flush: function() {},
flags: {log: {}}
};
Platform = WebComponents;
CustomElements = {
useNative: true,
ready: true,
takeRecords: function() {},
instanceof: function(obj, base) {
return obj instanceof base;
}
};
HTMLImports = {
useNative: true
};
addEventListener('HTMLImportsLoaded', function() {
document.dispatchEvent(
new CustomEvent('WebComponentsReady', {bubbles: true})
);
});
// ShadowDOM
ShadowDOMPolyfill = null;
wrap = unwrap = function(n){
return n;
};
}
/*
Create polyfill scope and feature detect native support.
*/
window.HTMLImports = window.HTMLImports || {flags:{}};
(function(scope) {
/**
Basic setup and simple module executer. We collect modules and then execute
the code later, only if it's necessary for polyfilling.
*/
var IMPORT_LINK_TYPE = 'import';
var useNative = Boolean(IMPORT_LINK_TYPE in document.createElement('link'));
/**
Support `currentScript` on all browsers as `document._currentScript.`
NOTE: We cannot polyfill `document.currentScript` because it's not possible
both to override and maintain the ability to capture the native value.
Therefore we choose to expose `_currentScript` both when native imports
and the polyfill are in use.
*/
// NOTE: ShadowDOMPolyfill intrusion.
var hasShadowDOMPolyfill = Boolean(window.ShadowDOMPolyfill);
var wrap = function(node) {
return hasShadowDOMPolyfill ? ShadowDOMPolyfill.wrapIfNeeded(node) : node;
};
var rootDocument = wrap(document);
var currentScriptDescriptor = {
get: function() {
var script = HTMLImports.currentScript || document.currentScript ||
// NOTE: only works when called in synchronously executing code.
// readyState should check if `loading` but IE10 is
// interactive when scripts run so we cheat.
(document.readyState !== 'complete' ?
document.scripts[document.scripts.length - 1] : null);
return wrap(script);
},
configurable: true
};
Object.defineProperty(document, '_currentScript', currentScriptDescriptor);
Object.defineProperty(rootDocument, '_currentScript', currentScriptDescriptor);
/**
Add support for the `HTMLImportsLoaded` event and the `HTMLImports.whenReady`
method. This api is necessary because unlike the native implementation,
script elements do not force imports to resolve. Instead, users should wrap
code in either an `HTMLImportsLoaded` hander or after load time in an
`HTMLImports.whenReady(callback)` call.
NOTE: This module also supports these apis under the native implementation.
Therefore, if this file is loaded, the same code can be used under both
the polyfill and native implementation.
*/
var isIE = /Trident/.test(navigator.userAgent);
// call a callback when all HTMLImports in the document at call time
// (or at least document ready) have loaded.
// 1. ensure the document is in a ready state (has dom), then
// 2. watch for loading of imports and call callback when done
function whenReady(callback, doc) {
doc = doc || rootDocument;
// if document is loading, wait and try again
whenDocumentReady(function() {
watchImportsLoad(callback, doc);
}, doc);
}
// call the callback when the document is in a ready state (has dom)
var requiredReadyState = isIE ? 'complete' : 'interactive';
var READY_EVENT = 'readystatechange';
function isDocumentReady(doc) {
return (doc.readyState === 'complete' ||
doc.readyState === requiredReadyState);
}
// call when we ensure the document is in a ready state
function whenDocumentReady(callback, doc) {
if (!isDocumentReady(doc)) {
var checkReady = function() {
if (doc.readyState === 'complete' ||
doc.readyState === requiredReadyState) {
doc.removeEventListener(READY_EVENT, checkReady);
whenDocumentReady(callback, doc);
}
};
doc.addEventListener(READY_EVENT, checkReady);
} else if (callback) {
callback();
}
}
function markTargetLoaded(event) {
event.target.__loaded = true;
}
// call when we ensure all imports have loaded
function watchImportsLoad(callback, doc) {
var imports = doc.querySelectorAll('link[rel=import]');
var loaded = 0, l = imports.length;
function checkDone(d) {
if ((loaded == l) && callback) {
callback();
}
}
function loadedImport(e) {
markTargetLoaded(e);
loaded++;
checkDone();
}
if (l) {
for (var i=0, imp; (i>> 0 && s !== '';
}
function toNumber(s) {
return +s;
}
function isObject(obj) {
return obj === Object(obj);
}
var numberIsNaN = global.Number.isNaN || function(value) {
return typeof value === 'number' && global.isNaN(value);
}
function areSameValue(left, right) {
if (left === right)
return left !== 0 || 1 / left === 1 / right;
if (numberIsNaN(left) && numberIsNaN(right))
return true;
return left !== left && right !== right;
}
var createObject = ('__proto__' in {}) ?
function(obj) { return obj; } :
function(obj) {
var proto = obj.__proto__;
if (!proto)
return obj;
var newObject = Object.create(proto);
Object.getOwnPropertyNames(obj).forEach(function(name) {
Object.defineProperty(newObject, name,
Object.getOwnPropertyDescriptor(obj, name));
});
return newObject;
};
var identStart = '[\$_a-zA-Z]';
var identPart = '[\$_a-zA-Z0-9]';
var identRegExp = new RegExp('^' + identStart + '+' + identPart + '*' + '$');
function getPathCharType(char) {
if (char === undefined)
return 'eof';
var code = char.charCodeAt(0);
switch(code) {
case 0x5B: // [
case 0x5D: // ]
case 0x2E: // .
case 0x22: // "
case 0x27: // '
case 0x30: // 0
return char;
case 0x5F: // _
case 0x24: // $
return 'ident';
case 0x20: // Space
case 0x09: // Tab
case 0x0A: // Newline
case 0x0D: // Return
case 0xA0: // No-break space
case 0xFEFF: // Byte Order Mark
case 0x2028: // Line Separator
case 0x2029: // Paragraph Separator
return 'ws';
}
// a-z, A-Z
if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A))
return 'ident';
// 1-9
if (0x31 <= code && code <= 0x39)
return 'number';
return 'else';
}
var pathStateMachine = {
'beforePath': {
'ws': ['beforePath'],
'ident': ['inIdent', 'append'],
'[': ['beforeElement'],
'eof': ['afterPath']
},
'inPath': {
'ws': ['inPath'],
'.': ['beforeIdent'],
'[': ['beforeElement'],
'eof': ['afterPath']
},
'beforeIdent': {
'ws': ['beforeIdent'],
'ident': ['inIdent', 'append']
},
'inIdent': {
'ident': ['inIdent', 'append'],
'0': ['inIdent', 'append'],
'number': ['inIdent', 'append'],
'ws': ['inPath', 'push'],
'.': ['beforeIdent', 'push'],
'[': ['beforeElement', 'push'],
'eof': ['afterPath', 'push']
},
'beforeElement': {
'ws': ['beforeElement'],
'0': ['afterZero', 'append'],
'number': ['inIndex', 'append'],
"'": ['inSingleQuote', 'append', ''],
'"': ['inDoubleQuote', 'append', '']
},
'afterZero': {
'ws': ['afterElement', 'push'],
']': ['inPath', 'push']
},
'inIndex': {
'0': ['inIndex', 'append'],
'number': ['inIndex', 'append'],
'ws': ['afterElement'],
']': ['inPath', 'push']
},
'inSingleQuote': {
"'": ['afterElement'],
'eof': ['error'],
'else': ['inSingleQuote', 'append']
},
'inDoubleQuote': {
'"': ['afterElement'],
'eof': ['error'],
'else': ['inDoubleQuote', 'append']
},
'afterElement': {
'ws': ['afterElement'],
']': ['inPath', 'push']
}
}
function noop() {}
function parsePath(path) {
var keys = [];
var index = -1;
var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath';
var actions = {
push: function() {
if (key === undefined)
return;
keys.push(key);
key = undefined;
},
append: function() {
if (key === undefined)
key = newChar
else
key += newChar;
}
};
function maybeUnescapeQuote() {
if (index >= path.length)
return;
var nextChar = path[index + 1];
if ((mode == 'inSingleQuote' && nextChar == "'") ||
(mode == 'inDoubleQuote' && nextChar == '"')) {
index++;
newChar = nextChar;
actions.append();
return true;
}
}
while (mode) {
index++;
c = path[index];
if (c == '\\' && maybeUnescapeQuote(mode))
continue;
type = getPathCharType(c);
typeMap = pathStateMachine[mode];
transition = typeMap[type] || typeMap['else'] || 'error';
if (transition == 'error')
return; // parse error;
mode = transition[0];
action = actions[transition[1]] || noop;
newChar = transition[2] === undefined ? c : transition[2];
action();
if (mode === 'afterPath') {
return keys;
}
}
return; // parse error
}
function isIdent(s) {
return identRegExp.test(s);
}
var constructorIsPrivate = {};
function Path(parts, privateToken) {
if (privateToken !== constructorIsPrivate)
throw Error('Use Path.get to retrieve path objects');
for (var i = 0; i < parts.length; i++) {
this.push(String(parts[i]));
}
if (hasEval && this.length) {
this.getValueFrom = this.compiledGetValueFromFn();
}
}
// TODO(rafaelw): Make simple LRU cache
var pathCache = {};
function getPath(pathString) {
if (pathString instanceof Path)
return pathString;
if (pathString == null || pathString.length == 0)
pathString = '';
if (typeof pathString != 'string') {
if (isIndex(pathString.length)) {
// Constructed with array-like (pre-parsed) keys
return new Path(pathString, constructorIsPrivate);
}
pathString = String(pathString);
}
var path = pathCache[pathString];
if (path)
return path;
var parts = parsePath(pathString);
if (!parts)
return invalidPath;
var path = new Path(parts, constructorIsPrivate);
pathCache[pathString] = path;
return path;
}
Path.get = getPath;
function formatAccessor(key) {
if (isIndex(key)) {
return '[' + key + ']';
} else {
return '["' + key.replace(/"/g, '\\"') + '"]';
}
}
Path.prototype = createObject({
__proto__: [],
valid: true,
toString: function() {
var pathString = '';
for (var i = 0; i < this.length; i++) {
var key = this[i];
if (isIdent(key)) {
pathString += i ? '.' + key : key;
} else {
pathString += formatAccessor(key);
}
}
return pathString;
},
getValueFrom: function(obj, directObserver) {
for (var i = 0; i < this.length; i++) {
if (obj == null)
return;
obj = obj[this[i]];
}
return obj;
},
iterateObjects: function(obj, observe) {
for (var i = 0; i < this.length; i++) {
if (i)
obj = obj[this[i - 1]];
if (!isObject(obj))
return;
observe(obj, this[i]);
}
},
compiledGetValueFromFn: function() {
var str = '';
var pathString = 'obj';
str += 'if (obj != null';
var i = 0;
var key;
for (; i < (this.length - 1); i++) {
key = this[i];
pathString += isIdent(key) ? '.' + key : formatAccessor(key);
str += ' &&\n ' + pathString + ' != null';
}
str += ')\n';
var key = this[i];
pathString += isIdent(key) ? '.' + key : formatAccessor(key);
str += ' return ' + pathString + ';\nelse\n return undefined;';
return new Function('obj', str);
},
setValueFrom: function(obj, value) {
if (!this.length)
return false;
for (var i = 0; i < this.length - 1; i++) {
if (!isObject(obj))
return false;
obj = obj[this[i]];
}
if (!isObject(obj))
return false;
obj[this[i]] = value;
return true;
}
});
var invalidPath = new Path('', constructorIsPrivate);
invalidPath.valid = false;
invalidPath.getValueFrom = invalidPath.setValueFrom = function() {};
var MAX_DIRTY_CHECK_CYCLES = 1000;
function dirtyCheck(observer) {
var cycles = 0;
while (cycles < MAX_DIRTY_CHECK_CYCLES && observer.check_()) {
cycles++;
}
if (testingExposeCycleCount)
global.dirtyCheckCycleCount = cycles;
return cycles > 0;
}
function objectIsEmpty(object) {
for (var prop in object)
return false;
return true;
}
function diffIsEmpty(diff) {
return objectIsEmpty(diff.added) &&
objectIsEmpty(diff.removed) &&
objectIsEmpty(diff.changed);
}
function diffObjectFromOldObject(object, oldObject) {
var added = {};
var removed = {};
var changed = {};
for (var prop in oldObject) {
var newValue = object[prop];
if (newValue !== undefined && newValue === oldObject[prop])
continue;
if (!(prop in object)) {
removed[prop] = undefined;
continue;
}
if (newValue !== oldObject[prop])
changed[prop] = newValue;
}
for (var prop in object) {
if (prop in oldObject)
continue;
added[prop] = object[prop];
}
if (Array.isArray(object) && object.length !== oldObject.length)
changed.length = object.length;
return {
added: added,
removed: removed,
changed: changed
};
}
var eomTasks = [];
function runEOMTasks() {
if (!eomTasks.length)
return false;
for (var i = 0; i < eomTasks.length; i++) {
eomTasks[i]();
}
eomTasks.length = 0;
return true;
}
var runEOM = hasObserve ? (function(){
return function(fn) {
return Promise.resolve().then(fn);
}
})() :
(function() {
return function(fn) {
eomTasks.push(fn);
};
})();
var observedObjectCache = [];
function newObservedObject() {
var observer;
var object;
var discardRecords = false;
var first = true;
function callback(records) {
if (observer && observer.state_ === OPENED && !discardRecords)
observer.check_(records);
}
return {
open: function(obs) {
if (observer)
throw Error('ObservedObject in use');
if (!first)
Object.deliverChangeRecords(callback);
observer = obs;
first = false;
},
observe: function(obj, arrayObserve) {
object = obj;
if (arrayObserve)
Array.observe(object, callback);
else
Object.observe(object, callback);
},
deliver: function(discard) {
discardRecords = discard;
Object.deliverChangeRecords(callback);
discardRecords = false;
},
close: function() {
observer = undefined;
Object.unobserve(object, callback);
observedObjectCache.push(this);
}
};
}
/*
* The observedSet abstraction is a perf optimization which reduces the total
* number of Object.observe observations of a set of objects. The idea is that
* groups of Observers will have some object dependencies in common and this
* observed set ensures that each object in the transitive closure of
* dependencies is only observed once. The observedSet acts as a write barrier
* such that whenever any change comes through, all Observers are checked for
* changed values.
*
* Note that this optimization is explicitly moving work from setup-time to
* change-time.
*
* TODO(rafaelw): Implement "garbage collection". In order to move work off
* the critical path, when Observers are closed, their observed objects are
* not Object.unobserve(d). As a result, it's possible that if the observedSet
* is kept open, but some Observers have been closed, it could cause "leaks"
* (prevent otherwise collectable objects from being collected). At some
* point, we should implement incremental "gc" which keeps a list of
* observedSets which may need clean-up and does small amounts of cleanup on a
* timeout until all is clean.
*/
function getObservedObject(observer, object, arrayObserve) {
var dir = observedObjectCache.pop() || newObservedObject();
dir.open(observer);
dir.observe(object, arrayObserve);
return dir;
}
var observedSetCache = [];
function newObservedSet() {
var observerCount = 0;
var observers = [];
var objects = [];
var rootObj;
var rootObjProps;
function observe(obj, prop) {
if (!obj)
return;
if (obj === rootObj)
rootObjProps[prop] = true;
if (objects.indexOf(obj) < 0) {
objects.push(obj);
Object.observe(obj, callback);
}
observe(Object.getPrototypeOf(obj), prop);
}
function allRootObjNonObservedProps(recs) {
for (var i = 0; i < recs.length; i++) {
var rec = recs[i];
if (rec.object !== rootObj ||
rootObjProps[rec.name] ||
rec.type === 'setPrototype') {
return false;
}
}
return true;
}
function callback(recs) {
if (allRootObjNonObservedProps(recs))
return;
var observer;
for (var i = 0; i < observers.length; i++) {
observer = observers[i];
if (observer.state_ == OPENED) {
observer.iterateObjects_(observe);
}
}
for (var i = 0; i < observers.length; i++) {
observer = observers[i];
if (observer.state_ == OPENED) {
observer.check_();
}
}
}
var record = {
objects: objects,
get rootObject() { return rootObj; },
set rootObject(value) {
rootObj = value;
rootObjProps = {};
},
open: function(obs, object) {
observers.push(obs);
observerCount++;
obs.iterateObjects_(observe);
},
close: function(obs) {
observerCount--;
if (observerCount > 0) {
return;
}
for (var i = 0; i < objects.length; i++) {
Object.unobserve(objects[i], callback);
Observer.unobservedCount++;
}
observers.length = 0;
objects.length = 0;
rootObj = undefined;
rootObjProps = undefined;
observedSetCache.push(this);
if (lastObservedSet === this)
lastObservedSet = null;
},
};
return record;
}
var lastObservedSet;
function getObservedSet(observer, obj) {
if (!lastObservedSet || lastObservedSet.rootObject !== obj) {
lastObservedSet = observedSetCache.pop() || newObservedSet();
lastObservedSet.rootObject = obj;
}
lastObservedSet.open(observer, obj);
return lastObservedSet;
}
var UNOPENED = 0;
var OPENED = 1;
var CLOSED = 2;
var RESETTING = 3;
var nextObserverId = 1;
function Observer() {
this.state_ = UNOPENED;
this.callback_ = undefined;
this.target_ = undefined; // TODO(rafaelw): Should be WeakRef
this.directObserver_ = undefined;
this.value_ = undefined;
this.id_ = nextObserverId++;
}
Observer.prototype = {
open: function(callback, target) {
if (this.state_ != UNOPENED)
throw Error('Observer has already been opened.');
addToAll(this);
this.callback_ = callback;
this.target_ = target;
this.connect_();
this.state_ = OPENED;
return this.value_;
},
close: function() {
if (this.state_ != OPENED)
return;
removeFromAll(this);
this.disconnect_();
this.value_ = undefined;
this.callback_ = undefined;
this.target_ = undefined;
this.state_ = CLOSED;
},
deliver: function() {
if (this.state_ != OPENED)
return;
dirtyCheck(this);
},
report_: function(changes) {
try {
this.callback_.apply(this.target_, changes);
} catch (ex) {
Observer._errorThrownDuringCallback = true;
console.error('Exception caught during observer callback: ' +
(ex.stack || ex));
}
},
discardChanges: function() {
this.check_(undefined, true);
return this.value_;
}
}
var collectObservers = !hasObserve;
var allObservers;
Observer._allObserversCount = 0;
if (collectObservers) {
allObservers = [];
}
function addToAll(observer) {
Observer._allObserversCount++;
if (!collectObservers)
return;
allObservers.push(observer);
}
function removeFromAll(observer) {
Observer._allObserversCount--;
}
var runningMicrotaskCheckpoint = false;
global.Platform = global.Platform || {};
global.Platform.performMicrotaskCheckpoint = function() {
if (runningMicrotaskCheckpoint)
return;
if (!collectObservers)
return;
runningMicrotaskCheckpoint = true;
var cycles = 0;
var anyChanged, toCheck;
do {
cycles++;
toCheck = allObservers;
allObservers = [];
anyChanged = false;
for (var i = 0; i < toCheck.length; i++) {
var observer = toCheck[i];
if (observer.state_ != OPENED)
continue;
if (observer.check_())
anyChanged = true;
allObservers.push(observer);
}
if (runEOMTasks())
anyChanged = true;
} while (cycles < MAX_DIRTY_CHECK_CYCLES && anyChanged);
if (testingExposeCycleCount)
global.dirtyCheckCycleCount = cycles;
runningMicrotaskCheckpoint = false;
};
if (collectObservers) {
global.Platform.clearObservers = function() {
allObservers = [];
};
}
function ObjectObserver(object) {
Observer.call(this);
this.value_ = object;
this.oldObject_ = undefined;
}
ObjectObserver.prototype = createObject({
__proto__: Observer.prototype,
arrayObserve: false,
connect_: function(callback, target) {
if (hasObserve) {
this.directObserver_ = getObservedObject(this, this.value_,
this.arrayObserve);
} else {
this.oldObject_ = this.copyObject(this.value_);
}
},
copyObject: function(object) {
var copy = Array.isArray(object) ? [] : {};
for (var prop in object) {
copy[prop] = object[prop];
};
if (Array.isArray(object))
copy.length = object.length;
return copy;
},
check_: function(changeRecords, skipChanges) {
var diff;
var oldValues;
if (hasObserve) {
if (!changeRecords)
return false;
oldValues = {};
diff = diffObjectFromChangeRecords(this.value_, changeRecords,
oldValues);
} else {
oldValues = this.oldObject_;
diff = diffObjectFromOldObject(this.value_, this.oldObject_);
}
if (diffIsEmpty(diff))
return false;
if (!hasObserve)
this.oldObject_ = this.copyObject(this.value_);
this.report_([
diff.added || {},
diff.removed || {},
diff.changed || {},
function(property) {
return oldValues[property];
}
]);
return true;
},
disconnect_: function() {
if (hasObserve) {
this.directObserver_.close();
this.directObserver_ = undefined;
} else {
this.oldObject_ = undefined;
}
},
deliver: function() {
if (this.state_ != OPENED)
return;
if (hasObserve)
this.directObserver_.deliver(false);
else
dirtyCheck(this);
},
discardChanges: function() {
if (this.directObserver_)
this.directObserver_.deliver(true);
else
this.oldObject_ = this.copyObject(this.value_);
return this.value_;
}
});
function ArrayObserver(array) {
if (!Array.isArray(array))
throw Error('Provided object is not an Array');
ObjectObserver.call(this, array);
}
ArrayObserver.prototype = createObject({
__proto__: ObjectObserver.prototype,
arrayObserve: true,
copyObject: function(arr) {
return arr.slice();
},
check_: function(changeRecords) {
var splices;
if (hasObserve) {
if (!changeRecords)
return false;
splices = projectArraySplices(this.value_, changeRecords);
} else {
splices = calcSplices(this.value_, 0, this.value_.length,
this.oldObject_, 0, this.oldObject_.length);
}
if (!splices || !splices.length)
return false;
if (!hasObserve)
this.oldObject_ = this.copyObject(this.value_);
this.report_([splices]);
return true;
}
});
ArrayObserver.applySplices = function(previous, current, splices) {
splices.forEach(function(splice) {
var spliceArgs = [splice.index, splice.removed.length];
var addIndex = splice.index;
while (addIndex < splice.index + splice.addedCount) {
spliceArgs.push(current[addIndex]);
addIndex++;
}
Array.prototype.splice.apply(previous, spliceArgs);
});
};
function PathObserver(object, path) {
Observer.call(this);
this.object_ = object;
this.path_ = getPath(path);
this.directObserver_ = undefined;
}
PathObserver.prototype = createObject({
__proto__: Observer.prototype,
get path() {
return this.path_;
},
connect_: function() {
if (hasObserve)
this.directObserver_ = getObservedSet(this, this.object_);
this.check_(undefined, true);
},
disconnect_: function() {
this.value_ = undefined;
if (this.directObserver_) {
this.directObserver_.close(this);
this.directObserver_ = undefined;
}
},
iterateObjects_: function(observe) {
this.path_.iterateObjects(this.object_, observe);
},
check_: function(changeRecords, skipChanges) {
var oldValue = this.value_;
this.value_ = this.path_.getValueFrom(this.object_);
if (skipChanges || areSameValue(this.value_, oldValue))
return false;
this.report_([this.value_, oldValue, this]);
return true;
},
setValue: function(newValue) {
if (this.path_)
this.path_.setValueFrom(this.object_, newValue);
}
});
function CompoundObserver(reportChangesOnOpen) {
Observer.call(this);
this.reportChangesOnOpen_ = reportChangesOnOpen;
this.value_ = [];
this.directObserver_ = undefined;
this.observed_ = [];
}
var observerSentinel = {};
CompoundObserver.prototype = createObject({
__proto__: Observer.prototype,
connect_: function() {
if (hasObserve) {
var object;
var needsDirectObserver = false;
for (var i = 0; i < this.observed_.length; i += 2) {
object = this.observed_[i]
if (object !== observerSentinel) {
needsDirectObserver = true;
break;
}
}
if (needsDirectObserver)
this.directObserver_ = getObservedSet(this, object);
}
this.check_(undefined, !this.reportChangesOnOpen_);
},
disconnect_: function() {
for (var i = 0; i < this.observed_.length; i += 2) {
if (this.observed_[i] === observerSentinel)
this.observed_[i + 1].close();
}
this.observed_.length = 0;
this.value_.length = 0;
if (this.directObserver_) {
this.directObserver_.close(this);
this.directObserver_ = undefined;
}
},
addPath: function(object, path) {
if (this.state_ != UNOPENED && this.state_ != RESETTING)
throw Error('Cannot add paths once started.');
var path = getPath(path);
this.observed_.push(object, path);
if (!this.reportChangesOnOpen_)
return;
var index = this.observed_.length / 2 - 1;
this.value_[index] = path.getValueFrom(object);
},
addObserver: function(observer) {
if (this.state_ != UNOPENED && this.state_ != RESETTING)
throw Error('Cannot add observers once started.');
this.observed_.push(observerSentinel, observer);
if (!this.reportChangesOnOpen_)
return;
var index = this.observed_.length / 2 - 1;
this.value_[index] = observer.open(this.deliver, this);
},
startReset: function() {
if (this.state_ != OPENED)
throw Error('Can only reset while open');
this.state_ = RESETTING;
this.disconnect_();
},
finishReset: function() {
if (this.state_ != RESETTING)
throw Error('Can only finishReset after startReset');
this.state_ = OPENED;
this.connect_();
return this.value_;
},
iterateObjects_: function(observe) {
var object;
for (var i = 0; i < this.observed_.length; i += 2) {
object = this.observed_[i]
if (object !== observerSentinel)
this.observed_[i + 1].iterateObjects(object, observe)
}
},
check_: function(changeRecords, skipChanges) {
var oldValues;
for (var i = 0; i < this.observed_.length; i += 2) {
var object = this.observed_[i];
var path = this.observed_[i+1];
var value;
if (object === observerSentinel) {
var observable = path;
value = this.state_ === UNOPENED ?
observable.open(this.deliver, this) :
observable.discardChanges();
} else {
value = path.getValueFrom(object);
}
if (skipChanges) {
this.value_[i / 2] = value;
continue;
}
if (areSameValue(value, this.value_[i / 2]))
continue;
oldValues = oldValues || [];
oldValues[i / 2] = this.value_[i / 2];
this.value_[i / 2] = value;
}
if (!oldValues)
return false;
// TODO(rafaelw): Having observed_ as the third callback arg here is
// pretty lame API. Fix.
this.report_([this.value_, oldValues, this.observed_]);
return true;
}
});
function identFn(value) { return value; }
function ObserverTransform(observable, getValueFn, setValueFn,
dontPassThroughSet) {
this.callback_ = undefined;
this.target_ = undefined;
this.value_ = undefined;
this.observable_ = observable;
this.getValueFn_ = getValueFn || identFn;
this.setValueFn_ = setValueFn || identFn;
// TODO(rafaelw): This is a temporary hack. PolymerExpressions needs this
// at the moment because of a bug in it's dependency tracking.
this.dontPassThroughSet_ = dontPassThroughSet;
}
ObserverTransform.prototype = {
open: function(callback, target) {
this.callback_ = callback;
this.target_ = target;
this.value_ =
this.getValueFn_(this.observable_.open(this.observedCallback_, this));
return this.value_;
},
observedCallback_: function(value) {
value = this.getValueFn_(value);
if (areSameValue(value, this.value_))
return;
var oldValue = this.value_;
this.value_ = value;
this.callback_.call(this.target_, this.value_, oldValue);
},
discardChanges: function() {
this.value_ = this.getValueFn_(this.observable_.discardChanges());
return this.value_;
},
deliver: function() {
return this.observable_.deliver();
},
setValue: function(value) {
value = this.setValueFn_(value);
if (!this.dontPassThroughSet_ && this.observable_.setValue)
return this.observable_.setValue(value);
},
close: function() {
if (this.observable_)
this.observable_.close();
this.callback_ = undefined;
this.target_ = undefined;
this.observable_ = undefined;
this.value_ = undefined;
this.getValueFn_ = undefined;
this.setValueFn_ = undefined;
}
}
var expectedRecordTypes = {
add: true,
update: true,
delete: true
};
function diffObjectFromChangeRecords(object, changeRecords, oldValues) {
var added = {};
var removed = {};
for (var i = 0; i < changeRecords.length; i++) {
var record = changeRecords[i];
if (!expectedRecordTypes[record.type]) {
console.error('Unknown changeRecord type: ' + record.type);
console.error(record);
continue;
}
if (!(record.name in oldValues))
oldValues[record.name] = record.oldValue;
if (record.type == 'update')
continue;
if (record.type == 'add') {
if (record.name in removed)
delete removed[record.name];
else
added[record.name] = true;
continue;
}
// type = 'delete'
if (record.name in added) {
delete added[record.name];
delete oldValues[record.name];
} else {
removed[record.name] = true;
}
}
for (var prop in added)
added[prop] = object[prop];
for (var prop in removed)
removed[prop] = undefined;
var changed = {};
for (var prop in oldValues) {
if (prop in added || prop in removed)
continue;
var newValue = object[prop];
if (oldValues[prop] !== newValue)
changed[prop] = newValue;
}
return {
added: added,
removed: removed,
changed: changed
};
}
function newSplice(index, removed, addedCount) {
return {
index: index,
removed: removed,
addedCount: addedCount
};
}
var EDIT_LEAVE = 0;
var EDIT_UPDATE = 1;
var EDIT_ADD = 2;
var EDIT_DELETE = 3;
function ArraySplice() {}
ArraySplice.prototype = {
// Note: This function is *based* on the computation of the Levenshtein
// "edit" distance. The one change is that "updates" are treated as two
// edits - not one. With Array splices, an update is really a delete
// followed by an add. By retaining this, we optimize for "keeping" the
// maximum array items in the original array. For example:
//
// 'xxxx123' -> '123yyyy'
//
// With 1-edit updates, the shortest path would be just to update all seven
// characters. With 2-edit updates, we delete 4, leave 3, and add 4. This
// leaves the substring '123' intact.
calcEditDistances: function(current, currentStart, currentEnd,
old, oldStart, oldEnd) {
// "Deletion" columns
var rowCount = oldEnd - oldStart + 1;
var columnCount = currentEnd - currentStart + 1;
var distances = new Array(rowCount);
// "Addition" rows. Initialize null column.
for (var i = 0; i < rowCount; i++) {
distances[i] = new Array(columnCount);
distances[i][0] = i;
}
// Initialize null row
for (var j = 0; j < columnCount; j++)
distances[0][j] = j;
for (var i = 1; i < rowCount; i++) {
for (var j = 1; j < columnCount; j++) {
if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1]))
distances[i][j] = distances[i - 1][j - 1];
else {
var north = distances[i - 1][j] + 1;
var west = distances[i][j - 1] + 1;
distances[i][j] = north < west ? north : west;
}
}
}
return distances;
},
// This starts at the final weight, and walks "backward" by finding
// the minimum previous weight recursively until the origin of the weight
// matrix.
spliceOperationsFromEditDistances: function(distances) {
var i = distances.length - 1;
var j = distances[0].length - 1;
var current = distances[i][j];
var edits = [];
while (i > 0 || j > 0) {
if (i == 0) {
edits.push(EDIT_ADD);
j--;
continue;
}
if (j == 0) {
edits.push(EDIT_DELETE);
i--;
continue;
}
var northWest = distances[i - 1][j - 1];
var west = distances[i - 1][j];
var north = distances[i][j - 1];
var min;
if (west < north)
min = west < northWest ? west : northWest;
else
min = north < northWest ? north : northWest;
if (min == northWest) {
if (northWest == current) {
edits.push(EDIT_LEAVE);
} else {
edits.push(EDIT_UPDATE);
current = northWest;
}
i--;
j--;
} else if (min == west) {
edits.push(EDIT_DELETE);
i--;
current = west;
} else {
edits.push(EDIT_ADD);
j--;
current = north;
}
}
edits.reverse();
return edits;
},
/**
* Splice Projection functions:
*
* A splice map is a representation of how a previous array of items
* was transformed into a new array of items. Conceptually it is a list of
* tuples of
*
*
*
* which are kept in ascending index order of. The tuple represents that at
* the |index|, |removed| sequence of items were removed, and counting forward
* from |index|, |addedCount| items were added.
*/
/**
* Lacking individual splice mutation information, the minimal set of
* splices can be synthesized given the previous state and final state of an
* array. The basic approach is to calculate the edit distance matrix and
* choose the shortest path through it.
*
* Complexity: O(l * p)
* l: The length of the current array
* p: The length of the old array
*/
calcSplices: function(current, currentStart, currentEnd,
old, oldStart, oldEnd) {
var prefixCount = 0;
var suffixCount = 0;
var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart);
if (currentStart == 0 && oldStart == 0)
prefixCount = this.sharedPrefix(current, old, minLength);
if (currentEnd == current.length && oldEnd == old.length)
suffixCount = this.sharedSuffix(current, old, minLength - prefixCount);
currentStart += prefixCount;
oldStart += prefixCount;
currentEnd -= suffixCount;
oldEnd -= suffixCount;
if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0)
return [];
if (currentStart == currentEnd) {
var splice = newSplice(currentStart, [], 0);
while (oldStart < oldEnd)
splice.removed.push(old[oldStart++]);
return [ splice ];
} else if (oldStart == oldEnd)
return [ newSplice(currentStart, [], currentEnd - currentStart) ];
var ops = this.spliceOperationsFromEditDistances(
this.calcEditDistances(current, currentStart, currentEnd,
old, oldStart, oldEnd));
var splice = undefined;
var splices = [];
var index = currentStart;
var oldIndex = oldStart;
for (var i = 0; i < ops.length; i++) {
switch(ops[i]) {
case EDIT_LEAVE:
if (splice) {
splices.push(splice);
splice = undefined;
}
index++;
oldIndex++;
break;
case EDIT_UPDATE:
if (!splice)
splice = newSplice(index, [], 0);
splice.addedCount++;
index++;
splice.removed.push(old[oldIndex]);
oldIndex++;
break;
case EDIT_ADD:
if (!splice)
splice = newSplice(index, [], 0);
splice.addedCount++;
index++;
break;
case EDIT_DELETE:
if (!splice)
splice = newSplice(index, [], 0);
splice.removed.push(old[oldIndex]);
oldIndex++;
break;
}
}
if (splice) {
splices.push(splice);
}
return splices;
},
sharedPrefix: function(current, old, searchLength) {
for (var i = 0; i < searchLength; i++)
if (!this.equals(current[i], old[i]))
return i;
return searchLength;
},
sharedSuffix: function(current, old, searchLength) {
var index1 = current.length;
var index2 = old.length;
var count = 0;
while (count < searchLength && this.equals(current[--index1], old[--index2]))
count++;
return count;
},
calculateSplices: function(current, previous) {
return this.calcSplices(current, 0, current.length, previous, 0,
previous.length);
},
equals: function(currentValue, previousValue) {
return currentValue === previousValue;
}
};
var arraySplice = new ArraySplice();
function calcSplices(current, currentStart, currentEnd,
old, oldStart, oldEnd) {
return arraySplice.calcSplices(current, currentStart, currentEnd,
old, oldStart, oldEnd);
}
function intersect(start1, end1, start2, end2) {
// Disjoint
if (end1 < start2 || end2 < start1)
return -1;
// Adjacent
if (end1 == start2 || end2 == start1)
return 0;
// Non-zero intersect, span1 first
if (start1 < start2) {
if (end1 < end2)
return end1 - start2; // Overlap
else
return end2 - start2; // Contained
} else {
// Non-zero intersect, span2 first
if (end2 < end1)
return end2 - start1; // Overlap
else
return end1 - start1; // Contained
}
}
function mergeSplice(splices, index, removed, addedCount) {
var splice = newSplice(index, removed, addedCount);
var inserted = false;
var insertionOffset = 0;
for (var i = 0; i < splices.length; i++) {
var current = splices[i];
current.index += insertionOffset;
if (inserted)
continue;
var intersectCount = intersect(splice.index,
splice.index + splice.removed.length,
current.index,
current.index + current.addedCount);
if (intersectCount >= 0) {
// Merge the two splices
splices.splice(i, 1);
i--;
insertionOffset -= current.addedCount - current.removed.length;
splice.addedCount += current.addedCount - intersectCount;
var deleteCount = splice.removed.length +
current.removed.length - intersectCount;
if (!splice.addedCount && !deleteCount) {
// merged splice is a noop. discard.
inserted = true;
} else {
var removed = current.removed;
if (splice.index < current.index) {
// some prefix of splice.removed is prepended to current.removed.
var prepend = splice.removed.slice(0, current.index - splice.index);
Array.prototype.push.apply(prepend, removed);
removed = prepend;
}
if (splice.index + splice.removed.length > current.index + current.addedCount) {
// some suffix of splice.removed is appended to current.removed.
var append = splice.removed.slice(current.index + current.addedCount - splice.index);
Array.prototype.push.apply(removed, append);
}
splice.removed = removed;
if (current.index < splice.index) {
splice.index = current.index;
}
}
} else if (splice.index < current.index) {
// Insert splice here.
inserted = true;
splices.splice(i, 0, splice);
i++;
var offset = splice.addedCount - splice.removed.length
current.index += offset;
insertionOffset += offset;
}
}
if (!inserted)
splices.push(splice);
}
function createInitialSplices(array, changeRecords) {
var splices = [];
for (var i = 0; i < changeRecords.length; i++) {
var record = changeRecords[i];
switch(record.type) {
case 'splice':
mergeSplice(splices, record.index, record.removed.slice(), record.addedCount);
break;
case 'add':
case 'update':
case 'delete':
if (!isIndex(record.name))
continue;
var index = toNumber(record.name);
if (index < 0)
continue;
mergeSplice(splices, index, [record.oldValue], 1);
break;
default:
console.error('Unexpected record type: ' + JSON.stringify(record));
break;
}
}
return splices;
}
function projectArraySplices(array, changeRecords) {
var splices = [];
createInitialSplices(array, changeRecords).forEach(function(splice) {
if (splice.addedCount == 1 && splice.removed.length == 1) {
if (splice.removed[0] !== array[splice.index])
splices.push(splice);
return
};
splices = splices.concat(calcSplices(array, splice.index, splice.index + splice.addedCount,
splice.removed, 0, splice.removed.length));
});
return splices;
}
// Export the observe-js object for **Node.js**, with
// backwards-compatibility for the old `require()` API. If we're in
// the browser, export as a global object.
var expose = global;
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
expose = exports = module.exports;
}
expose = exports;
}
expose.Observer = Observer;
expose.Observer.runEOM_ = runEOM;
expose.Observer.observerSentinel_ = observerSentinel; // for testing.
expose.Observer.hasObjectObserve = hasObserve;
expose.ArrayObserver = ArrayObserver;
expose.ArrayObserver.calculateSplices = function(current, previous) {
return arraySplice.calculateSplices(current, previous);
};
expose.ArraySplice = ArraySplice;
expose.ObjectObserver = ObjectObserver;
expose.PathObserver = PathObserver;
expose.CompoundObserver = CompoundObserver;
expose.Path = Path;
expose.ObserverTransform = ObserverTransform;
})(typeof global !== 'undefined' && global && typeof module !== 'undefined' && module ? global : this || window);
// Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
// This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
// The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
// The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
// Code distributed by Google as part of the polymer project is also
// subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
(function(global) {
'use strict';
var filter = Array.prototype.filter.call.bind(Array.prototype.filter);
function getTreeScope(node) {
while (node.parentNode) {
node = node.parentNode;
}
return typeof node.getElementById === 'function' ? node : null;
}
Node.prototype.bind = function(name, observable) {
console.error('Unhandled binding to Node: ', this, name, observable);
};
Node.prototype.bindFinished = function() {};
function updateBindings(node, name, binding) {
var bindings = node.bindings_;
if (!bindings)
bindings = node.bindings_ = {};
if (bindings[name])
binding[name].close();
return bindings[name] = binding;
}
function returnBinding(node, name, binding) {
return binding;
}
function sanitizeValue(value) {
return value == null ? '' : value;
}
function updateText(node, value) {
node.data = sanitizeValue(value);
}
function textBinding(node) {
return function(value) {
return updateText(node, value);
};
}
var maybeUpdateBindings = returnBinding;
Object.defineProperty(Platform, 'enableBindingsReflection', {
get: function() {
return maybeUpdateBindings === updateBindings;
},
set: function(enable) {
maybeUpdateBindings = enable ? updateBindings : returnBinding;
return enable;
},
configurable: true
});
Text.prototype.bind = function(name, value, oneTime) {
if (name !== 'textContent')
return Node.prototype.bind.call(this, name, value, oneTime);
if (oneTime)
return updateText(this, value);
var observable = value;
updateText(this, observable.open(textBinding(this)));
return maybeUpdateBindings(this, name, observable);
}
function updateAttribute(el, name, conditional, value) {
if (conditional) {
if (value)
el.setAttribute(name, '');
else
el.removeAttribute(name);
return;
}
el.setAttribute(name, sanitizeValue(value));
}
function attributeBinding(el, name, conditional) {
return function(value) {
updateAttribute(el, name, conditional, value);
};
}
Element.prototype.bind = function(name, value, oneTime) {
var conditional = name[name.length - 1] == '?';
if (conditional) {
this.removeAttribute(name);
name = name.slice(0, -1);
}
if (oneTime)
return updateAttribute(this, name, conditional, value);
var observable = value;
updateAttribute(this, name, conditional,
observable.open(attributeBinding(this, name, conditional)));
return maybeUpdateBindings(this, name, observable);
};
var checkboxEventType;
(function() {
// Attempt to feature-detect which event (change or click) is fired first
// for checkboxes.
var div = document.createElement('div');
var checkbox = div.appendChild(document.createElement('input'));
checkbox.setAttribute('type', 'checkbox');
var first;
var count = 0;
checkbox.addEventListener('click', function(e) {
count++;
first = first || 'click';
});
checkbox.addEventListener('change', function() {
count++;
first = first || 'change';
});
var event = document.createEvent('MouseEvent');
event.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false,
false, false, false, 0, null);
checkbox.dispatchEvent(event);
// WebKit/Blink don't fire the change event if the element is outside the
// document, so assume 'change' for that case.
checkboxEventType = count == 1 ? 'change' : first;
})();
function getEventForInputType(element) {
switch (element.type) {
case 'checkbox':
return checkboxEventType;
case 'radio':
case 'select-multiple':
case 'select-one':
return 'change';
case 'range':
if (/Trident|MSIE/.test(navigator.userAgent))
return 'change';
default:
return 'input';
}
}
function updateInput(input, property, value, santizeFn) {
input[property] = (santizeFn || sanitizeValue)(value);
}
function inputBinding(input, property, santizeFn) {
return function(value) {
return updateInput(input, property, value, santizeFn);
}
}
function noop() {}
function bindInputEvent(input, property, observable, postEventFn) {
var eventType = getEventForInputType(input);
function eventHandler() {
observable.setValue(input[property]);
observable.discardChanges();
(postEventFn || noop)(input);
Platform.performMicrotaskCheckpoint();
}
input.addEventListener(eventType, eventHandler);
return {
close: function() {
input.removeEventListener(eventType, eventHandler);
observable.close();
},
observable_: observable
}
}
function booleanSanitize(value) {
return Boolean(value);
}
// |element| is assumed to be an HTMLInputElement with |type| == 'radio'.
// Returns an array containing all radio buttons other than |element| that
// have the same |name|, either in the form that |element| belongs to or,
// if no form, in the document tree to which |element| belongs.
//
// This implementation is based upon the HTML spec definition of a
// "radio button group":
// http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.html#radio-button-group
//
function getAssociatedRadioButtons(element) {
if (element.form) {
return filter(element.form.elements, function(el) {
return el != element &&
el.tagName == 'INPUT' &&
el.type == 'radio' &&
el.name == element.name;
});
} else {
var treeScope = getTreeScope(element);
if (!treeScope)
return [];
var radios = treeScope.querySelectorAll(
'input[type="radio"][name="' + element.name + '"]');
return filter(radios, function(el) {
return el != element && !el.form;
});
}
}
function checkedPostEvent(input) {
// Only the radio button that is getting checked gets an event. We
// therefore find all the associated radio buttons and update their
// check binding manually.
if (input.tagName === 'INPUT' &&
input.type === 'radio') {
getAssociatedRadioButtons(input).forEach(function(radio) {
var checkedBinding = radio.bindings_.checked;
if (checkedBinding) {
// Set the value directly to avoid an infinite call stack.
checkedBinding.observable_.setValue(false);
}
});
}
}
HTMLInputElement.prototype.bind = function(name, value, oneTime) {
if (name !== 'value' && name !== 'checked')
return HTMLElement.prototype.bind.call(this, name, value, oneTime);
this.removeAttribute(name);
var sanitizeFn = name == 'checked' ? booleanSanitize : sanitizeValue;
var postEventFn = name == 'checked' ? checkedPostEvent : noop;
if (oneTime)
return updateInput(this, name, value, sanitizeFn);
var observable = value;
var binding = bindInputEvent(this, name, observable, postEventFn);
updateInput(this, name,
observable.open(inputBinding(this, name, sanitizeFn)),
sanitizeFn);
// Checkboxes may need to update bindings of other checkboxes.
return updateBindings(this, name, binding);
}
HTMLTextAreaElement.prototype.bind = function(name, value, oneTime) {
if (name !== 'value')
return HTMLElement.prototype.bind.call(this, name, value, oneTime);
this.removeAttribute('value');
if (oneTime)
return updateInput(this, 'value', value);
var observable = value;
var binding = bindInputEvent(this, 'value', observable);
updateInput(this, 'value',
observable.open(inputBinding(this, 'value', sanitizeValue)));
return maybeUpdateBindings(this, name, binding);
}
function updateOption(option, value) {
var parentNode = option.parentNode;;
var select;
var selectBinding;
var oldValue;
if (parentNode instanceof HTMLSelectElement &&
parentNode.bindings_ &&
parentNode.bindings_.value) {
select = parentNode;
selectBinding = select.bindings_.value;
oldValue = select.value;
}
option.value = sanitizeValue(value);
if (select && select.value != oldValue) {
selectBinding.observable_.setValue(select.value);
selectBinding.observable_.discardChanges();
Platform.performMicrotaskCheckpoint();
}
}
function optionBinding(option) {
return function(value) {
updateOption(option, value);
}
}
HTMLOptionElement.prototype.bind = function(name, value, oneTime) {
if (name !== 'value')
return HTMLElement.prototype.bind.call(this, name, value, oneTime);
this.removeAttribute('value');
if (oneTime)
return updateOption(this, value);
var observable = value;
var binding = bindInputEvent(this, 'value', observable);
updateOption(this, observable.open(optionBinding(this)));
return maybeUpdateBindings(this, name, binding);
}
HTMLSelectElement.prototype.bind = function(name, value, oneTime) {
if (name === 'selectedindex')
name = 'selectedIndex';
if (name !== 'selectedIndex' && name !== 'value')
return HTMLElement.prototype.bind.call(this, name, value, oneTime);
this.removeAttribute(name);
if (oneTime)
return updateInput(this, name, value);
var observable = value;
var binding = bindInputEvent(this, name, observable);
updateInput(this, name,
observable.open(inputBinding(this, name)));
// Option update events may need to access select bindings.
return updateBindings(this, name, binding);
}
})(this);
// Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
// This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
// The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
// The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
// Code distributed by Google as part of the polymer project is also
// subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
(function(global) {
'use strict';
function assert(v) {
if (!v)
throw new Error('Assertion failed');
}
var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);
function getFragmentRoot(node) {
var p;
while (p = node.parentNode) {
node = p;
}
return node;
}
function searchRefId(node, id) {
if (!id)
return;
var ref;
var selector = '#' + id;
while (!ref) {
node = getFragmentRoot(node);
if (node.protoContent_)
ref = node.protoContent_.querySelector(selector);
else if (node.getElementById)
ref = node.getElementById(id);
if (ref || !node.templateCreator_)
break
node = node.templateCreator_;
}
return ref;
}
function getInstanceRoot(node) {
while (node.parentNode) {
node = node.parentNode;
}
return node.templateCreator_ ? node : null;
}
var Map;
if (global.Map && typeof global.Map.prototype.forEach === 'function') {
Map = global.Map;
} else {
Map = function() {
this.keys = [];
this.values = [];
};
Map.prototype = {
set: function(key, value) {
var index = this.keys.indexOf(key);
if (index < 0) {
this.keys.push(key);
this.values.push(value);
} else {
this.values[index] = value;
}
},
get: function(key) {
var index = this.keys.indexOf(key);
if (index < 0)
return;
return this.values[index];
},
delete: function(key, value) {
var index = this.keys.indexOf(key);
if (index < 0)
return false;
this.keys.splice(index, 1);
this.values.splice(index, 1);
return true;
},
forEach: function(f, opt_this) {
for (var i = 0; i < this.keys.length; i++)
f.call(opt_this || this, this.values[i], this.keys[i], this);
}
};
}
// JScript does not have __proto__. We wrap all object literals with
// createObject which uses Object.create, Object.defineProperty and
// Object.getOwnPropertyDescriptor to create a new object that does the exact
// same thing. The main downside to this solution is that we have to extract
// all those property descriptors for IE.
var createObject = ('__proto__' in {}) ?
function(obj) { return obj; } :
function(obj) {
var proto = obj.__proto__;
if (!proto)
return obj;
var newObject = Object.create(proto);
Object.getOwnPropertyNames(obj).forEach(function(name) {
Object.defineProperty(newObject, name,
Object.getOwnPropertyDescriptor(obj, name));
});
return newObject;
};
// IE does not support have Document.prototype.contains.
if (typeof document.contains != 'function') {
Document.prototype.contains = function(node) {
if (node === this || node.parentNode === this)
return true;
return this.documentElement.contains(node);
}
}
var BIND = 'bind';
var REPEAT = 'repeat';
var IF = 'if';
var templateAttributeDirectives = {
'template': true,
'repeat': true,
'bind': true,
'ref': true
};
var semanticTemplateElements = {
'THEAD': true,
'TBODY': true,
'TFOOT': true,
'TH': true,
'TR': true,
'TD': true,
'COLGROUP': true,
'COL': true,
'CAPTION': true,
'OPTION': true,
'OPTGROUP': true
};
var hasTemplateElement = typeof HTMLTemplateElement !== 'undefined';
if (hasTemplateElement) {
// TODO(rafaelw): Remove when fix for
// https://codereview.chromium.org/164803002/
// makes it to Chrome release.
(function() {
var t = document.createElement('template');
var d = t.content.ownerDocument;
var html = d.appendChild(d.createElement('html'));
var head = html.appendChild(d.createElement('head'));
var base = d.createElement('base');
base.href = document.baseURI;
head.appendChild(base);
})();
}
var allTemplatesSelectors = 'template, ' +
Object.keys(semanticTemplateElements).map(function(tagName) {
return tagName.toLowerCase() + '[template]';
}).join(', ');
function isSVGTemplate(el) {
return el.tagName == 'template' &&
el.namespaceURI == 'http://www.w3.org/2000/svg';
}
function isHTMLTemplate(el) {
return el.tagName == 'TEMPLATE' &&
el.namespaceURI == 'http://www.w3.org/1999/xhtml';
}
function isAttributeTemplate(el) {
return Boolean(semanticTemplateElements[el.tagName] &&
el.hasAttribute('template'));
}
function isTemplate(el) {
if (el.isTemplate_ === undefined)
el.isTemplate_ = el.tagName == 'TEMPLATE' || isAttributeTemplate(el);
return el.isTemplate_;
}
// FIXME: Observe templates being added/removed from documents
// FIXME: Expose imperative API to decorate and observe templates in
// "disconnected tress" (e.g. ShadowRoot)
document.addEventListener('DOMContentLoaded', function(e) {
bootstrapTemplatesRecursivelyFrom(document);
// FIXME: Is this needed? Seems like it shouldn't be.
Platform.performMicrotaskCheckpoint();
}, false);
function forAllTemplatesFrom(node, fn) {
var subTemplates = node.querySelectorAll(allTemplatesSelectors);
if (isTemplate(node))
fn(node)
forEach(subTemplates, fn);
}
function bootstrapTemplatesRecursivelyFrom(node) {
function bootstrap(template) {
if (!HTMLTemplateElement.decorate(template))
bootstrapTemplatesRecursivelyFrom(template.content);
}
forAllTemplatesFrom(node, bootstrap);
}
if (!hasTemplateElement) {
/**
* This represents a element.
* @constructor
* @extends {HTMLElement}
*/
global.HTMLTemplateElement = function() {
throw TypeError('Illegal constructor');
};
}
var hasProto = '__proto__' in {};
function mixin(to, from) {
Object.getOwnPropertyNames(from).forEach(function(name) {
Object.defineProperty(to, name,
Object.getOwnPropertyDescriptor(from, name));
});
}
// http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#dfn-template-contents-owner
function getOrCreateTemplateContentsOwner(template) {
var doc = template.ownerDocument
if (!doc.defaultView)
return doc;
var d = doc.templateContentsOwner_;
if (!d) {
// TODO(arv): This should either be a Document or HTMLDocument depending
// on doc.
d = doc.implementation.createHTMLDocument('');
while (d.lastChild) {
d.removeChild(d.lastChild);
}
doc.templateContentsOwner_ = d;
}
return d;
}
function getTemplateStagingDocument(template) {
if (!template.stagingDocument_) {
var owner = template.ownerDocument;
if (!owner.stagingDocument_) {
owner.stagingDocument_ = owner.implementation.createHTMLDocument('');
owner.stagingDocument_.isStagingDocument = true;
// TODO(rafaelw): Remove when fix for
// https://codereview.chromium.org/164803002/
// makes it to Chrome release.
var base = owner.stagingDocument_.createElement('base');
base.href = document.baseURI;
owner.stagingDocument_.head.appendChild(base);
owner.stagingDocument_.stagingDocument_ = owner.stagingDocument_;
}
template.stagingDocument_ = owner.stagingDocument_;
}
return template.stagingDocument_;
}
// For non-template browsers, the parser will disallow in certain
// locations, so we allow "attribute templates" which combine the template
// element with the top-level container node of the content, e.g.
//
// Bar
//
// becomes
//
//
// + #document-fragment
// +
// + Bar
//
function extractTemplateFromAttributeTemplate(el) {
var template = el.ownerDocument.createElement('template');
el.parentNode.insertBefore(template, el);
var attribs = el.attributes;
var count = attribs.length;
while (count-- > 0) {
var attrib = attribs[count];
if (templateAttributeDirectives[attrib.name]) {
if (attrib.name !== 'template')
template.setAttribute(attrib.name, attrib.value);
el.removeAttribute(attrib.name);
}
}
return template;
}
function extractTemplateFromSVGTemplate(el) {
var template = el.ownerDocument.createElement('template');
el.parentNode.insertBefore(template, el);
var attribs = el.attributes;
var count = attribs.length;
while (count-- > 0) {
var attrib = attribs[count];
template.setAttribute(attrib.name, attrib.value);
el.removeAttribute(attrib.name);
}
el.parentNode.removeChild(el);
return template;
}
function liftNonNativeTemplateChildrenIntoContent(template, el, useRoot) {
var content = template.content;
if (useRoot) {
content.appendChild(el);
return;
}
var child;
while (child = el.firstChild) {
content.appendChild(child);
}
}
var templateObserver;
if (typeof MutationObserver == 'function') {
templateObserver = new MutationObserver(function(records) {
for (var i = 0; i < records.length; i++) {
records[i].target.refChanged_();
}
});
}
/**
* Ensures proper API and content model for template elements.
* @param {HTMLTemplateElement} opt_instanceRef The template element which
* |el| template element will return as the value of its ref(), and whose
* content will be used as source when createInstance() is invoked.
*/
HTMLTemplateElement.decorate = function(el, opt_instanceRef) {
if (el.templateIsDecorated_)
return false;
var templateElement = el;
templateElement.templateIsDecorated_ = true;
var isNativeHTMLTemplate = isHTMLTemplate(templateElement) &&
hasTemplateElement;
var bootstrapContents = isNativeHTMLTemplate;
var liftContents = !isNativeHTMLTemplate;
var liftRoot = false;
if (!isNativeHTMLTemplate) {
if (isAttributeTemplate(templateElement)) {
assert(!opt_instanceRef);
templateElement = extractTemplateFromAttributeTemplate(el);
templateElement.templateIsDecorated_ = true;
isNativeHTMLTemplate = hasTemplateElement;
liftRoot = true;
} else if (isSVGTemplate(templateElement)) {
templateElement = extractTemplateFromSVGTemplate(el);
templateElement.templateIsDecorated_ = true;
isNativeHTMLTemplate = hasTemplateElement;
}
}
if (!isNativeHTMLTemplate) {
fixTemplateElementPrototype(templateElement);
var doc = getOrCreateTemplateContentsOwner(templateElement);
templateElement.content_ = doc.createDocumentFragment();
}
if (opt_instanceRef) {
// template is contained within an instance, its direct content must be
// empty
templateElement.instanceRef_ = opt_instanceRef;
} else if (liftContents) {
liftNonNativeTemplateChildrenIntoContent(templateElement,
el,
liftRoot);
} else if (bootstrapContents) {
bootstrapTemplatesRecursivelyFrom(templateElement.content);
}
return true;
};
// TODO(rafaelw): This used to decorate recursively all templates from a given
// node. This happens by default on 'DOMContentLoaded', but may be needed
// in subtrees not descendent from document (e.g. ShadowRoot).
// Review whether this is the right public API.
HTMLTemplateElement.bootstrap = bootstrapTemplatesRecursivelyFrom;
var htmlElement = global.HTMLUnknownElement || HTMLElement;
var contentDescriptor = {
get: function() {
return this.content_;
},
enumerable: true,
configurable: true
};
if (!hasTemplateElement) {
// Gecko is more picky with the prototype than WebKit. Make sure to use the
// same prototype as created in the constructor.
HTMLTemplateElement.prototype = Object.create(htmlElement.prototype);
Object.defineProperty(HTMLTemplateElement.prototype, 'content',
contentDescriptor);
}
function fixTemplateElementPrototype(el) {
if (hasProto)
el.__proto__ = HTMLTemplateElement.prototype;
else
mixin(el, HTMLTemplateElement.prototype);
}
function ensureSetModelScheduled(template) {
if (!template.setModelFn_) {
template.setModelFn_ = function() {
template.setModelFnScheduled_ = false;
var map = getBindings(template,
template.delegate_ && template.delegate_.prepareBinding);
processBindings(template, map, template.model_);
};
}
if (!template.setModelFnScheduled_) {
template.setModelFnScheduled_ = true;
Observer.runEOM_(template.setModelFn_);
}
}
mixin(HTMLTemplateElement.prototype, {
bind: function(name, value, oneTime) {
if (name != 'ref')
return Element.prototype.bind.call(this, name, value, oneTime);
var self = this;
var ref = oneTime ? value : value.open(function(ref) {
self.setAttribute('ref', ref);
self.refChanged_();
});
this.setAttribute('ref', ref);
this.refChanged_();
if (oneTime)
return;
if (!this.bindings_) {
this.bindings_ = { ref: value };
} else {
this.bindings_.ref = value;
}
return value;
},
processBindingDirectives_: function(directives) {
if (this.iterator_)
this.iterator_.closeDeps();
if (!directives.if && !directives.bind && !directives.repeat) {
if (this.iterator_) {
this.iterator_.close();
this.iterator_ = undefined;
}
return;
}
if (!this.iterator_) {
this.iterator_ = new TemplateIterator(this);
}
this.iterator_.updateDependencies(directives, this.model_);
if (templateObserver) {
templateObserver.observe(this, { attributes: true,
attributeFilter: ['ref'] });
}
return this.iterator_;
},
createInstance: function(model, bindingDelegate, delegate_) {
if (bindingDelegate)
delegate_ = this.newDelegate_(bindingDelegate);
else if (!delegate_)
delegate_ = this.delegate_;
if (!this.refContent_)
this.refContent_ = this.ref_.content;
var content = this.refContent_;
if (content.firstChild === null)
return emptyInstance;
var map = getInstanceBindingMap(content, delegate_);
var stagingDocument = getTemplateStagingDocument(this);
var instance = stagingDocument.createDocumentFragment();
instance.templateCreator_ = this;
instance.protoContent_ = content;
instance.bindings_ = [];
instance.terminator_ = null;
var instanceRecord = instance.templateInstance_ = {
firstNode: null,
lastNode: null,
model: model
};
var i = 0;
var collectTerminator = false;
for (var child = content.firstChild; child; child = child.nextSibling) {
// The terminator of the instance is the clone of the last child of the
// content. If the last child is an active template, it may produce
// instances as a result of production, so simply collecting the last
// child of the instance after it has finished producing may be wrong.
if (child.nextSibling === null)
collectTerminator = true;
var clone = cloneAndBindInstance(child, instance, stagingDocument,
map.children[i++],
model,
delegate_,
instance.bindings_);
clone.templateInstance_ = instanceRecord;
if (collectTerminator)
instance.terminator_ = clone;
}
instanceRecord.firstNode = instance.firstChild;
instanceRecord.lastNode = instance.lastChild;
instance.templateCreator_ = undefined;
instance.protoContent_ = undefined;
return instance;
},
get model() {
return this.model_;
},
set model(model) {
this.model_ = model;
ensureSetModelScheduled(this);
},
get bindingDelegate() {
return this.delegate_ && this.delegate_.raw;
},
refChanged_: function() {
if (!this.iterator_ || this.refContent_ === this.ref_.content)
return;
this.refContent_ = undefined;
this.iterator_.valueChanged();
this.iterator_.updateIteratedValue(this.iterator_.getUpdatedValue());
},
clear: function() {
this.model_ = undefined;
this.delegate_ = undefined;
if (this.bindings_ && this.bindings_.ref)
this.bindings_.ref.close()
this.refContent_ = undefined;
if (!this.iterator_)
return;
this.iterator_.valueChanged();
this.iterator_.close()
this.iterator_ = undefined;
},
setDelegate_: function(delegate) {
this.delegate_ = delegate;
this.bindingMap_ = undefined;
if (this.iterator_) {
this.iterator_.instancePositionChangedFn_ = undefined;
this.iterator_.instanceModelFn_ = undefined;
}
},
newDelegate_: function(bindingDelegate) {
if (!bindingDelegate)
return;
function delegateFn(name) {
var fn = bindingDelegate && bindingDelegate[name];
if (typeof fn != 'function')
return;
return function() {
return fn.apply(bindingDelegate, arguments);
};
}
return {
bindingMaps: {},
raw: bindingDelegate,
prepareBinding: delegateFn('prepareBinding'),
prepareInstanceModel: delegateFn('prepareInstanceModel'),
prepareInstancePositionChanged:
delegateFn('prepareInstancePositionChanged')
};
},
set bindingDelegate(bindingDelegate) {
if (this.delegate_) {
throw Error('Template must be cleared before a new bindingDelegate ' +
'can be assigned');
}
this.setDelegate_(this.newDelegate_(bindingDelegate));
},
get ref_() {
var ref = searchRefId(this, this.getAttribute('ref'));
if (!ref)
ref = this.instanceRef_;
if (!ref)
return this;
var nextRef = ref.ref_;
return nextRef ? nextRef : ref;
}
});
// Returns
// a) undefined if there are no mustaches.
// b) [TEXT, (ONE_TIME?, PATH, DELEGATE_FN, TEXT)+] if there is at least one mustache.
function parseMustaches(s, name, node, prepareBindingFn) {
if (!s || !s.length)
return;
var tokens;
var length = s.length;
var startIndex = 0, lastIndex = 0, endIndex = 0;
var onlyOneTime = true;
while (lastIndex < length) {
var startIndex = s.indexOf('{{', lastIndex);
var oneTimeStart = s.indexOf('[[', lastIndex);
var oneTime = false;
var terminator = '}}';
if (oneTimeStart >= 0 &&
(startIndex < 0 || oneTimeStart < startIndex)) {
startIndex = oneTimeStart;
oneTime = true;
terminator = ']]';
}
endIndex = startIndex < 0 ? -1 : s.indexOf(terminator, startIndex + 2);
if (endIndex < 0) {
if (!tokens)
return;
tokens.push(s.slice(lastIndex)); // TEXT
break;
}
tokens = tokens || [];
tokens.push(s.slice(lastIndex, startIndex)); // TEXT
var pathString = s.slice(startIndex + 2, endIndex).trim();
tokens.push(oneTime); // ONE_TIME?
onlyOneTime = onlyOneTime && oneTime;
var delegateFn = prepareBindingFn &&
prepareBindingFn(pathString, name, node);
// Don't try to parse the expression if there's a prepareBinding function
if (delegateFn == null) {
tokens.push(Path.get(pathString)); // PATH
} else {
tokens.push(null);
}
tokens.push(delegateFn); // DELEGATE_FN
lastIndex = endIndex + 2;
}
if (lastIndex === length)
tokens.push(''); // TEXT
tokens.hasOnePath = tokens.length === 5;
tokens.isSimplePath = tokens.hasOnePath &&
tokens[0] == '' &&
tokens[4] == '';
tokens.onlyOneTime = onlyOneTime;
tokens.combinator = function(values) {
var newValue = tokens[0];
for (var i = 1; i < tokens.length; i += 4) {
var value = tokens.hasOnePath ? values : values[(i - 1) / 4];
if (value !== undefined)
newValue += value;
newValue += tokens[i + 3];
}
return newValue;
}
return tokens;
};
function processOneTimeBinding(name, tokens, node, model) {
if (tokens.hasOnePath) {
var delegateFn = tokens[3];
var value = delegateFn ? delegateFn(model, node, true) :
tokens[2].getValueFrom(model);
return tokens.isSimplePath ? value : tokens.combinator(value);
}
var values = [];
for (var i = 1; i < tokens.length; i += 4) {
var delegateFn = tokens[i + 2];
values[(i - 1) / 4] = delegateFn ? delegateFn(model, node) :
tokens[i + 1].getValueFrom(model);
}
return tokens.combinator(values);
}
function processSinglePathBinding(name, tokens, node, model) {
var delegateFn = tokens[3];
var observer = delegateFn ? delegateFn(model, node, false) :
new PathObserver(model, tokens[2]);
return tokens.isSimplePath ? observer :
new ObserverTransform(observer, tokens.combinator);
}
function processBinding(name, tokens, node, model) {
if (tokens.onlyOneTime)
return processOneTimeBinding(name, tokens, node, model);
if (tokens.hasOnePath)
return processSinglePathBinding(name, tokens, node, model);
var observer = new CompoundObserver();
for (var i = 1; i < tokens.length; i += 4) {
var oneTime = tokens[i];
var delegateFn = tokens[i + 2];
if (delegateFn) {
var value = delegateFn(model, node, oneTime);
if (oneTime)
observer.addPath(value)
else
observer.addObserver(value);
continue;
}
var path = tokens[i + 1];
if (oneTime)
observer.addPath(path.getValueFrom(model))
else
observer.addPath(model, path);
}
return new ObserverTransform(observer, tokens.combinator);
}
function processBindings(node, bindings, model, instanceBindings) {
for (var i = 0; i < bindings.length; i += 2) {
var name = bindings[i]
var tokens = bindings[i + 1];
var value = processBinding(name, tokens, node, model);
var binding = node.bind(name, value, tokens.onlyOneTime);
if (binding && instanceBindings)
instanceBindings.push(binding);
}
node.bindFinished();
if (!bindings.isTemplate)
return;
node.model_ = model;
var iter = node.processBindingDirectives_(bindings);
if (instanceBindings && iter)
instanceBindings.push(iter);
}
function parseWithDefault(el, name, prepareBindingFn) {
var v = el.getAttribute(name);
return parseMustaches(v == '' ? '{{}}' : v, name, el, prepareBindingFn);
}
function parseAttributeBindings(element, prepareBindingFn) {
assert(element);
var bindings = [];
var ifFound = false;
var bindFound = false;
for (var i = 0; i < element.attributes.length; i++) {
var attr = element.attributes[i];
var name = attr.name;
var value = attr.value;
// Allow bindings expressed in attributes to be prefixed with underbars.
// We do this to allow correct semantics for browsers that don't implement
// where certain attributes might trigger side-effects -- and
// for IE which sanitizes certain attributes, disallowing mustache
// replacements in their text.
while (name[0] === '_') {
name = name.substring(1);
}
if (isTemplate(element) &&
(name === IF || name === BIND || name === REPEAT)) {
continue;
}
var tokens = parseMustaches(value, name, element,
prepareBindingFn);
if (!tokens)
continue;
bindings.push(name, tokens);
}
if (isTemplate(element)) {
bindings.isTemplate = true;
bindings.if = parseWithDefault(element, IF, prepareBindingFn);
bindings.bind = parseWithDefault(element, BIND, prepareBindingFn);
bindings.repeat = parseWithDefault(element, REPEAT, prepareBindingFn);
if (bindings.if && !bindings.bind && !bindings.repeat)
bindings.bind = parseMustaches('{{}}', BIND, element, prepareBindingFn);
}
return bindings;
}
function getBindings(node, prepareBindingFn) {
if (node.nodeType === Node.ELEMENT_NODE)
return parseAttributeBindings(node, prepareBindingFn);
if (node.nodeType === Node.TEXT_NODE) {
var tokens = parseMustaches(node.data, 'textContent', node,
prepareBindingFn);
if (tokens)
return ['textContent', tokens];
}
return [];
}
function cloneAndBindInstance(node, parent, stagingDocument, bindings, model,
delegate,
instanceBindings,
instanceRecord) {
var clone = parent.appendChild(stagingDocument.importNode(node, false));
var i = 0;
for (var child = node.firstChild; child; child = child.nextSibling) {
cloneAndBindInstance(child, clone, stagingDocument,
bindings.children[i++],
model,
delegate,
instanceBindings);
}
if (bindings.isTemplate) {
HTMLTemplateElement.decorate(clone, node);
if (delegate)
clone.setDelegate_(delegate);
}
processBindings(clone, bindings, model, instanceBindings);
return clone;
}
function createInstanceBindingMap(node, prepareBindingFn) {
var map = getBindings(node, prepareBindingFn);
map.children = {};
var index = 0;
for (var child = node.firstChild; child; child = child.nextSibling) {
map.children[index++] = createInstanceBindingMap(child, prepareBindingFn);
}
return map;
}
var contentUidCounter = 1;
// TODO(rafaelw): Setup a MutationObserver on content which clears the id
// so that bindingMaps regenerate when the template.content changes.
function getContentUid(content) {
var id = content.id_;
if (!id)
id = content.id_ = contentUidCounter++;
return id;
}
// Each delegate is associated with a set of bindingMaps, one for each
// content which may be used by a template. The intent is that each binding
// delegate gets the opportunity to prepare the instance (via the prepare*
// delegate calls) once across all uses.
// TODO(rafaelw): Separate out the parse map from the binding map. In the
// current implementation, if two delegates need a binding map for the same
// content, the second will have to reparse.
function getInstanceBindingMap(content, delegate_) {
var contentId = getContentUid(content);
if (delegate_) {
var map = delegate_.bindingMaps[contentId];
if (!map) {
map = delegate_.bindingMaps[contentId] =
createInstanceBindingMap(content, delegate_.prepareBinding) || [];
}
return map;
}
var map = content.bindingMap_;
if (!map) {
map = content.bindingMap_ =
createInstanceBindingMap(content, undefined) || [];
}
return map;
}
Object.defineProperty(Node.prototype, 'templateInstance', {
get: function() {
var instance = this.templateInstance_;
return instance ? instance :
(this.parentNode ? this.parentNode.templateInstance : undefined);
}
});
var emptyInstance = document.createDocumentFragment();
emptyInstance.bindings_ = [];
emptyInstance.terminator_ = null;
function TemplateIterator(templateElement) {
this.closed = false;
this.templateElement_ = templateElement;
this.instances = [];
this.deps = undefined;
this.iteratedValue = [];
this.presentValue = undefined;
this.arrayObserver = undefined;
}
TemplateIterator.prototype = {
closeDeps: function() {
var deps = this.deps;
if (deps) {
if (deps.ifOneTime === false)
deps.ifValue.close();
if (deps.oneTime === false)
deps.value.close();
}
},
updateDependencies: function(directives, model) {
this.closeDeps();
var deps = this.deps = {};
var template = this.templateElement_;
var ifValue = true;
if (directives.if) {
deps.hasIf = true;
deps.ifOneTime = directives.if.onlyOneTime;
deps.ifValue = processBinding(IF, directives.if, template, model);
ifValue = deps.ifValue;
// oneTime if & predicate is false. nothing else to do.
if (deps.ifOneTime && !ifValue) {
this.valueChanged();
return;
}
if (!deps.ifOneTime)
ifValue = ifValue.open(this.updateIfValue, this);
}
if (directives.repeat) {
deps.repeat = true;
deps.oneTime = directives.repeat.onlyOneTime;
deps.value = processBinding(REPEAT, directives.repeat, template, model);
} else {
deps.repeat = false;
deps.oneTime = directives.bind.onlyOneTime;
deps.value = processBinding(BIND, directives.bind, template, model);
}
var value = deps.value;
if (!deps.oneTime)
value = value.open(this.updateIteratedValue, this);
if (!ifValue) {
this.valueChanged();
return;
}
this.updateValue(value);
},
/**
* Gets the updated value of the bind/repeat. This can potentially call
* user code (if a bindingDelegate is set up) so we try to avoid it if we
* already have the value in hand (from Observer.open).
*/
getUpdatedValue: function() {
var value = this.deps.value;
if (!this.deps.oneTime)
value = value.discardChanges();
return value;
},
updateIfValue: function(ifValue) {
if (!ifValue) {
this.valueChanged();
return;
}
this.updateValue(this.getUpdatedValue());
},
updateIteratedValue: function(value) {
if (this.deps.hasIf) {
var ifValue = this.deps.ifValue;
if (!this.deps.ifOneTime)
ifValue = ifValue.discardChanges();
if (!ifValue) {
this.valueChanged();
return;
}
}
this.updateValue(value);
},
updateValue: function(value) {
if (!this.deps.repeat)
value = [value];
var observe = this.deps.repeat &&
!this.deps.oneTime &&
Array.isArray(value);
this.valueChanged(value, observe);
},
valueChanged: function(value, observeValue) {
if (!Array.isArray(value))
value = [];
if (value === this.iteratedValue)
return;
this.unobserve();
this.presentValue = value;
if (observeValue) {
this.arrayObserver = new ArrayObserver(this.presentValue);
this.arrayObserver.open(this.handleSplices, this);
}
this.handleSplices(ArrayObserver.calculateSplices(this.presentValue,
this.iteratedValue));
},
getLastInstanceNode: function(index) {
if (index == -1)
return this.templateElement_;
var instance = this.instances[index];
var terminator = instance.terminator_;
if (!terminator)
return this.getLastInstanceNode(index - 1);
if (terminator.nodeType !== Node.ELEMENT_NODE ||
this.templateElement_ === terminator) {
return terminator;
}
var subtemplateIterator = terminator.iterator_;
if (!subtemplateIterator)
return terminator;
return subtemplateIterator.getLastTemplateNode();
},
getLastTemplateNode: function() {
return this.getLastInstanceNode(this.instances.length - 1);
},
insertInstanceAt: function(index, fragment) {
var previousInstanceLast = this.getLastInstanceNode(index - 1);
var parent = this.templateElement_.parentNode;
this.instances.splice(index, 0, fragment);
parent.insertBefore(fragment, previousInstanceLast.nextSibling);
},
extractInstanceAt: function(index) {
var previousInstanceLast = this.getLastInstanceNode(index - 1);
var lastNode = this.getLastInstanceNode(index);
var parent = this.templateElement_.parentNode;
var instance = this.instances.splice(index, 1)[0];
while (lastNode !== previousInstanceLast) {
var node = previousInstanceLast.nextSibling;
if (node == lastNode)
lastNode = previousInstanceLast;
instance.appendChild(parent.removeChild(node));
}
return instance;
},
getDelegateFn: function(fn) {
fn = fn && fn(this.templateElement_);
return typeof fn === 'function' ? fn : null;
},
handleSplices: function(splices) {
if (this.closed || !splices.length)
return;
var template = this.templateElement_;
if (!template.parentNode) {
this.close();
return;
}
ArrayObserver.applySplices(this.iteratedValue, this.presentValue,
splices);
var delegate = template.delegate_;
if (this.instanceModelFn_ === undefined) {
this.instanceModelFn_ =
this.getDelegateFn(delegate && delegate.prepareInstanceModel);
}
if (this.instancePositionChangedFn_ === undefined) {
this.instancePositionChangedFn_ =
this.getDelegateFn(delegate &&
delegate.prepareInstancePositionChanged);
}
// Instance Removals
var instanceCache = new Map;
var removeDelta = 0;
for (var i = 0; i < splices.length; i++) {
var splice = splices[i];
var removed = splice.removed;
for (var j = 0; j < removed.length; j++) {
var model = removed[j];
var instance = this.extractInstanceAt(splice.index + removeDelta);
if (instance !== emptyInstance) {
instanceCache.set(model, instance);
}
}
removeDelta -= splice.addedCount;
}
// Instance Insertions
for (var i = 0; i < splices.length; i++) {
var splice = splices[i];
var addIndex = splice.index;
for (; addIndex < splice.index + splice.addedCount; addIndex++) {
var model = this.iteratedValue[addIndex];
var instance = instanceCache.get(model);
if (instance) {
instanceCache.delete(model);
} else {
if (this.instanceModelFn_) {
model = this.instanceModelFn_(model);
}
if (model === undefined) {
instance = emptyInstance;
} else {
instance = template.createInstance(model, undefined, delegate);
}
}
this.insertInstanceAt(addIndex, instance);
}
}
instanceCache.forEach(function(instance) {
this.closeInstanceBindings(instance);
}, this);
if (this.instancePositionChangedFn_)
this.reportInstancesMoved(splices);
},
reportInstanceMoved: function(index) {
var instance = this.instances[index];
if (instance === emptyInstance)
return;
this.instancePositionChangedFn_(instance.templateInstance_, index);
},
reportInstancesMoved: function(splices) {
var index = 0;
var offset = 0;
for (var i = 0; i < splices.length; i++) {
var splice = splices[i];
if (offset != 0) {
while (index < splice.index) {
this.reportInstanceMoved(index);
index++;
}
} else {
index = splice.index;
}
while (index < splice.index + splice.addedCount) {
this.reportInstanceMoved(index);
index++;
}
offset += splice.addedCount - splice.removed.length;
}
if (offset == 0)
return;
var length = this.instances.length;
while (index < length) {
this.reportInstanceMoved(index);
index++;
}
},
closeInstanceBindings: function(instance) {
var bindings = instance.bindings_;
for (var i = 0; i < bindings.length; i++) {
bindings[i].close();
}
},
unobserve: function() {
if (!this.arrayObserver)
return;
this.arrayObserver.close();
this.arrayObserver = undefined;
},
close: function() {
if (this.closed)
return;
this.unobserve();
for (var i = 0; i < this.instances.length; i++) {
this.closeInstanceBindings(this.instances[i]);
}
this.instances.length = 0;
this.closeDeps();
this.templateElement_.iterator_ = undefined;
this.closed = true;
}
};
// Polyfill-specific API.
HTMLTemplateElement.forAllTemplatesFrom_ = forAllTemplatesFrom;
})(this);
(function(scope) {
'use strict';
// feature detect for URL constructor
var hasWorkingUrl = false;
if (!scope.forceJURL) {
try {
var u = new URL('b', 'http://a');
hasWorkingUrl = u.href === 'http://a/b';
} catch(e) {}
}
if (hasWorkingUrl)
return;
var relative = Object.create(null);
relative['ftp'] = 21;
relative['file'] = 0;
relative['gopher'] = 70;
relative['http'] = 80;
relative['https'] = 443;
relative['ws'] = 80;
relative['wss'] = 443;
var relativePathDotMapping = Object.create(null);
relativePathDotMapping['%2e'] = '.';
relativePathDotMapping['.%2e'] = '..';
relativePathDotMapping['%2e.'] = '..';
relativePathDotMapping['%2e%2e'] = '..';
function isRelativeScheme(scheme) {
return relative[scheme] !== undefined;
}
function invalid() {
clear.call(this);
this._isInvalid = true;
}
function IDNAToASCII(h) {
if ('' == h) {
invalid.call(this)
}
// XXX
return h.toLowerCase()
}
function percentEscape(c) {
var unicode = c.charCodeAt(0);
if (unicode > 0x20 &&
unicode < 0x7F &&
// " # < > ? `
[0x22, 0x23, 0x3C, 0x3E, 0x3F, 0x60].indexOf(unicode) == -1
) {
return c;
}
return encodeURIComponent(c);
}
function percentEscapeQuery(c) {
// XXX This actually needs to encode c using encoding and then
// convert the bytes one-by-one.
var unicode = c.charCodeAt(0);
if (unicode > 0x20 &&
unicode < 0x7F &&
// " # < > ` (do not escape '?')
[0x22, 0x23, 0x3C, 0x3E, 0x60].indexOf(unicode) == -1
) {
return c;
}
return encodeURIComponent(c);
}
var EOF = undefined,
ALPHA = /[a-zA-Z]/,
ALPHANUMERIC = /[a-zA-Z0-9\+\-\.]/;
function parse(input, stateOverride, base) {
function err(message) {
errors.push(message)
}
var state = stateOverride || 'scheme start',
cursor = 0,
buffer = '',
seenAt = false,
seenBracket = false,
errors = [];
loop: while ((input[cursor - 1] != EOF || cursor == 0) && !this._isInvalid) {
var c = input[cursor];
switch (state) {
case 'scheme start':
if (c && ALPHA.test(c)) {
buffer += c.toLowerCase(); // ASCII-safe
state = 'scheme';
} else if (!stateOverride) {
buffer = '';
state = 'no scheme';
continue;
} else {
err('Invalid scheme.');
break loop;
}
break;
case 'scheme':
if (c && ALPHANUMERIC.test(c)) {
buffer += c.toLowerCase(); // ASCII-safe
} else if (':' == c) {
this._scheme = buffer;
buffer = '';
if (stateOverride) {
break loop;
}
if (isRelativeScheme(this._scheme)) {
this._isRelative = true;
}
if ('file' == this._scheme) {
state = 'relative';
} else if (this._isRelative && base && base._scheme == this._scheme) {
state = 'relative or authority';
} else if (this._isRelative) {
state = 'authority first slash';
} else {
state = 'scheme data';
}
} else if (!stateOverride) {
buffer = '';
cursor = 0;
state = 'no scheme';
continue;
} else if (EOF == c) {
break loop;
} else {
err('Code point not allowed in scheme: ' + c)
break loop;
}
break;
case 'scheme data':
if ('?' == c) {
query = '?';
state = 'query';
} else if ('#' == c) {
this._fragment = '#';
state = 'fragment';
} else {
// XXX error handling
if (EOF != c && '\t' != c && '\n' != c && '\r' != c) {
this._schemeData += percentEscape(c);
}
}
break;
case 'no scheme':
if (!base || !(isRelativeScheme(base._scheme))) {
err('Missing scheme.');
invalid.call(this);
} else {
state = 'relative';
continue;
}
break;
case 'relative or authority':
if ('/' == c && '/' == input[cursor+1]) {
state = 'authority ignore slashes';
} else {
err('Expected /, got: ' + c);
state = 'relative';
continue
}
break;
case 'relative':
this._isRelative = true;
if ('file' != this._scheme)
this._scheme = base._scheme;
if (EOF == c) {
this._host = base._host;
this._port = base._port;
this._path = base._path.slice();
this._query = base._query;
break loop;
} else if ('/' == c || '\\' == c) {
if ('\\' == c)
err('\\ is an invalid code point.');
state = 'relative slash';
} else if ('?' == c) {
this._host = base._host;
this._port = base._port;
this._path = base._path.slice();
this._query = '?';
state = 'query';
} else if ('#' == c) {
this._host = base._host;
this._port = base._port;
this._path = base._path.slice();
this._query = base._query;
this._fragment = '#';
state = 'fragment';
} else {
var nextC = input[cursor+1]
var nextNextC = input[cursor+2]
if (
'file' != this._scheme || !ALPHA.test(c) ||
(nextC != ':' && nextC != '|') ||
(EOF != nextNextC && '/' != nextNextC && '\\' != nextNextC && '?' != nextNextC && '#' != nextNextC)) {
this._host = base._host;
this._port = base._port;
this._path = base._path.slice();
this._path.pop();
}
state = 'relative path';
continue;
}
break;
case 'relative slash':
if ('/' == c || '\\' == c) {
if ('\\' == c) {
err('\\ is an invalid code point.');
}
if ('file' == this._scheme) {
state = 'file host';
} else {
state = 'authority ignore slashes';
}
} else {
if ('file' != this._scheme) {
this._host = base._host;
this._port = base._port;
}
state = 'relative path';
continue;
}
break;
case 'authority first slash':
if ('/' == c) {
state = 'authority second slash';
} else {
err("Expected '/', got: " + c);
state = 'authority ignore slashes';
continue;
}
break;
case 'authority second slash':
state = 'authority ignore slashes';
if ('/' != c) {
err("Expected '/', got: " + c);
continue;
}
break;
case 'authority ignore slashes':
if ('/' != c && '\\' != c) {
state = 'authority';
continue;
} else {
err('Expected authority, got: ' + c);
}
break;
case 'authority':
if ('@' == c) {
if (seenAt) {
err('@ already seen.');
buffer += '%40';
}
seenAt = true;
for (var i = 0; i < buffer.length; i++) {
var cp = buffer[i];
if ('\t' == cp || '\n' == cp || '\r' == cp) {
err('Invalid whitespace in authority.');
continue;
}
// XXX check URL code points
if (':' == cp && null === this._password) {
this._password = '';
continue;
}
var tempC = percentEscape(cp);
(null !== this._password) ? this._password += tempC : this._username += tempC;
}
buffer = '';
} else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) {
cursor -= buffer.length;
buffer = '';
state = 'host';
continue;
} else {
buffer += c;
}
break;
case 'file host':
if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) {
if (buffer.length == 2 && ALPHA.test(buffer[0]) && (buffer[1] == ':' || buffer[1] == '|')) {
state = 'relative path';
} else if (buffer.length == 0) {
state = 'relative path start';
} else {
this._host = IDNAToASCII.call(this, buffer);
buffer = '';
state = 'relative path start';
}
continue;
} else if ('\t' == c || '\n' == c || '\r' == c) {
err('Invalid whitespace in file host.');
} else {
buffer += c;
}
break;
case 'host':
case 'hostname':
if (':' == c && !seenBracket) {
// XXX host parsing
this._host = IDNAToASCII.call(this, buffer);
buffer = '';
state = 'port';
if ('hostname' == stateOverride) {
break loop;
}
} else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c) {
this._host = IDNAToASCII.call(this, buffer);
buffer = '';
state = 'relative path start';
if (stateOverride) {
break loop;
}
continue;
} else if ('\t' != c && '\n' != c && '\r' != c) {
if ('[' == c) {
seenBracket = true;
} else if (']' == c) {
seenBracket = false;
}
buffer += c;
} else {
err('Invalid code point in host/hostname: ' + c);
}
break;
case 'port':
if (/[0-9]/.test(c)) {
buffer += c;
} else if (EOF == c || '/' == c || '\\' == c || '?' == c || '#' == c || stateOverride) {
if ('' != buffer) {
var temp = parseInt(buffer, 10);
if (temp != relative[this._scheme]) {
this._port = temp + '';
}
buffer = '';
}
if (stateOverride) {
break loop;
}
state = 'relative path start';
continue;
} else if ('\t' == c || '\n' == c || '\r' == c) {
err('Invalid code point in port: ' + c);
} else {
invalid.call(this);
}
break;
case 'relative path start':
if ('\\' == c)
err("'\\' not allowed in path.");
state = 'relative path';
if ('/' != c && '\\' != c) {
continue;
}
break;
case 'relative path':
if (EOF == c || '/' == c || '\\' == c || (!stateOverride && ('?' == c || '#' == c))) {
if ('\\' == c) {
err('\\ not allowed in relative path.');
}
var tmp;
if (tmp = relativePathDotMapping[buffer.toLowerCase()]) {
buffer = tmp;
}
if ('..' == buffer) {
this._path.pop();
if ('/' != c && '\\' != c) {
this._path.push('');
}
} else if ('.' == buffer && '/' != c && '\\' != c) {
this._path.push('');
} else if ('.' != buffer) {
if ('file' == this._scheme && this._path.length == 0 && buffer.length == 2 && ALPHA.test(buffer[0]) && buffer[1] == '|') {
buffer = buffer[0] + ':';
}
this._path.push(buffer);
}
buffer = '';
if ('?' == c) {
this._query = '?';
state = 'query';
} else if ('#' == c) {
this._fragment = '#';
state = 'fragment';
}
} else if ('\t' != c && '\n' != c && '\r' != c) {
buffer += percentEscape(c);
}
break;
case 'query':
if (!stateOverride && '#' == c) {
this._fragment = '#';
state = 'fragment';
} else if (EOF != c && '\t' != c && '\n' != c && '\r' != c) {
this._query += percentEscapeQuery(c);
}
break;
case 'fragment':
if (EOF != c && '\t' != c && '\n' != c && '\r' != c) {
this._fragment += c;
}
break;
}
cursor++;
}
}
function clear() {
this._scheme = '';
this._schemeData = '';
this._username = '';
this._password = null;
this._host = '';
this._port = '';
this._path = [];
this._query = '';
this._fragment = '';
this._isInvalid = false;
this._isRelative = false;
}
// Does not process domain names or IP addresses.
// Does not handle encoding for the query parameter.
function jURL(url, base /* , encoding */) {
if (base !== undefined && !(base instanceof jURL))
base = new jURL(String(base));
this._url = url;
clear.call(this);
var input = url.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g, '');
// encoding = encoding || 'utf-8'
parse.call(this, input, null, base);
}
jURL.prototype = {
get href() {
if (this._isInvalid)
return this._url;
var authority = '';
if ('' != this._username || null != this._password) {
authority = this._username +
(null != this._password ? ':' + this._password : '') + '@';
}
return this.protocol +
(this._isRelative ? '//' + authority + this.host : '') +
this.pathname + this._query + this._fragment;
},
set href(href) {
clear.call(this);
parse.call(this, href);
},
get protocol() {
return this._scheme + ':';
},
set protocol(protocol) {
if (this._isInvalid)
return;
parse.call(this, protocol + ':', 'scheme start');
},
get host() {
return this._isInvalid ? '' : this._port ?
this._host + ':' + this._port : this._host;
},
set host(host) {
if (this._isInvalid || !this._isRelative)
return;
parse.call(this, host, 'host');
},
get hostname() {
return this._host;
},
set hostname(hostname) {
if (this._isInvalid || !this._isRelative)
return;
parse.call(this, hostname, 'hostname');
},
get port() {
return this._port;
},
set port(port) {
if (this._isInvalid || !this._isRelative)
return;
parse.call(this, port, 'port');
},
get pathname() {
return this._isInvalid ? '' : this._isRelative ?
'/' + this._path.join('/') : this._schemeData;
},
set pathname(pathname) {
if (this._isInvalid || !this._isRelative)
return;
this._path = [];
parse.call(this, pathname, 'relative path start');
},
get search() {
return this._isInvalid || !this._query || '?' == this._query ?
'' : this._query;
},
set search(search) {
if (this._isInvalid || !this._isRelative)
return;
this._query = '?';
if ('?' == search[0])
search = search.slice(1);
parse.call(this, search, 'query');
},
get hash() {
return this._isInvalid || !this._fragment || '#' == this._fragment ?
'' : this._fragment;
},
set hash(hash) {
if (this._isInvalid)
return;
this._fragment = '#';
if ('#' == hash[0])
hash = hash.slice(1);
parse.call(this, hash, 'fragment');
},
get origin() {
var host;
if (this._isInvalid || !this._scheme) {
return '';
}
// javascript: Gecko returns String(""), WebKit/Blink String("null")
// Gecko throws error for "data://"
// data: Gecko returns "", Blink returns "data://", WebKit returns "null"
// Gecko returns String("") for file: mailto:
// WebKit/Blink returns String("SCHEME://") for file: mailto:
switch (this._scheme) {
case 'data':
case 'file':
case 'javascript':
case 'mailto':
return 'null';
}
host = this.host;
if (!host) {
return '';
}
return this._scheme + '://' + host;
}
};
// Copy over the static methods
var OriginalURL = scope.URL;
if (OriginalURL) {
jURL.createObjectURL = function(blob) {
// IE extension allows a second optional options argument.
// http://msdn.microsoft.com/en-us/library/ie/hh772302(v=vs.85).aspx
return OriginalURL.createObjectURL.apply(OriginalURL, arguments);
};
jURL.revokeObjectURL = function(url) {
OriginalURL.revokeObjectURL(url);
};
}
scope.URL = jURL;
})(this);
(function(scope) {
var iterations = 0;
var callbacks = [];
var twiddle = document.createTextNode('');
function endOfMicrotask(callback) {
twiddle.textContent = iterations++;
callbacks.push(callback);
}
function atEndOfMicrotask() {
while (callbacks.length) {
callbacks.shift()();
}
}
new (window.MutationObserver || JsMutationObserver)(atEndOfMicrotask)
.observe(twiddle, {characterData: true})
;
// exports
scope.endOfMicrotask = endOfMicrotask;
// bc
Platform.endOfMicrotask = endOfMicrotask;
})(Polymer);
(function(scope) {
/**
* @class Polymer
*/
// imports
var endOfMicrotask = scope.endOfMicrotask;
// logging
var log = window.WebComponents ? WebComponents.flags.log : {};
// inject style sheet
var style = document.createElement('style');
style.textContent = 'template {display: none !important;} /* injected by platform.js */';
var head = document.querySelector('head');
head.insertBefore(style, head.firstChild);
/**
* Force any pending data changes to be observed before
* the next task. Data changes are processed asynchronously but are guaranteed
* to be processed, for example, before painting. This method should rarely be
* needed. It does nothing when Object.observe is available;
* when Object.observe is not available, Polymer automatically flushes data
* changes approximately every 1/10 second.
* Therefore, `flush` should only be used when a data mutation should be
* observed sooner than this.
*
* @method flush
*/
// flush (with logging)
var flushing;
function flush() {
if (!flushing) {
flushing = true;
endOfMicrotask(function() {
flushing = false;
log.data && console.group('flush');
Platform.performMicrotaskCheckpoint();
log.data && console.groupEnd();
});
}
};
// polling dirty checker
// flush periodically if platform does not have object observe.
if (!Observer.hasObjectObserve) {
var FLUSH_POLL_INTERVAL = 125;
window.addEventListener('WebComponentsReady', function() {
flush();
// watch document visiblity to toggle dirty-checking
var visibilityHandler = function() {
// only flush if the page is visibile
if (document.visibilityState === 'hidden') {
if (scope.flushPoll) {
clearInterval(scope.flushPoll);
}
} else {
scope.flushPoll = setInterval(flush, FLUSH_POLL_INTERVAL);
}
};
if (typeof document.visibilityState === 'string') {
document.addEventListener('visibilitychange', visibilityHandler);
}
visibilityHandler();
});
} else {
// make flush a no-op when we have Object.observe
flush = function() {};
}
if (window.CustomElements && !CustomElements.useNative) {
var originalImportNode = Document.prototype.importNode;
Document.prototype.importNode = function(node, deep) {
var imported = originalImportNode.call(this, node, deep);
CustomElements.upgradeAll(imported);
return imported;
};
}
// exports
scope.flush = flush;
// bc
Platform.flush = flush;
})(window.Polymer);
(function(scope) {
var urlResolver = {
resolveDom: function(root, url) {
url = url || baseUrl(root);
this.resolveAttributes(root, url);
this.resolveStyles(root, url);
// handle template.content
var templates = root.querySelectorAll('template');
if (templates) {
for (var i = 0, l = templates.length, t; (i < l) && (t = templates[i]); i++) {
if (t.content) {
this.resolveDom(t.content, url);
}
}
}
},
resolveTemplate: function(template) {
this.resolveDom(template.content, baseUrl(template));
},
resolveStyles: function(root, url) {
var styles = root.querySelectorAll('style');
if (styles) {
for (var i = 0, l = styles.length, s; (i < l) && (s = styles[i]); i++) {
this.resolveStyle(s, url);
}
}
},
resolveStyle: function(style, url) {
url = url || baseUrl(style);
style.textContent = this.resolveCssText(style.textContent, url);
},
resolveCssText: function(cssText, baseUrl, keepAbsolute) {
cssText = replaceUrlsInCssText(cssText, baseUrl, keepAbsolute, CSS_URL_REGEXP);
return replaceUrlsInCssText(cssText, baseUrl, keepAbsolute, CSS_IMPORT_REGEXP);
},
resolveAttributes: function(root, url) {
if (root.hasAttributes && root.hasAttributes()) {
this.resolveElementAttributes(root, url);
}
// search for attributes that host urls
var nodes = root && root.querySelectorAll(URL_ATTRS_SELECTOR);
if (nodes) {
for (var i = 0, l = nodes.length, n; (i < l) && (n = nodes[i]); i++) {
this.resolveElementAttributes(n, url);
}
}
},
resolveElementAttributes: function(node, url) {
url = url || baseUrl(node);
URL_ATTRS.forEach(function(v) {
var attr = node.attributes[v];
var value = attr && attr.value;
var replacement;
if (value && value.search(URL_TEMPLATE_SEARCH) < 0) {
if (v === 'style') {
replacement = replaceUrlsInCssText(value, url, false, CSS_URL_REGEXP);
} else {
replacement = resolveRelativeUrl(url, value);
}
attr.value = replacement;
}
});
}
};
var CSS_URL_REGEXP = /(url\()([^)]*)(\))/g;
var CSS_IMPORT_REGEXP = /(@import[\s]+(?!url\())([^;]*)(;)/g;
var URL_ATTRS = ['href', 'src', 'action', 'style', 'url'];
var URL_ATTRS_SELECTOR = '[' + URL_ATTRS.join('],[') + ']';
var URL_TEMPLATE_SEARCH = '{{.*}}';
var URL_HASH = '#';
function baseUrl(node) {
var u = new URL(node.ownerDocument.baseURI);
u.search = '';
u.hash = '';
return u;
}
function replaceUrlsInCssText(cssText, baseUrl, keepAbsolute, regexp) {
return cssText.replace(regexp, function(m, pre, url, post) {
var urlPath = url.replace(/["']/g, '');
urlPath = resolveRelativeUrl(baseUrl, urlPath, keepAbsolute);
return pre + '\'' + urlPath + '\'' + post;
});
}
function resolveRelativeUrl(baseUrl, url, keepAbsolute) {
// do not resolve '/' absolute urls
if (url && url[0] === '/') {
return url;
}
var u = new URL(url, baseUrl);
return keepAbsolute ? u.href : makeDocumentRelPath(u.href);
}
function makeDocumentRelPath(url) {
var root = baseUrl(document.documentElement);
var u = new URL(url, root);
if (u.host === root.host && u.port === root.port &&
u.protocol === root.protocol) {
return makeRelPath(root, u);
} else {
return url;
}
}
// make a relative path from source to target
function makeRelPath(sourceUrl, targetUrl) {
var source = sourceUrl.pathname;
var target = targetUrl.pathname;
var s = source.split('/');
var t = target.split('/');
while (s.length && s[0] === t[0]){
s.shift();
t.shift();
}
for (var i = 0, l = s.length - 1; i < l; i++) {
t.unshift('..');
}
// empty '#' is discarded but we need to preserve it.
var hash = (targetUrl.href.slice(-1) === URL_HASH) ? URL_HASH : targetUrl.hash;
return t.join('/') + targetUrl.search + hash;
}
// exports
scope.urlResolver = urlResolver;
})(Polymer);
(function(scope) {
var endOfMicrotask = Polymer.endOfMicrotask;
// Generic url loader
function Loader(regex) {
this.cache = Object.create(null);
this.map = Object.create(null);
this.requests = 0;
this.regex = regex;
}
Loader.prototype = {
// TODO(dfreedm): there may be a better factoring here
// extract absolute urls from the text (full of relative urls)
extractUrls: function(text, base) {
var matches = [];
var matched, u;
while ((matched = this.regex.exec(text))) {
u = new URL(matched[1], base);
matches.push({matched: matched[0], url: u.href});
}
return matches;
},
// take a text blob, a root url, and a callback and load all the urls found within the text
// returns a map of absolute url to text
process: function(text, root, callback) {
var matches = this.extractUrls(text, root);
// every call to process returns all the text this loader has ever received
var done = callback.bind(null, this.map);
this.fetch(matches, done);
},
// build a mapping of url -> text from matches
fetch: function(matches, callback) {
var inflight = matches.length;
// return early if there is no fetching to be done
if (!inflight) {
return callback();
}
// wait for all subrequests to return
var done = function() {
if (--inflight === 0) {
callback();
}
};
// start fetching all subrequests
var m, req, url;
for (var i = 0; i < inflight; i++) {
m = matches[i];
url = m.url;
req = this.cache[url];
// if this url has already been requested, skip requesting it again
if (!req) {
req = this.xhr(url);
req.match = m;
this.cache[url] = req;
}
// wait for the request to process its subrequests
req.wait(done);
}
},
handleXhr: function(request) {
var match = request.match;
var url = match.url;
// handle errors with an empty string
var response = request.response || request.responseText || '';
this.map[url] = response;
this.fetch(this.extractUrls(response, url), request.resolve);
},
xhr: function(url) {
this.requests++;
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.send();
request.onerror = request.onload = this.handleXhr.bind(this, request);
// queue of tasks to run after XHR returns
request.pending = [];
request.resolve = function() {
var pending = request.pending;
for(var i = 0; i < pending.length; i++) {
pending[i]();
}
request.pending = null;
};
// if we have already resolved, pending is null, async call the callback
request.wait = function(fn) {
if (request.pending) {
request.pending.push(fn);
} else {
endOfMicrotask(fn);
}
};
return request;
}
};
scope.Loader = Loader;
})(Polymer);
(function(scope) {
var urlResolver = scope.urlResolver;
var Loader = scope.Loader;
function StyleResolver() {
this.loader = new Loader(this.regex);
}
StyleResolver.prototype = {
regex: /@import\s+(?:url)?["'\(]*([^'"\)]*)['"\)]*;/g,
// Recursively replace @imports with the text at that url
resolve: function(text, url, callback) {
var done = function(map) {
callback(this.flatten(text, url, map));
}.bind(this);
this.loader.process(text, url, done);
},
// resolve the textContent of a style node
resolveNode: function(style, url, callback) {
var text = style.textContent;
var done = function(text) {
style.textContent = text;
callback(style);
};
this.resolve(text, url, done);
},
// flatten all the @imports to text
flatten: function(text, base, map) {
var matches = this.loader.extractUrls(text, base);
var match, url, intermediate;
for (var i = 0; i < matches.length; i++) {
match = matches[i];
url = match.url;
// resolve any css text to be relative to the importer, keep absolute url
intermediate = urlResolver.resolveCssText(map[url], url, true);
// flatten intermediate @imports
intermediate = this.flatten(intermediate, base, map);
text = text.replace(match.matched, intermediate);
}
return text;
},
loadStyles: function(styles, base, callback) {
var loaded=0, l = styles.length;
// called in the context of the style
function loadedStyle(style) {
loaded++;
if (loaded === l && callback) {
callback();
}
}
for (var i=0, s; (i
HTMLElement.getPrototypeForTag = function(tag) {
var prototype = !tag ? HTMLElement.prototype : registry[tag];
// TODO(sjmiles): creating is likely to have wasteful side-effects
return prototype || Object.getPrototypeOf(document.createElement(tag));
};
// we have to flag propagation stoppage for the event dispatcher
var originalStopPropagation = Event.prototype.stopPropagation;
Event.prototype.stopPropagation = function() {
this.cancelBubble = true;
originalStopPropagation.apply(this, arguments);
};
// polyfill DOMTokenList
// * add/remove: allow these methods to take multiple classNames
// * toggle: add a 2nd argument which forces the given state rather
// than toggling.
var add = DOMTokenList.prototype.add;
var remove = DOMTokenList.prototype.remove;
DOMTokenList.prototype.add = function() {
for (var i = 0; i < arguments.length; i++) {
add.call(this, arguments[i]);
}
};
DOMTokenList.prototype.remove = function() {
for (var i = 0; i < arguments.length; i++) {
remove.call(this, arguments[i]);
}
};
DOMTokenList.prototype.toggle = function(name, bool) {
if (arguments.length == 1) {
bool = !this.contains(name);
}
bool ? this.add(name) : this.remove(name);
};
DOMTokenList.prototype.switch = function(oldName, newName) {
oldName && this.remove(oldName);
newName && this.add(newName);
};
// add array() to NodeList, NamedNodeMap, HTMLCollection
var ArraySlice = function() {
return Array.prototype.slice.call(this);
};
var namedNodeMap = (window.NamedNodeMap || window.MozNamedAttrMap || {});
NodeList.prototype.array = ArraySlice;
namedNodeMap.prototype.array = ArraySlice;
HTMLCollection.prototype.array = ArraySlice;
// utility
function createDOM(inTagOrNode, inHTML, inAttrs) {
var dom = typeof inTagOrNode == 'string' ?
document.createElement(inTagOrNode) : inTagOrNode.cloneNode(true);
dom.innerHTML = inHTML;
if (inAttrs) {
for (var n in inAttrs) {
dom.setAttribute(n, inAttrs[n]);
}
}
return dom;
}
// exports
scope.createDOM = createDOM;
})(Polymer);
(function(scope) {
// super
// `arrayOfArgs` is an optional array of args like one might pass
// to `Function.apply`
// TODO(sjmiles):
// $super must be installed on an instance or prototype chain
// as `super`, and invoked via `this`, e.g.
// `this.super();`
// will not work if function objects are not unique, for example,
// when using mixins.
// The memoization strategy assumes each function exists on only one
// prototype chain i.e. we use the function object for memoizing)
// perhaps we can bookkeep on the prototype itself instead
function $super(arrayOfArgs) {
// since we are thunking a method call, performance is important here:
// memoize all lookups, once memoized the fast path calls no other
// functions
//
// find the caller (cannot be `strict` because of 'caller')
var caller = $super.caller;
// memoized 'name of method'
var nom = caller.nom;
// memoized next implementation prototype
var _super = caller._super;
if (!_super) {
if (!nom) {
nom = caller.nom = nameInThis.call(this, caller);
}
if (!nom) {
console.warn('called super() on a method not installed declaratively (has no .nom property)');
}
// super prototype is either cached or we have to find it
// by searching __proto__ (at the 'top')
// invariant: because we cache _super on fn below, we never reach
// here from inside a series of calls to super(), so it's ok to
// start searching from the prototype of 'this' (at the 'top')
// we must never memoize a null super for this reason
_super = memoizeSuper(caller, nom, getPrototypeOf(this));
}
// our super function
var fn = _super[nom];
if (fn) {
// memoize information so 'fn' can call 'super'
if (!fn._super) {
// must not memoize null, or we lose our invariant above
memoizeSuper(fn, nom, _super);
}
// invoke the inherited method
// if 'fn' is not function valued, this will throw
return fn.apply(this, arrayOfArgs || []);
}
}
function nameInThis(value) {
var p = this.__proto__;
while (p && p !== HTMLElement.prototype) {
// TODO(sjmiles): getOwnPropertyNames is absurdly expensive
var n$ = Object.getOwnPropertyNames(p);
for (var i=0, l=n$.length, n; icancelAsync to cancel the
* asynchronous call.
*
* @method async
* @param {Function|String} method
* @param {any|Array} args
* @param {number} timeout
*/
async: function(method, args, timeout) {
// when polyfilling Object.observe, ensure changes
// propagate before executing the async method
Polymer.flush();
// second argument to `apply` must be an array
args = (args && args.length) ? args : [args];
// function to invoke
var fn = function() {
(this[method] || method).apply(this, args);
}.bind(this);
// execute `fn` sooner or later
var handle = timeout ? setTimeout(fn, timeout) :
requestAnimationFrame(fn);
// NOTE: switch on inverting handle to determine which time is used.
return timeout ? handle : ~handle;
},
/**
* Cancels a pending callback that was scheduled via
* async .
*
* @method cancelAsync
* @param {handle} handle Handle of the `async` to cancel.
*/
cancelAsync: function(handle) {
if (handle < 0) {
cancelAnimationFrame(~handle);
} else {
clearTimeout(handle);
}
},
/**
* Fire an event.
*
* @method fire
* @returns {Object} event
* @param {string} type An event name.
* @param {any} detail
* @param {Node} onNode Target node.
* @param {Boolean} bubbles Set false to prevent bubbling, defaults to true
* @param {Boolean} cancelable Set false to prevent cancellation, defaults to true
*/
fire: function(type, detail, onNode, bubbles, cancelable) {
var node = onNode || this;
var detail = detail === null || detail === undefined ? {} : detail;
var event = new CustomEvent(type, {
bubbles: bubbles !== undefined ? bubbles : true,
cancelable: cancelable !== undefined ? cancelable : true,
detail: detail
});
node.dispatchEvent(event);
return event;
},
/**
* Fire an event asynchronously.
*
* @method asyncFire
* @param {string} type An event name.
* @param detail
* @param {Node} toNode Target node.
*/
asyncFire: function(/*inType, inDetail*/) {
this.async("fire", arguments);
},
/**
* Remove class from old, add class to anew, if they exist.
*
* @param classFollows
* @param anew A node.
* @param old A node
* @param className
*/
classFollows: function(anew, old, className) {
if (old) {
old.classList.remove(className);
}
if (anew) {
anew.classList.add(className);
}
},
/**
* Inject HTML which contains markup bound to this element into
* a target element (replacing target element content).
*
* @param String html to inject
* @param Element target element
*/
injectBoundHTML: function(html, element) {
var template = document.createElement('template');
template.innerHTML = html;
var fragment = this.instanceTemplate(template);
if (element) {
element.textContent = '';
element.appendChild(fragment);
}
return fragment;
}
};
// no-operation function for handy stubs
var nop = function() {};
// null-object for handy stubs
var nob = {};
// deprecated
utils.asyncMethod = utils.async;
// exports
scope.api.instance.utils = utils;
scope.nop = nop;
scope.nob = nob;
})(Polymer);
(function(scope) {
// imports
var log = window.WebComponents ? WebComponents.flags.log : {};
var EVENT_PREFIX = 'on-';
// instance events api
var events = {
// read-only
EVENT_PREFIX: EVENT_PREFIX,
// event listeners on host
addHostListeners: function() {
var events = this.eventDelegates;
log.events && (Object.keys(events).length > 0) && console.log('[%s] addHostListeners:', this.localName, events);
// NOTE: host events look like bindings but really are not;
// (1) we don't want the attribute to be set and (2) we want to support
// multiple event listeners ('host' and 'instance') and Node.bind
// by default supports 1 thing being bound.
for (var type in events) {
var methodName = events[type];
PolymerGestures.addEventListener(this, type, this.element.getEventHandler(this, this, methodName));
}
},
// call 'method' or function method on 'obj' with 'args', if the method exists
dispatchMethod: function(obj, method, args) {
if (obj) {
log.events && console.group('[%s] dispatch [%s]', obj.localName, method);
var fn = typeof method === 'function' ? method : obj[method];
if (fn) {
fn[args ? 'apply' : 'call'](obj, args);
}
log.events && console.groupEnd();
// NOTE: dirty check right after calling method to ensure
// changes apply quickly; in a very complicated app using high
// frequency events, this can be a perf concern; in this case,
// imperative handlers can be used to avoid flushing.
Polymer.flush();
}
}
};
// exports
scope.api.instance.events = events;
/**
* @class Polymer
*/
/**
* Add a gesture aware event handler to the given `node`. Can be used
* in place of `element.addEventListener` and ensures gestures will function
* as expected on mobile platforms. Please note that Polymer's declarative
* event handlers include this functionality by default.
*
* @method addEventListener
* @param {Node} node node on which to listen
* @param {String} eventType name of the event
* @param {Function} handlerFn event handler function
* @param {Boolean} capture set to true to invoke event capturing
* @type Function
*/
// alias PolymerGestures event listener logic
scope.addEventListener = function(node, eventType, handlerFn, capture) {
PolymerGestures.addEventListener(wrap(node), eventType, handlerFn, capture);
};
/**
* Remove a gesture aware event handler on the given `node`. To remove an
* event listener, the exact same arguments are required that were passed
* to `Polymer.addEventListener`.
*
* @method removeEventListener
* @param {Node} node node on which to listen
* @param {String} eventType name of the event
* @param {Function} handlerFn event handler function
* @param {Boolean} capture set to true to invoke event capturing
* @type Function
*/
scope.removeEventListener = function(node, eventType, handlerFn, capture) {
PolymerGestures.removeEventListener(wrap(node), eventType, handlerFn, capture);
};
})(Polymer);
(function(scope) {
// instance api for attributes
var attributes = {
// copy attributes defined in the element declaration to the instance
// e.g. tabIndex is copied
// to the element instance here.
copyInstanceAttributes: function () {
var a$ = this._instanceAttributes;
for (var k in a$) {
if (!this.hasAttribute(k)) {
this.setAttribute(k, a$[k]);
}
}
},
// for each attribute on this, deserialize value to property as needed
takeAttributes: function() {
// if we have no publish lookup table, we have no attributes to take
// TODO(sjmiles): ad hoc
if (this._publishLC) {
for (var i=0, a$=this.attributes, l=a$.length, a; (a=a$[i]) && i= 0) {
return;
}
// get original value
var currentValue = this[name];
// deserialize Boolean or Number values from attribute
var value = this.deserializeValue(value, currentValue);
// only act if the value has changed
if (value !== currentValue) {
// install new value (has side-effects)
this[name] = value;
}
}
},
// return the published property matching name, or undefined
propertyForAttribute: function(name) {
var match = this._publishLC && this._publishLC[name];
return match;
},
// convert representation of `stringValue` based on type of `currentValue`
deserializeValue: function(stringValue, currentValue) {
return scope.deserializeValue(stringValue, currentValue);
},
// convert to a string value based on the type of `inferredType`
serializeValue: function(value, inferredType) {
if (inferredType === 'boolean') {
return value ? '' : undefined;
} else if (inferredType !== 'object' && inferredType !== 'function'
&& value !== undefined) {
return value;
}
},
// serializes `name` property value and updates the corresponding attribute
// note that reflection is opt-in.
reflectPropertyToAttribute: function(name) {
var inferredType = typeof this[name];
// try to intelligently serialize property value
var serializedValue = this.serializeValue(this[name], inferredType);
// boolean properties must reflect as boolean attributes
if (serializedValue !== undefined) {
this.setAttribute(name, serializedValue);
// TODO(sorvell): we should remove attr for all properties
// that have undefined serialization; however, we will need to
// refine the attr reflection system to achieve this; pica, for example,
// relies on having inferredType object properties not removed as
// attrs.
} else if (inferredType === 'boolean') {
this.removeAttribute(name);
}
}
};
// exports
scope.api.instance.attributes = attributes;
})(Polymer);
(function(scope) {
/**
* @class polymer-base
*/
// imports
var log = window.WebComponents ? WebComponents.flags.log : {};
// magic words
var OBSERVE_SUFFIX = 'Changed';
// element api
var empty = [];
var updateRecord = {
object: undefined,
type: 'update',
name: undefined,
oldValue: undefined
};
var numberIsNaN = Number.isNaN || function(value) {
return typeof value === 'number' && isNaN(value);
};
function areSameValue(left, right) {
if (left === right)
return left !== 0 || 1 / left === 1 / right;
if (numberIsNaN(left) && numberIsNaN(right))
return true;
return left !== left && right !== right;
}
// capture A's value if B's value is null or undefined,
// otherwise use B's value
function resolveBindingValue(oldValue, value) {
if (value === undefined && oldValue === null) {
return value;
}
return (value === null || value === undefined) ? oldValue : value;
}
var properties = {
// creates a CompoundObserver to observe property changes
// NOTE, this is only done there are any properties in the `observe` object
createPropertyObserver: function() {
var n$ = this._observeNames;
if (n$ && n$.length) {
var o = this._propertyObserver = new CompoundObserver(true);
this.registerObserver(o);
// TODO(sorvell): may not be kosher to access the value here (this[n]);
// previously we looked at the descriptor on the prototype
// this doesn't work for inheritance and not for accessors without
// a value property
for (var i=0, l=n$.length, n; (i work)
HTMLTemplateElement.decorate(template);
// ensure a default bindingDelegate
var syntax = this.syntax || (!template.bindingDelegate &&
this.element.syntax);
var dom = template.createInstance(this, syntax);
var observers = dom.bindings_;
for (var i = 0; i < observers.length; i++) {
this.registerObserver(observers[i]);
}
return dom;
},
// Called by TemplateBinding/NodeBind to setup a binding to the given
// property. It's overridden here to support property bindings
// in addition to attribute bindings that are supported by default.
bind: function(name, observable, oneTime) {
var property = this.propertyForAttribute(name);
if (!property) {
// TODO(sjmiles): this mixin method must use the special form
// of `super` installed by `mixinMethod` in declaration/prototype.js
return this.mixinSuper(arguments);
} else {
// use n-way Polymer binding
var observer = this.bindProperty(property, observable, oneTime);
// NOTE: reflecting binding information is typically required only for
// tooling. It has a performance cost so it's opt-in in Node.bind.
if (Platform.enableBindingsReflection && observer) {
observer.path = observable.path_;
this._recordBinding(property, observer);
}
if (this.reflect[property]) {
this.reflectPropertyToAttribute(property);
}
return observer;
}
},
_recordBinding: function(name, observer) {
this.bindings_ = this.bindings_ || {};
this.bindings_[name] = observer;
},
// Called by TemplateBinding when all bindings on an element have been
// executed. This signals that all element inputs have been gathered
// and it's safe to ready the element, create shadow-root and start
// data-observation.
bindFinished: function() {
this.makeElementReady();
},
// called at detached time to signal that an element's bindings should be
// cleaned up. This is done asynchronously so that users have the chance
// to call `cancelUnbindAll` to prevent unbinding.
asyncUnbindAll: function() {
if (!this._unbound) {
log.unbind && console.log('[%s] asyncUnbindAll', this.localName);
this._unbindAllJob = this.job(this._unbindAllJob, this.unbindAll, 0);
}
},
/**
* This method should rarely be used and only if
* `cancelUnbindAll` has been called to
* prevent element unbinding. In this case, the element's bindings will
* not be automatically cleaned up and it cannot be garbage collected
* by the system. If memory pressure is a concern or a
* large amount of elements need to be managed in this way, `unbindAll`
* can be called to deactivate the element's bindings and allow its
* memory to be reclaimed.
*
* @method unbindAll
*/
unbindAll: function() {
if (!this._unbound) {
this.closeObservers();
this.closeNamedObservers();
this._unbound = true;
}
},
/**
* Call in `detached` to prevent the element from unbinding when it is
* detached from the dom. The element is unbound as a cleanup step that
* allows its memory to be reclaimed.
* If `cancelUnbindAll` is used, consider calling
* `unbindAll` when the element is no longer
* needed. This will allow its memory to be reclaimed.
*
* @method cancelUnbindAll
*/
cancelUnbindAll: function() {
if (this._unbound) {
log.unbind && console.warn('[%s] already unbound, cannot cancel unbindAll', this.localName);
return;
}
log.unbind && console.log('[%s] cancelUnbindAll', this.localName);
if (this._unbindAllJob) {
this._unbindAllJob = this._unbindAllJob.stop();
}
}
};
function unbindNodeTree(node) {
forNodeTree(node, _nodeUnbindAll);
}
function _nodeUnbindAll(node) {
node.unbindAll();
}
function forNodeTree(node, callback) {
if (node) {
callback(node);
for (var child = node.firstChild; child; child = child.nextSibling) {
forNodeTree(child, callback);
}
}
}
var mustachePattern = /\{\{([^{}]*)}}/;
// exports
scope.bindPattern = mustachePattern;
scope.api.instance.mdv = mdv;
})(Polymer);
(function(scope) {
/**
* Common prototype for all Polymer Elements.
*
* @class polymer-base
* @homepage polymer.github.io
*/
var base = {
/**
* Tags this object as the canonical Base prototype.
*
* @property PolymerBase
* @type boolean
* @default true
*/
PolymerBase: true,
/**
* Debounce signals.
*
* Call `job` to defer a named signal, and all subsequent matching signals,
* until a wait time has elapsed with no new signal.
*
* debouncedClickAction: function(e) {
* // processClick only when it's been 100ms since the last click
* this.job('click', function() {
* this.processClick;
* }, 100);
* }
*
* @method job
* @param String {String} job A string identifier for the job to debounce.
* @param Function {Function} callback A function that is called (with `this` context) when the wait time elapses.
* @param Number {Number} wait Time in milliseconds (ms) after the last signal that must elapse before invoking `callback`
* @type Handle
*/
job: function(job, callback, wait) {
if (typeof job === 'string') {
var n = '___' + job;
this[n] = Polymer.job.call(this, this[n], callback, wait);
} else {
// TODO(sjmiles): suggest we deprecate this call signature
return Polymer.job.call(this, job, callback, wait);
}
},
/**
* Invoke a superclass method.
*
* Use `super()` to invoke the most recently overridden call to the
* currently executing function.
*
* To pass arguments through, use the literal `arguments` as the parameter
* to `super()`.
*
* nextPageAction: function(e) {
* // invoke the superclass version of `nextPageAction`
* this.super(arguments);
* }
*
* To pass custom arguments, arrange them in an array.
*
* appendSerialNo: function(value, serial) {
* // prefix the superclass serial number with our lot # before
* // invoking the superlcass
* return this.super([value, this.lotNo + serial])
* }
*
* @method super
* @type Any
* @param {args) An array of arguments to use when calling the superclass method, or null.
*/
super: Polymer.super,
/**
* Lifecycle method called when the element is instantiated.
*
* Override `created` to perform custom create-time tasks. No need to call
* super-class `created` unless you are extending another Polymer element.
* Created is called before the element creates `shadowRoot` or prepares
* data-observation.
*
* @method created
* @type void
*/
created: function() {
},
/**
* Lifecycle method called when the element has populated it's `shadowRoot`,
* prepared data-observation, and made itself ready for API interaction.
*
* @method ready
* @type void
*/
ready: function() {
},
/**
* Low-level lifecycle method called as part of standard Custom Elements
* operation. Polymer implements this method to provide basic default
* functionality. For custom create-time tasks, implement `created`
* instead, which is called immediately after `createdCallback`.
*
* @method createdCallback
*/
createdCallback: function() {
if (this.templateInstance && this.templateInstance.model) {
console.warn('Attributes on ' + this.localName + ' were data bound ' +
'prior to Polymer upgrading the element. This may result in ' +
'incorrect binding types.');
}
this.created();
this.prepareElement();
if (!this.ownerDocument.isStagingDocument) {
this.makeElementReady();
}
},
// system entry point, do not override
prepareElement: function() {
if (this._elementPrepared) {
console.warn('Element already prepared', this.localName);
return;
}
this._elementPrepared = true;
// storage for shadowRoots info
this.shadowRoots = {};
// install property observers
this.createPropertyObserver();
this.openPropertyObserver();
// install boilerplate attributes
this.copyInstanceAttributes();
// process input attributes
this.takeAttributes();
// add event listeners
this.addHostListeners();
},
// system entry point, do not override
makeElementReady: function() {
if (this._readied) {
return;
}
this._readied = true;
this.createComputedProperties();
this.parseDeclarations(this.__proto__);
// NOTE: Support use of the `unresolved` attribute to help polyfill
// custom elements' `:unresolved` feature.
this.removeAttribute('unresolved');
// user entry point
this.ready();
},
/**
* Low-level lifecycle method called as part of standard Custom Elements
* operation. Polymer implements this method to provide basic default
* functionality. For custom tasks in your element, implement `attributeChanged`
* instead, which is called immediately after `attributeChangedCallback`.
*
* @method attributeChangedCallback
*/
attributeChangedCallback: function(name, oldValue) {
// TODO(sjmiles): adhoc filter
if (name !== 'class' && name !== 'style') {
this.attributeToProperty(name, this.getAttribute(name));
}
if (this.attributeChanged) {
this.attributeChanged.apply(this, arguments);
}
},
/**
* Low-level lifecycle method called as part of standard Custom Elements
* operation. Polymer implements this method to provide basic default
* functionality. For custom create-time tasks, implement `attached`
* instead, which is called immediately after `attachedCallback`.
*
* @method attachedCallback
*/
attachedCallback: function() {
// when the element is attached, prevent it from unbinding.
this.cancelUnbindAll();
// invoke user action
if (this.attached) {
this.attached();
}
if (!this.hasBeenAttached) {
this.hasBeenAttached = true;
if (this.domReady) {
this.async('domReady');
}
}
},
/**
* Implement to access custom elements in dom descendants, ancestors,
* or siblings. Because custom elements upgrade in document order,
* elements accessed in `ready` or `attached` may not be upgraded. When
* `domReady` is called, all registered custom elements are guaranteed
* to have been upgraded.
*
* @method domReady
*/
/**
* Low-level lifecycle method called as part of standard Custom Elements
* operation. Polymer implements this method to provide basic default
* functionality. For custom create-time tasks, implement `detached`
* instead, which is called immediately after `detachedCallback`.
*
* @method detachedCallback
*/
detachedCallback: function() {
if (!this.preventDispose) {
this.asyncUnbindAll();
}
// invoke user action
if (this.detached) {
this.detached();
}
// TODO(sorvell): bc
if (this.leftView) {
this.leftView();
}
},
/**
* Walks the prototype-chain of this element and allows specific
* classes a chance to process static declarations.
*
* In particular, each polymer-element has it's own `template`.
* `parseDeclarations` is used to accumulate all element `template`s
* from an inheritance chain.
*
* `parseDeclaration` static methods implemented in the chain are called
* recursively, oldest first, with the `` associated
* with the current prototype passed as an argument.
*
* An element may override this method to customize shadow-root generation.
*
* @method parseDeclarations
*/
parseDeclarations: function(p) {
if (p && p.element) {
this.parseDeclarations(p.__proto__);
p.parseDeclaration.call(this, p.element);
}
},
/**
* Perform init-time actions based on static information in the
* `` instance argument.
*
* For example, the standard implementation locates the template associated
* with the given `` and stamps it into a shadow-root to
* implement shadow inheritance.
*
* An element may override this method for custom behavior.
*
* @method parseDeclaration
*/
parseDeclaration: function(elementElement) {
var template = this.fetchTemplate(elementElement);
if (template) {
var root = this.shadowFromTemplate(template);
this.shadowRoots[elementElement.name] = root;
}
},
/**
* Given a ``, find an associated template (if any) to be
* used for shadow-root generation.
*
* An element may override this method for custom behavior.
*
* @method fetchTemplate
*/
fetchTemplate: function(elementElement) {
return elementElement.querySelector('template');
},
/**
* Create a shadow-root in this host and stamp `template` as it's
* content.
*
* An element may override this method for custom behavior.
*
* @method shadowFromTemplate
*/
shadowFromTemplate: function(template) {
if (template) {
// make a shadow root
var root = this.createShadowRoot();
// stamp template
// which includes parsing and applying MDV bindings before being
// inserted (to avoid {{}} in attribute values)
// e.g. to prevent from generating a 404.
var dom = this.instanceTemplate(template);
// append to shadow dom
root.appendChild(dom);
// perform post-construction initialization tasks on shadow root
this.shadowRootReady(root, template);
// return the created shadow root
return root;
}
},
// utility function that stamps a into light-dom
lightFromTemplate: function(template, refNode) {
if (template) {
// TODO(sorvell): mark this element as an eventController so that
// event listeners on bound nodes inside it will be called on it.
// Note, the expectation here is that events on all descendants
// should be handled by this element.
this.eventController = this;
// stamp template
// which includes parsing and applying MDV bindings before being
// inserted (to avoid {{}} in attribute values)
// e.g. to prevent from generating a 404.
var dom = this.instanceTemplate(template);
// append to shadow dom
if (refNode) {
this.insertBefore(dom, refNode);
} else {
this.appendChild(dom);
}
// perform post-construction initialization tasks on ahem, light root
this.shadowRootReady(this);
// return the created shadow root
return dom;
}
},
shadowRootReady: function(root) {
// locate nodes with id and store references to them in this.$ hash
this.marshalNodeReferences(root);
},
// locate nodes with id and store references to them in this.$ hash
marshalNodeReferences: function(root) {
// establish $ instance variable
var $ = this.$ = this.$ || {};
// populate $ from nodes with ID from the LOCAL tree
if (root) {
var n$ = root.querySelectorAll("[id]");
for (var i=0, l=n$.length, n; (ipolymer-base in it's prototype chain.
*
* @method isBase
* @param Object {Object} object Object to test.
* @type Boolean
*/
function isBase(object) {
return object.hasOwnProperty('PolymerBase')
}
// name a base constructor for dev tools
/**
* The Polymer base-class constructor.
*
* @property Base
* @type Function
*/
function PolymerBase() {};
PolymerBase.prototype = base;
base.constructor = PolymerBase;
// exports
scope.Base = PolymerBase;
scope.isBase = isBase;
scope.api.instance.base = base;
})(Polymer);
(function(scope) {
// imports
var log = window.WebComponents ? WebComponents.flags.log : {};
var hasShadowDOMPolyfill = window.ShadowDOMPolyfill;
// magic words
var STYLE_SCOPE_ATTRIBUTE = 'element';
var STYLE_CONTROLLER_SCOPE = 'controller';
var styles = {
STYLE_SCOPE_ATTRIBUTE: STYLE_SCOPE_ATTRIBUTE,
/**
* Installs external stylesheets and
================================================
FILE: src/mui/mui-eblock/mui-eblock.html
================================================
================================================
FILE: src/mui/mui-group/mui-group.html
================================================
================================================
FILE: src/mui/mui-knob/mui-knob.html
================================================
{{label}}
================================================
FILE: src/mui/mui-knobh/mui-knobh.html
================================================
================================================
FILE: src/mui/mui-meter/mui-meter.html
================================================
================================================
FILE: src/mui/mui-pianoroll/mui-pianoroll.html
================================================
================================================
FILE: src/mui/mui-rack/mui-rack.html
================================================
================================================
FILE: src/mui/mui-select/mui-select.html
================================================
{{ label }}
▾
{{ key }}
================================================
FILE: src/mui/mui-spectrum/mui-spectrum.html
================================================
================================================
FILE: src/mui/mui-vkey/mui-vkey.html
================================================
================================================
FILE: src/mui/mui.html
================================================
================================================
FILE: src/mui/mui.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
window.MUI = {};
/**
* Mouse responder. 2D coordinate detection and event handler.
* @class
* @param {String} senderID Specified Sender ID.
* @param {Object} targetElement Target DOM element.
* @param {Function} MUICallback Event-handling callback.
*/
function MouseResponder(senderID, targetElement, MUICallback) {
this.senderId = senderID;
this.container = targetElement;
this.callback = MUICallback;
// bound function references
this.ondragged = this.dragged.bind(this);
this.onreleased = this.released.bind(this);
// timestamp
this._prevTS = 0;
// init with onclick
this.onclicked(targetElement);
}
MouseResponder.prototype = {
getEventData: function (event) {
var r = this.container.getBoundingClientRect();
return {
x: event.clientX - r.left,
y: event.clientY - r.top,
ctrlKey: event.ctrlKey,
altKey: event.altKey,
shiftKey: event.shiftKey,
metaKey: event.metaKey
};
},
onclicked: function (target) {
target.addEventListener('mousedown', function (event) {
event.preventDefault();
this._prevTS = event.timeStamp;
var p = this.getEventData(event);
this.callback(this.senderId, 'clicked', p);
window.addEventListener('mousemove', this.ondragged, false);
window.addEventListener('mouseup', this.onreleased, false);
}.bind(this), false);
},
dragged: function (event) {
event.preventDefault();
if (event.timeStamp - this._prevTS < 16.7) {
return;
}
this._prevTS = event.timeStamp;
var p = this.getEventData(event);
this.callback(this.senderId, 'dragged', p);
},
released: function (event) {
event.preventDefault();
var p = this.getEventData(event);
this.callback(this.senderId, 'released', p);
window.removeEventListener('mousemove', this.ondragged, false);
window.removeEventListener('mouseup', this.onreleased, false);
}
};
/**
* Keyboard responder, the keyboard event handler.
* @class
* @param {String} senderID Specified Sender ID.
* @param {Object} targetElement Target DOM element.
* @param {Function} MUICallback Event-handling callback.
*/
function KeyResponder(senderID, targetElement, MUICallback) {
this.senderId = senderID;
this.container = targetElement;
this.callback = MUICallback;
// bound function references
this.onkeypress = this.keypressed.bind(this);
this.onblur = this.finished.bind(this);
// init with onclick
this.onfocus(targetElement);
}
KeyResponder.prototype = {
onfocus: function () {
this.container.addEventListener('mousedown', function (event) {
this.callback(this.senderId, 'clicked', null);
this.container.addEventListener('keypress', this.onkeypress, false);
this.container.addEventListener('blur', this.onblur, false);
}.bind(this), false);
},
keypressed: function (event) {
this.callback(this.senderId, 'keypressed', event.keyCode);
},
finished: function (event) {
this.callback(this.senderId, 'finished', null);
this.container.removeEventListener('keypress', this.onkeypress, false);
this.container.removeEventListener('blur', this.onblur, false);
}
};
MUI.$ = function (elementId) {
return document.getElementById(elementId);
};
MUI.start = function (onreadyFn) {
// check up depedency: platform
if (WX.isObject(window.Platform)) {
// start function when polymer is ready
window.addEventListener('polymer-ready', onreadyFn);
} else {
WX.Log.error('FATAL: WebComponentPolyfill/Polymer is not loaded.');
}
};
MUI.isPointInArea = function (point, area) {
return (area.x <= point.x && point.x <= area.x + area.w) &&
(area.y <= point.y && point.y <= area.y + area.h);
};
MUI.buildControls = function (plugin, targetId) {
var targetEl = document.getElementById(targetId);
targetEl.label = plugin.info.name;
for (var param in plugin.params) {
var p = plugin.params[param];
switch (p.type) {
case 'Generic':
var knob = document.createElement('mui-knob');
knob.link(plugin, param);
targetEl.appendChild(knob);
break;
case 'Itemized':
var select = document.createElement('mui-select');
select.link(plugin, param);
targetEl.appendChild(select);
break;
case 'Boolean':
var button = document.createElement('mui-button');
button.type = 'toggle';
button.link(plugin, param);
targetEl.appendChild(button);
break;
}
}
};
MUI.removeChildren = function (targetId) {
var targetEl = document.getElementById(targetId);
while (targetEl.firstChild) {
targetEl.removeChild(targetEl.firstChild);
}
};
MUI.MouseResponder = function (senderID, targetElement, MUICallback) {
return new MouseResponder(senderID, targetElement, MUICallback);
};
MUI.KeyResponder = function (senderID, targetElement, MUICallback) {
return new KeyResponder(senderID, targetElement, MUICallback);
};
================================================
FILE: src/plug_ins/CMP1/cmp1.js
================================================
/**
* @wapl CMP1
* @author Hongchan Choi (hoch, hongchan.choi@gmail.com)
*/
(function (WX) {
'use strict';
/** REQUIRED: plug-in constructor **/
function CMP1(preset) {
// REQUIRED: define plug-in type
WX.PlugIn.defineType(this, 'Processor');
// node creation and patching
this._comp = WX.Comp();
this._makeup = WX.Gain();
this._input.to(this._comp).to(this._makeup).to(this._output);
// define parameters
WX.defineParams(this, {
threshold: {
type: 'Generic',
name: 'Threshold',
default: -8.0,
min: -60.0,
max: 0.0,
unit: 'Decibels'
},
knee: {
type: 'Generic',
name: 'Knee',
default: 20,
min: 0,
max: 40,
unit: 'Decibels'
},
ratio: {
type: 'Generic',
name: 'Ratio',
default: 4,
min: 1,
max: 20
},
attack: {
type: 'Generic',
name: 'Attack',
default: 0.025,
min: 0,
max: 1,
unit: 'Seconds'
},
release: {
type: 'Generic',
name: 'Release',
default: 0.25,
min: 0.0,
max: 1.0,
unit: 'Seconds'
},
makeup: {
type: 'Generic',
name: 'Makeup',
default: 0.0,
min: 0.0,
max: 24.0,
unit: 'Decibels'
}
});
// REQUIRED: initializing instance with preset
WX.PlugIn.initPreset(this, preset);
}
/** REQUIRED: plug-in prototype **/
CMP1.prototype = {
// REQUIRED: plug-in info
info: {
name: 'CMP1',
version: '0.0.1',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Processor',
description: 'Basic compressor'
},
// REQUIRED: plug-in default preset
defaultPreset: {
threshold: -8,
knee: 20,
ratio: 4,
attack: 0.025,
release: 0.25,
makeup: 0,
},
$threshold: function (value, time, rampType) {
this._comp.threshold.set(value, time, rampType);
},
$knee: function (value, time, rampType) {
this._comp.knee.set(value, time, rampType);
},
$ratio: function (value, time, rampType) {
this._comp.ratio.set(value, time, rampType);
},
$attack: function (value, time, rampType) {
this._comp.attack.set(value, time, rampType);
},
$release: function (value, time, rampType) {
this._comp.release.set(value, time, rampType);
},
$makeup: function (value, time, rampType) {
this._makeup.gain.set(WX.dbtolin(value), time, rampType);
}
};
// REQUIRED: extending plug-in prototype with modules
WX.PlugIn.extendPrototype(CMP1, 'Processor');
// REQUIRED: registering plug-in into WX ecosystem
WX.PlugIn.register(CMP1);
})(WX);
================================================
FILE: src/plug_ins/Chorus/chorus.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
(function (WX) {
'use strict';
/**
* Implements chorus effect by Jon Dattorro.
* @type {WAPL}
* @param {Object} preset Parameter preset.
* @param {Number} preset.rate Chorus rate.
* @param {Number} preset.depth Chorus depth.
* @param {Number} preset.intensity Chorus intesity.
* @param {Number} preset.blend Chorus blend.
* @param {Number} preset.mix Mix between wet and dry signal.
*/
function Chorus(preset) {
WX.PlugIn.defineType(this, 'Processor');
this._dry = WX.Gain();
this._wet = WX.Gain();
var _splitter = WX.Splitter(2);
var _merger = WX.Merger(2);
// left stream
this._LStream = WX.Gain();
this._LDelayVibrato = WX.Delay();
this._LDelayFixed = WX.Delay();
this._LFeedback = WX.Gain();
this._LFeedforward = WX.Gain();
this._LBlend = WX.Gain();
// right stream
this._RStream = WX.Gain();
this._RDelayVibrato = WX.Delay();
this._RDelayFixed = WX.Delay();
this._RFeedback = WX.Gain();
this._RFeedforward = WX.Gain();
this._RBlend = WX.Gain();
// input
this._input.to(_splitter, this._dry);
// left connection
_splitter.connect(this._LStream, 0, 0);
this._LStream.to(this._LDelayVibrato);
this._LStream.to(this._LDelayFixed);
this._LDelayVibrato.to(this._LFeedforward);
this._LDelayVibrato.connect(_merger, 0, 0);
this._LDelayFixed.to(this._LFeedback);
this._LFeedback.to(this._LStream);
this._LBlend.connect(_merger, 0, 0);
// right connection
_splitter.connect(this._RStream, 1, 0);
this._RStream.to(this._RDelayVibrato);
this._RStream.to(this._RDelayFixed);
this._RDelayVibrato.to(this._RFeedforward);
this._RDelayVibrato.connect(_merger, 0, 1);
this._RDelayFixed.to(this._RFeedback);
this._RFeedback.to(this._RStream);
this._RBlend.connect(_merger, 0, 1);
// output
_merger.to(this._wet);
this._dry.to(this._output);
this._wet.to(this._output);
// LFO modulation
this._lfo = WX.OSC();
this._LDepth = WX.Gain();
this._RDepth = WX.Gain();
this._lfo.to(this._LDepth, this._RDepth);
this._LDepth.to(this._LDelayVibrato.delayTime);
this._RDepth.to(this._RDelayVibrato.delayTime);
this._lfo.start(0);
// unexposed initial settings
this._lfo.type = 'sine';
this._lfo.frequency.value = 0.15;
// dtime setting
this._LDepth.gain.value = 0.013;
this._RDepth.gain.value = -0.017;
this._LDelayVibrato.delayTime.value = 0.013;
this._LDelayFixed.delayTime.value = 0.005;
this._RDelayVibrato.delayTime.value = 0.017;
this._RDelayFixed.delayTime.value = 0.007;
// define parameters
WX.defineParams(this, {
rate: {
type: 'Generic',
name: 'Rate',
default: 0.1,
min: 0.1,
max: 1.0,
unit: 'Hertz'
},
intensity: {
type: 'Generic',
name: 'Intensity',
default: 0.1,
min: 0.01,
max: 1.0
},
mix: {
type: 'Generic',
name: 'Mix',
default: 0.6,
min: 0.0,
max: 1.0,
}
});
WX.PlugIn.initPreset(this, preset);
}
Chorus.prototype = {
info: {
name: 'Chorus',
version: '0.0.1',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Processor',
description: 'Basic chorus effect'
},
defaultPreset: {
rate: 0.5,
intensity: 0.0,
mix: 0.75
},
$rate: function (value, time, rampType) {
value = WX.clamp(value, 0.0, 1.0) * 0.29 + 0.01;
this._lfo.frequency.set(value, time, rampType);
},
$intensity: function (value, time, rampType) {
value = WX.clamp(value, 0.0, 1.0);
var blend = 1.0 - (value * 0.2929);
var feedforward = value * 0.2929 + 0.7071;
var feedback = value * 0.7071;
this._LBlend.gain.set(blend, time, rampType);
this._RBlend.gain.set(blend, time, rampType);
this._LFeedforward.gain.set(feedforward, time, rampType);
this._RFeedforward.gain.set(feedforward, time, rampType);
this._LFeedback.gain.set(feedback, time, rampType);
this._RFeedback.gain.set(feedback, time, rampType);
},
$mix: function (value, time, rampType) {
this._dry.gain.set(1.0 - value, time, rampType);
this._dry.gain.set(value, time, rampType);
}
};
WX.PlugIn.extendPrototype(Chorus, 'Processor');
WX.PlugIn.register(Chorus);
})(WX);
================================================
FILE: src/plug_ins/ConVerb/converb.js
================================================
/**
* @wapl ConVerb
* @author Hongchan Choi (hoch, hongchan.choi@gmail.com)
*/
(function (WX) {
'use strict';
/** REQUIRED: plug-in constructor **/
function ConVerb(preset) {
// REQUIRED: define plug-in type
WX.PlugIn.defineType(this, 'Processor');
// any flags or instance variables
this.ready = false;
this.clip = null;
// node creation and patching
this._dry = WX.Gain();
this._wet = WX.Gain();
this._convolver = WX.Convolver();
this._input.to(this._dry, this._convolver);
this._convolver.to(this._wet);
this._dry.to(this._output);
this._wet.to(this._output);
// define parameters
WX.defineParams(this, {
mix: {
type: 'Generic',
name: 'Mix',
default: 0.2,
min: 0.0,
max: 1.0
}
});
// REQUIRED: initializing instance with preset
WX.PlugIn.initPreset(this, preset);
}
/** REQUIRED: plug-in prototype **/
ConVerb.prototype = {
// REQUIRED: plug-in info
info: {
name: 'ConVerb',
version: '0.0.1',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Processor',
description: 'Convolution Reverb'
},
// REQUIRED: plug-in default preset
defaultPreset: {
mix: 0.2
},
/** handlers **/
$mix: function (value, time, rampType) {
this._dry.gain.set(1.0 - value, time, rampType);
this._wet.gain.set(value, time, rampType);
},
_onprogress: function (event, clip) {
},
_onloaded: function (clip) {
this.setClip(clip);
},
isReady: function () {
return this.ready;
},
setClip: function (clip) {
this.clip = clip;
this._convolver.buffer = this.clip.buffer;
this.ready = true;
},
loadClip: function (clip) {
WX.loadClip(
clip,
this._onloaded.bind(this),
this._onprogress.bind(this)
);
}
};
// REQUIRED: extending plug-in prototype with modules
WX.PlugIn.extendPrototype(ConVerb, 'Processor');
// REQUIRED: registering plug-in into WX ecosystem
WX.PlugIn.register(ConVerb);
})(WX);
================================================
FILE: src/plug_ins/EQ4/eq4.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
(function (WX) {
'use strict';
// Internal unit biqual filter
function UnitFilter(filterType, frequency) {
this._input = WX.Gain();
this._bypass = WX.Gain();
this._biquad = WX.Filter();
this._input.to(this._biquad);
this._bypass.gain.value = 0.0;
this._biquad.type = filterType;
this._biquad.frequency.value = frequency;
this._active = true;
}
UnitFilter.prototype = {
setInput: function (inputNode) {
inputNode.to(this._input, this._bypass);
},
setOutput: function (outputNode) {
this._biquad.to(outputNode);
this._bypass.to(outputNode);
},
cascade: function (unitFilter) {
this._biquad.to(unitFilter._input, unitFilter._bypass);
this._bypass.to(unitFilter._input, unitFilter._bypass);
},
toggle: function (bool) {
this._active = bool;
if (this._active) {
this._input.gain.value = 1.0;
this._bypass.gain.value = 0.0;
} else {
this._input.gain.value = 0.0;
this._bypass.gain.value = 1.0;
}
},
setFilterType: function (filterType) {
this._biquad.type = filterType;
},
setAll: function (freq, Q, gain, time, rampType) {
this._biquad.frequency.set(freq, time, rampType);
this._biquad.Q.set(Q, time, rampType);
this._biquad.gain.set(gain, time, rampType);
},
setFrequency: function (value, time, rampType) {
this._biquad.frequency.set(value, time, rampType);
},
setQ: function (value, time, rampType) {
this._biquad.Q.set(value, time, rampType);
},
setGain: function (value, time, rampType) {
this._biquad.gain.set(value, time, rampType);
},
// TO FIX: for filter graph drawing.
getFrequencyResponse: function (canvasWidth, numOctaves) {
var frequencyHz = new Float32Array(canvasWidth);
var magResponse = new Float32Array(canvasWidth);
var phaseResponse = new Float32Array(canvasWidth);
var nyquist = 0.5 * WX.srate;
for (var i = 0; i < width; ++i) {
// Convert to log frequency scale (octaves).
frequencyHz[i] = nyquist * Math.pow(2.0, noctaves * (i / width - 1.0));
}
filter.getFrequencyResponse(frequencyHz, magResponse, phaseResponse);
return {
freq: frequencyHz,
mag: magResponse
};
}
};
/**
* Implements a standard 4-band parametric equalizer.
* @type {WAPL}
* @param {Object} preset Parameter preset.
* @param {Boolean} preset.band1Active Band 1 active switch.
* @param {Itemized} preset.band1Type Band 1 filter type.
* @param {Number} preset.band1Freq Band 1 frequency.
* @param {Number} preset.band1Q Band 1 Q.
* @param {Number} preset.band1Gain Band 1 gain (decibels).
* @param {Boolean} preset.band2Active Band 2 active switch.
* @param {Itemized} preset.band2Type Band 2 filter type.
* @param {Number} preset.band2Freq Band 2 frequency.
* @param {Number} preset.band2Q Band 2 Q.
* @param {Number} preset.band2Gain Band 2 gain (decibels).
* @param {Boolean} preset.band3Active Band 3 active switch.
* @param {Itemized} preset.band3Type Band 3 filter type.
* @param {Number} preset.band3Freq Band 3 frequency.
* @param {Number} preset.band3Q Band 3 Q.
* @param {Number} preset.band3Gain Band 3 gain (decibels).
* @param {Boolean} preset.band4Active Band 4 active switch.
* @param {Itemized} preset.band4Type Band 4 filter type.
* @param {Number} preset.band4Freq Band 4 frequency.
* @param {Number} preset.band4Q Band 4 Q.
* @param {Number} preset.band4Gain Band 4 gain (decibels).
*/
function EQ4(preset) {
WX.PlugIn.defineType(this, 'Processor');
this._band1 = new UnitFilter('lowshelf', 80);
this._band2 = new UnitFilter('peaking', 500);
this._band3 = new UnitFilter('peaking', 3500);
this._band4 = new UnitFilter('highshelf', 10000);
this._band1.setInput(this._input);
this._band1.cascade(this._band2);
this._band2.cascade(this._band3);
this._band3.cascade(this._band4);
this._band4.setOutput(this._output);
// define parameters
WX.defineParams(this, {
band1Active: {
type: 'Boolean',
name: 'On 1',
default: true
},
band1Type: {
type: 'Itemized',
name: 'Type 1',
default: 'lowshelf',
model: WX.FILTER_TYPES
},
band1Freq: {
type: 'Generic',
name: 'Freq 1',
default: 80,
min: 10,
max: WX.srate * 0.5,
unit: 'Hertz'
},
band1Q: {
type: 'Generic',
name: 'Q 1',
default: 0.0,
min: 0.01,
max: 1000
},
band1Gain: {
type: 'Generic',
name: 'Gain 1',
default: 0.0,
min: -40,
max: 40,
unit: 'Decibels'
},
band2Active: {
type: 'Boolean',
name: 'On 2',
default: true
},
band2Type: {
type: 'Itemized',
name: 'Type 2',
default: 'peaking',
model: WX.FILTER_TYPES
},
band2Freq: {
type: 'Generic',
name: 'Freq 2',
default: 500,
min: 10,
max: WX.srate * 0.5,
unit: 'Hertz'
},
band2Q: {
type: 'Generic',
name: 'Q 2',
default: 0.0,
min: 0.01,
max: 1000
},
band2Gain: {
type: 'Generic',
name: 'Gain 2',
default: 0.0,
min: -40,
max: 40,
unit: 'Decibels'
},
band3Active: {
type: 'Boolean',
name: 'On 3',
default: true
},
band3Type: {
type: 'Itemized',
name: 'Type 3',
default: 'peaking',
model: WX.FILTER_TYPES
},
band3Freq: {
type: 'Generic',
name: 'Freq 3',
default: 3500,
min: 10,
max: WX.srate * 0.5,
unit: 'Hertz'
},
band3Q: {
type: 'Generic',
name: 'Q 3',
default: 0.0,
min: 0.01,
max: 1000
},
band3Gain: {
type: 'Generic',
name: 'Gain 3',
default: 0.0,
min: -40,
max: 40,
unit: 'Decibels'
},
band4Active: {
type: 'Boolean',
name: 'On 4',
default: true
},
band4Type: {
type: 'Itemized',
name: 'Type 4',
default: 'highshelf',
model: WX.FILTER_TYPES
},
band4Freq: {
type: 'Generic',
name: 'Freq 4',
default: 12000,
min: 10,
max: WX.srate * 0.5,
unit: 'Hertz'
},
band4Q: {
type: 'Generic',
name: 'Q 4',
default: 0.0,
min: 0.01,
max: 1000
},
band4Gain: {
type: 'Generic',
name: 'Gain 4',
default: 0.0,
min: -40,
max: 40,
unit: 'Decibels'
}
});
WX.PlugIn.initPreset(this, preset);
}
EQ4.prototype = {
info: {
name: 'EQ4',
version: '0.0.1',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Processor',
description: '4-band Parametric Equalizer'
},
defaultPreset: {
band1Active: true,
band1Type: 'lowshelf',
band1Freq: 80,
band1Q: 0.0,
band1Gain: 0.0,
band2Active: true,
band2Type: 'peaking',
band2Freq: 500,
band2Q: 0.0,
band2Gain: 0.0,
band3Active: true,
band3Type: 'peaking',
band3Freq: 3500,
band3Q: 0.0,
band3Gain: 0.0,
band4Active: true,
band4Type: 'highshelf',
band4Freq: 12000,
band4Q: 0.0,
band4Gain: 0.0
},
$band1Active: function (value, time, rampType) {
this._band1.toggle(value);
},
$band1Type: function (value, time, rampType) {
this._band1.setFilterType(value);
},
$band1Freq: function (value, time, rampType) {
this._band1.setFrequency(value, time, rampType);
},
$band1Q: function (value, time, rampType) {
this._band1.setQ(value, time, rampType);
},
$band1Gain: function (value, time, rampType) {
this._band1.setGain(value, time, rampType);
},
$band2Active: function (value, time, rampType) {
this._band2.toggle(value);
},
$band2Type: function (value, time, rampType) {
this._band2.setFilterType(value);
},
$band2Freq: function (value, time, rampType) {
this._band2.setFrequency(value, time, rampType);
},
$band2Q: function (value, time, rampType) {
this._band2.setQ(value, time, rampType);
},
$band2Gain: function (value, time, rampType) {
this._band2.setGain(value, time, rampType);
},
$band3Active: function (value, time, rampType) {
this._band3.toggle(value);
},
$band3Type: function (value, time, rampType) {
this._band3.setFilterType(value);
},
$band3Freq: function (value, time, rampType) {
this._band3.setFrequency(value, time, rampType);
},
$band3Q: function (value, time, rampType) {
this._band3.setQ(value, time, rampType);
},
$band3Gain: function (value, time, rampType) {
this._band3.setGain(value, time, rampType);
},
$band4Active: function (value, time, rampType) {
this._band4.toggle(value);
},
$band4Type: function (value, time, rampType) {
this._band4.setFilterType(value);
},
$band4Freq: function (value, time, rampType) {
this._band4.setFrequency(value, time, rampType);
},
$band4Q: function (value, time, rampType) {
this._band4.setQ(value, time, rampType);
},
$band4Gain: function (value, time, rampType) {
this._band4.setGain(value, time, rampType);
}
};
WX.PlugIn.extendPrototype(EQ4, 'Processor');
WX.PlugIn.register(EQ4);
})(WX);
================================================
FILE: src/plug_ins/FMK1/fmk1.js
================================================
/**
* @wapl FMK1
* @author Hongchan Choi (hoch, hongchan.choi@gmail.com)
*/
(function (WX) {
'use strict';
/**
* FMOperator class.
* @param {[type]} outputNode [description]
*/
function FMVoice(synth) {
this.parent = synth;
this.params = synth.params;
this.voiceKey = null;
this._minDur = null;
this._mod = WX.OSC();
this._modGain = WX.Gain();
this._car = WX.OSC();
this._carGain = WX.Gain();
this._mod.to(this._modGain);
this._modGain.connect(this._car.frequency);
this._car.to(this._carGain).to(this.parent._filter);
this._mod2 = WX.OSC();
this._modGain2 = WX.Gain();
this._car2 = WX.OSC();
this._carGain2 = WX.Gain();
this._mod2.to(this._modGain2);
this._modGain2.connect(this._car2.frequency);
this._car2.to(this._carGain2).to(this.parent._filter);
}
FMVoice.prototype = {
noteOn: function (pitch, velocity, time) {
var p = this.params,
freq = WX.mtof(pitch),
hr = p.harmonicRatio.get(),
mi = p.modulationIndex.get(),
att = p.attack.get(),
dec = p.decay.get(),
sus = p.sustain.get(),
bal = p.balance.get(),
scale = WX.veltoamp(velocity);
// 1: start generation
this._mod.start(time);
this._car.start(time);
// set fm parameters: freq, hr, mi-attack, mi-decay
this._car.frequency.set(freq, time, 0);
this._mod.frequency.set(freq * hr, time, 0);
this._modGain.gain.set(freq * hr * mi, time, 0);
this._modGain.gain.set(0.1, time + 1.5, 2);
// envelope: ads
this._carGain.gain.set(0.0, time, 0);
this._carGain.gain.set(scale * bal, time + att, 1);
this._carGain.gain.set(sus * scale * bal, [time + att, dec], 3);
// 2: start generation
this._mod2.start(time);
this._car2.start(time);
// set fm parameters: freq, hr, mi-attack, mi-decay
this._car2.frequency.set(freq * 2, time, 0);
this._mod2.frequency.set(freq * hr, time, 0);
this._modGain2.gain.set(freq * hr * mi * 0.5, time, 0);
this._modGain2.gain.set(0.5, time + 1.5, 2);
// envelope: ads
this._carGain2.gain.set(0.0, time, 0);
this._carGain2.gain.set(scale * (1 - bal), time + att, 1);
this._carGain2.gain.set(sus * scale * (1 - bal), [time + att, dec], 3);
// get minDur
this.minDur = time + att + dec;
},
noteOff: function (pitch, velocity, time) {
if (this.minDur) {
time = time < WX.now ? WX.now : time;
var p = this.params,
rel = p.release.get();
this.voiceKey = pitch;
this._mod.stop(this.minDur + rel + 2.0);
this._car.stop(this.minDur + rel + 2.0);
this._mod2.stop(this.minDur + rel + 2.0);
this._car2.stop(this.minDur + rel + 2.0);
if (time < this.minDur) {
this._carGain.gain.cancel(this.minDur);
this._carGain.gain.set(0.0, [this.minDur, rel], 3);
this._carGain2.gain.cancel(this.minDur);
this._carGain2.gain.set(0.0, [this.minDur, rel], 3);
} else {
this._carGain.gain.set(0.0, [time, rel], 3);
this._carGain2.gain.set(0.0, [time, rel], 3);
}
}
}
};
// REQUIRED: plug-in constructor
function FMK1(preset) {
// REQUIRED: adding necessary modules
WX.PlugIn.defineType(this, 'Generator');
this.numVoice = 0;
// naive voice management
this.voices = [];
for (var i = 0; i < 128; i++) {
this.voices[i] = [];
}
// patching
this._filter = WX.Filter();
this._filter.to(this._output);
// parameter definition
WX.defineParams(this, {
harmonicRatio: {
type: 'Generic',
name: 'HRatio',
default: 4,
min: 1,
max: 60
},
modulationIndex: {
type: 'Generic',
name: 'ModIdx',
default: 1,
min: 0.0,
max: 2.0
},
attack: {
type: 'Generic',
name: 'Att',
default: 0.005,
min: 0.0,
max: 5.0,
unit: 'Seconds'
},
decay: {
type: 'Generic',
name: 'Dec',
default: 0.04,
min: 0.0,
max: 5.0,
unit: 'Seconds'
},
sustain: {
type: 'Generic',
name: 'Sus',
default: 0.25,
min: 0.0,
max: 1.0
},
release: {
type: 'Generic',
name: 'Rel',
default: 0.2,
min: 0.0,
max: 10.0,
unit: 'Seconds'
},
balance: {
type: 'Generic',
name: 'Balance',
default: 0.5,
min: 0.0,
max: 1.0
},
filterType: {
type: 'Itemized',
name: 'FiltType',
default: 'highshelf',
model: WX.FILTER_TYPES
},
filterFrequency: {
type: 'Generic',
name: 'FiltFreq',
default: 2500,
min: 20,
max: 20000,
unit: 'Hertz'
},
filterQ: {
type: 'Generic',
name: 'FiltQ',
default: 0.0,
min: 0.0,
max: 40.0
},
filterGain: {
type: 'Generic',
name: 'FiltGain',
default: 0.0,
min: -40.0,
max: 40.0,
unit: 'Decibels'
}
});
// REQUIRED: initializing instance with preset
WX.PlugIn.initPreset(this, preset);
}
/** REQUIRED: plug-in prototype **/
FMK1.prototype = {
// REQUIRED: plug-in info
info: {
name: 'FMK1',
version: '0.0.1',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Generator',
description: 'FM Bell-based Keys'
},
// REQUIRED: plug-in default preset
defaultPreset: {
harmonicRatio: 10,
modulationIndex: 1.8,
attack: 0.002,
decay: 0.03,
sustain: 0.65,
release: 0.55,
balance: 0.7165,
filterType: 'highshelf',
filterFrequency: 7000,
filterQ: 0.0,
filterGain: -3.0,
output: 0.3
},
// REQUIRED: handlers for each parameter
$balance: function (value, time, rampType) {
},
$filterType: function (value, time, rampType) {
this._filter.type = value;
},
$filterFrequency: function (value, time, rampType) {
this._filter.frequency.set(value, time, rampType);
},
$filterQ: function (value, time, rampType) {
this._filter.Q.set(value, time, rampType);
},
$filterGain: function (value, time, rampType) {
this._filter.gain.set(value, time, rampType);
},
noteOn: function (pitch, velocity, time) {
time = (time || WX.now);
var voice = new FMVoice(this);
this.voices[pitch].push(voice);
this.numVoice++;
voice.noteOn(pitch, velocity, time);
},
noteOff: function (pitch, velocity, time) {
time = (time || WX.now);
var playing = this.voices[pitch];
for (var i = 0; i < playing.length; i++) {
playing[i].noteOff(pitch, velocity, time);
this.numVoice--;
}
// TODO: is this performant enough?
this.voices[pitch] = [];
},
onData: function (action, data) {
switch (action) {
case 'noteon':
this.noteOn(data.pitch, data.velocity);
break;
case 'noteoff':
this.noteOff(data.pitch, data.velocity);
break;
}
}
};
// REQUIRED: extending plug-in prototype with modules
WX.PlugIn.extendPrototype(FMK1, 'Generator');
// REQUIRED: registering plug-in into WX ecosystem
WX.PlugIn.register(FMK1);
})(WX);
================================================
FILE: src/plug_ins/Fader/fader.js
================================================
/**
* @wapl Fader
* @author Hongchan Choi (hoch, hongchan.choi@gmail.com)
*/
(function (WX) {
function Fader(preset) {
// adding modules
WX.PlugIn.defineType(this, 'Processor');
// node creation and patching
// this._panner = WX.Panner();
// this._input.to(this._panner).to(this._output);
this._input.to(this._output);
// this._panner.panningModel = 'equalpower';
WX.defineParams(this, {
output: {
type: 'Generic',
name: 'Output',
default: 1.0,
min: 0.0,
max: 3.9810717055349722,
unit: 'LinearGain'
},
mute: {
type: 'Boolean',
name: 'Mute',
default: false
},
// pan: {
// type: 'Generic',
// name: 'Pan',
// default: 0.0,
// min: -1.0,
// max: 1.0
// },
dB: {
type: 'Generic',
name: 'dB',
default: 0.0,
min: -60,
max: 12.0,
unit: 'Decibels'
}
});
// initialize preset
WX.PlugIn.initPreset(this, preset);
}
Fader.prototype = {
info: {
name: 'Fader',
version: '0.0.3',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Processor',
description: 'Channel Fader'
},
defaultPreset: {
mute: false,
// pan: 0.0,
dB: 0.0
},
$mute: function (value, time, rampType) {
if (value) {
this._outlet.gain.set(0.0, WX.now, 0);
} else {
this._outlet.gain.set(1.0, WX.now, 0);
}
},
// $pan: function (value, time, rampType) {
// // TODO: compensate pan model attenuation (z=0.5)
// this._panner.setPosition(value, 0, 0.5);
// },
$dB: function (value, time, rampType) {
this.params.output.set(WX.dbtolin(value), time, rampType);
// console.log(this);
// this._output.gain.set(WX.dbtolin(value), WX.now + 0.02, 1);
}
};
WX.PlugIn.extendPrototype(Fader, 'Processor');
WX.PlugIn.register(Fader);
// NOTE: built in master output fader
WX.Master = WX.Fader();
WX.Master.to(WX._ctx.destination);
})(WX);
================================================
FILE: src/plug_ins/FilterBank/filterbank.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
(function (WX) {
'use strict';
// Pre-defined scales: ionian, lydian, aeolian, and mixolydian.
var SCALES = [
{ key: 'Ionian', value: 'ionian' },
{ key: 'Lydian', value: 'lydian' },
{ key: 'Mixolydian', value: 'mixolydian' },
{ key: 'Aeolian', value: 'aeolian' }
];
// Pitch class for scales.
var PITCHES = {
'ionian': [0, 7, 14, 21, 28, 35, 43, 48],
'lydian': [0, 6, 16, 21, 26, 35, 42, 48],
'mixolydian': [0, 5, 16, 23, 26, 33, 41, 48],
'aeolian': [0, 7, 15, 22, 26, 34, 39, 48]
};
// Number of bands. A band is consist of cascaded two bandpass filters.
var NUM_BANDS = 8;
/**
* Implements harmonized 8-band filterbank.
* @type {WAPL}
* @param {Object} preset Parameter preset.
* @param {Number} preset.pitch
* @param {Number} preset.scale
* @param {Number} preset.slope
* @param {Number} preset.width
* @param {Number} preset.detune
*/
function FilterBank(preset) {
WX.PlugIn.defineType(this, 'Processor');
// Cascading 2 filters (serial connection) for sharp resonance.
this._filters1 = [];
this._filters2 = [];
this._gains = [];
this._summing = WX.Gain();
for (var i = 0; i < NUM_BANDS; ++i) {
this._filters1[i] = WX.Filter();
this._filters2[i] = WX.Filter();
this._gains[i] = WX.Gain();
this._filters1[i].type = 'bandpass';
this._filters2[i].type = 'bandpass';
this._input.to(this._filters1[i]);
this._filters1[i].to(this._filters2[i]).to(this._gains[i]);
this._gains[i].to(this._summing);
}
this._summing.to(this._output);
// Gain compensation. The resulting loudness of filterbank is fairly small.
this._summing.gain.value = 35.0;
// Parameter definition
WX.defineParams(this, {
pitch: {
type: 'Generic',
name: 'Pitch',
default: 24,
min: 12,
max: 48
},
scale: {
type: 'Itemized',
name: 'Scale',
default: 'lydian',
model: SCALES
},
slope: {
type: 'Generic',
name: 'Harmonics',
default: 0.26,
min: 0.1,
max: 0.75
},
width: {
type: 'Generic',
name: 'Width',
default: 0.49,
min: 0.0,
max: 1.0
},
detune: {
type: 'Generic',
name: 'Detune',
default: 0.0,
min: 0.0,
max: 1.0
}
});
WX.PlugIn.initPreset(this, preset);
}
FilterBank.prototype = {
info: {
name: 'FilterBank',
version: '0.0.1',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Processor',
description: 'Harmonized 8-band filterbank'
},
defaultPreset: {
pitch: 34,
scale: 'lydian',
slope: 0.65,
width: 0.15,
detune: 0.0
},
// Change frequency of filters
$pitch: function (value, time, rampType) {
var f0 = WX.mtof(value);
for (var i = 0; i < NUM_BANDS; i++) {
this._filters1[i].frequency.set(f0, time, rampType);
this._filters2[i].frequency.set(f0, time, rampType);
}
},
// Change detune of filters. (Note that this is in cents.)
$scale: function (value, time, rampType) {
time = (WX.now || time);
var pitches = PITCHES[value];
for (var i = 1; i < NUM_BANDS; i++) {
this._filters1[i].detune.set(pitches[i] * 100, time, rampType);
this._filters2[i].detune.set(pitches[i] * 100, time, rampType);
}
},
$slope: function (value, time, rampType) {
for (var i = 0; i < NUM_BANDS; i++) {
// Gain balancing formula.
var gain = 1.0 + Math.sin(Math.PI + (Math.PI/2 * (value + i/NUM_BANDS)));
this._gains[i].gain.set(gain, time, rampType);
}
},
$width: function (value, time, rampType) {
for (var i = 1; i < NUM_BANDS; i++) {
// Q formula.
var q = 2 + 90 * Math.pow((1 - i / NUM_BANDS), value);
this._filters1[i].Q.set(q, time, rampType);
this._filters2[i].Q.set(q, time, rampType);
}
},
// TO FIX: detune handler
$detune: function (value, time, rampType) {
},
getScaleModel: function () {
return SCALES.slice(0);
}
// TO FIX: noteon, noteoff. Interactive features.
};
WX.PlugIn.extendPrototype(FilterBank, 'Processor');
WX.PlugIn.register(FilterBank);
})(WX);
================================================
FILE: src/plug_ins/Impulse/Impulse.js
================================================
/**
* @wapl Impulse
* @author Hongchan Choi (hoch, hongchan.choi@gmail.com)
*/
(function (WX) {
'use strict';
// pre-generation of impulse data
// NOTE: static data for all Impulse instances
var binSize = 2048,
mag = new Float32Array(binSize),
phase = new Float32Array(binSize);
for (var i = 0; i < binSize; ++i) {
mag[i] = 1.0;
phase[i] = 0.0;
}
var DATA = WX.PeriodicWave(mag, phase);
/** REQUIRED: plug-in constructor **/
function Impulse(preset) {
// REQUIRED: adding necessary modules
WX.PlugIn.defineType(this, 'Generator');
this._impulse = WX.OSC();
this._impulse.to(this._output);
this._impulse.start(0);
this._impulse.setPeriodicWave(DATA);
WX.defineParams(this, {
freq: {
type: 'Generic',
name: 'Freq',
default: 1.0,
min: 0.1,
max: 60.0,
unit: 'Hertz'
}
});
// REQUIRED: initializing instance with preset
WX.PlugIn.initPreset(this, preset);
}
/** REQUIRED: plug-in prototype **/
Impulse.prototype = {
// REQUIRED: plug-in info
info: {
name: 'Impulse',
version: '0.0.1',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Generator',
description: 'Impulse (train) Generator'
},
// REQUIRED: plug-in default preset
defaultPreset: {
freq: 1.0
},
// REQUIRED: if you have a parameter,
// corresponding handler is required.
$freq: function (value, time, rampType) {
this._impulse.frequency.set(value, time, rampType);
}
};
// REQUIRED: extending plug-in prototype with modules
WX.PlugIn.extendPrototype(Impulse, 'Generator');
// REQUIRED: registering plug-in into WX ecosystem
WX.PlugIn.register(Impulse);
})(WX);
================================================
FILE: src/plug_ins/Noise/noise.js
================================================
/**
* @wapl Noise
* @author Hongchan Choi (hoch, hongchan.choi@gmail.com)
*/
(function (WX) {
'use strict';
// const noise type
var NOISETYPE = [
{ key: 'White', value: 'white' },
{ key: 'Pink', value: 'pink' }
];
// pre-generation of gaussian white noise
// http://www.musicdsp.org/showone.php?id=113
function createGaussian(duration) {
var length = Math.floor(WX.srate * duration);
var noiseFloat32 = new Float32Array(length);
for (var i = 0; i < length; i++) {
var r1 = Math.log(Math.random()), r2 = Math.PI * Math.random();
noiseFloat32[i] = Math.sqrt(-2.0 * r1) * Math.cos(2.0 * r2) * 0.5;
}
var noiseBuffer = WX.Buffer(2, length, WX.srate);
noiseBuffer.getChannelData(0).set(noiseFloat32, 0);
noiseBuffer.getChannelData(1).set(noiseFloat32, 0);
return noiseBuffer;
}
// NEEDS TO BE TESTED
// pre-generation of pink noise
// http://home.earthlink.net/~ltrammell/tech/pinkalg.htm
// http://home.earthlink.net/~ltrammell/tech/pinkgen.c
function createPink(duration) {
var length = Math.floor(WX.srate * duration);
var noiseFloat32 = new Float32Array(length);
// pink noise specific
var pA = [3.8024, 2.9694, 2.5970, 3.0870, 3.4006],
pSum = [0.00198, 0.01478, 0.06378, 0.23378, 0.91578],
pASum = 15.8564,
sample = 0,
contrib = [0.0, 0.0, 0.0, 0.0, 0.0];
for (var i = 0; i < length; i++) {
var ur1 = Math.random(), ur2 = Math.random();
for (var j = 0; j < 5; j++) {
if (ur1 <= pSum[j]) {
sample -= contrib[j];
contrib[j] = 2 * (ur2 - 0.5) * pA[j];
sample += contrib[j];
break;
}
}
noiseFloat32[i] = sample / pASum;
}
// console.log(noiseFloat32); // debug
var noiseBuffer = WX.Buffer(2, length, WX.srate);
noiseBuffer.getChannelData(0).set(noiseFloat32, 0);
noiseBuffer.getChannelData(1).set(noiseFloat32, 0);
return noiseBuffer;
}
var _baseBufferGaus = createGaussian(10.0),
_baseBufferPink = createPink(10.0);
/**
* [Noise description]
* @param {[type]} preset [description]
*/
function Noise(preset) {
// REQUIRED: adding necessary modules
WX.PlugIn.defineType(this, 'Generator');
this._bufferGaus = createGaussian(9.73);
this._bufferPink = createPink(9.73);
this._src1 = WX.Source();
this._src2 = WX.Source();
this._src1.to(this._output);
this._src2.to(this._output);
this._src1.loop = true;
this._src2.loop = true;
this._src1.start(0);
this._src2.start(0);
WX.defineParams(this, {
type: {
type: 'Itemized',
name: 'Type',
default: 'white',
model: NOISETYPE
}
});
// REQUIRED: initializing instance with preset
WX.PlugIn.initPreset(this, preset);
}
/** REQUIRED: plug-in prototype **/
Noise.prototype = {
// REQUIRED: plug-in info
info: {
name: 'Noise',
version: '0.0.1',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Generator',
description: 'White and Pink Noise Generator'
},
// REQUIRED: plug-in default preset
defaultPreset: {
type: 'white'
},
$type: function (value, time, rampType) {
switch (value) {
case 'white':
this._src1.buffer = _baseBufferGaus;
this._src2.buffer = this._bufferGaus;
this._src1.loopStart = Math.random() * 10.0;
break;
case 'pink':
this._src1.buffer = _baseBufferPink;
this._src2.buffer = this._bufferPink;
this._src1.loopStart = Math.random() * 10.0;
break;
}
}
};
// REQUIRED: extending plug-in prototype with modules
WX.PlugIn.extendPrototype(Noise, 'Generator');
// REQUIRED: registering plug-in into WX ecosystem
WX.PlugIn.register(Noise);
})(WX);
================================================
FILE: src/plug_ins/SP1/sp1.js
================================================
/**
* @wapl SP1
* @author Hongchan Choi (hoch, hongchan.choi@gmail.com)
*/
// TODO
// - filter? mod?
// -
(function (WX) {
'use strict';
// internal abstraction for polyphony impl
function SP1Voice(sampler) {
this.parent = sampler;
this.params = sampler.params;
this.voiceKey = null;
this.minDur = null;
this._src = WX.Source();
this._srcGain = WX.Gain();
this._src.to(this._srcGain).to(this.parent._filter);
this._src.loop = true;
this._src.buffer = this.parent.clip.buffer;
// this._src.onended = function () {
// // DO SOMETHING
// }.bind(this);
}
SP1Voice.prototype = {
noteOn: function (pitch, velocity, time) {
var p = this.params,
basePitch = p.tune.get(),
att = p.ampAttack.get(),
dec = p.ampDecay.get(),
sus = p.ampSustain.get(),
scale = p.velocityMod.get() ? WX.veltoamp(velocity) : 1.0;
if (p.pitchMod.get()) {
this._src.playbackRate.value = Math.pow(2, (pitch - basePitch) / 12);
}
this._src.start(time);
this._srcGain.gain.set(0.0, time, 0);
this._srcGain.gain.set(scale, time + att, 1);
this._srcGain.gain.set(sus * scale, [time + att, dec], 3);
// calculate minimum duration
// if noteOff comes after minDur, cancel AParam is not needed
this.minDur = time + att + dec;
},
noteOff: function (pitch, velocity, time) {
if (this.minDur) {
time = time < WX.now ? WX.now : time;
var p = this.params,
rel = p.ampRelease.get();
this.voiceKey = pitch;
this._src.stop(this.minDur + rel + 1.0);
// if noteOff happens before minDur
// : cancel scheduled ADS envelope and then start releasing
if (time < this.minDur) {
this._srcGain.gain.cancel(this.minDur);
this._srcGain.gain.set(0.0, [this.minDur, rel], 3);
} else {
this._srcGain.gain.set(0.0, [time, rel], 3);
}
}
}
};
/** REQUIRED: plug-in constructor **/
function SP1(preset) {
// REQUIRED: adding necessary modules
WX.PlugIn.defineType(this, 'Generator');
this.ready = false;
this.clip = null;
this.numVoice = 0;
// naive voice management
this.voices = [];
for (var i = 0; i < 128; i++) {
this.voices[i] = [];
}
// patching
this._filter = WX.Filter();
this._filter.to(this._output);
// parameter definition
WX.defineParams(this, {
tune: {
type: 'Generic',
name: 'Tune',
default: 48,
min: 0,
max: 127,
unit: 'Semitone'
},
pitchMod: {
type: 'Boolean',
name: 'PitchMod',
default: true
},
velocityMod: {
type: 'Boolean',
name: 'VeloMod',
default: true
},
ampAttack: {
type: 'Generic',
name: 'Att',
default: 0.02,
min: 0.0,
max: 5.0,
unit: 'Seconds'
},
ampDecay: {
type: 'Generic',
name: 'Dec',
default: 0.04,
min: 0.0,
max: 5.0,
unit: 'Seconds'
},
ampSustain: {
type: 'Generic',
name: 'Sus',
default: 0.25,
min: 0.0,
max: 1.0,
unit: 'LinearGain'
},
ampRelease: {
type: 'Generic',
name: 'Rel',
default: 0.2,
min: 0.0,
max: 10.0,
unit: 'Seconds'
},
filterType: {
type: 'Itemized',
name: 'FiltType',
default: 'lowpass',
model: WX.FILTER_TYPES
},
filterFrequency: {
type: 'Generic',
name: 'FiltFreq',
default: 2500,
min: 20,
max: 20000,
unit: 'Hertz'
},
filterQ: {
type: 'Generic',
name: 'FiltQ',
default: 0.0,
min: 0.0,
max: 40.0
},
filterGain: {
type: 'Generic',
name: 'FiltGain',
default: 0.0,
min: -40.0,
max: 40.0,
unit: 'LinearGain'
}
});
// REQUIRED: initializing instance with preset
WX.PlugIn.initPreset(this, preset);
}
/** REQUIRED: plug-in prototype **/
SP1.prototype = {
// REQUIRED: plug-in info
info: {
name: 'SP1',
version: '0.0.1',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Generator',
description: 'Versatile Single-Zone Sampler'
},
// REQUIRED: plug-in default preset
defaultPreset: {
tune: 60,
pitchMod: true,
velocityMod: true,
ampAttack: 0.01,
ampDecay: 0.44,
ampSustain: 0.06,
ampRelease: 0.06,
filterType: 'LP',
filterFrequency: 5000,
filterQ: 0.0,
filterGain: 0.0,
output: 1.0
},
// REQUIRED: if you have a parameter,
// corresponding handler is required.
$filterType: function (value, time, rampType) {
this._filter.type = value;
},
$filterFrequency: function (value, time, rampType) {
this._filter.frequency.set(value, time, rampType);
},
$filterQ: function (value, time, rampType) {
this._filter.Q.set(value, time, rampType);
},
$filterGain: function (value, time, rampType) {
this._filter.gain.set(value, time, rampType);
},
noteOn: function (pitch, velocity, time) {
time = (time || WX.now);
var voice = new SP1Voice(this);
this.voices[pitch].push(voice);
this.numVoice++;
voice.noteOn(pitch, velocity, time);
},
noteOff: function (pitch, velocity, time) {
time = (time || WX.now);
var playing = this.voices[pitch];
for (var i = 0; i < playing.length; i++) {
playing[i].noteOff(pitch, velocity, time);
this.numVoice--;
}
// TODO: is this performant enough?
this.voices[pitch] = [];
},
// realtime input data responder (Ktrl responder)
onData: function (action, data) {
switch (action) {
case 'noteon':
this.noteOn(data.pitch, data.velocity);
break;
case 'noteoff':
this.noteOff(data.pitch, data.velocity);
break;
}
},
_onprogress: function (event, clip) {
// TODO
},
_onloaded: function (clip) {
this.setClip(clip);
WX.Log.info('Clip loaded:', clip.name);
},
onReady: null,
isReady: function () {
return this.ready;
},
setClip: function (clip) {
this.clip = clip;
this.ready = true;
if (this.onReady) {
this.onReady();
}
},
loadClip: function (clip) {
WX.loadClip(
clip,
this._onloaded.bind(this),
this._onprogress.bind(this)
);
}
};
// REQUIRED: extending plug-in prototype with modules
WX.PlugIn.extendPrototype(SP1, 'Generator');
// REQUIRED: registering plug-in into WX ecosystem
WX.PlugIn.register(SP1);
})(WX);
================================================
FILE: src/plug_ins/SimpleOsc/SimpleOsc.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
(function (WX) {
'use strict';
/**
* Implements SimpleOsc insturment.
* @type {WAPL}
* @name SimpleOsc
* @class
* @memberOf WX
* @param {Object} preset Parameter preset.
* @param {GenericParam} preset.oscType Oscillator type.
* @param {GenericParam} preset.oscFreq Oscillator frequency.
* @param {ItermizedParam} preset.lfoType LFO type.
* @param {GenericParam} preset.lfoRate LFO rate.
* @param {GenericParam} preset.lfoDepth LFO depth.
*/
function SimpleOsc(preset) {
// REQUIRED: adding necessary modules
WX.PlugIn.defineType(this, 'Generator');
// patching, lfo frequency modulation
this._lfo = WX.OSC();
this._lfoGain = WX.Gain();
this._osc = WX.OSC();
this._amp = WX.Gain();
this._osc.to(this._amp).to(this._output);
this._lfo.to(this._lfoGain).to(this._osc.detune);
this._lfo.start(0);
this._osc.start(0);
this._amp.gain.value = 0.0;
// parameter definition
WX.defineParams(this, {
oscType: {
type: 'Itemized',
name: 'Waveform',
default: 'sine', // all code-side representation should be 'value'
model: WX.WAVEFORMS
},
oscFreq: {
type: 'Generic',
name: 'Freq',
default: WX.mtof(60),
min: 20.0,
max: 5000.0,
unit: 'Hertz'
},
lfoType: {
type: 'Itemized',
name: 'LFO Type',
default: 'sine',
model: WX.WAVEFORMS
},
lfoRate: {
type: 'Generic',
name: 'Rate',
default: 1.0,
min: 0.0,
max: 20.0,
unit: 'Hertz'
},
lfoDepth: {
type: 'Generic',
name: 'Depth',
default: 1.0,
min: 0.0,
max: 500.0,
unit: 'LinearGain'
}
});
WX.PlugIn.initPreset(this, preset);
}
SimpleOsc.prototype = {
info: {
name: 'SimpleOsc',
version: '0.0.2',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Generator',
description: '1 OSC with LFO'
},
defaultPreset: {
oscType: 'sine',
oscFreq: WX.mtof(60),
lfoType: 'sine',
lfoRate: 1.0,
lfoDepth: 1.0
},
$oscType: function (value, time, rampType) {
this._osc.type = value;
},
$oscFreq: function (value, time, rampType) {
this._osc.frequency.set(value, time, rampType);
},
$lfoType: function (value, time, rampType) {
this._lfo.type = value;
},
$lfoRate: function (value, time, rampType) {
this._lfo.frequency.set(value, time, rampType);
},
$lfoDepth: function (value, time, rampType) {
this._lfoGain.gain.set(value, time, rampType);
},
/**
* Start a note with pitch, velocity at time in seconds.
* @param {Number} pitch MIDI pitch
* @param {Number} velocity MIDI velocity.
* @param {Number} time Time in seconds.
*/
noteOn: function (pitch, velocity, time) {
time = (time || WX.now);
this._amp.gain.set(velocity / 127, [time, 0.02], 3);
this.params.oscFreq.set(WX.mtof(pitch), time + 0.02, 0);
// this.$oscFreq(WX.mtof(pitch), time + 0.02, 0);
},
/**
* Stop a note at time in seconds.
* @param {Number} time Time in seconds.
*/
noteOff: function (time) {
time = (time || WX.now);
this._amp.gain.set(0.0, [time, 0.2], 3);
},
/**
* Route incoming event data from other WAAX input devices.
* @param {String} action Action type: ['noteon', 'noteoff']
* @param {Object} data Event data.
* @param {Object} data.pitch MIDI Pitch
* @param {Object} data.velocity MIDI Velocity.
*/
onData: function (action, data) {
switch (action) {
case 'noteon':
this.noteOn(data.pitch, data.velocity);
break;
case 'noteoff':
this.noteOff();
break;
}
}
};
WX.PlugIn.extendPrototype(SimpleOsc, 'Generator');
WX.PlugIn.register(SimpleOsc);
})(WX);
================================================
FILE: src/plug_ins/StereoDelay/StereoDelay.js
================================================
/**
* @wapl StereoDelay
* @author Hongchan Choi (hoch, hongchan.choi@gmail.com)
*/
(function (WX) {
'use strict';
/** REQUIRED: plug-in constructor **/
function StereoDelay(preset) {
// REQUIRED: adding necessary modules
WX.PlugIn.defineType(this, 'Processor');
// patching
this._lDelay = WX.Delay();
this._rDelay = WX.Delay();
this._lFeedback = WX.Gain();
this._rFeedback = WX.Gain();
this._lXtalk = WX.Gain();
this._rXtalk = WX.Gain();
this._dry = WX.Gain();
this._wet = WX.Gain();
var _splitter = WX.Splitter(2);
var _merger = WX.Merger(2);
// source distribution
this._input.to(_splitter, this._dry);
// left channel
_splitter.connect(this._lDelay, 0);
this._lDelay.to(this._lFeedback);
this._lFeedback.to(this._lDelay, this._rXtalk);
this._lXtalk.to(this._lDelay);
this._lDelay.connect(_merger, 0, 0);
// right channel
// NOTE: splitter only uses left channel feed.
// (to be revisited)
_splitter.connect(this._rDelay, 0);
this._rDelay.to(this._rFeedback);
this._rFeedback.to(this._rDelay, this._lXtalk);
this._rXtalk.to(this._rDelay);
this._rDelay.connect(_merger, 0, 1);
// summing
_merger.to(this._wet);
this._dry.to(this._output);
this._wet.to(this._output);
// parameters
WX.defineParams(this, {
delayTimeLeft: {
type: 'Generic',
name: 'L Delay',
default: 0.125,
min: 0.025,
max: 5,
unit: 'Seconds'
},
delayTimeRight: {
type: 'Generic',
name: 'R Delay',
default: 0.25,
min: 0.025,
max: 5,
unit: 'Seconds'
},
feedbackLeft: {
type: 'Generic',
name: 'L FB',
default: 0.25,
min: 0.0,
max: 1.0
},
feedbackRight: {
type: 'Generic',
name: 'R FB',
default: 0.125,
min: 0.0,
max: 1.0
},
crosstalk: {
type: 'Generic',
name: 'Crosstalk',
default: 0.1,
min: 0.0,
max: 1.0
},
mix: {
type: 'Generic',
name: 'Mix',
default: 0.2,
min: 0.0,
max: 1.0
}
});
// REQUIRED: initializing instance with preset
WX.PlugIn.initPreset(this, preset);
}
/** REQUIRED: plug-in prototype **/
StereoDelay.prototype = {
// REQUIRED: plug-in info
info: {
name: 'StereoDelay',
version: '0.0.3',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Processor',
description: 'Pingpong Delay with Feedback Control'
},
// REQUIRED: plug-in default preset
defaultPreset: {
delayTimeLeft: 0.125,
delayTimeRight: 0.250,
feedbackLeft: 0.250,
feedbackRight: 0.125,
crosstalk: 0.1,
mix: 0.2
},
$delayTimeLeft: function (value, time, rampType) {
this._lDelay.delayTime.set(value, time, rampType);
},
$delayTimeRight: function (value, time, rampType) {
this._rDelay.delayTime.set(value, time, rampType);
},
$feedbackLeft: function (value, time, rampType) {
this._lFeedback.gain.set(value, time, rampType);
},
$feedbackRight: function (value, time, rampType) {
this._rFeedback.gain.set(value, time, rampType);
},
$crosstalk: function (value, time, rampType) {
this._lXtalk.gain.set(value, time, rampType);
this._rXtalk.gain.set(value, time, rampType);
},
$mix: function (value, time, rampType) {
this._dry.gain.set(1.0 - value, time, rampType);
this._wet.gain.set(value, time, rampType);
}
};
// REQUIRED: extending plug-in prototype with modules
WX.PlugIn.extendPrototype(StereoDelay, 'Processor');
// REQUIRED: registering plug-in into WX ecosystem
WX.PlugIn.register(StereoDelay);
})(WX);
================================================
FILE: src/plug_ins/WXS1/wxs1.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
(function (WX) {
'use strict';
/**
* Implements monophonic subtractive synthsizer.
* @type {WAPL}
* @param {Object} preset Parameter preset.
* @param {Number} preset.osc1type Oscillator 1 waveform type.
* @param {Number} preset.osc1octave Oscillator 1 octave.
* @param {Number} preset.osc1gain Oscillator 1 gain.
* @param {Number} preset.osc2type Oscillator 2 waveform type.
* @param {Number} preset.osc2detune Oscillator 2 detune.
* @param {Number} preset.osc2gain Oscillator 2 gain.
* @param {Number} preset.glide Pitch glide time in seconds.
* @param {Number} preset.cutoff LPF cutoff frequency.
* @param {Number} preset.reso LPF resonance.
* @param {Number} preset.filterMod Filter modulation amount.
* @param {Number} preset.filterAttack Filter envelope attack.
* @param {Number} preset.filterDecay Filter envelope decay.
* @param {Number} preset.filterSustain Filter envelope sustain.
* @param {Number} preset.filterRelease Filter envelope release.
* @param {Number} preset.ampAttack Amplitude envelope attack.
* @param {Number} preset.ampDecay Amplitude envelope decay.
* @param {Number} preset.ampSustain Amplitude envelope sustain.
* @param {Number} preset.ampRelease Amplitude envelope release.
* @param {Number} preset.output Plug-in output gain.
*/
function WXS1(preset) {
WX.PlugIn.defineType(this, 'Generator');
this._osc1 = WX.OSC();
this._osc2 = WX.OSC();
this._osc1gain = WX.Gain();
this._osc2gain = WX.Gain();
this._lowpass = WX.Filter();
this._amp = WX.Gain();
this._osc1.to(this._osc1gain).to(this._lowpass);
this._osc2.to(this._osc2gain).to(this._lowpass);
this._lowpass.to(this._amp);
this._amp.to(this._output);
this._osc1.start(0);
this._osc2.start(0);
// close envelope by default
this._amp.gain.value = 0.0;
// for monophonic behaviour
this._pitchTimeStamps = {};
// parameter definition
WX.defineParams(this, {
osc1type: {
type: 'Itemized',
name: 'Waveform',
default: 'square',
model: WX.WAVEFORMS
},
osc1octave: {
type: 'Generic',
name: 'Octave',
default: 0,
min: -5,
max: 5,
unit: 'Octave'
},
osc1gain: {
type: 'Generic',
name: 'Gain',
default: 0.5,
min: 0.0,
max: 1.0,
unit: 'LinearGain'
},
osc2type: {
type: 'Itemized',
name: 'Waveform',
default: 'square',
model: WX.WAVEFORMS
},
osc2detune: {
type: 'Generic',
name: 'Semitone',
default: 0,
min: -60,
max: 60,
unit: 'Semitone'
},
osc2gain: {
type: 'Generic',
name: 'Gain',
default: 0.5,
min: 0.0,
max: 1.0,
unit: 'LinearGain'
},
glide: {
type: 'Generic',
name: 'Glide',
default: 0.02,
min: 0.006,
max: 1.0,
unit: 'Seconds'
},
cutoff: {
type: 'Generic',
name: 'Cutoff',
default: 1000,
min: 20,
max: 5000,
unit: 'Hertz'
},
reso: {
type: 'Generic',
name: 'Reso',
default: 0.0,
min: 0.0,
max: 20.0,
unit: ''
},
filterMod: {
type: 'Generic',
name: 'FiltMod',
default: 1.0,
min: 0.25,
max: 8.0,
unit: ''
},
filterAttack: {
type: 'Generic',
name: 'FiltAtt',
default: 0.02,
min: 0.0,
max: 5.0,
unit: 'Seconds'
},
filterDecay: {
type: 'Generic',
name: 'FiltDec',
default: 0.04,
min: 0.0,
max: 5.0,
unit: 'Seconds'
},
filterSustain: {
type: 'Generic',
name: 'FiltSus',
default: 0.25,
min: 0.0,
max: 1.0
},
filterRelease: {
type: 'Generic',
name: 'FiltRel',
default: 0.2,
min: 0.0,
max: 10.0,
unit: 'Seconds'
},
ampAttack: {
type: 'Generic',
name: 'Att',
default: 0.02,
min: 0.0,
max: 5.0,
unit: 'Seconds'
},
ampDecay: {
type: 'Generic',
name: 'Dec',
default: 0.04,
min: 0.0,
max: 5.0,
unit: 'Seconds'
},
ampSustain: {
type: 'Generic',
name: 'Sus',
default: 0.25,
min: 0.0,
max: 1.0
},
ampRelease: {
type: 'Generic',
name: 'Rel',
default: 0.2,
min: 0.0,
max: 10.0,
unit: 'Seconds'
}
});
WX.PlugIn.initPreset(this, preset);
}
WXS1.prototype = {
info: {
name: 'WXS1',
version: '0.0.3',
api_version: '1.0.0-alpha',
author: 'Hongchan Choi',
type: 'Generator',
description: '2 OSC Monophonic Subtractive Synth'
},
defaultPreset: {
osc1type: 'square',
osc1octave: -1,
osc1gain: 0.4,
osc2type: 'square',
osc2detune: 7.0,
osc2gain: 0.4,
glide: 0.02,
cutoff: 140,
reso: 18.0,
filterMod: 7,
filterAttack: 0.01,
filterDecay: 0.07,
filterSustain: 0.5,
filterRelease: 0.03,
ampAttack: 0.01,
ampDecay: 0.44,
ampSustain: 0.2,
ampRelease: 0.06,
output: 0.8
},
$osc1type: function (value, time, rampType) {
this._osc1.type = value;
},
$osc1octave: function (value, time, rampType) {
this._osc1.detune.set(value * 1200, time, rampType);
},
$osc1gain: function (value, time, rampType) {
this._osc1gain.gain.set(value, time, rampType);
},
$osc2type: function (value, time, rampType) {
this._osc2.type = value;
},
$osc2detune: function (value, time, rampType) {
this._osc2.detune.set(value * 100, time, rampType);
},
$osc2gain: function (value, time, rampType) {
this._osc2gain.gain.set(value, time, rampType);
},
$cutoff: function (value, time, rampType) {
this._lowpass.frequency.set(value, time, rampType);
},
$reso: function (value, time, rampType) {
this._lowpass.Q.set(value, time, rampType);
},
// Returns a key index with the most recent pitch in the map. If all keys
// are off, returns null.
_getCurrentPitch: function () {
var latestPitch = null,
latestTimeStamp = 0;
for (var pitch in this._pitchTimeStamps) {
var timeStamp = this._pitchTimeStamps[pitch];
if (timeStamp > latestTimeStamp) {
latestTimeStamp = timeStamp;
latestPitch = pitch;
}
}
return latestPitch;
},
_changePitch: function (pitch, time) {
time = (time || WX.now) + this.params.glide.get();
var freq = WX.mtof(pitch);
this._osc1.frequency.set(freq, time, 1);
this._osc2.frequency.set(freq, time, 1);
},
_startEnvelope: function (time) {
time = (time || WX.now);
var p = this.params,
aAtt = p.ampAttack.get(),
aDec = p.ampDecay.get(),
fAmt = p.filterMod.get() * 1200,
fAtt = p.filterAttack.get(),
fDec = p.filterDecay.get(),
fSus = p.filterSustain.get();
// attack
this._amp.gain.set(1.0, [time, aAtt], 3);
this._lowpass.detune.set(fAmt, [time, fAtt], 3);
// decay
this._amp.gain.set(fSus, [time + aAtt, aDec], 3);
this._lowpass.detune.set(fAmt * fSus, [time + fAtt, fDec], 3);
},
_releaseEnvelope: function (time) {
time = (time || WX.now);
var p = this.params;
// cancel pre-programmed envelope data points
this._amp.gain.cancel(time);
this._lowpass.detune.cancel(time);
// release
this._amp.gain.set(0.0, [time, p.ampRelease.get()], 3);
this._lowpass.detune.set(0.0, [time, p.filterRelease.get()], 3);
},
onData: function (action, data) {
switch (action) {
case 'noteon':
this._pitchTimeStamps[data.pitch] = data.time;
var pitch = this._getCurrentPitch();
// The first key will start envelopes.
if (Object.keys(this._pitchTimeStamps).length === 1) {
this._changePitch(pitch, data.time);
this._startEnvelope(data.time);
} else {
this._changePitch(pitch, data.time);
}
break;
case 'noteoff':
if (this._pitchTimeStamps.hasOwnProperty(data.pitch)) {
delete this._pitchTimeStamps[data.pitch];
}
var pitch = this._getCurrentPitch();
// There is no key pressed. Release envelope.
if (pitch === null) {
this._releaseEnvelope(data.time);
} else {
this._changePitch(pitch, data.time);
}
break;
}
}
};
WX.PlugIn.extendPrototype(WXS1, 'Generator');
WX.PlugIn.register(WXS1);
})(WX);
================================================
FILE: src/waax.core.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
//
// Parameter Abstractions
//
// parameter types for internal reference
var PARAM_TYPES = [
'Generic',
'Itemized',
'Boolean'
];
// units for paramter
var PARAM_UNITS = [
'',
'Octave',
'Semitone',
'Seconds',
'Milliseconds',
'Samples',
'Hertz',
'Cents',
'Decibels',
'LinearGain',
'Percent',
'BPM'
];
// utility: check if the param arg is numeric
function wxparam_checkNumeric(arg, defaultValue) {
if (WX.isNumber(arg)) {
return arg;
} else if (arg === undefined) {
return defaultValue;
} else {
WX.Log.error('Invalid parameter configuration.');
}
}
// Parameter factory. Creates an instance of paramter class.
function wxparam_create(options) {
if (PARAM_TYPES.indexOf(options.type) < 0) {
WX.Log.error('Invalid Parameter Type.');
}
switch (options.type) {
case 'Generic':
return new GenericParam(options);
case 'Itemized':
return new ItemizedParam(options);
case 'Boolean':
return new BooleanParam(options);
}
}
/**
* Generic parameter(numerical and ranged) abstraction. Usually called by
* {@link WX.defineParams} method.
* @name GenericParam
* @class
* @param {Object} options Parameter configruation.
* @param {String} options.name User-defined parameter name.
* @param {String} options.unit Parameter unit.
* @param {Number} options.default Default value.
* @param {Number} options.value Parameter value.
* @param {Number} options.min Minimum value.
* @param {Number} options.max Maximum value.
* @param {Object} options._parent Reference to associated Plug-in.
*/
function GenericParam(options) {
this.init(options);
}
GenericParam.prototype = {
/**
* Initializes instance with options.
* @memberOf GenericParam
* @param {Object} options Paramter configuration.
*/
init: function (options) {
this.type = 'Generic';
this.name = (options.name || 'Parameter');
this.unit = (options.unit || '');
this.value = this.default = wxparam_checkNumeric(options.default, 0.0);
this.min = wxparam_checkNumeric(options.min, 0.0);
this.max = wxparam_checkNumeric(options.max, 1.0);
// parent, reference to the plug-in
this._parent = options._parent;
// handler callback
this.$callback = options._parent['$' + options._paramId];
},
/**
* Sets parameter value with time and ramp type. Calls back
* a corresponding handler.
* @memberOf GenericParam
* @param {Number} value Parameter target value
* @param {Number|Array} time time or array of [start time, time constant]
* @param {Number} rampType WAAX ramp type
*/
set: function (value, time, rampType) {
// set value in this parameter instance
this.value = WX.clamp(value, this.min, this.max);
// then call hander if it's defined
if (this.$callback) {
this.$callback.call(this._parent, this.value, time, rampType);
}
},
/**
* Returns the paramter value. Note that this is not a computed value
* of WA AudioParam instance.
* @memberOf GenericParam
* @return {Number} Latest paramter value.
*/
get: function () {
return this.value;
}
};
/**
* Itemized parameter abstraction. Usually called by {@link WX.defineParams}
* method.
* @name ItemizedParam
* @class
* @param {Object} options Parameter configruation.
* @param {String} options.name User-defined parameter name.
* @param {String} options.model Option items.
* @param {Number} options.default Default item.
* @param {Number} options.value Current item.
* @param {Object} options._parent Reference to associated Plug-in.
*/
function ItemizedParam(options) {
this.init(options);
}
ItemizedParam.prototype = {
/**
* Initializes instance with options.
* @memberOf ItemizedParam
* @param {Object} options Paramter configuration.
*/
init: function (options) {
// assertion
if (!WX.isArray(options.model)) {
Log.error('Model is missing.');
}
if (!WX.validateModel(options.model)) {
Log.error('Invalid Model.');
}
// initialization
this.type = 'Itemized';
this.name = (options.name || 'Select');
this.model = options.model;
this.default = (options.default || this.model[0].value);
this.value = this.default;
// caching parent
this._parent = options._parent;
// handler callback assignment
this.$callback = options._parent['$' + options._paramId];
},
/**
* Sets parameter value with time and ramp type. Calls back
* a corresponding handler.
* @memberOf ItemizedParam
* @param {Number} value Parameter target value
* @param {Number|Array} time time or array of
* [start time, time constant]
* @param {Number} rampType WAAX ramp type
*/
set: function (value, time, rampType) {
// check if value is valid
if (WX.findKeyByValue(this.model, value)) {
this.value = value;
if (this.$callback) {
this.$callback.call(this._parent, this.value, time, rampType);
}
} else {
WX.Log.warn('Invalid value (value not found in model).');
}
},
/**
* Returns the paramter value. Note that this is not a computed value
* of WA AudioParam instance.
* @memberOf ItemizedParam
* @return {Number} Latest paramter value.
*/
get: function () {
return this.value;
},
/**
* Returns the reference of items (WAAX model).
* @memberOf ItemizedParam
* @return {Array} WAAX model associated with the parameter.
*/
getModel: function () {
return this.model;
}
};
/**
* Boolean parameter abstraction. Usually called by {@link WX.defineParams}
* method.
* @name BooleanParam
* @class
* @param {Object} options Parameter configruation.
* @param {String} options.name User-defined parameter name.
* @param {Number} options.default Default value.
* @param {Number} options.value Current value.
* @param {Object} options._parent Reference to associated Plug-in.
*/
function BooleanParam(options) {
this.init(options);
}
BooleanParam.prototype = {
/**
* Initializes instance with options.
* @memberOf BooleanParam
* @param {Object} options Paramter configuration.
*/
init: function (options) {
if (!WX.isBoolean(options.default)) {
Log.error('Invalid value for Boolean Parameter.');
}
this.type = 'Boolean';
this.name = (options.name || 'Toggle');
this.value = this.default = options.default;
this._parent = options._parent;
// handler callback assignment
this.$callback = options._parent['$' + options._paramId];
},
/**
* Sets parameter value with time and ramp type. Calls back
* a corresponding handler.
* @memberOf BooleanParam
* @param {Number} value Parameter target value
* @param {Number|Array} time time or array of
* [start time, time constant]
* @param {Number} rampType WAAX ramp type
*/
set: function (value, time, rampType) {
if (WX.isBoolean(value)) {
this.value = value;
if (this.$callback) {
this.$callback.call(this._parent, this.value, time, rampType);
}
}
},
/**
* Returns the paramter value. Note that this is not a computed value
* of WA AudioParam instance.
* @memberOf BooleanParam
* @return {Number} Latest paramter value.
*/
get: function () {
return this.value;
}
};
/**
* Defines parameters by specified options.
* @memberOf WX
* @param {Object} plugin WAAX Plug-in
* @param {Object} paramOptions A collection of parameter option objects
* . See {@link GenericParam}, {@link ItemizedParam} and
* {@link BooleanParam} for available parameter options.
* @example
* WX.defineParams(this, {
* oscFreq: {
* type: 'Generic',
* name: 'Freq',
* default: WX.mtof(60),
* min: 20.0,
* max: 5000.0,
* unit: 'Hertz'
* },
* ...
* };
*/
WX.defineParams = function (plugin, paramOptions) {
for (var key in paramOptions) {
paramOptions[key]._parent = plugin;
paramOptions[key]._paramId = key;
plugin.params[key] = wxparam_create(paramOptions[key]);
}
};
/**
* Create an envelope generator function for WA audioParam.
* @param {...Array} array Data points of
* [value, offset time, ramp type]
* @returns {Function} Envelope generator function.
* function(start time, scale factor)
* @example
* // build an envelope generator with relative data points
* var env = WX.Envelope([0.0, 0.0], [0.8, 0.01, 1], [0.0, 0.3, 2]);
* // changes gain with an envelope starts at 2.0 sec with 1.2
* amplification.
* synth.set('gain', env(2.0, 1.2));
*/
WX.Envelope = function () {
var args = arguments;
return function (startTime, amplifier) {
var env = [];
startTime = (startTime || 0);
amplifier = (amplifier || 1.0);
for (var i = 0; i < args.length; i++) {
var val = args[i][0], time;
// when time argument is array, branch to setTargetAtValue
if (WX.isArray(args[i][1])) {
time = [startTime + args[i][1][0], startTime + args[i][1][1]];
env.push([val * amplifier, time, 3]);
}
// otherwise use step, linear or exponential ramp
else {
time = startTime + args[i][1];
env.push([val * amplifier, time, (args[i][2] || 0)]);
}
}
return env;
};
};
//
// Plug-in Abstractions
//
// plug-in types
var PLUGIN_TYPES = [
'Generator',
'Processor',
'Analyzer'
];
// registered plug-ins
var registered = {
Generator: [],
Processor: [],
Analyzer: []
};
/**
* Plug-In base class.
* @name PlugInAbstract
* @class
*/
function PlugInAbstract () {
this.params = {};
}
PlugInAbstract.prototype = {
/**
* Connects a plug-in output to the other plug-in's input or a WA node.
* Note that this does not support multiple outgoing connection.
* (fanning-out)
* @memberOf PlugInAbstract
* @param {WAPL|AudioNode} target WAPL(Web Audio Plug-In)
* compatible plug-in or WA node.
* @returns {WAPL|AudioNode}
*/
to: function (target) {
// when the target is a plug-in with inlet.
if (target._inlet) {
this._outlet.to(target._inlet);
return target;
}
// or it might simply be a WA node.
else {
try {
this._outlet.to(target);
return target;
} catch (error) {
WX.Log.error('Connection failed. Invalid patching.');
}
}
},
/**
* Disconnects all outgoing connections fomr plug-in.
* @memberOf PlugInAbstract
*/
cut: function () {
this._outlet.cut();
},
/**
* Sets a plug-in parameter. Supports dynamic parameter assignment.
* @memberOf PlugInAbstract
* @param {String} param Parameter name.
* @param {Array|Number} arg An array of data points or a single value.
* @return {WAPL} Self-reference for method chaining.
* @example
* // setting parameter with an array
* myeffect.set('gain', [[0.0], [1.0, 0.01, 1], [0.0, 0.5, 2]]);
* // setting parameter with a value (immediate change)
* myeffect.set('gain', 0.0);
*/
set: function (param, arg) {
if (WX.hasParam(this, param)) {
// check if arg is a value or array
if (WX.isArray(arg)) {
// if env is an array, iterate envelope data
// where array is arg_i = [value, time, rampType]
for (var i = 0; i < arg.length; i++) {
this.params[param].set.apply(this, arg[i]);
}
} else {
// otherwise change the value immediately
this.params[param].set(arg, WX.now, 0);
}
}
return this;
},
/**
* Gets a paramter value.
* @memberOf PlugInAbstract
* @param {String} param Parameter name.
* @return {*} Paramter value. Returns null when a paramter not found.
*/
get: function (param) {
if (WX.hasParam(this, param)) {
return this.params[param].get();
} else {
return null;
}
},
/**
* Sets plug-in preset, which is a collection of parameters. Note that
* setting a preset changes all the associated parameters immediatley.
* @memberOf PlugInAbstract
* @param {Object} preset A collection of paramters.
*/
setPreset: function (preset) {
for (var param in preset) {
this.params[param].set(preset[param], WX.now, 0);
}
},
/**
* Gets a current plug-in paramters as a collection. Note that the
* collection is created on the fly. It is a clone of current parameter
* values.
* @memberOf PlugInAbstract
* @return {Object} Preset.
*/
getPreset: function () {
var preset = {};
for (var param in this.params) {
preset[param] = this.params[param].get();
}
return preset;
}
};
/**
* Generator plug-in class. No audio inlet.
* @name Generator
* @class
* @extends PlugInAbstract
* @param {Object} params
* @param {Number} params.output Plug-in output gain.
*/
function Generator() {
// extends PlugInAbstract
PlugInAbstract.call(this);
// creating essential WA nodes
this._output = WX.Gain();
this._outlet = WX.Gain();
// and patching
this._output.to(this._outlet);
// paramter definition
WX.defineParams(this, {
output: {
type: 'Generic',
name: 'Output',
default: 1.0,
min: 0.0,
max: 1.0,
unit: 'LinearGain'
}
});
}
Generator.prototype = {
/**
* Parameter handler for params.output
* @memberOf Generator
* @private
*/
$output: function (value, time, rampType) {
this._output.gain.set(value, time, rampType);
}
};
// extends prototype with PlugInAbstract
WX.extend(Generator.prototype, PlugInAbstract.prototype);
/**
* Processor plug-in class. Features both inlet and outlet.
* @name Processor
* @class
* @extends PlugInAbstract
* @param {Object} params
* @param {Generic} params.input Input gain.
* @param {Generic} params.output Output gain.
* @param {Boolean} params.bypass Bypass switch.
*/
function Processor() {
// extends PlugInAbstract
PlugInAbstract.call(this);
// WA nodes
this._inlet = WX.Gain();
this._input = WX.Gain();
this._output = WX.Gain();
this._active = WX.Gain();
this._bypass = WX.Gain();
this._outlet = WX.Gain();
// patching
this._inlet.to(this._input, this._bypass);
this._output.to(this._active).to(this._outlet);
this._bypass.to(this._outlet);
// initialization for bypass
this._active.gain.value = 1.0;
this._bypass.gain.value = 0.0;
WX.defineParams(this, {
input: {
type: 'Generic',
name: 'Input',
default: 1.0,
min: 0.0,
max: 1.0,
unit: 'LinearGain'
},
output: {
type: 'Generic',
name: 'Output',
default: 1.0,
min: 0.0,
max: 1.0,
unit: 'LinearGain'
},
bypass: {
type: 'Boolean',
name: 'Bypass',
default: false
}
});
}
Processor.prototype = {
/**
* Parameter handler for params.input
* @memberOf Processor
* @private
*/
$input: function (value, time, rampType) {
this._input.gain.set(value, time, rampType);
},
/**
* Parameter handler for params.output
* @memberOf Processor
* @private
*/
$output: function (value, time, rampType) {
this._output.gain.set(value, time, rampType);
},
/**
* Parameter handler for params.bypass
* @memberOf Processor
* @private
*/
$bypass: function(value, time, rampType) {
time = (time || WX.now);
if (value) {
this._active.gain.set(0.0, time, 0);
this._bypass.gain.set(1.0, time, 0);
} else {
this._active.gain.set(1.0, time, 0);
this._bypass.gain.set(0.0, time, 0);
}
}
};
// extends PlugInAbstract
WX.extend(Processor.prototype, Generator.prototype);
/**
* Analyzer plug-in class. Features both inlet, outlet and analyzer.
* @name Analyzer
* @class
* @extends PlugInAbstract
* @param {Object} params
* @param {Generic} params.input Input gain.
*/
function Analyzer() {
PlugInAbstract.call(this);
this._inlet = WX.Gain();
this._input = WX.Gain();
this._analyzer = WX.Analyzer();
this._outlet = WX.Gain();
this._inlet.to(this._input).to(this._analyzer);
this._inlet.to(this._outlet);
WX.defineParams(this, {
input: {
type: 'Generic',
name: 'Input',
default: 1.0,
min: 0.0,
max: 1.0,
unit: 'LinearGain'
}
});
}
Analyzer.prototype = {
/**
* Parameter handler for params.input
* @private
* @memberOf Analyzer
*/
$input: function (value, time, xtype) {
this._input.gain.set(value, time, xtype);
}
};
// extends PlugInAbstract
WX.extend(Analyzer.prototype, PlugInAbstract.prototype);
//
// Plug-in utilities
//
/**
* @namespace WX.PlugIn
*/
WX.PlugIn = {};
/**
* Defines type of a plug-in. Required in plug-in definition.
* @param {WAPL} plugin Target plug-in.
* @param {String} type Plug-in type. ['Generator', 'Processor',
* 'Analyzer']
*/
WX.PlugIn.defineType = function (plugin, type) {
// check: length should be less than 3
if (PLUGIN_TYPES.indexOf(type) < 0) {
WX.Log.error('Invalid Plug-in type.');
}
// branch on plug-in type
switch (type) {
case 'Generator':
Generator.call(plugin);
break;
case 'Processor':
Processor.call(plugin);
break;
case 'Analyzer':
Analyzer.call(plugin);
break;
}
};
/**
* Initializes plug-in preset. Merges default preset with user-defined
* preset. Required in plug-in definition.
* @param {WAPL} plugin Target plug-in.
* @param {Object} preset Preset.
*/
WX.PlugIn.initPreset = function (plugin, preset) {
var merged = WX.clone(plugin.defaultPreset);
WX.extend(merged, preset);
plugin.setPreset(merged);
};
/**
* Extends plug-in prototype according to the type. Required in plug-in
* definition.
* @param {WAPL} plugin Target plug-in.
* @param {String} type Plug-in type. ['Generator', 'Processor',
* 'Analyzer']
*/
WX.PlugIn.extendPrototype = function (plugin, type) {
// check: length should be less than 3
if (PLUGIN_TYPES.indexOf(type) < 0) {
WX.Log.error('Invalid Plug-in type.');
}
// branch on plug-in type
switch (type) {
case 'Generator':
WX.extend(plugin.prototype, Generator.prototype);
break;
case 'Processor':
WX.extend(plugin.prototype, Processor.prototype);
break;
case 'Analyzer':
WX.extend(plugin.prototype, Analyzer.prototype);
break;
}
};
/**
* Registers the plug-in prototype to WX namespace. Required in plug-in
* definition.
* @param {Function} PlugInClass Class reference (function name) of
* plug-in.
*/
WX.PlugIn.register = function (PlugInClass) {
var info = PlugInClass.prototype.info;
// hard check version info
if (WX.getVersion() < info.api_version) {
// FATAL: PlugInClass is incompatible with WX Core.
WX.Log.error(PlugInClass.name, ': FATAL. incompatible WAAX version.');
}
// register PlugInClass in WX namespace
registered[info.type].push(PlugInClass.name);
WX[PlugInClass.name] = function (preset) {
return new PlugInClass(preset);
};
};
/**
* Returns a list of regsitered plug-ins of a certain type.
* @param {String} type Plug-in Type.
* @return {Array} A list of plug-ins.
*/
WX.PlugIn.getRegistered = function (type) {
var plugins = null;
if (PLUGIN_TYPES.indexOf(type) > -1) {
switch (type) {
case undefined:
plugins = registered.Generator.slice(0);
plugins = plugins.concat(registered.Processor.slice(0));
plugins = plugins.concat(registered.Analyzer.slice(0));
break;
case 'Generator':
plugins = registered.Generator.slice(0);
break;
case 'Processor':
plugins = registered.Processor.slice(0);
break;
case 'Analyzer':
plugins = registered.Analyzer.slice(0);
break;
}
}
return plugins;
};
// WAAX is ready to serve!
WX.Log.info('WAAX', WX.getVersion(), '(' + WX.srate + 'Hz)');
================================================
FILE: src/waax.extension.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
/**
* Injects into window.AudioNode
* @namespace AudioNode
*/
/**
* Connects a WA node to the other WA nodes. Note that this method is
* injected to AudioNode.prototype. Supports multiple outgoing
* connections. (fanning out) Returns the first WA node to enable method
* chaining.
* @memberOf AudioNode
* @param {...AudioNode} nodes WA nodes.
* @return {AudioNode} The first target WA node.
*/
AudioNode.prototype.to = function () {
for (var i = 0; i < arguments.length; i++) {
this.connect(arguments[i]);
}
return arguments[0];
};
/**
* Disconnects outgoing connection of a WA node. Note that this method is
* injected to AudioNode.prototype.
* @memberOf AudioNode
*/
AudioNode.prototype.cut = function () {
this.disconnect();
};
/**
* Injects into window.AudioParam
* @namespace AudioParam
*/
/**
* Equivalent of AudioParam.cancelScheduledValues. Cancels scheduled value
* after a given starting time.
* @memberOf AudioParam
* @method
* @see http://www.w3.org/TR/webaudio/#dfn-cancelScheduledValues
*/
AudioParam.prototype.cancel = AudioParam.prototype.cancelScheduledValues;
/**
* Manipulates AudioParam dynamically.
* @memberOf AudioParam
* @param {Number} value Target parameter value
* @param {Number|Array} time Automation start time. With rampType 3, this
* argument must be an array specifying [start time, time constant].
* @param {Number} rampType Automation ramp type. 0 = step, 1 = linear,
* 2 = exponential, 3 = target value [start, time constant].
* @see http://www.w3.org/TR/webaudio/#methodsandparams-AudioParam-section
*/
AudioParam.prototype.set = function (value, time, rampType) {
var now = WX.now;
switch (rampType) {
case 0:
case undefined:
time = (time < now) ? now : time;
this.setValueAtTime(value, time);
// TO FIX: when node is not connected, automation will not work
// this hack handles the error
if (time <= now && value !== this.value) {
this.value = value;
}
break;
case 1:
time = (time < now) ? now : time;
this.linearRampToValueAtTime(value, time);
break;
case 2:
time = (time < now) ? now : time;
value = value <= 0.0 ? 0.00001 : value;
this.exponentialRampToValueAtTime(value, time);
break;
case 3:
time[0] = (time[0] < now) ? now : time[0];
value = value <= 0.0 ? 0.00001 : value;
this.setTargetAtTime(value, time[0], time[1]);
break;
}
};
// ECMA Script 5 getter for current time and srate.
Object.defineProperties(WX, {
/**
* Returns current audio context time. (ECMA Script 5 Getter)
* @memberOf WX
* @return {Number} Current audio context time in seconds.
*/
now: {
get: function () {
return WX._ctx.currentTime;
}
},
/**
* Returns current audio device sample rate. (ECMA Script 5 Getter)
* @memberOf WX
* @return {Number} Current sample rate.
*/
srate: {
get: function () {
return WX._ctx.sampleRate;
},
}
});
/**
* Creates an instance of WA Gain node.
* @return {AudioNode} WA Gain node.
* @see http://www.w3.org/TR/webaudio/#GainNode
*/
WX.Gain = function() {
return WX._ctx.createGain();
};
/**
* Creates an instance of WA Oscillator node.
* @return {AudioNode} WA Oscillator node.
* @see http://www.w3.org/TR/webaudio/#OscillatorNode
*/
WX.OSC = function() {
return WX._ctx.createOscillator();
};
/**
* Creates an instance of WA Delay node.
* @return {AudioNode} WA Delay node.
* @see http://www.w3.org/TR/webaudio/#DelayNode
*/
WX.Delay = function() {
return WX._ctx.createDelay();
};
/**
* Creates an instance of WA BiquadFilter node.
* @return {AudioNode} WA BiquadFilter node.
* @see http://www.w3.org/TR/webaudio/#BiquadFilterNode
*/
WX.Filter = function() {
return WX._ctx.createBiquadFilter();
};
/**
* Creates an instance of WA DynamicCompressor node.
* @return {AudioNode} WA DynamicsCompressor node.
* @see http://www.w3.org/TR/webaudio/#DynamicsCompressorNode
*/
WX.Comp = function() {
return WX._ctx.createDynamicsCompressor();
};
/**
* Creates an instance of WA Convolver node.
* @return {AudioNode} WA Convolver node.
* @see http://www.w3.org/TR/webaudio/#ConvolverNode
*/
WX.Convolver = function() {
return WX._ctx.createConvolver();
};
/**
* Creates an instance of WA WaveShaper node.
* @return {AudioNode} WA WaveShaper node.
* @see http://www.w3.org/TR/webaudio/#WaveShaperNode
*/
WX.WaveShaper = function() {
return WX._ctx.createWaveShaper();
};
/**
* Creates an instance of WA BufferSource node.
* @return {AudioNode} WA BufferSource node.
* @see http://www.w3.org/TR/webaudio/#BufferSourceNode
*/
WX.Source = function() {
return WX._ctx.createBufferSource();
};
/**
* Creates an instance of WA Analyzer node.
* @return {AudioNode} WA Analyzer node.
* @see http://www.w3.org/TR/webaudio/#AnalyzerNode
*/
WX.Analyzer = function() {
return WX._ctx.createAnalyser();
};
/**
* Creates an instance of WA Panner node.
* @return {AudioNode} WA Panner node.
* @see http://www.w3.org/TR/webaudio/#PannerNode
*/
WX.Panner = function() {
return WX._ctx.createPanner();
};
/**
* Creates an instance of WA PerodicWave object.
* @return {AudioNode} WA PeriodicWave object.
* @see http://www.w3.org/TR/webaudio/#PeriodicWave
*/
WX.PeriodicWave = function () {
return WX._ctx.createPeriodicWave.apply(WX._ctx, arguments);
};
/**
* Creates an instance of WA Splitter node.
* @return {AudioNode} WA Splitter node.
* @see http://www.w3.org/TR/webaudio/#SplitterNode
*/
WX.Splitter = function () {
return WX._ctx.createChannelSplitter.apply(WX._ctx, arguments);
};
/**
* Creates an instance of WA Merger node.
* @return {AudioNode} WA Merger node.
* @see http://www.w3.org/TR/webaudio/#MergerNode
*/
WX.Merger = function () {
return WX._ctx.createChannelMerger.apply(WX._ctx, arguments);
};
/**
* Creates an instance of WA Buffer object.
* @return {AudioNode} WA Buffer object.
* @see http://www.w3.org/TR/webaudio/#Buffer
*/
WX.Buffer = function () {
return WX._ctx.createBuffer.apply(WX._ctx, arguments);
};
WX.WAVEFORMS = [
{ key: 'Sine', value: 'sine' },
{ key: 'Square', value: 'square' },
{ key: 'Sawtooth', value: 'sawtooth' },
{ key: 'Triangle', value: 'triangle' }
];
WX.FILTER_TYPES = [
{ key:'LP' , value: 'lowpass' },
{ key:'HP' , value: 'highpass' },
{ key:'BP' , value: 'bandpass' },
{ key:'LS' , value: 'lowshelf' },
{ key:'HS' , value: 'highshelf' },
{ key:'PK' , value: 'peaking' },
{ key:'BR' , value: 'notch' },
{ key:'AP' , value: 'allpass' }
];
================================================
FILE: src/waax.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
/**
* @namespace WX
*/
window.WX = {
_VERSION: '1.0.0-alpha3'
};
// Monkey patching and feature detection.
var _webkitAC = window.hasOwnProperty('webkitAudioContext');
var _AC = window.hasOwnProperty('AudioContext');
if (!_webkitAC && !_AC) {
throw new Error('Error: Web Audio API is not supported on your browser.');
} else {
if (_webkitAC && !_AC) {
window.AudioContext = window.webkitAudioContext;
}
}
// Create AudioContext.
WX._ctx = new AudioContext();
// Checking the minimum dependency.
var _dependency = [
// 'createStereoPanner',
];
for (var i = 0; i < _dependency.length; i++) {
if (typeof WX._ctx[_dependency[i]] === 'undefined') {
var message = 'Error: "' + _dependency + '" is not supported in AudioContext.';
throw new Error(message);
}
}
/**
* @typedef WAPL
* @description Web Audio API Plug-in Object.
* @type {Object}
*/
/**
* @typedef WXModel
* @description Contains a model for data binding.
* @type {Array}
* @example
* var model = [
* { key:'Sine', value:'sine' },
* { key:'Sawtooth', value:'sawtooth' }
* ...
* ];
*/
/**
* @typedef WXClip
* @description WAAX abstraction of sample and meta data.
* @type {Object}
* @property {String} name User-defined name of sample.
* @property {String} url URL of audio file.
* @property {Object} buffer A placeholder for ArrayBuffer object.
* @example
* var clip = {
* name: 'Cool Sample',
* url: 'http://mystaticdata.com/samples/coolsample.wav',
* buffer: null
* };
*/
/**
* @typedef WXSampleZone
* @description WAAX abstraction of sampler instrument data.
* @type {Object}
* @property {WXClip} clip WXClip object.
* @property {Number} basePitch Original sample pitch.
* @property {Boolean} loop Looping flag.
* @property {Number} loopStart Loop start point in seconds.
* @property {Number} loopEnd Loop end point in seconds.
* @property {Number} pitchLow Low pitch bound.
* @property {Number} pitchHigh High pitch bound.
* @property {Number} velocityLow Low velocity bound.
* @property {Number} velocityHigh High velocity bound.
* @property {Boolean} pitchModulation Switch for pitch sensitivity modulation.
* @property {Boolean} velocityModulatio Switch for velocity sensitivity modulation.
* @example
* var sampleZone = {
* clip: WXClip
* basePitch: 60 // samples original pitch
* loop: true,
* loopStart: 0.1,
* loopEnd: 0.5,
* pitchLow: 12, // pitch low bound
* pitchHigh: 96, // pitch high bound
* velocityLow: 12, // velocity lower bound
* velocityHigh: 127, // velocity high bound
* pitchModulation: true, // use pitch modulation
* velocityModulatio: true // use velocity moduation
* };
*/
================================================
FILE: src/waax.timebase.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
/**
* @typedef MBST
* @description Measure, beat, sixteenth, tick aka musical timebase.
* @type {Object}
* @property {Number} measure Measure.
* @property {Number} beat Beat.
* @property {Number} sixteenth Sixteenth.
* @property {Number} tick Tick.
*/
// MBT - measure, beat, sixteenth, tick aka musical timebase.
// Tick - atomic unit for musical timebase.
// Second - unit for linear timebase.
var _TICKS_PER_BEAT = 480,
_TICKS_PER_MEASURE = _TICKS_PER_BEAT * 4,
_TICKS_PER_SIXTEENTH = _TICKS_PER_BEAT * 0.25;
// internal: unique ID for notes/events (4 bytes uid generator)
// see: http://stackoverflow.com/a/8809472/4155261
function generate_uid4(){
var t = Date.now();
var id = 'xxxx'.replace(/[x]/g, function(c) {
var r = (t + Math.random() * 16) % 16 | 0;
t = Math.floor(t / 16);
return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16);
});
return id;
}
/**
* Note abstraction. Instantiated by {@link WX.Note}.
* @name Note
* @class
* @param {Number} pitch MIDI pitch.
* @param {Number} velocity MIDI velocity.
* @param {Number} start Start time in tick.
* @param {Number} duration Note durtion in tick.
*/
function Note(pitch, velocity, start, duration) {
this.pitch = (pitch || 60);
this.velocity = (velocity || 100);
this.start = (start || 0);
this.duration = (duration || 120);
}
Note.prototype = {
/**
* Changes note duration by delta.
* @memberOf Note
* @param {Number} delta Duration.
*/
changeDuration: function (delta) {
this.duration += ~~(delta);
this.duration = Math.max(this.duration, 1);
},
/**
* Returns the note end time in tick.
* @memberOf Note
* @return {Number}
*/
getEnd: function () {
return this.start + this.duration;
},
/**
* Sets note properties from a note.
* @memberOf Note
* @param {Note} note
*/
setNote: function (note) {
for (var prop in note) {
this[prop] = note[prop];
}
},
/**
* Moves note pitch by delta.
* @memberOf Note
* @param {Number} delta Pitch displacement.
*/
translatePitch: function (delta) {
this.pitch += ~~(delta);
this.pitch = WX.clamp(this.pitch, 0, 127);
},
/**
* Moves note pitch by delta.
* @memberOf Note
* @param {Number} delta Pitch displacement.
*/
translateStart: function (delta) {
this.start += ~~(delta);
this.start = Math.max(this.start, 0);
},
/**
* Returns current properties in a string.
* @memberOf Note
* @return {String}
*/
toString: function () {
return this.pitch + ',' + this.velocity + ',' +
this.start + ',' + this.duration;
},
/**
* Returns start time for numeric operation.
* @memberOf Note
* @return {Number}
*/
valueOf: function () {
return this.start;
}
};
/**
* NoteClip abstraction. A collection of Note objects. Instantiated by
* WX.NoteClip.
* @name NoteClip
* @class
*/
function NoteClip() {
this._init();
}
NoteClip.prototype = {
// Initializes or resets note clip.
_init: function () {
this.notes = {};
this.start = 0;
this.end = 0;
this.size = 0;
},
/**
* Deletes and returns a note from clip. Returns null when not found.
* @memberOf NoteClip
* @param {String} id Note ID.
* @return {Note | null}
*/
delete: function (id) {
if (this.notes.hasOwnProperty(id)) {
var note = this.notes[id];
delete this.notes[id];
this.size--;
return note;
}
return null;
},
/**
* Flush out noteclip content.
* @memberOf NoteClip
*/
empty: function () {
this.notes = {};
this.size = 0;
},
/**
* Find a note that contains a point specified by pitch and tick. Then
* returns note ID. Returns null when not found.
* Note that this impl might produce an undesirable result since it
* simply catches the first note detected in the iteration. There is no
* perfect solution over this problem, so I chose to make it robust
* instead.
* @memberOf NoteClip
* @param {Number} pitch MIDI pitch
* @param {Number} tick Position in tick.
* @return {String | null}
*/
findNoteIdAtPosition: function (pitch, tick) {
for (var id in this.notes) {
var note = this.notes[id];
if (note && note.pitch === pitch) {
if (note.start <= tick && tick <= note.getEnd()) {
return id;
}
}
}
return null;
},
/**
* Find notes in a area specified by pitch and tick. Then returns an array
* of notes. Returns null when nothing in target area.
* Note that this method only care for start time of note. If a note
* starts before than the area, it will not be included in the selection.
* @memberOf NoteClip
* @param {Number} minPitch Lower bound pitch.
* @param {Number} maxPitch Upper bound pitch.
* @param {Number} startTick Start time in tick.
* @param {Number} endTick End time in tick.
* @return {Array | null}
*/
findNotesIdInArea: function (minPitch, maxPitch, startTick, endTick) {
var bucket = [];
for (var id in this.notes) {
var note = this.notes[id];
if (note) {
if (minPitch <= note.pitch && note.pitch <= maxPitch) {
if (startTick <= note.start && note.start <= endTick) {
bucket.push(id);
}
}
}
}
return (bucket.length > 0) ? bucket : null;
},
/**
* Returns note from clip with id.
* @memberOf NoteClip
* @param {String} id Note ID.
* @return {Object}
*/
get: function (id) {
if (this.hasId(id)) {
return this.notes[id];
}
return null;
},
/**
* Retunrs an array with all the ID of notes.
* @memberOf NoteClip
* @return {Array}
*/
getAllId: function () {
return Object.keys(this.notes);
},
/**
* Returns clip size.
* @memberOf NoteClip
* @return {Number}
*/
getSize: function () {
return this.size;
},
/**
* Probes clip with note id.
* @memberOf NoteClip
* @param {String} id Note id.
* @return {Boolean} True if clip contains the note.
*/
hasId: function (id) {
return this.notes.hasOwnProperty(id);
},
/**
* Iterates all the notes with callback function.
* @memberOf NoteClip
* @param {callback_noteclip_iterate} callback Process for iteration.
*/
iterate: function (callback) {
var index = 0;
for (var id in this.notes) {
callback(id, this.notes[id], index++);
}
},
/**
* Callback for note clip interation. Called by NoteClip.iterate.
* @callback callback_noteclip_iterate
* @param {String} id Note ID.
* @param {Note} Note object.
* @param {Number} index Iteration index.
* @see NoteClip.iterate
*/
/**
* Pushes a new note into clip. Returns ID of the new note.
* @memberOf NoteClip
* @param {Note} note
* @return {String} Note ID.
*/
push: function (note) {
var id;
do {
id = generate_uid4();
} while (this.hasId(id));
this.notes[id] = note;
this.size++;
return id;
},
/**
* Set a note with a specific ID. If the same ID found, update properties
* of the note. Otherwise, create a new note with a specified ID. Usually
* this method is used in the collaborative context.
* @memberOf NoteClip
* @param {String} id Note ID.
* @param {Note} note Note object.
*/
set: function (id, note) {
if (this.hasId(id)) {
this.notes[id].setNote(note);
} else {
this.notes[id] = note;
}
},
/**
* Returns an array of notes within a timespan. Returns null when not found.
* Note that this method actually returns Note objects, not IDs.
* @memberOf NoteClip
* @param {Number} start Start time in tick.
* @param {Number} end End time in tick.
* @return {Array | null}
*/
scanNotesInTimeSpan: function (start, end) {
var bucket = [];
for (var id in this.notes) {
var note = this.notes[id];
if (note) {
if (start <= note.start && note.start <= end) {
bucket.push(note);
}
}
}
return (bucket.length > 0) ? bucket : null;
}
};
//
// Router class should be here.
//
// NOTES:
// absolute time: time in audioContext
// sec: linear time in seconds
// tick: musical time, atomic unit of MBT timebase (varies on BPM)
// * if not specified in method signature, it is handled as tick
// all the internal calculation should be in seconds
/**
* Transport abstraction. Singleton and instantiated by default.
* @name Transport
* @class
* @param {Number} BPM Beat per minute.
*/
function Transport(BPM) {
this._init(BPM || 120);
}
Transport.prototype = {
// TEMPORARY
_flushPlaybackQueue: function () {
this._playbackQueue.length = 0;
},
// Sets current playhead position by seconds (audioContext).
_setPlayheadPosition: function (second) {
this._now = second;
this._absOrigin = WX.now - this._now;
},
// Scans notes in current scan range.
_scheduleNotesInScanRange: function () {
if (this._needsScan) {
var notes = null;
if (this._source) {
notes = this._source.scanNotesInTimeSpan(
this.sec2tick(this._scanStart),
this.sec2tick(this._scanEnd)
);
}
// push notes into playbackQueue
if (notes) {
for (var i = 0; i < notes.length; i++) {
if (this._playbackQueue.indexOf(notes[i]) < 0) {
this._playbackQueue.push(notes[i]);
}
}
}
this._needsScan = false;
}
// send queued notes to target plug-ins
if (this._playbackQueue.length > 0) {
for (var i = 0; i < this._playbackQueue.length; i++) {
var note = this._playbackQueue[i],
start = this._absOrigin + this.tick2sec(note.start),
end = start + this.tick2sec(note.dur);
// schedule notes by onData method
// this.targets[0].onData('noteon', {
// pitch: note.pitch,
// velocity: note.velocity,
// time: start
// });
// this.targets[0].onData('noteoff', {
// pitch: note.pitch,
// velocity: 0,
// time :end
// });
}
}
},
// Move the scan range of scan forward by runner.
_advanceScanRange: function () {
// Advances the scan range to the next block, if the scan end point is
// close enough (< 16.7ms) to playhead.
if (this._scanEnd - this._now < 0.0167) {
this._scanStart = this._scanEnd;
this._scanEnd = this._scanStart + this._lookAhead;
this._needsScan = true;
}
},
// Reset the scan range based on current playhead position.
_resetScanRange: function () {
this._scanStart = this._now;
this._scanEnd = this._scanStart + this._lookahead;
this._needsScan = true;
},
// Update assigned transport MUI element. update data:
// MBST format of now_t, loopEnd, loopStart, BPM
_updateView: function () {
},
// Runs the transport (update every 16.7ms)
_run: function () {
// console.log(this._now);
if (this._RUNNING) {
// add time elapsed to now_t by checking now_ac
var absNow = WX.now;
this._now += (absNow - this._absLastNow);
this._absLastNow = absNow;
// scan notes in range
this._scheduleNotesInScanRange();
// advance when transport is running
this._advanceScanRange();
// update transport view
this._updateView();
// flush played notes
this._flushPlaybackQueue();
// check loop flag
if (this._LOOP) {
if (this._loopEnd - (this._now + this._lookAhead) < 0) {
this._setPlayheadPosition(this._loopStart - (this._loopEnd - this._now));
}
}
// TODO
// Transport should have a router (note clips > in-out > plug-in)
// 0. iterate through the router entries
// 1. note clip entry in router -> get target plug-in
// 2. scan note clips
// 3. send data to plug-in
//
// if there is any registered plug-in for metronome
// schedule metronome as well
// TODO: pulse metronome
// if (this.USE_METRONOME && this.metronome) {
// // if nextPulse is in lookahead range, schedule it
// this.metronome.play(this.now, this.lookahead);
// }
// ORIGINAL CODE:
// var tick = mtime2tick(this.nextClick);
// if (this.getLinearTime(tick) < this.ltime_now + this.lookahead) {
// var accent = (this.nextClick.beat % 4 === 0) ? true : false;
// this.metronome.play(this.getAbsoluteTime(tick), accent);
// this.nextClick.beat += 1;
// }
}
// schedule next step
requestAnimationFrame(this._run.bind(this));
},
// initializing transport with BPM
_init: function (BPM) {
// origin in absolute time and 'now' reference
this._BPM = BPM;
this._lastBPM = BPM;
// beats/ticks in seconds
this._BIS = 0.0;
this._TIS = 0.0;
// origin by audio context time
// it is '0' position of playhead in linear time
this._absOrigin = 0.0;
this._absLastNow = 0.0;
// now, loop, lookAhead by transport time
this._now = 0.0;
this._loopStart = 0.0;
this._loopEnd = 0.0;
this._lookAhead = 0.0;
// playback scan range and dirty flag
this._scanStart = 0.0;
this._scanEnd = this._lookAhead;
this._needsScan = true;
// transport view element
this._transportView = null;
// TEMPORARY
this._playbackQueue = [];
this._source = null; // noteclip
this._target = null; // plug-in
// switches
this._RUNNING = false;
this._LOOP = false;
this._USE_METRONOME = false;
// set BPM and initiate runner
this.setBPM(BPM);
this._run();
},
// Transport Public Methods
//
// NOTE: the max integer in JavaScript is 9007199254740992.
// With this number as ticks, the maximum offset of a note is
// { measure: 1145324612, beat: 1, sixteenth: 0, tick: 32 }
// It is about 38177487 minutes in BPM of 120. (~636291 hours, ~26512 days)
// So, this number is good enough to cover general music making.
//
// Having separated notation like { measure, beat, sixteenth, tick } is
// not really necessary. It is easy to translate both way when it's needed.
// (i.e. displaying timing data on UI.)
/**
* Converts tick to second based on transport tempo.
* @memberOf Transport
* @param {Number} tick Tick (atomic musical time unit)
* @return {Number}
*/
tick2sec: function (tick) {
return tick * this._TIS;
},
/**
* Converts second to tick based on transport tempo.
* @memberOf Transport
* @param {Number} sec Second
* @return {Number}
*/
sec2tick: function (sec) {
return sec / this._TIS;
},
/**
* Starts playback.
* @memberOf Transport
*/
start: function () {
// Arrange time references.
var absNow = WX.now;
this._absOrigin = absNow - this._now;
this._absLastNow = absNow;
// Reset scan range.
this._resetScanRange();
// Transport is running.
this._RUNNING = true;
},
/**
* Pauses current playback.
* @memberOf Transport
*/
pause: function () {
this._RUNNING = false;
this._flushPlaybackQueue();
},
/**
* Sets playhead position by tick.
* @memberOf Transport
* @param {Number} tick Playhead position in ticks.
*/
setNow: function (tick) {
this._setPlayheadPosition(this.tick2sec(tick));
this._resetScanRange();
},
/**
* Returns playhead position by tick.
* @memberOf Transport
* @return {Number}
*/
getNow: function () {
return this.sec2tick(this._now);
},
/**
* Rewinds playhead to the beginning.
* @memberOf Transport
*/
rewind: function () {
this._setPlayheadPosition(0.0);
},
/**
* Sets loop start position by tick.
* @memberOf Transport
* @param {Number} tick Loop start in tick.
*/
setLoopStart: function (tick) {
this._loopStart = this.tick2sec(tick);
},
/**
* Sets loop end position by tick.
* @memberOf Transport
* @param {Number} tick Loop end in tick.
*/
setLoopEnd: function (tick) {
this._loopEnd = this.tick2sec(tick);
},
/**
* Returns loop start by tick.
* @memberOf Transport
* @return {Number}
*/
getLoopStart: function () {
return this.sec2tick(this._loopStart);
},
/**
* Returns loop end by tick.
* @memberOf Transport
* @return {Number}
*/
getLoopEnd: function () {
return this.sec2tick(this._loopEnd);
},
/**
* Toggles or sets loop status.
* @memberOf Transport
* @param {Boolean|undefined} bool Loop state. If undefined, it toggles the current state.
*/
toggleLoop: function (bool) {
if (bool === undefined) {
this._LOOP = !this._LOOP;
} else {
if (WX.isBoolean(bool)) {
this._LOOP = bool;
} else {
WX.Log.error('Invalid parameter. Use boolean value.');
}
}
},
/**
* Sets transport BPM.
* @memberOf Transport
* @param {Number} BPM Beat per minute.
*/
setBPM: function (BPM) {
// calculates change factor
this._BPM = (BPM || 120);
var factor = this._lastBPM / this._BPM;
this._lastBPM = this._BPM;
// recalcualte beat in seconds, tick in seconds
this._BIS = 60.0 / this._BPM;
this._TIS = this._BIS / _TICKS_PER_BEAT;
// lookahead is 16 ticks (1/128th note)
this._lookAhead = this._TIS * 16;
// update time references based on tempo change
this._now *= factor;
this._loopStart *= factor;
this._loopEnd *= factor;
this._absOrigin = WX.now - this._now;
},
/**
* Returns current BPM.
* @memberOf Transport
* @return {Number}
*/
getBPM: function () {
return this._BPM;
},
/**
* Returns current running status of transport.
* @memberOf Transport
* @return {Boolean}
*/
isRunning: function () {
return this._RUNNING;
}
};
/**
* Converts MBST(measure, beat, sixteenth, tick) format to tick.
* @memberOf WX
* @param {MBST} Musical time in MBST format.
* @return {Number} Musical time in tick.
*/
WX.mbst2tick = function (mtime) {
return (mtime.measure || 0) * _TICKS_PER_MEASURE +
(mtime.beat || 0) * _TICKS_PER_BEAT +
(mtime.sixteenth || 0) * _TICKS_PER_SIXTEENTH +
(mtime.tick || 0);
};
/**
* Converts tick to MBST(measure, beat, sixteenth, tick) format.
* @memberOf WX
* @param {Number} tick Tick.
* @return {MBST} Musical time in MBST format.
*/
WX.tick2mbst = function (tick) {
return {
measure: ~~(tick / _TICKS_PER_MEASURE),
beat: ~~((tick % _TICKS_PER_MEASURE) / _TICKS_PER_BEAT),
sixteenth: ~~((tick % _TICKS_PER_BEAT) / _TICKS_PER_SIXTEENTH),
tick: ~~(tick % _TICKS_PER_SIXTEENTH)
};
};
/**
* Creates a Note instance.
* @memberOf WX
* @param {Number} pitch MIDI pitch (0~127)
* @param {Number} velocity MIDI velocity (0~127)
* @param {Number} start Note start time in tick.
* @param {Number} duration Note durtion in tick.
* @return {Note}
*/
WX.Note = function (pitch, velocity, start, duration) {
return new Note(pitch, velocity, start, duration);
};
/**
* Create a NoteClip instance.
* @memberOf WX
* @return {NoteClip}
*/
WX.NoteClip = function () {
return new NoteClip();
};
/**
* Singleton instance of Transporter.
* @type {Transport}
*/
WX.Transport = new Transport(120);
================================================
FILE: src/waax.util.js
================================================
// Copyright 2011-2014 Hongchan Choi. All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
/**
* Features system-wide logging utilities.
* @namespace WX.Log
*/
WX.Log = {
// log level. (1: info, 2: warn, 3: error)
_LEVEL: 1
};
/**
* Sets logging level.
* @param {Number} level logging level { 1: info, 2: warn, 3: error }
*/
WX.Log.setLevel = function (level) {
this._LEVEL = level;
};
/**
* Prints an informative message on console.
* @param {...String} message messages to be printed
*/
WX.Log.info = function () {
if (this._LEVEL > 1) return;
var args = Array.prototype.slice.call(arguments);
args.unshift('[WX:info]');
window.console.log.apply(window.console, args);
};
/**
* Prints a warning message on console.
* @param {...String} message messages to be printed.
*/
WX.Log.warn = function () {
if (this._LEVEL > 2) return;
var args = Array.prototype.slice.call(arguments);
args.unshift('[WX:warn]');
window.console.log.apply(window.console, args);
};
/**
* Prints an error message on console and throws an error.
* @param {...String} message messages to be printed.
*/
WX.Log.error = function () {
var args = Array.prototype.slice.call(arguments);
args.unshift('[WX:error]');
window.console.log.apply(window.console, args);
throw new Error('[WX:error]');
};
/**
* Returns WAAX API version number. (semantic version)
* @returns {Number} WAAX API version number
* @see http://semver.org/
*/
WX.getVersion = function () {
return this._VERSION;
};
// Object manipulation and music math.
// : Features utilities for object manipulation, music math and more.
// Note that all utility methods are under WX namespace.
// References
// : http://underscorejs.org/docs/underscore.html
// : https://github.com/libpd/libpd/blob/master/pure-data/src/x_acoustics.c
/**
* Checks if an argument is a JS object.
* @returns {Boolean}
*/
WX.isObject = function (obj) {
return obj === Object(obj);
};
/**
* Checks if an argument is a JS function.
* @returns {Boolean}
*/
WX.isFunction = function (fn) {
return toString.call(fn) === '[object Function]';
};
/**
* Checks if an argument is a JS array.
* @returns {Boolean}
*/
WX.isArray = function (arr) {
return toString.call(arr) === '[object Array]';
};
/**
* Checks if the type of an argument is Number.
* @returns {Boolean}
*/
WX.isNumber = function (num) {
return toString.call(num) === '[object Number]';
};
/**
* Checks if the type of an argument is Boolean.
* @returns {Boolean}
*/
WX.isBoolean = function (bool) {
return toString.call(bool) === '[object Boolean]';
};
/**
* Checks if a WAAX plug-in has a parameter
* @param {String} param Parameter name
* @returns {Boolean}
*/
WX.hasParam = function(plugin, param) {
return hasOwnProperty.call(plugin.params, param);
};
/**
* Extends target object with the source object. If two objects have
* duplicates, properties in target object will be overwritten.
* @param {Object} target Object to be extended
* @param {Object} source Object to be injected
* @returns {Object} A merged object.
*/
WX.extend = function (target, source) {
for (var prop in source) {
target[prop] = source[prop];
}
return target;
};
/**
* Retunrs a clone of JS object. This returns shallow copy.
* @param {Object} source Object to be cloned
* @returns {Object} Cloned object
*/
WX.clone = function (source) {
var obj = {};
for (var prop in source) {
obj[prop] = source[prop];
}
return obj;
};
/**
* Validates a WAAX model. This verifies if all the keys in the
* model is unique. WAAX model is a collection of key-value pairs
* that is useful for data binding or templating.
* @param {Array} model WAAX model
* @returns {Boolean}
* @example
* // Example WAAX model for waveform types
* var model = [
* { key:'Sine', value:'sine' },
* { key:'Sawtooth', value:'sawtooth' }
* ...
* ];
*/
WX.validateModel = function (model) {
if (model.length === 0) {
return false;
}
var keys = [];
for (var i = 0; i < model.length; i++) {
if (keys.indexOf(model[i].key) > -1) {
return false;
} else {
keys.push(model[i].key);
}
}
return true;
};
/**
* Finds a key from a model by a value.
* @param {Array} model WAAX model
* @param {*} value Value in model
* @returns {String|null} Key or null when not found.
* @see {@link WX.Model} for WAAX model specification.
*/
WX.findKeyByValue = function (model, value) {
for (var i = 0; i < model.length; i++) {
if (model[i].value === value) {
return model[i].key;
}
}
return null;
};
/**
* Finds a value from a model by a key.
* @param {Array} model WAAX model
* @param {String} key Key in model
* @returns {*|null} Value or null when not found.
* @see {@link WX.validateModel} for WAAX model specification.
*/
WX.findValueByKey = function (model, key) {
for (var i = 0; i < model.length; i++) {
if (model[i].key === key) {
return model[i].value;
}
}
return null;
};
/**
* Clamps a number into a range specified by min and max.
* @param {Number} value Value to be clamped
* @param {Number} min Range minimum
* @param {Number} max Range maximum
* @return {Number} Clamped value
*/
WX.clamp = function (value, min, max) {
return Math.min(Math.max(value, min), max);
};
/**
* Generates a floating point random number between min and max.
* @param {Number} min Range minimum
* @param {Number} max Range maximum
* @return {Number} A floating point random number
*/
WX.random2f = function (min, max) {
return min + Math.random() * (max - min);
};
/**
* Generates an integer random number between min and max.
* @param {Number} min Range minimum
* @param {Number} max Range maximum
* @return {Number} An integer random number
*/
WX.random2 = function (min, max) {
return Math.round(min + Math.random() * (max - min));
};
/**
* Converts a MIDI pitch number to frequency.
* @param {Number} midi MIDI pitch (0 ~ 127)
* @return {Number} Frequency (Hz)
*/
WX.mtof = function (midi) {
if (midi <= -1500) return 0;
else if (midi > 1499) return 3.282417553401589e+38;
else return 440.0 * Math.pow(2, (Math.floor(midi) - 69) / 12.0);
};
/**
* Converts frequency to MIDI pitch.
* @param {Number} freq Frequency
* @return {Number} MIDI pitch
*/
WX.ftom = function (freq) {
return Math.floor(
freq > 0 ?
Math.log(freq/440.0) / Math.LN2 * 12 + 69 : -1500
);
};
/**
* Converts power to decibel. Note that it is off by 100dB to make it
* easy to use MIDI velocity to change volume. This is the same
* convention that PureData uses. This behaviour might change in the
* future.
* @param {Number} power Power
* @return {Number} Decibel
*/
WX.powtodb = function (power) {
if (power <= 0) return 0;
else {
var db = 100 + 10.0 / Math.LN10 * Math.log(power);
return db < 0 ? 0 : db;
}
};
/**
* Converts decibel to power. Note that it is off by 100dB to make it
* easy to use MIDI velocity to change volume. This is the same
* convention that PureData uses. This behaviour might change in the
* future.
* @param {Number} db Decibel
* @return {Number} Power
*/
WX.dbtopow = function (db) {
if (db <= 0) return 0;
else {
// TODO: what is 870?
if (db > 870) db = 870;
return Math.exp(Math.LN10 * 0.1 * (db - 100.0));
}
};
/**
* Converts RMS(root-mean-square) to decibel.
* @param {Number} rms RMS value
* @return {Number} Decibel
*/
WX.rmstodb = function (rms) {
if (rms <= 0) return 0;
else {
var db = 100 + 20.0 / Math.LN10 * Math.log(rms);
return db < 0 ? 0 : db;
}
};
/**
* Converts decibel to RMS(root-mean-square).
* @param {Number} db Decibel
* @return {Number} RMS value
*/
WX.dbtorms = function (db) {
if (db <= 0) return 0;
else {
// TO FIX: what is 485?
if (db > 485) db = 485;
return Math.exp(Math.LN10 * 0.05 * (db - 100.0));
}
};
/**
* Converts linear amplitude to decibel.
* @param {Number} lin Linear amplitude
* @return {Number} Decibel
*/
WX.lintodb = function (lin) {
// if below -100dB, set to -100dB to prevent taking log of zero
return 20.0 * (lin > 0.00001 ? (Math.log(lin) / Math.LN10) : -5.0);
};
/**
* Converts decibel to linear amplitude. Useful for dBFS conversion.
* @param {Number} db Decibel
* @return {Number} Linear amplitude
*/
WX.dbtolin = function (db) {
return Math.pow(10.0, db / 20.0);
};
/**
* Converts MIDI velocity to linear amplitude.
* @param {Number} velocity MIDI velocity
* @return {Number} Linear amplitude
*/
WX.veltoamp = function (velocity) {
// TODO: velocity curve here?
return velocity / 127;
};
/**
* Loads WAAX clip by XHR loading
* @param {Object} clip WAAX Clip
* @param {callback_loadClip_oncomplete} oncomplete Function called when
* completed.
* @param {callback_loadClip_onprogress} onprogress Optional.
* Callback for progress report.
* @example
* // Creates a WAAX clip on the fly.
* var clip = {
* name: 'Cool Sample',
* url: 'http://mystaticdata.com/samples/coolsample.wav',
* buffer: null
* };
* // Loads the clip and assign the buffer to a sampler plug-in.
* WX.loadClip(clip, function (clip) {
* mySampler.setBuffer(clip.buffer);
* });
*/
WX.loadClip = function (clip, oncomplete, onprogress) {
if (!oncomplete) {
WX.Log.error('Specify `oncomplete` action.');
return;
}
var xhr = new XMLHttpRequest();
xhr.open('GET', clip.url, true);
xhr.responseType = 'arraybuffer';
xhr.onprogress = function (event) {
if (onprogress) {
onprogress(event, clip);
}
};
xhr.onload = function (event) {
try {
WX._ctx.decodeAudioData(
xhr.response,
function (buffer) {
clip.buffer = buffer;
oncomplete(clip);
}
);
} catch (error) {
WX.Log.error('Loading clip failed. (XHR failure)', error.message, clip.url);
}
};
xhr.send();
};
/**
* Callback for clip loading completion. Called by {@link WX.loadClip}.
* @callback callback_loadclip_oncomplete
* @param {Object} clip WAAX clip
* @see WX.loadClip
*/
/**
* Callback for clip loading progress report. called by {@link WX.loadClip}.
* @callback callback_loadclip_onprogress
* @param {Object} event XHR progress event object
* @param {Object} clip WAAX clip
* @see WX.loadClip
* @see https://dvcs.w3.org/hg/progress/raw-file/tip/Overview.html
*/
================================================
FILE: test/index.html
================================================
Test Suite | WAAX
WX : Web Audio API eXtension
================================================
FILE: test/test-core.js
================================================
// test-core.js
// caching
var expect = chai.expect,
should = chai.should();
// Utilities
describe('Core: Utilities - object, music math and more.', function() {
describe('getVersion()', function () {
it('should return API version number.', function () {
expect(WX.getVersion()).to.equal('1.0.0-alpha3');
});
});
describe('Log.info(arg)', function () {
it('should print info message in the console.', function (done) {
WX.Log.info('this is', 'info', 'message.');
done();
});
});
describe('Log.warn(arg)', function () {
it('should print warning message in the console.', function (done) {
WX.Log.warn('this is', 'warning', 'message.');
done();
});
});
describe('Log.error(arg)', function () {
it('should print message and throw error.', function () {
expect(function () {
WX.Log.error('this is', 'error', 'message.');
}).to.throw(Error);
});
});
describe('isObject(arg)', function () {
it('should return true when input is JS object.', function () {
expect(WX.isObject({})).to.equal(true);
expect(WX.isObject([])).to.equal(true);
expect(WX.isObject('Hey')).to.equal(false);
expect(WX.isObject(1.0)).to.equal(false);
});
});
describe('isArray(arg)', function () {
it('should return true when input is Array.', function () {
expect(WX.isArray([])).to.equal(true);
expect(WX.isArray({})).to.equal(false);
expect(WX.isArray(1.0)).to.equal(false);
});
});
describe('isNumber(arg)', function () {
it('should return true when input is Number.', function () {
expect(WX.isNumber(1.0)).to.equal(true);
expect(WX.isNumber('Number')).to.equal(false);
expect(WX.isNumber({})).to.equal(false);
});
});
describe('isBoolean(arg)', function () {
it('should return true when input is Boolean.', function () {
expect(WX.isBoolean(true)).to.equal(true);
expect(WX.isBoolean(false)).to.equal(true);
expect(WX.isBoolean(1)).to.equal(false);
});
});
describe('hasParam(plugin, param)', function () {
it('should return true when plugin has the parameter.', function () {
var plugin = { params: { 'parameter': 0.0 } };
expect(WX.hasParam(plugin, 'parameter')).to.equal(true);
expect(WX.hasParam(plugin, 'notParameter')).to.equal(false);
});
});
describe('extend(target, source)', function () {
it('should add source to target object and return the extended target.',
function () {
var source = { a: 1, b: 2 },
target = { b: 3, c: 4 },
result = { a: 1, b: 2, c: 4 };
expect(WX.extend(target, source)).deep.equal(result);
}
);
});
describe('clone(source)', function () {
it('should return a cloned object.', function () {
var source = { a: 1, b: 2 },
result = { a: 1, b: 2 };
expect(WX.clone(source)).deep.equal(result);
});
});
describe('validateModel(model)', function () {
it('returns true when all the keys are unique in a model.', function () {
var valid = [
{ key:'Sine', value:'sine' },
{ key:'Sinusoid', value:'sine' }
];
var invalid = [
{ key:'Sine', value:'sine' },
{ key:'Sine', value:'sinusoid' }
];
expect(WX.validateModel(valid)).to.equal(true);
expect(WX.validateModel(invalid)).to.equal(false);
});
});
describe('findKeyByValue(model, value)', function () {
it('returns a key assodicated with a value. null when not found.',
function () {
var model = [
{ key:'Sine', value:'sine' },
{ key:'Sinusoid', value:'sine' }
];
expect(WX.findKeyByValue(model, 'sine')).to.equal('Sine');
expect(WX.findKeyByValue(model, 'sinusoid')).to.equal(null);
}
);
});
describe('findValueByKey(model, key)', function () {
it('returns a key assodicated with a value. null when not found.',
function () {
var model = [
{ key:'Sine', value:'sine' },
{ key:'Sine', value:'sinusoid' }
];
expect(WX.findValueByKey(model, 'Sine')).to.equal('sine');
expect(WX.findValueByKey(model, 'Sawtooth')).to.equal(null);
}
);
});
describe('clamp(value, min, max)', function () {
it('should clamp value into between min and max.', function () {
expect(WX.clamp(1.5, 0.0, 1.0)).to.equal(1.0);
expect(WX.clamp(-0.5, 0.0, 1.0)).to.equal(0.0);
});
});
describe('random2f(min, max)', function () {
it('should generate a floating point random value between min and max.',
function () {
var rnd = WX.random2f(0.0, 10.0);
expect(rnd).to.be.within(0.0, 10.0);
expect(parseInt(rnd, 10) === rnd).to.equal(false);
}
);
});
describe('random2(min, max)', function () {
it('should generate an integer random value between min and max.',
function () {
var rnd = WX.random2(0, 10);
expect(rnd).to.be.within(0, 10);
expect(parseInt(rnd, 10) === rnd).to.equal(true);
}
);
});
describe('mtof(midi)', function () {
it('should return frequency from MIDI pitch.', function () {
expect(WX.mtof(69)).to.equal(440.0);
expect(WX.mtof(-1500)).to.equal(0);
expect(WX.mtof(1500)).to.equal(3.282417553401589e+38);
});
});
describe('ftom(freq)', function () {
it('should return MIDI pitch from frequency.', function () {
expect(WX.ftom(440)).to.equal(69);
expect(WX.ftom(-1)).to.equal(-1500);
expect(WX.ftom(22050)).to.equal(136);
});
});
describe('powtodb(power)', function () {
it('should return decibel from signal power.', function () {
expect(WX.powtodb(1.0)).to.equal(100.0);
expect(WX.powtodb(10.0)).to.equal(110.0);
});
});
describe('dbtopow(db)', function () {
it('should return power from decibel.', function () {
expect(WX.dbtopow(0.0)).to.equal(0.0);
expect(WX.dbtopow(100.0)).to.equal(1.0);
});
});
describe('rmstodb(rms)', function () {
it('should return decibel from RMS.', function () {
expect(WX.rmstodb(0.0)).to.equal(0.0);
expect(WX.rmstodb(100.0)).to.equal(140.0);
});
});
describe('dbtorms(db)', function () {
it('should return RMS from decibel.', function () {
expect(WX.dbtorms(0.0)).to.equal(0.0);
expect(WX.dbtorms(100.0)).to.equal(1.0);
});
});
describe('veltoamp(vel)', function () {
it('should return linear amp from velocity.', function () {
expect(WX.veltoamp(0)).to.equal(0);
expect(WX.veltoamp(63)).to.equal(0.49606299212598426);
expect(WX.veltoamp(64)).to.equal(0.5039370078740157);
expect(WX.veltoamp(127)).to.equal(1.0);
});
});
});
// Core: Audio System
describe('Core: Audio System', function() {
describe('context', function () {
it('should be AudioContext.', function () {
expect(typeof WX._ctx.createGain).to.equal('function');
});
});
// TODO: there might be inconsistent between now and currentTime. be advise.
describe('now (getter)', function () {
it('should return current time in audio context.', function () {
expect(WX.now).to.be.at.least(0.0);
expect(WX.now).to.equal(WX._ctx.currentTime);
});
});
describe('srate (getter)', function () {
it('should return current sample rate of audio context.', function () {
expect(WX.srate).to.be.above(22050);
expect(WX.srate).to.equal(WX._ctx.sampleRate);
});
});
describe('Gain()', function () {
it('should return a gain node.', function () {
expect(WX.Gain()).to.have.property('gain');
});
});
describe('OSC()', function () {
it('should return an oscillator node.', function () {
expect(WX.OSC()).to.have.property('frequency');
});
});
describe('Delay()', function () {
it('should return a delay node.', function () {
expect(WX.Delay()).to.have.property('delayTime');
});
});
describe('Filter()', function () {
it('should return a biquad filter node.', function () {
expect(WX.Filter()).to.have.property('frequency');
});
});
describe('Comp()', function () {
it('should return a compressor node.', function () {
expect(WX.Comp()).to.have.property('threshold');
});
});
describe('Convolver()', function () {
it('should return a convolver node.', function () {
expect(WX.Convolver()).to.have.property('buffer');
});
});
describe('WaveShaper()', function () {
it('should return a waveshaper node.', function () {
expect(WX.WaveShaper()).to.have.property('curve');
});
});
describe('Source()', function () {
it('should return a audio buffer source node.', function () {
expect(WX.Source()).to.have.property('buffer');
});
});
describe('Analyzer()', function () {
it('should return an analyzer node.', function () {
expect(WX.Analyzer()).to.have.property('fftSize');
});
});
describe('Panner()', function () {
it('should return a panner node.', function () {
expect(WX.Panner()).to.have.property('panningModel');
});
});
describe('PeriodicWave()', function () {
it('should return a periodic wave object.', function () {
var mag = new Float32Array(256),
phase = new Float32Array(256),
wave = WX.PeriodicWave(mag, phase);
expect(typeof wave).to.equal('object');
});
});
describe('Splitter()', function () {
it('should return a channel splitter node.', function () {
var splitter = WX.Splitter();
expect(splitter.numberOfOutputs).to.equal(6);
});
});
describe('Merger()', function () {
it('should return a channel merger node.', function () {
var merger = WX.Merger();
expect(merger.numberOfInputs).to.equal(6);
});
});
describe('Buffer()', function () {
it('should return a buffer source.', function () {
expect(WX.Buffer(2, 1.0, 44100).length).to.equal(1);
expect(WX.Buffer(1, 2.0, 48000).length).to.equal(2);
});
});
describe('Envelope(arg)', function () {
it('should return an envelope generator.', function () {
var env = WX.Envelope([0.0, 0.0], [0.8, 0.01, 1], [0.0, 0.3, 2]);
var t = WX.now;
expect(env(1.0, 0.5)).deep.equal([
[0.0, 1.0, 0], [0.4, 1.01, 1], [0.0, 1.3, 2]
]);
});
});
describe('defineParams(plugin, paramDefs)', function () {
it('should define parameter instances in a plugin.', function () {
var flag = false;
var plugin = {
params: {},
$testParam: function () {
flag = true;
}
};
WX.defineParams(plugin, {
'testParam': {
type: 'Generic', unit: '',
default: 0.01, min: 0.0, max: 1.0
}
});
expect(plugin.params.testParam.get()).to.equal(0.01);
plugin.params.testParam.set(0.5, 0, 0);
expect(flag).to.equal(true);
expect(plugin.params.testParam.get()).to.equal(0.5);
});
});
describe('loadClip', function () {
it('should return a audio buffer after xhr loading success.', function (done) {
var clip = { name: 'ziggy', url: '../sound/loops/drums.mp3' };
var progress = false, complete = false;
WX.loadClip(clip,
function (clip) {
complete = true;
expect(progress).to.equal(true);
expect(complete).to.equal(true);
expect(clip.buffer.duration).to.be.within(5.19, 5.24);
done();
},
function (event) {
progress = true;
expect(event.loaded).to.be.within(0, event.total);
}
);
});
});
});
// Core: Plug-in Utilities
describe('Core: Plug-in Utilities', function () {
// dummy setup for testing
function MyGenerator(preset) {
WX.PlugIn.defineType(this, 'Generator');
WX.defineParams(this, {
p1: {
type: 'Boolean',
default: false
},
p2: {
type: 'Boolean',
default: true
}
});
WX.PlugIn.initPreset(this, preset);
}
MyGenerator.prototype = {
info: {
api_version: '1.0.0-alpha2',
type: 'Generator'
},
defaultPreset: {
p1: false,
p2: true
},
$p1: function(value, time, xtype) {
return value ? 'pass' : 'fail';
},
$p2: function(value, time, xtype) {
return value ? 'pass' : 'fail';
}
};
WX.PlugIn.extendPrototype(MyGenerator, 'Generator');
function MyProcessor() {
WX.PlugIn.defineType(this, 'Processor');
}
MyProcessor.prototype = {};
WX.PlugIn.extendPrototype(MyProcessor, 'Processor');
function MyAnalyzer() {
WX.PlugIn.defineType(this, 'Analyzer');
}
MyAnalyzer.prototype = {};
WX.PlugIn.extendPrototype(MyAnalyzer, 'Analyzer');
var gen = new MyGenerator({
p1: true,
p2: false
});
var pro = new MyProcessor(),
ana = new MyAnalyzer();
describe('defineType(plugin, type)', function () {
it('should import required components to plugin based on type specifier.',
function () {
expect(gen).to.contain.keys('params', '_output', '_outlet');
expect(pro).to.contain.keys('params', '_inlet', '_bypass');
expect(ana).to.contain.keys('params', '_inlet', '_input');
}
);
});
describe('extendPrototype(plugin, type)', function () {
it('should extend prototype with core plugin methods.',
function () {
// generator
expect(gen).to.respondTo('get');
expect(gen).to.respondTo('set');
expect(gen).to.respondTo('getPreset');
expect(gen).to.respondTo('setPreset');
expect(gen).to.respondTo('$output');
expect(gen).to.respondTo('cut');
expect(gen).to.respondTo('to');
expect(gen.$p1(true)).to.equal('pass');
expect(gen.$p2(false)).to.equal('fail');
// processor
expect(pro).to.respondTo('get');
expect(pro).to.respondTo('set');
expect(pro).to.respondTo('getPreset');
expect(pro).to.respondTo('setPreset');
expect(pro).to.respondTo('$bypass');
expect(pro).to.respondTo('$input');
expect(pro).to.respondTo('$output');
expect(pro).to.respondTo('cut');
expect(pro).to.respondTo('to');
// analyzer
expect(ana).to.respondTo('get');
expect(ana).to.respondTo('set');
expect(ana).to.respondTo('getPreset');
expect(ana).to.respondTo('setPreset');
expect(ana).to.respondTo('$input');
expect(ana).to.respondTo('cut');
expect(ana).to.respondTo('to');
}
);
});
describe('initPreset(plugin, preset)', function () {
it('should initialize plugin preset from arguments and default preset.',
function () {
expect(gen.get('p1')).to.equal(true);
expect(gen.get('p2')).to.equal(false);
}
);
});
describe('register(pluginConstructor)', function () {
it('should register plugin class under namespace WX.',
function () {
WX.PlugIn.register(MyGenerator);
var myGen = WX.MyGenerator();
expect(myGen).to.respondTo('get');
expect(myGen).to.respondTo('set');
expect(myGen).to.respondTo('getPreset');
expect(myGen).to.respondTo('setPreset');
expect(myGen).to.respondTo('$output');
expect(myGen).to.respondTo('cut');
expect(myGen).to.respondTo('to');
expect(myGen.$p1(true)).to.equal('pass');
expect(myGen.$p2(false)).to.equal('fail');
}
);
});
});
// Stock PlugIn: Fader
// Because Fader is included in the core as 'WX.Master'
describe('Plug-in: Fader', function () {
it('should set parameters correctly. (BEEP)', function (done) {
// test patch: osc is needed to run the AudioParam automation
var osc = WX.OSC();
var fader = WX.Fader({ bypass: true });
osc.start(0);
osc.to(fader._inlet);
fader.to(WX.Master);
fader.set('input', 0.25);
fader.set('dB', -6.0);
// test preset values
var preset = fader.getPreset();
expect(fader._inlet).to.have.property('gain');
expect(fader._outlet).to.have.property('gain');
expect(preset.bypass).to.equal(true);
expect(preset.mute).to.equal(false);
expect(preset.input).to.equal(0.25);
expect(preset.dB).to.equal(-6.0);
expect(fader.info.name).to.equal('Fader');
// TO FIX (hoch): revise .set method for all 3 browsers. Chrome and Safari
// work same way, so fix this for the FireFox.
setTimeout(function () {
fader._output.gain.cancel(0);
expect(fader._output.gain.value).to.equal(0.5011872053146362);
osc.stop(0);
done();
}, 100);
});
});
================================================
FILE: test/test-setup.js
================================================
(function (WX) {
var domClientVersion = document.querySelector('#client-ver'),
domWAAXVersion = document.querySelector('#waax-ver');
domClientVersion.textContent = 'CLIENT VERSION: ' +
navigator.userAgent.toLowerCase();
domWAAXVersion.textContent = 'WAAX VERSION: ' + WX.getVersion();
})(WX);
================================================
FILE: test/test-timebase.js
================================================
/**
* test-timebase.js
*
* @description mocha + chai test suite for WAAX:timebase library
* @author hoch (hongchan.choi@gmail.com)
* @version 1.0.0-alpha2
*/
// caching
var expect = chai.expect,
should = chai.should();
var TX = WX.Transport;
// test data
var TEST_DATA = [
[0, 0, 997, 1007], [1, 1, 534, 613], [2, 2, 988, 1102],
[3, 3, 85, 141], [4, 4, 1001, 1108], [5, 5, 722, 832],
[6, 6, 751, 779], [7, 7, 720, 730], [8, 8, 1188, 1265],
[9, 9, 992, 1007], [10, 10, 255, 374], [11, 11, 143, 158],
[12, 12, 565, 684], [13, 13, 286, 327], [14, 14, 215, 264],
[15, 15, 806, 833], [16, 16, 255, 269], [17, 17, 134, 216],
[18, 18, 993, 1102], [19, 19, 1006, 1114], [20, 20, 794, 813],
[21, 21, 773, 807], [22, 22, 1065, 1088], [23, 23, 1010, 1051],
[24, 24, 792, 840], [25, 25, 1114, 1178], [26, 26, 266, 318],
[27, 27, 1005, 1034], [28, 28, 307, 391], [29, 29, 573, 676],
[30, 30, 701, 805], [31, 31, 648, 687], [32, 32, 437, 474],
[33, 33, 158, 265], [34, 34, 680, 718], [35, 35, 227, 337],
[36, 36, 510, 592], [37, 37, 792, 825], [38, 38, 260, 294],
[39, 39, 518, 590], [40, 40, 350, 419], [41, 41, 493, 562],
[42, 42, 968, 993], [43, 43, 721, 746], [44, 44, 299, 340],
[45, 45, 484, 557], [46, 46, 485, 505], [47, 47, 1091, 1149],
[48, 48, 800, 826], [49, 49, 360, 443], [50, 50, 356, 463],
[51, 51, 668, 753], [52, 52, 227, 248], [53, 53, 245, 321],
[54, 54, 457, 487], [55, 55, 1059, 1139], [56, 56, 1148, 1190],
[57, 57, 12, 31], [58, 58, 799, 852], [59, 59, 515, 529],
[60, 60, 627, 682], [61, 61, 651, 678], [62, 62, 431, 535],
[63, 63, 1082, 1147], [64, 64, 1189, 1288], [65, 65, 1085, 1147],
[66, 66, 819, 935], [67, 67, 658, 685], [68, 68, 941, 1010],
[69, 69, 587, 702], [70, 70, 434, 495], [71, 71, 35, 60],
[72, 72, 677, 792], [73, 73, 230, 307], [74, 74, 149, 183],
[75, 75, 303, 357], [76, 76, 632, 683], [77, 77, 484, 576],
[78, 78, 415, 432], [79, 79, 781, 838], [80, 80, 556, 577],
[81, 81, 55, 164], [82, 82, 522, 550], [83, 83, 566, 626],
[84, 84, 1012, 1061], [85, 85, 734, 789], [86, 86, 587, 685],
[87, 87, 232, 248], [88, 88, 707, 729], [89, 89, 199, 230],
[90, 90, 683, 710], [91, 91, 183, 221], [92, 92, 495, 503],
[93, 93, 271, 291], [94, 94, 34, 137], [95, 95, 392, 454],
[96, 96, 499, 595], [97, 97, 99, 126], [98, 98, 808, 916],
[99, 99, 832, 885], [100, 100, 995, 1024], [101, 101, 808, 907],
[102, 102, 1041, 1148], [103, 103, 126, 225], [104, 104, 467, 570],
[105, 105, 150, 160], [106, 106, 519, 571], [107, 107, 106, 125],
[108, 108, 1072, 1184], [109, 109, 1121, 1178], [110, 110, 84, 194],
[111, 111, 851, 954], [112, 112, 383, 469], [113, 113, 1135, 1187],
[114, 114, 540, 576], [115, 115, 804, 892], [116, 116, 1146, 1212],
[117, 117, 244, 353], [118, 118, 359, 468], [119, 119, 311, 362],
[120, 120, 324, 378], [121, 121, 841, 881], [122, 122, 264, 288],
[123, 123, 737, 815], [124, 124, 510, 525], [125, 125, 245, 286],
[126, 126, 222, 266]
];
/**
* Utilities
*/
describe('Timebase: Util', function() {
describe('mbst2tick(mtime)', function () {
it('should return tick from MBST time.', function () {
expect(WX.mbst2tick({ beat: 0, tick: 240 })).to.equal(240);
expect(WX.mbst2tick({ beat: 1, tick: 0 })).to.equal(480);
expect(WX.mbst2tick({ beat: 1, tick: 240 })).to.equal(720);
});
});
describe('tick2mbst(tick)', function () {
it('should return MBST format from tick.', function () {
expect(WX.tick2mbst(240)).deep.equal({
measure: 0, beat: 0, sixteenth: 2, tick: 0
});
expect(WX.tick2mbst(480)).deep.equal({
measure: 0, beat: 1, sixteenth: 0, tick: 0
});
expect(WX.tick2mbst(740)).deep.equal({
measure: 0, beat: 1, sixteenth: 2, tick: 20
});
});
});
});
/**
* @class Note
*/
describe('Timebase: Note', function() {
describe('WX.Note(pitch, velocity, start, duration)', function () {
it('should return a Note object.', function () {
var note1 = WX.Note(60, 64, 0, 120);
expect(note1).to.have.property('pitch', 60);
expect(note1).to.have.property('velocity', 64);
expect(note1).to.have.property('start', 0);
expect(note1).to.have.property('duration', 120);
});
});
describe('changeDuration(delta)', function () {
it('should change note duration.', function () {
var note = WX.Note(60, 64, 0, 120);
note.changeDuration(120);
expect(note).to.have.property('duration', 240);
});
});
describe('setNote(Note)', function () {
it('should copy note properties.', function () {
var note1 = WX.Note(1, 2, 3, 4),
note2 = WX.Note();
note2.setNote(note1);
expect(note2).deep.equal(note1);
});
});
describe('translatePitch(delta)', function () {
it('should move note`s pitch by delta.', function () {
var note = WX.Note(60, 64, 0, 120);
note.translatePitch(32);
expect(note).to.have.property('pitch', 92);
note.translatePitch(64);
expect(note).to.have.property('pitch', 127);
note.translatePitch(-128);
expect(note).to.have.property('pitch', 0);
});
});
describe('translateStart(delta)', function () {
it('should move a note tick by delta.', function () {
var note = WX.Note(60, 64, 0, 120);
note.translateStart(10);
expect(note).to.have.property('start', 10);
expect(note.getEnd()).to.equal(130);
note.translateStart(-20);
expect(note).to.have.property('start', 0);
expect(note.getEnd()).to.equal(120);
});
});
describe('valueOf()', function () {
it('should return start time for easier comparison.', function () {
var note1 = WX.Note(60, 64, 0, 120),
note2 = WX.Note(60, 64, 120, 240);
expect(note1 < note2).to.equal(true);
});
});
describe('toString()', function () {
it('should return string from data.', function () {
var note1 = WX.Note(60, 64, 0, 120);
expect(note1.toString()).to.equal('60,64,0,120');
});
});
});
/**
* @class NoteClip
*/
describe('Timebase: NoteClip', function() {
describe('push(note)', function () {
it('should push multiple notes into a clip.', function () {
var noteclip = WX.NoteClip();
for (var i = 0; i < TEST_DATA.length; i++) {
noteclip.push(WX.Note.apply(WX, TEST_DATA[i]));
}
expect(noteclip.getSize()).to.equal(127);
});
});
describe('findNoteIdAtPosition(pitch, tick)', function () {
it('should return a note at pitch/tick position.', function () {
var noteclip = WX.NoteClip();
for (var n = 0; n < 10; n++) {
noteclip.push(WX.Note(60 + n, 64, 120 * n, 120 * n + 120));
}
expect(noteclip.findNoteIdAtPosition(60, 60)).to.not.equal(null);
expect(noteclip.findNoteIdAtPosition(61, 60)).to.equal(null);
});
});
describe('findNotesIdInArea(minPitch, maxPitch, startTick, endTick)',
function () {
it('should return notes at in pitch/time area.', function () {
var noteclip = WX.NoteClip();
for (var n = 0; n < 10; n++) {
noteclip.push(WX.Note(60 + n, 64, 120 * n, 120 * n + 120));
}
expect(noteclip.findNotesIdInArea(62, 64, 60, 240)).to.have.length(1);
expect(noteclip.findNotesIdInArea(66, 69, 600, 960)).to.have.length(3);
expect(noteclip.findNotesIdInArea(60, 69, 0, 1200)).to.have.length(10);
}
);
});
describe('scanNotesInTimeSpan(start, end)', function () {
it('should return notes at between start/end time.', function () {
var noteclip = WX.NoteClip();
for (var n = 0; n < 10; n++) {
noteclip.push(WX.Note(60 + n, 64, 120 * n, 120 * n + 120));
}
expect(noteclip.scanNotesInTimeSpan(60, 240)).to.have.length(2);
expect(noteclip.scanNotesInTimeSpan(600, 960)).to.have.length(4);
expect(noteclip.scanNotesInTimeSpan(0, 1200)).to.have.length(10);
});
});
describe('getSize()', function () {
it('should return note clip size.', function () {
var noteclip = WX.NoteClip();
var data = [];
for (var n = 0; n < 4; n++) {
data.push(WX.Note(60 + n, 64, 120 * n, 120 * n + 120));
}
var id0 = noteclip.push(data[0]);
var id1 = noteclip.push(data[1]);
noteclip.push(data[2]);
expect(noteclip.getSize()).to.equal(3);
noteclip.delete(id0);
expect(noteclip.getSize()).to.equal(2);
noteclip.delete(id1);
expect(noteclip.getSize()).to.equal(1);
});
});
describe('empty()', function () {
it('should empty the list.', function () {
var input = [];
for (var n = 0; n < 10; n++) {
var start = ~~(Math.random() * 1200);
input.push(WX.Note(60, 64, start, start + 120));
}
var noteclip = WX.NoteClip();
for (var i = 0; i < input.length; i++) {
noteclip.push(input[i]);
}
noteclip.empty();
expect(noteclip.getSize()).to.equal(0);
});
});
describe('iterate(callback)', function () {
it('should iterate all items with callback function.', function () {
var id = [],
noteclip = WX.NoteClip();
for (var n = 0; n < 3; n++) {
id[n] = noteclip.push(WX.Note(60 + n, 64, 120 * n, 120 * n + 120));
}
noteclip.iterate(function (id, note, index) {
note.duration *= 0.5;
note.start *= 0.5;
});
expect(noteclip.get([id[0]])).to.have.property('start', 0);
expect(noteclip.get([id[1]])).to.have.property('start', 60);
expect(noteclip.get([id[2]])).to.have.property('duration', 180);
});
});
describe('delete(note)', function () {
it('should delete items from noteclip.', function () {
var id, noteclip = WX.NoteClip();
for (var i = 0; i < TEST_DATA.length; i++) {
noteclip.push(WX.Note.apply(WX, TEST_DATA[i]));
}
id = noteclip.getAllId();
for (i = 0; i < ~~(TEST_DATA.length / 2); i++) {
noteclip.delete(id[i]);
}
var rest = TEST_DATA.length - ~~(TEST_DATA.length / 2);
expect(noteclip.getSize()).deep.equal(rest);
});
});
describe('hasId(id)', function () {
it('should return true if a note with a certain id exists in the clip.',
function () {
var noteclip = WX.NoteClip(), id;
for (var i = 0; i < TEST_DATA.length; i++) {
noteclip.push(WX.Note.apply(WX, TEST_DATA[i]));
}
id = noteclip.getAllId();
expect(noteclip.hasId(id[0])).to.equal(true);
expect(noteclip.hasId('x071')).to.equal(false);
}
);
});
});
/**
* @class Transporter
*/
describe('Timebase: Transport', function() {
beforeEach(function () {
// this will reset BPM/oldBPM to 120
TX.setBPM(120);
TX.setBPM(120);
TX.setNow(0);
});
// OK
describe('tick2sec(tick)', function () {
it('should convert tick to second.', function () {
expect(TX.tick2sec(480)).to.equal(0.5);
TX.setBPM(60);
expect(TX.tick2sec(480)).to.equal(1.0);
});
});
// OK
describe('sec2tick(sec)', function () {
it('should convert second to tick.', function () {
expect(TX.sec2tick(1.0)).to.equal(960);
TX.setBPM(60);
expect(TX.sec2tick(1.0)).to.equal(480);
});
});
// OK
describe('setBPM(BPM)', function () {
it('should set current BPM and rearrange timeline.', function () {
TX.setBPM(60); // BPM 120 -> BPM 60
expect(TX._BPM).to.equal(60);
expect(TX._BIS).to.equal(1);
expect(TX._TIS).to.be.within(0.002083, 0.002084);
expect(TX._lookAhead).to.be.within(0.033333, 0.033334);
});
});
// OK
describe('setNow(tick)', function () {
it('should set current playback position in tick.', function () {
TX.setNow(240);
expect(TX.tick2sec(TX.getNow())).to.equal(0.25);
TX.setBPM(60);
TX.setNow(240);
expect(TX.tick2sec(TX.getNow())).to.equal(0.5);
});
});
// OK
describe('setLoop(start, end)', function () {
it('should set loop points in tick.', function () {
TX.setLoopStart(120);
TX.setLoopEnd(480);
expect(TX._loopStart).to.equal(0.125);
expect(TX._loopEnd).to.equal(0.5);
});
});
describe('getBPM()', function () {
it('should return current BPM.', function () {
expect(TX.getBPM()).to.equal(120);
TX.setBPM(60);
expect(TX.getBPM()).to.equal(60);
});
});
describe('getNow()', function () {
it('should return current playhead position in tick.', function () {
TX.setNow(480);
// logic: tick is musical unit, does not change on BPM change
TX.setBPM(60);
expect(TX.getNow()).to.equal(480);
TX.setBPM(120);
expect(TX.getNow()).to.equal(480);
});
});
describe('Timed operation: start(), pause(), rewind()', function () {
it('should start, pause and rewind properly.', function (done) {
TX.start();
setTimeout(function () {
expect(TX.isRunning()).to.equal(true);
// when BPM=120, 500ms is 1 beat, that is 480 tick.
expect(TX.getNow()).to.be.within(465, 505);
setTimeout(function () {
TX.pause();
expect(TX.isRunning()).to.equal(false);
// when BPM=120, 1000ms is 2 beats, that is 960 tick.
expect(TX.getNow()).to.be.within(945, 990);
TX.rewind();
expect(TX.getNow()).to.be.equal(0.0);
done();
}, 500);
}, 500);
});
});
});