├── .editorconfig ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── README.md ├── codecov.yml ├── deploy_key.enc ├── intern.json ├── package-lock.json ├── package.json ├── run-benchmark.js ├── src ├── Container.ts ├── Injector.ts ├── NodeHandler.ts ├── Registry.ts ├── RegistryHandler.ts ├── WidgetBase.ts ├── animations │ └── cssTransitions.ts ├── d.ts ├── decorators │ ├── afterRender.ts │ ├── alwaysRender.ts │ ├── beforeProperties.ts │ ├── beforeRender.ts │ ├── customElement.ts │ ├── diffProperty.ts │ ├── handleDecorator.ts │ ├── inject.ts │ └── registry.ts ├── diff.ts ├── interfaces.d.ts ├── meta │ ├── Base.ts │ ├── Dimensions.ts │ ├── Drag.ts │ ├── Focus.ts │ ├── Intersection.ts │ ├── Matches.ts │ ├── Resize.ts │ └── WebAnimation.ts ├── mixins │ ├── Focus.ts │ ├── I18n.ts │ ├── Projector.ts │ └── Themed.ts ├── registerCustomElement.ts ├── tsx.ts └── vdom.ts ├── tests ├── benchmark │ ├── LICENSE │ ├── app │ │ ├── App.ts │ │ ├── Button.ts │ │ ├── Buttons.ts │ │ ├── Row.ts │ │ ├── Store.ts │ │ ├── index.html │ │ ├── main.css │ │ └── main.ts │ ├── runner │ │ ├── css │ │ │ ├── currentStyle.css │ │ │ ├── main.css │ │ │ ├── useMinimalCss.css │ │ │ └── useOriginalBootstrap.css │ │ ├── process-benchmark-results.ts │ │ └── src │ │ │ ├── benchmarkCli.ts │ │ │ ├── benchmarkRunner.ts │ │ │ ├── benchmarks.ts │ │ │ ├── common.ts │ │ │ ├── createResultJS.ts │ │ │ ├── createResultTable.ts │ │ │ └── webdriverAccess.ts │ └── vanillajs-non-keyed │ │ ├── Main.ts │ │ └── index.html ├── functional │ ├── Drag.ts │ ├── all.ts │ └── meta │ │ ├── Drag.html │ │ └── Drag.ts ├── run.html ├── support │ ├── likelySubtags.ts │ ├── loadCustomElements.ts │ ├── loadJsdom.ts │ ├── nls │ │ ├── fr │ │ │ └── greetings.ts │ │ └── greetings.ts │ ├── sendEvent.ts │ ├── styles │ │ ├── baseTheme3.css.ts │ │ ├── extraClasses1.css.ts │ │ ├── extraClasses2.css.ts │ │ ├── testWidget1.css.ts │ │ ├── testWidget1Theme1.css.ts │ │ ├── testWidget1Theme2.css.ts │ │ ├── testWidget1Theme3.css.ts │ │ ├── testWidget2.css.ts │ │ ├── testWidget2Theme1.css.ts │ │ ├── testWidget2Theme2.css.ts │ │ ├── testWidget2Theme3.css.ts │ │ ├── theme1.css.ts │ │ ├── theme2.css.ts │ │ └── theme3.css.ts │ └── util.ts └── unit │ ├── Container.ts │ ├── Injector.ts │ ├── NodeHandler.ts │ ├── Registry.ts │ ├── RegistryHandler.ts │ ├── WidgetBase.ts │ ├── all.ts │ ├── d.ts │ ├── decorators │ ├── afterRender.ts │ ├── all.ts │ ├── alwaysRender.ts │ ├── beforeProperties.ts │ ├── beforeRender.ts │ ├── customElement.ts │ ├── diffProperty.ts │ ├── inject.ts │ └── registry.ts │ ├── diff.ts │ ├── meta │ ├── Dimensions.ts │ ├── Drag.ts │ ├── Focus.ts │ ├── Intersection.ts │ ├── Matches.ts │ ├── Resize.ts │ ├── WebAnimation.ts │ ├── all.ts │ └── meta.ts │ ├── mixins │ ├── Focus.ts │ ├── I18n.ts │ ├── Projector.ts │ ├── Themed.ts │ └── all.ts │ ├── registerCustomElement.ts │ ├── tsx.ts │ ├── tsxIntegration.tsx │ ├── vdom.ts │ └── waitFor.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [{package.json,.travis.yml}] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case users don't have core.autocrlf set 2 | * text=auto 3 | 4 | # Files that should always be normalized and converted to native line 5 | # endings on checkout. 6 | *.js text 7 | *.json text 8 | *.ts text 9 | *.md text 10 | *.yml text 11 | LICENSE text 12 | 13 | # Files that are truly binary and should not be modified 14 | *.png binary 15 | *.jpg binary 16 | *.jpeg binary 17 | *.gif binary 18 | *.jar binary 19 | *.zip binary 20 | *.psd binary 21 | *.enc binary 22 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Thank You 2 | 3 | We very much welcome contributions to Dojo 2. 4 | 5 | Because we have so many repositories that are part of Dojo 2, we have located our [Contributing Guidelines](https://github.com/dojo/meta/blob/master/CONTRIBUTING.md) in our [Dojo 2 Meta Repository](https://github.com/dojo/meta#readme). 6 | 7 | Look forward to working with you on Dojo 2!!! 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | **Bug / Enhancement** 13 | 14 | 15 | 16 | Package Version: 17 | 18 | **Code** 19 | 20 | 21 | 22 | **Expected behavior:** 23 | 24 | 25 | 26 | **Actual behavior:** 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Type:** bug / feature 2 | 3 | The following has been addressed in the PR: 4 | 5 | * [ ] There is a related issue 6 | * [ ] All code has been formatted with [`prettier`](https://prettier.io/) as per the [readme code style guidelines](./../#code-style) 7 | * [ ] Unit or Functional tests are included in the PR 8 | 9 | 17 | 18 | **Description:** 19 | 20 | Resolves #??? 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /bower_components 3 | /dist 4 | /html-report 5 | /node_modules 6 | /typings 7 | .baseDir.ts 8 | .tscache 9 | coverage-unmapped.json 10 | coverage-final.json 11 | coverage-final.lcov 12 | npm-debug.log 13 | /_apidoc 14 | deploy_key 15 | /benchmark-results 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '6' 5 | addons: 6 | chrome: stable 7 | env: 8 | global: 9 | - SAUCE_USERNAME: dojo2-ts-ci 10 | - SAUCE_ACCESS_KEY: e92610e3-834e-4bec-a3b5-6f7b9d874601 11 | - BROWSERSTACK_USERNAME: dylanschiemann2 12 | - BROWSERSTACK_ACCESS_KEY: 4Q2g8YAc9qeZzB2hECnS 13 | before_install: 14 | - if [ ${TRAVIS_BRANCH-""} == "master" ] && [ -n ${encrypted_12c8071d2874_key-""} 15 | ]; then openssl aes-256-cbc -K $encrypted_12c8071d2874_key -iv $encrypted_12c8071d2874_iv 16 | -in deploy_key.enc -out deploy_key -d; fi 17 | install: 18 | - travis_retry npm install grunt-cli 19 | - travis_retry npm install 20 | script: 21 | - grunt 22 | - grunt intern:browserstack --test-reporter 23 | - grunt uploadCoverage 24 | - grunt dist 25 | - grunt doc 26 | - grunt dev 27 | - npm run benchmark 28 | notifications: 29 | slack: 30 | secure: SUW1XF8Nggc5m+Xw3U7qaMFM9Tfn0NmvZePI84K9INDlLx+YxVZouteKN0XW9WsgJbI0ZiJE0CthJRS/twcjW4hO/LaizXYbqQaC+3pEUGajgYL1rbpqr0BfKmPKplvR/LbbTMo/GfUOOmU5F5ajWh1HWadHp1Dj/GSPzms97FLq0Xs0bWe/saX17v69V+36X7jyIrWPdQ3nVH284bbLMKnlNQ7t0LuPD748m1neLGNdQTC2R7P2eFoRS5hR61qY4m2my96wH2PndVbt1HuTW2S61wAX3DCQ+5a21bTnv+VCawvhlEZq3/zgr9w0M7WAeFqbdA7gwvaAV3N80JAdvwX60Wfw4rfwADTwj/KAAgF3dbc4iYTwUunCjZKydgUn1WWIG2sLBb0u2GfXAWINOWeDm2tZPPFx9nB/8Fjxnasp85F9TXxhyMXkybKaV0hf57rjaGTDXkl3PxzFTOnpakZU/+Gv68UhO+VwDjuQakBuCg43xV9jrVHWZ941LP13bBhNh2u3UKJafrBQLf6SchbQyx8/iWwLgHNuHpskKjoHIo/bOTqFaHHo6o4UHnT5KeLdXLvyxM5pg7UyYHrT1T4kUzbVQtWGCba5EI+H3A11RLwKjKir4utdMsZgkJ8+ORNW499OOlk8Hfs2tGs/EbH9yxGTWzLEJWiu5LWJt5Y= 31 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | require('grunt-dojo2').initConfig(grunt, { 3 | ts: { 4 | dist: { 5 | exclude: [ 'tests/**/*.tsx', 'tests/**/*.ts' ] 6 | } 7 | }, 8 | typedoc: { 9 | options: { 10 | ignoreCompilerErrors: true // Remove this once compile errors are resolved 11 | } 12 | }, 13 | intern: { 14 | version: 4 15 | } 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The "New" BSD License 2 | ********************* 3 | 4 | Copyright (c) 2016-2017, [JS Foundation](https://js.foundation/). 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | * Neither the name of the JS Foundation nor the names of its contributors 16 | may be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | notify: 3 | slack: 4 | default: 5 | url: "secret:GDejEuyC9fs2nRPP1Esq7DKWEd9XgOfJa11LDX/6+v+mmwEsPUfxh0XIbd6cyGr3EfflKKcVowP2HYad/cHGkyiOK/bYjFNiNLJh/+8ByGxz1ay/4SCxmSaD/820WfULar+l73BmMP3ULj3buPAhs0c/bK1q+w3wsh3uKmCj6LA=" 6 | threshold: 2 7 | attachments: "sunburst, diff" 8 | comment: 9 | branches: 10 | - master 11 | - feature/* 12 | -------------------------------------------------------------------------------- /deploy_key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dojo/widget-core/280efefdb9f5333a004d3118b603e611e7d9fb0c/deploy_key.enc -------------------------------------------------------------------------------- /intern.json: -------------------------------------------------------------------------------- 1 | { 2 | "capabilities+": { 3 | "project": "Dojo 2", 4 | "name": "@dojo/widget-core" 5 | }, 6 | "environments": [ 7 | { "browserName": "node" } 8 | ], 9 | "suites": [ 10 | "./_build/tests/unit/all.js" 11 | ], 12 | "functionalSuites": [ 13 | "./_build/tests/functional/all.js" 14 | ], 15 | "browser": { 16 | "loader": { 17 | "script": "./node_modules/grunt-dojo2/lib/intern/internLoader.js", 18 | "options": { 19 | "packages": [ 20 | { "name": "src", "location": "_build/src" }, 21 | { "name": "tests", "location": "_build/tests" }, 22 | { "name": "cldr-data", "location": "node_modules/cldr-data" }, 23 | { "name": "cldrjs", "location": "node_modules/cldrjs" }, 24 | { "name": "globalize", "location": "node_modules/globalize", "main": "dist/globalize" } 25 | ], 26 | "map": { 27 | "globalize": { 28 | "cldr": "cldrjs/dist/cldr", 29 | "cldr/event": "cldrjs/dist/cldr/event", 30 | "cldr/supplemental": "cldrjs/dist/cldr/supplemental", 31 | "cldr/unresolved": "cldrjs/dist/cldr/unresolved" 32 | } 33 | } 34 | } 35 | }, 36 | "plugins": [ 37 | { 38 | "script": "./node_modules/@dojo/shim/browser.js", 39 | "useLoader": true 40 | }, 41 | { 42 | "script": "./tests/support/loadCustomElements.js", 43 | "useLoader": true 44 | } 45 | ] 46 | }, 47 | "node" : { 48 | "plugins": [ 49 | { 50 | "script": "./_build/tests/support/loadJsdom.js", 51 | "useLoader": true 52 | } 53 | ] 54 | }, 55 | "coverage": [ 56 | "./_build/src/**/*.js" 57 | ], 58 | "configs": { 59 | "local": { 60 | "tunnel": "selenium", 61 | "environments+": [ 62 | { "browserName": "chrome" } 63 | ] 64 | }, 65 | "browserstack": { 66 | "tunnel": "browserstack", 67 | "capabilities+": { 68 | "browserstack.debug": false 69 | }, 70 | "environments+": [ 71 | { "browserName": "edge" }, 72 | { "browserName": "internet explorer", "version": "11" }, 73 | { "browserName": "chrome", "platform": "WINDOWS" }, 74 | { "browserName": "firefox", "os": "Windows", "os_version": "10", "browser_version": "58.0" }, 75 | { "browserName": "safari", "version": "10", "platform": "MAC" } 76 | ] 77 | }, 78 | "saucelabs": { 79 | "tunnel": "saucelabs", 80 | "capabilities+": { 81 | "fixSessionCapabilities": false 82 | }, 83 | 84 | "defaultTimeout": 10000, 85 | "environments+": [ 86 | { "browserName": "internet explorer", "version": [ "11.0" ], "platform": "Windows 7" }, 87 | { "browserName": "firefox", "version": "43", "platform": "Windows 10" }, 88 | { "browserName": "chrome", "platform": "Windows 10" } 89 | ], 90 | "maxConcurrency": 4 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dojo/widget-core", 3 | "version": "2.0.8-pre", 4 | "description": "A core widget library for Dojo 2", 5 | "private": true, 6 | "homepage": "https://dojo.io", 7 | "bugs": { 8 | "url": "https://github.com/dojo/widget-core/issues" 9 | }, 10 | "license": "BSD-3-Clause", 11 | "files": [ 12 | "dist", 13 | "src" 14 | ], 15 | "engines": { 16 | "npm": ">=3.0.0" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/dojo/widget-core.git" 21 | }, 22 | "scripts": { 23 | "dev": "grunt dev", 24 | "benchmark": "node ./run-benchmark.js", 25 | "prepublish": "grunt peerDepInstall", 26 | "precommit": "lint-staged", 27 | "prettier": "prettier --write 'src/**/*.ts*' 'tests/**/*.ts*'", 28 | "test": "grunt test" 29 | }, 30 | "peerDependencies": { 31 | "@dojo/core": "^2.0.0", 32 | "@dojo/has": "^2.0.0", 33 | "@dojo/i18n": "^2.0.0", 34 | "@dojo/shim": "^2.0.0" 35 | }, 36 | "devDependencies": { 37 | "@dojo/loader": "^2.0.0", 38 | "@types/glob": "5.0.*", 39 | "@types/grunt": "0.4.*", 40 | "@types/jsdom": "2.0.*", 41 | "@types/node": "~9.6.5", 42 | "@types/ramda": "0.25.5", 43 | "@types/selenium-webdriver": "^3.0.8", 44 | "@types/sinon": "~4.1.2", 45 | "@types/yargs": "^8.0.2", 46 | "@webcomponents/webcomponentsjs": "1.1.0", 47 | "bootstrap": "^3.3.7", 48 | "chromedriver": "2.38.3", 49 | "codecov.io": "0.1.6", 50 | "glob": "^7.0.6", 51 | "grunt": "^1.0.1", 52 | "grunt-dojo2": "latest", 53 | "grunt-tslint": "5.0.1", 54 | "husky": "0.14.3", 55 | "intern": "~4.1.5", 56 | "jsdom": "^9.5.0", 57 | "jstat": "^1.7.1", 58 | "lint-staged": "6.0.0", 59 | "prettier": "1.9.2", 60 | "ramda": "0.25.0", 61 | "rimraf": "^2.6.2", 62 | "selenium-webdriver": "3.6.0", 63 | "sinon": "~4.1.3", 64 | "tslint": "5.2.0", 65 | "typescript": "~2.6.1" 66 | }, 67 | "dependencies": { 68 | "@types/web-animations-js": "2.2.5", 69 | "tslib": "~1.8.1" 70 | }, 71 | "lint-staged": { 72 | "*.{ts,tsx}": [ 73 | "prettier --write", 74 | "git add" 75 | ] 76 | }, 77 | "prettier": { 78 | "singleQuote": true, 79 | "tabWidth": 4, 80 | "useTabs": true, 81 | "parser": "typescript", 82 | "printWidth": 120, 83 | "arrowParens": "always" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /run-benchmark.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const rimraf = require('rimraf'); 3 | const { runBench } = require('./_build/tests/benchmark/runner/src/benchmarkRunner.js') 4 | const { processBenchmarkResults } = require('./_build/tests/benchmark/runner/process-benchmark-results.js') 5 | const http = require('http'); 6 | const url = require('url'); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const port = 8080; 10 | 11 | // Run a local webserver to serve our applications 12 | const server = http.createServer(function (req, res) { 13 | 14 | // parse URL 15 | const parsedUrl = url.parse(req.url); 16 | // extract URL path 17 | let pathname = `.${parsedUrl.pathname}`; 18 | // based on the URL path, extract the file extention. e.g. .js, .doc, ... 19 | const ext = path.parse(pathname).ext || '.html'; 20 | 21 | // maps file extention to MIME typere 22 | const map = { 23 | '.ico': 'image/x-icon', 24 | '.html': 'text/html', 25 | '.js': 'text/javascript', 26 | '.json': 'application/json', 27 | '.css': 'text/css', 28 | '.png': 'image/png', 29 | '.jpg': 'image/jpeg', 30 | '.wav': 'audio/wav', 31 | '.mp3': 'audio/mpeg', 32 | '.svg': 'image/svg+xml', 33 | '.pdf': 'application/pdf', 34 | '.doc': 'application/msword' 35 | }; 36 | 37 | fs.exists(pathname, function (exist) { 38 | if(!exist) { 39 | // console.error("Server Error: File not found: ", pathname); 40 | // if the file is not found, return 404 41 | res.statusCode = 404; 42 | res.end(`File ${pathname} not found!`); 43 | return; 44 | } 45 | 46 | // if is a directory search for index file matching the extention 47 | if (fs.statSync(pathname).isDirectory()) pathname += 'index' + ext; 48 | 49 | // read file from file system 50 | fs.readFile(pathname, function(err, data){ 51 | if(err){ 52 | res.statusCode = 500; 53 | res.end(`Error getting the file: ${err}.`); 54 | } else { 55 | // if the file is found, set Content-type and send data 56 | res.setHeader('Content-type', map[ext] || 'text/plain' ); 57 | res.end(data); 58 | } 59 | }); 60 | }); 61 | 62 | }).listen(parseInt(port)); 63 | 64 | rimraf('./benchmark-results', function () { 65 | console.log('Old benchmark files removed'); 66 | }); 67 | 68 | const headless = (process.argv[2] == "false") ? false : true; 69 | console.log("Running headless? ", headless, "\n"); 70 | 71 | runBench( 72 | ["vanillajs-non-keyed", "dojo2-v0.2.0-non-keyed"], 73 | [''], 74 | "benchmark-results", // Directory 75 | { count : 3, headless : headless } // args 76 | ).then(() => { 77 | // Close the Dojo server 78 | server.close(); 79 | processBenchmarkResults(); 80 | }) -------------------------------------------------------------------------------- /src/Container.ts: -------------------------------------------------------------------------------- 1 | import { WidgetBase } from './WidgetBase'; 2 | import { inject, GetProperties } from './decorators/inject'; 3 | import { Constructor, DNode, RegistryLabel } from './interfaces'; 4 | import { w } from './d'; 5 | import { alwaysRender } from './decorators/alwaysRender'; 6 | 7 | export type Container = Constructor>>; 8 | 9 | export function Container( 10 | component: Constructor | RegistryLabel, 11 | name: RegistryLabel, 12 | { getProperties }: { getProperties: GetProperties } 13 | ): Container { 14 | @alwaysRender() 15 | @inject({ name, getProperties }) 16 | class WidgetContainer extends WidgetBase> { 17 | protected render(): DNode { 18 | return w(component, this.properties, this.children); 19 | } 20 | } 21 | return WidgetContainer; 22 | } 23 | 24 | export default Container; 25 | -------------------------------------------------------------------------------- /src/Injector.ts: -------------------------------------------------------------------------------- 1 | import { Evented } from '@dojo/core/Evented'; 2 | import { EventObject } from '@dojo/core/interfaces'; 3 | 4 | export type InjectorEventMap = { 5 | invalidate: EventObject<'invalidate'>; 6 | }; 7 | 8 | export class Injector extends Evented { 9 | private _payload: T; 10 | private _invalidator: undefined | (() => void); 11 | 12 | constructor(payload: T) { 13 | super(); 14 | this._payload = payload; 15 | } 16 | 17 | public setInvalidator(invalidator: () => void) { 18 | this._invalidator = invalidator; 19 | } 20 | 21 | public get(): T { 22 | return this._payload; 23 | } 24 | 25 | public set(payload: T): void { 26 | this._payload = payload; 27 | if (this._invalidator) { 28 | this._invalidator(); 29 | } 30 | } 31 | } 32 | 33 | export default Injector; 34 | -------------------------------------------------------------------------------- /src/NodeHandler.ts: -------------------------------------------------------------------------------- 1 | import { Evented } from '@dojo/core/Evented'; 2 | import { EventObject } from '@dojo/core/interfaces'; 3 | import Map from '@dojo/shim/Map'; 4 | import { NodeHandlerInterface } from './interfaces'; 5 | 6 | /** 7 | * Enum to identify the type of event. 8 | * Listening to 'Projector' will notify when projector is created or updated 9 | * Listening to 'Widget' will notify when widget root is created or updated 10 | */ 11 | export enum NodeEventType { 12 | Projector = 'Projector', 13 | Widget = 'Widget' 14 | } 15 | 16 | export type NodeHandlerEventMap = { 17 | Projector: EventObject; 18 | Widget: EventObject; 19 | }; 20 | 21 | export class NodeHandler extends Evented implements NodeHandlerInterface { 22 | private _nodeMap = new Map(); 23 | 24 | public get(key: string): Element | undefined { 25 | return this._nodeMap.get(key); 26 | } 27 | 28 | public has(key: string): boolean { 29 | return this._nodeMap.has(key); 30 | } 31 | 32 | public add(element: Element, key: string): void { 33 | this._nodeMap.set(key, element); 34 | this.emit({ type: key }); 35 | } 36 | 37 | public addRoot(): void { 38 | this.emit({ type: NodeEventType.Widget }); 39 | } 40 | 41 | public addProjector(): void { 42 | this.emit({ type: NodeEventType.Projector }); 43 | } 44 | 45 | public clear(): void { 46 | this._nodeMap.clear(); 47 | } 48 | } 49 | 50 | export default NodeHandler; 51 | -------------------------------------------------------------------------------- /src/Registry.ts: -------------------------------------------------------------------------------- 1 | import Promise from '@dojo/shim/Promise'; 2 | import Map from '@dojo/shim/Map'; 3 | import Symbol from '@dojo/shim/Symbol'; 4 | import { EventObject } from '@dojo/core/interfaces'; 5 | import { Evented } from '@dojo/core/Evented'; 6 | import { 7 | Constructor, 8 | InjectorFactory, 9 | InjectorItem, 10 | RegistryLabel, 11 | WidgetBaseConstructor, 12 | WidgetBaseInterface 13 | } from './interfaces'; 14 | 15 | export type WidgetBaseConstructorFunction = () => Promise; 16 | 17 | export type ESMDefaultWidgetBaseFunction = () => Promise>; 18 | 19 | export type RegistryItem = 20 | | WidgetBaseConstructor 21 | | Promise 22 | | WidgetBaseConstructorFunction 23 | | ESMDefaultWidgetBaseFunction; 24 | 25 | /** 26 | * Widget base symbol type 27 | */ 28 | export const WIDGET_BASE_TYPE = Symbol('Widget Base'); 29 | 30 | export interface RegistryEventObject extends EventObject { 31 | action: string; 32 | item: WidgetBaseConstructor | InjectorFactory; 33 | } 34 | /** 35 | * Widget Registry Interface 36 | */ 37 | export interface RegistryInterface { 38 | /** 39 | * Define a WidgetRegistryItem against a label 40 | * 41 | * @param label The label of the widget to register 42 | * @param registryItem The registry item to define 43 | */ 44 | define(label: RegistryLabel, registryItem: RegistryItem): void; 45 | 46 | /** 47 | * Return a RegistryItem for the given label, null if an entry doesn't exist 48 | * 49 | * @param widgetLabel The label of the widget to return 50 | * @returns The RegistryItem for the widgetLabel, `null` if no entry exists 51 | */ 52 | get(label: RegistryLabel): Constructor | null; 53 | 54 | /** 55 | * Returns a boolean if an entry for the label exists 56 | * 57 | * @param widgetLabel The label to search for 58 | * @returns boolean indicating if a widget registry item exists 59 | */ 60 | has(label: RegistryLabel): boolean; 61 | 62 | /** 63 | * Define an Injector against a label 64 | * 65 | * @param label The label of the injector to register 66 | * @param registryItem The injector factory 67 | */ 68 | defineInjector(label: RegistryLabel, injectorFactory: InjectorFactory): void; 69 | 70 | /** 71 | * Return an Injector registry item for the given label, null if an entry doesn't exist 72 | * 73 | * @param label The label of the injector to return 74 | * @returns The RegistryItem for the widgetLabel, `null` if no entry exists 75 | */ 76 | getInjector(label: RegistryLabel): InjectorItem | null; 77 | 78 | /** 79 | * Returns a boolean if an injector for the label exists 80 | * 81 | * @param widgetLabel The label to search for 82 | * @returns boolean indicating if a injector registry item exists 83 | */ 84 | hasInjector(label: RegistryLabel): boolean; 85 | } 86 | 87 | /** 88 | * Checks is the item is a subclass of WidgetBase (or a WidgetBase) 89 | * 90 | * @param item the item to check 91 | * @returns true/false indicating if the item is a WidgetBaseConstructor 92 | */ 93 | export function isWidgetBaseConstructor(item: any): item is Constructor { 94 | return Boolean(item && item._type === WIDGET_BASE_TYPE); 95 | } 96 | 97 | export interface ESMDefaultWidgetBase { 98 | default: Constructor; 99 | __esModule?: boolean; 100 | } 101 | 102 | export function isWidgetConstructorDefaultExport(item: any): item is ESMDefaultWidgetBase { 103 | return Boolean( 104 | item && 105 | item.hasOwnProperty('__esModule') && 106 | item.hasOwnProperty('default') && 107 | isWidgetBaseConstructor(item.default) 108 | ); 109 | } 110 | 111 | /** 112 | * The Registry implementation 113 | */ 114 | export class Registry extends Evented<{}, RegistryLabel, RegistryEventObject> implements RegistryInterface { 115 | /** 116 | * internal map of labels and RegistryItem 117 | */ 118 | private _widgetRegistry: Map | undefined; 119 | 120 | private _injectorRegistry: Map | undefined; 121 | 122 | /** 123 | * Emit loaded event for registry label 124 | */ 125 | private emitLoadedEvent(widgetLabel: RegistryLabel, item: WidgetBaseConstructor | InjectorItem): void { 126 | this.emit({ 127 | type: widgetLabel, 128 | action: 'loaded', 129 | item 130 | }); 131 | } 132 | 133 | public define(label: RegistryLabel, item: RegistryItem): void { 134 | if (this._widgetRegistry === undefined) { 135 | this._widgetRegistry = new Map(); 136 | } 137 | 138 | if (this._widgetRegistry.has(label)) { 139 | throw new Error(`widget has already been registered for '${label.toString()}'`); 140 | } 141 | 142 | this._widgetRegistry.set(label, item); 143 | 144 | if (item instanceof Promise) { 145 | item.then( 146 | (widgetCtor) => { 147 | this._widgetRegistry!.set(label, widgetCtor); 148 | this.emitLoadedEvent(label, widgetCtor); 149 | return widgetCtor; 150 | }, 151 | (error) => { 152 | throw error; 153 | } 154 | ); 155 | } else if (isWidgetBaseConstructor(item)) { 156 | this.emitLoadedEvent(label, item); 157 | } 158 | } 159 | 160 | public defineInjector(label: RegistryLabel, injectorFactory: InjectorFactory): void { 161 | if (this._injectorRegistry === undefined) { 162 | this._injectorRegistry = new Map(); 163 | } 164 | 165 | if (this._injectorRegistry.has(label)) { 166 | throw new Error(`injector has already been registered for '${label.toString()}'`); 167 | } 168 | 169 | const invalidator = new Evented(); 170 | 171 | const injectorItem: InjectorItem = { 172 | injector: injectorFactory(() => invalidator.emit({ type: 'invalidate' })), 173 | invalidator 174 | }; 175 | 176 | this._injectorRegistry.set(label, injectorItem); 177 | this.emitLoadedEvent(label, injectorItem); 178 | } 179 | 180 | public get(label: RegistryLabel): Constructor | null { 181 | if (!this._widgetRegistry || !this.has(label)) { 182 | return null; 183 | } 184 | 185 | const item = this._widgetRegistry.get(label); 186 | 187 | if (isWidgetBaseConstructor(item)) { 188 | return item; 189 | } 190 | 191 | if (item instanceof Promise) { 192 | return null; 193 | } 194 | 195 | const promise = (item)(); 196 | this._widgetRegistry.set(label, promise); 197 | 198 | promise.then( 199 | (widgetCtor) => { 200 | if (isWidgetConstructorDefaultExport(widgetCtor)) { 201 | widgetCtor = widgetCtor.default; 202 | } 203 | 204 | this._widgetRegistry!.set(label, widgetCtor); 205 | this.emitLoadedEvent(label, widgetCtor); 206 | return widgetCtor; 207 | }, 208 | (error) => { 209 | throw error; 210 | } 211 | ); 212 | 213 | return null; 214 | } 215 | 216 | public getInjector(label: RegistryLabel): InjectorItem | null { 217 | if (!this._injectorRegistry || !this.hasInjector(label)) { 218 | return null; 219 | } 220 | 221 | return this._injectorRegistry.get(label)!; 222 | } 223 | 224 | public has(label: RegistryLabel): boolean { 225 | return Boolean(this._widgetRegistry && this._widgetRegistry.has(label)); 226 | } 227 | 228 | public hasInjector(label: RegistryLabel): boolean { 229 | return Boolean(this._injectorRegistry && this._injectorRegistry.has(label)); 230 | } 231 | } 232 | 233 | export default Registry; 234 | -------------------------------------------------------------------------------- /src/RegistryHandler.ts: -------------------------------------------------------------------------------- 1 | import { Map } from '@dojo/shim/Map'; 2 | import { Evented } from '@dojo/core/Evented'; 3 | import { EventObject } from '@dojo/core/interfaces'; 4 | import { Constructor, InjectorFactory, InjectorItem, RegistryLabel, WidgetBaseInterface } from './interfaces'; 5 | import { Registry, RegistryEventObject, RegistryItem } from './Registry'; 6 | 7 | export type RegistryHandlerEventMap = { 8 | invalidate: EventObject<'invalidate'>; 9 | }; 10 | 11 | export class RegistryHandler extends Evented { 12 | private _registry = new Registry(); 13 | private _registryWidgetLabelMap: Map = new Map(); 14 | private _registryInjectorLabelMap: Map = new Map(); 15 | protected baseRegistry?: Registry; 16 | 17 | constructor() { 18 | super(); 19 | this.own(this._registry); 20 | const destroy = () => { 21 | if (this.baseRegistry) { 22 | this._registryWidgetLabelMap.delete(this.baseRegistry); 23 | this._registryInjectorLabelMap.delete(this.baseRegistry); 24 | this.baseRegistry = undefined; 25 | } 26 | }; 27 | this.own({ destroy }); 28 | } 29 | 30 | public set base(baseRegistry: Registry) { 31 | if (this.baseRegistry) { 32 | this._registryWidgetLabelMap.delete(this.baseRegistry); 33 | this._registryInjectorLabelMap.delete(this.baseRegistry); 34 | } 35 | this.baseRegistry = baseRegistry; 36 | } 37 | 38 | public define(label: RegistryLabel, widget: RegistryItem): void { 39 | this._registry.define(label, widget); 40 | } 41 | 42 | public defineInjector(label: RegistryLabel, injector: InjectorFactory): void { 43 | this._registry.defineInjector(label, injector); 44 | } 45 | 46 | public has(label: RegistryLabel): boolean { 47 | return this._registry.has(label) || Boolean(this.baseRegistry && this.baseRegistry.has(label)); 48 | } 49 | 50 | public hasInjector(label: RegistryLabel): boolean { 51 | return this._registry.hasInjector(label) || Boolean(this.baseRegistry && this.baseRegistry.hasInjector(label)); 52 | } 53 | 54 | public get( 55 | label: RegistryLabel, 56 | globalPrecedence: boolean = false 57 | ): Constructor | null { 58 | return this._get(label, globalPrecedence, 'get', this._registryWidgetLabelMap); 59 | } 60 | 61 | public getInjector(label: RegistryLabel, globalPrecedence: boolean = false): InjectorItem | null { 62 | return this._get(label, globalPrecedence, 'getInjector', this._registryInjectorLabelMap); 63 | } 64 | 65 | private _get( 66 | label: RegistryLabel, 67 | globalPrecedence: boolean, 68 | getFunctionName: 'getInjector' | 'get', 69 | labelMap: Map 70 | ): any { 71 | const registries = globalPrecedence ? [this.baseRegistry, this._registry] : [this._registry, this.baseRegistry]; 72 | for (let i = 0; i < registries.length; i++) { 73 | const registry: any = registries[i]; 74 | if (!registry) { 75 | continue; 76 | } 77 | const item = registry[getFunctionName](label); 78 | const registeredLabels = labelMap.get(registry) || []; 79 | if (item) { 80 | return item; 81 | } else if (registeredLabels.indexOf(label) === -1) { 82 | const handle = registry.on(label, (event: RegistryEventObject) => { 83 | if ( 84 | event.action === 'loaded' && 85 | (this as any)[getFunctionName](label, globalPrecedence) === event.item 86 | ) { 87 | this.emit({ type: 'invalidate' }); 88 | } 89 | }); 90 | this.own(handle); 91 | labelMap.set(registry, [...registeredLabels, label]); 92 | } 93 | } 94 | return null; 95 | } 96 | } 97 | 98 | export default RegistryHandler; 99 | -------------------------------------------------------------------------------- /src/animations/cssTransitions.ts: -------------------------------------------------------------------------------- 1 | import { VNodeProperties } from './../interfaces'; 2 | 3 | let browserSpecificTransitionEndEventName = ''; 4 | let browserSpecificAnimationEndEventName = ''; 5 | 6 | function determineBrowserStyleNames(element: HTMLElement) { 7 | if ('WebkitTransition' in element.style) { 8 | browserSpecificTransitionEndEventName = 'webkitTransitionEnd'; 9 | browserSpecificAnimationEndEventName = 'webkitAnimationEnd'; 10 | } else if ('transition' in element.style || 'MozTransition' in element.style) { 11 | browserSpecificTransitionEndEventName = 'transitionend'; 12 | browserSpecificAnimationEndEventName = 'animationend'; 13 | } else { 14 | throw new Error('Your browser is not supported'); 15 | } 16 | } 17 | 18 | function initialize(element: HTMLElement) { 19 | if (browserSpecificAnimationEndEventName === '') { 20 | determineBrowserStyleNames(element); 21 | } 22 | } 23 | 24 | function runAndCleanUp(element: HTMLElement, startAnimation: () => void, finishAnimation: () => void) { 25 | initialize(element); 26 | 27 | let finished = false; 28 | 29 | let transitionEnd = function() { 30 | if (!finished) { 31 | finished = true; 32 | element.removeEventListener(browserSpecificTransitionEndEventName, transitionEnd); 33 | element.removeEventListener(browserSpecificAnimationEndEventName, transitionEnd); 34 | 35 | finishAnimation(); 36 | } 37 | }; 38 | 39 | startAnimation(); 40 | 41 | element.addEventListener(browserSpecificAnimationEndEventName, transitionEnd); 42 | element.addEventListener(browserSpecificTransitionEndEventName, transitionEnd); 43 | } 44 | 45 | function exit(node: HTMLElement, properties: VNodeProperties, exitAnimation: string, removeNode: () => void) { 46 | const activeClass = properties.exitAnimationActive || `${exitAnimation}-active`; 47 | 48 | runAndCleanUp( 49 | node, 50 | () => { 51 | node.classList.add(exitAnimation); 52 | 53 | requestAnimationFrame(function() { 54 | node.classList.add(activeClass); 55 | }); 56 | }, 57 | () => { 58 | removeNode(); 59 | } 60 | ); 61 | } 62 | 63 | function enter(node: HTMLElement, properties: VNodeProperties, enterAnimation: string) { 64 | const activeClass = properties.enterAnimationActive || `${enterAnimation}-active`; 65 | 66 | runAndCleanUp( 67 | node, 68 | () => { 69 | node.classList.add(enterAnimation); 70 | 71 | requestAnimationFrame(function() { 72 | node.classList.add(activeClass); 73 | }); 74 | }, 75 | () => { 76 | node.classList.remove(enterAnimation); 77 | node.classList.remove(activeClass); 78 | } 79 | ); 80 | } 81 | 82 | export default { 83 | enter, 84 | exit 85 | }; 86 | -------------------------------------------------------------------------------- /src/d.ts: -------------------------------------------------------------------------------- 1 | import Symbol from '@dojo/shim/Symbol'; 2 | import { 3 | Constructor, 4 | DefaultWidgetBaseInterface, 5 | DeferredVirtualProperties, 6 | DNode, 7 | VNode, 8 | RegistryLabel, 9 | VNodeProperties, 10 | WidgetBaseInterface, 11 | WNode, 12 | DomOptions 13 | } from './interfaces'; 14 | import { InternalVNode, RenderResult } from './vdom'; 15 | 16 | /** 17 | * The symbol identifier for a WNode type 18 | */ 19 | export const WNODE = Symbol('Identifier for a WNode.'); 20 | 21 | /** 22 | * The symbol identifier for a VNode type 23 | */ 24 | export const VNODE = Symbol('Identifier for a VNode.'); 25 | 26 | /** 27 | * The symbol identifier for a VNode type created using dom() 28 | */ 29 | export const DOMVNODE = Symbol('Identifier for a VNode created using existing dom.'); 30 | 31 | /** 32 | * Helper function that returns true if the `DNode` is a `WNode` using the `type` property 33 | */ 34 | export function isWNode( 35 | child: DNode 36 | ): child is WNode { 37 | return Boolean(child && typeof child !== 'string' && child.type === WNODE); 38 | } 39 | 40 | /** 41 | * Helper function that returns true if the `DNode` is a `VNode` using the `type` property 42 | */ 43 | export function isVNode(child: DNode): child is VNode { 44 | return Boolean(child && typeof child !== 'string' && (child.type === VNODE || child.type === DOMVNODE)); 45 | } 46 | 47 | /** 48 | * Helper function that returns true if the `DNode` is a `VNode` created with `dom()` using the `type` property 49 | */ 50 | export function isDomVNode(child: DNode): child is VNode { 51 | return Boolean(child && typeof child !== 'string' && child.type === DOMVNODE); 52 | } 53 | 54 | export function isElementNode(value: any): value is Element { 55 | return !!value.tagName; 56 | } 57 | 58 | /** 59 | * Interface for the decorate modifier 60 | */ 61 | export interface Modifier { 62 | (dNode: T, breaker: () => void): void; 63 | } 64 | 65 | /** 66 | * The predicate function for decorate 67 | */ 68 | export interface Predicate { 69 | (dNode: DNode): dNode is T; 70 | } 71 | 72 | /** 73 | * Decorator options 74 | */ 75 | export interface DecorateOptions { 76 | modifier: Modifier; 77 | predicate?: Predicate; 78 | shallow?: boolean; 79 | } 80 | 81 | /** 82 | * Generic decorate function for DNodes. The nodes are modified in place based on the provided predicate 83 | * and modifier functions. 84 | * 85 | * The children of each node are flattened and added to the array for decoration. 86 | * 87 | * If no predicate is supplied then the modifier will be executed on all nodes. A `breaker` function is passed to the 88 | * modifier which will drain the nodes array and exit the decoration. 89 | * 90 | * When the `shallow` options is set to `true` the only the top node or nodes will be decorated (only supported using 91 | * `DecorateOptions`). 92 | */ 93 | export function decorate(dNodes: DNode, options: DecorateOptions): DNode; 94 | export function decorate(dNodes: DNode[], options: DecorateOptions): DNode[]; 95 | export function decorate(dNodes: DNode | DNode[], options: DecorateOptions): DNode | DNode[]; 96 | export function decorate(dNodes: DNode, modifier: Modifier, predicate: Predicate): DNode; 97 | export function decorate(dNodes: DNode[], modifier: Modifier, predicate: Predicate): DNode[]; 98 | export function decorate( 99 | dNodes: RenderResult, 100 | modifier: Modifier, 101 | predicate: Predicate 102 | ): RenderResult; 103 | export function decorate(dNodes: DNode, modifier: Modifier): DNode; 104 | export function decorate(dNodes: DNode[], modifier: Modifier): DNode[]; 105 | export function decorate(dNodes: RenderResult, modifier: Modifier): RenderResult; 106 | export function decorate( 107 | dNodes: DNode | DNode[], 108 | optionsOrModifier: Modifier | DecorateOptions, 109 | predicate?: Predicate 110 | ): DNode | DNode[] { 111 | let shallow = false; 112 | let modifier; 113 | if (typeof optionsOrModifier === 'function') { 114 | modifier = optionsOrModifier; 115 | } else { 116 | modifier = optionsOrModifier.modifier; 117 | predicate = optionsOrModifier.predicate; 118 | shallow = optionsOrModifier.shallow || false; 119 | } 120 | 121 | let nodes = Array.isArray(dNodes) ? [...dNodes] : [dNodes]; 122 | function breaker() { 123 | nodes = []; 124 | } 125 | while (nodes.length) { 126 | const node = nodes.shift(); 127 | if (node) { 128 | if (!shallow && (isWNode(node) || isVNode(node)) && node.children) { 129 | nodes = [...nodes, ...node.children]; 130 | } 131 | if (!predicate || predicate(node)) { 132 | modifier(node, breaker); 133 | } 134 | } 135 | } 136 | return dNodes; 137 | } 138 | 139 | /** 140 | * Wrapper function for calls to create a widget. 141 | */ 142 | export function w( 143 | widgetConstructor: Constructor | RegistryLabel, 144 | properties: W['properties'], 145 | children: W['children'] = [] 146 | ): WNode { 147 | return { 148 | children, 149 | widgetConstructor, 150 | properties, 151 | type: WNODE 152 | }; 153 | } 154 | 155 | /** 156 | * Wrapper function for calls to create VNodes. 157 | */ 158 | export function v(tag: string, children: undefined | DNode[]): VNode; 159 | export function v(tag: string, properties: DeferredVirtualProperties | VNodeProperties, children?: DNode[]): VNode; 160 | export function v(tag: string): VNode; 161 | export function v( 162 | tag: string, 163 | propertiesOrChildren: VNodeProperties | DeferredVirtualProperties | DNode[] = {}, 164 | children: undefined | DNode[] = undefined 165 | ): VNode { 166 | let properties: VNodeProperties | DeferredVirtualProperties = propertiesOrChildren; 167 | let deferredPropertiesCallback; 168 | 169 | if (Array.isArray(propertiesOrChildren)) { 170 | children = propertiesOrChildren; 171 | properties = {}; 172 | } 173 | 174 | if (typeof properties === 'function') { 175 | deferredPropertiesCallback = properties; 176 | properties = {}; 177 | } 178 | 179 | return { 180 | tag, 181 | deferredPropertiesCallback, 182 | children, 183 | properties, 184 | type: VNODE 185 | }; 186 | } 187 | 188 | /** 189 | * Create a VNode for an existing DOM Node. 190 | */ 191 | export function dom( 192 | { node, attrs = {}, props = {}, on = {}, diffType = 'none' }: DomOptions, 193 | children?: DNode[] 194 | ): VNode { 195 | return { 196 | tag: isElementNode(node) ? node.tagName.toLowerCase() : '', 197 | properties: props, 198 | attributes: attrs, 199 | events: on, 200 | children, 201 | type: DOMVNODE, 202 | domNode: node, 203 | text: isElementNode(node) ? undefined : node.data, 204 | diffType 205 | } as InternalVNode; 206 | } 207 | -------------------------------------------------------------------------------- /src/decorators/afterRender.ts: -------------------------------------------------------------------------------- 1 | import { handleDecorator } from './handleDecorator'; 2 | 3 | /** 4 | * Decorator that can be used to register a function to run as an aspect to `render` 5 | */ 6 | export function afterRender(method: Function): (target: any) => void; 7 | export function afterRender(): (target: any, propertyKey: string) => void; 8 | export function afterRender(method?: Function) { 9 | return handleDecorator((target, propertyKey) => { 10 | target.addDecorator('afterRender', propertyKey ? target[propertyKey] : method); 11 | }); 12 | } 13 | 14 | export default afterRender; 15 | -------------------------------------------------------------------------------- /src/decorators/alwaysRender.ts: -------------------------------------------------------------------------------- 1 | import { WidgetBase } from './../WidgetBase'; 2 | import { handleDecorator } from './handleDecorator'; 3 | import { beforeProperties } from './beforeProperties'; 4 | 5 | export function alwaysRender() { 6 | return handleDecorator((target, propertyKey) => { 7 | beforeProperties(function(this: WidgetBase) { 8 | this.invalidate(); 9 | })(target); 10 | }); 11 | } 12 | 13 | export default alwaysRender; 14 | -------------------------------------------------------------------------------- /src/decorators/beforeProperties.ts: -------------------------------------------------------------------------------- 1 | import { handleDecorator } from './handleDecorator'; 2 | import { BeforeProperties } from './../interfaces'; 3 | 4 | /** 5 | * Decorator that adds the function passed of target method to be run 6 | * in the `beforeProperties` lifecycle. 7 | */ 8 | export function beforeProperties(method: BeforeProperties): (target: any) => void; 9 | export function beforeProperties(): (target: any, propertyKey: string) => void; 10 | export function beforeProperties(method?: BeforeProperties) { 11 | return handleDecorator((target, propertyKey) => { 12 | target.addDecorator('beforeProperties', propertyKey ? target[propertyKey] : method); 13 | }); 14 | } 15 | 16 | export default beforeProperties; 17 | -------------------------------------------------------------------------------- /src/decorators/beforeRender.ts: -------------------------------------------------------------------------------- 1 | import { handleDecorator } from './handleDecorator'; 2 | 3 | /** 4 | * Decorator that can be used to register a reducer function to run as an aspect before to `render` 5 | */ 6 | export function beforeRender(method: Function): (target: any) => void; 7 | export function beforeRender(): (target: any, propertyKey: string) => void; 8 | export function beforeRender(method?: Function) { 9 | return handleDecorator((target, propertyKey) => { 10 | target.addDecorator('beforeRender', propertyKey ? target[propertyKey] : method); 11 | }); 12 | } 13 | 14 | export default beforeRender; 15 | -------------------------------------------------------------------------------- /src/decorators/customElement.ts: -------------------------------------------------------------------------------- 1 | import { Constructor, WidgetProperties } from '../interfaces'; 2 | import { CustomElementChildType } from '../registerCustomElement'; 3 | import Registry from '../Registry'; 4 | 5 | export type CustomElementPropertyNames

= ((keyof P) | (keyof WidgetProperties))[]; 6 | 7 | /** 8 | * Defines the custom element configuration used by the customElement decorator 9 | */ 10 | export interface CustomElementConfig

{ 11 | /** 12 | * The tag of the custom element 13 | */ 14 | tag: string; 15 | 16 | /** 17 | * List of widget properties to expose as properties on the custom element 18 | */ 19 | properties?: CustomElementPropertyNames

; 20 | 21 | /** 22 | * List of attributes on the custom element to map to widget properties 23 | */ 24 | attributes?: CustomElementPropertyNames

; 25 | 26 | /** 27 | * List of events to expose 28 | */ 29 | events?: CustomElementPropertyNames

; 30 | 31 | childType?: CustomElementChildType; 32 | 33 | registryFactory?: () => Registry; 34 | } 35 | 36 | /** 37 | * This Decorator is provided properties that define the behavior of a custom element, and 38 | * registers that custom element. 39 | */ 40 | export function customElement

({ 41 | tag, 42 | properties = [], 43 | attributes = [], 44 | events = [], 45 | childType = CustomElementChildType.DOJO, 46 | registryFactory = () => new Registry() 47 | }: CustomElementConfig

) { 48 | return function>(target: T) { 49 | target.prototype.__customElementDescriptor = { 50 | tagName: tag, 51 | attributes, 52 | properties, 53 | events, 54 | childType, 55 | registryFactory 56 | }; 57 | }; 58 | } 59 | 60 | export default customElement; 61 | -------------------------------------------------------------------------------- /src/decorators/diffProperty.ts: -------------------------------------------------------------------------------- 1 | import { handleDecorator } from './handleDecorator'; 2 | import { DiffPropertyFunction } from './../interfaces'; 3 | import { auto } from './../diff'; 4 | 5 | /** 6 | * Decorator that can be used to register a function as a specific property diff 7 | * 8 | * @param propertyName The name of the property of which the diff function is applied 9 | * @param diffType The diff type, default is DiffType.AUTO. 10 | * @param diffFunction A diff function to run if diffType if DiffType.CUSTOM 11 | */ 12 | export function diffProperty( 13 | propertyName: string, 14 | diffFunction: DiffPropertyFunction = auto, 15 | reactionFunction?: Function 16 | ) { 17 | return handleDecorator((target, propertyKey) => { 18 | target.addDecorator(`diffProperty:${propertyName}`, diffFunction.bind(null)); 19 | target.addDecorator('registeredDiffProperty', propertyName); 20 | if (reactionFunction || propertyKey) { 21 | target.addDecorator('diffReaction', { 22 | propertyName, 23 | reaction: propertyKey ? target[propertyKey] : reactionFunction 24 | }); 25 | } 26 | }); 27 | } 28 | 29 | export default diffProperty; 30 | -------------------------------------------------------------------------------- /src/decorators/handleDecorator.ts: -------------------------------------------------------------------------------- 1 | export type DecoratorHandler = (target: any, propertyKey?: string) => void; 2 | 3 | /** 4 | * Generic decorator handler to take care of whether or not the decorator was called at the class level 5 | * or the method level. 6 | * 7 | * @param handler 8 | */ 9 | export function handleDecorator(handler: DecoratorHandler) { 10 | return function(target: any, propertyKey?: string, descriptor?: PropertyDescriptor) { 11 | if (typeof target === 'function') { 12 | handler(target.prototype, undefined); 13 | } else { 14 | handler(target, propertyKey); 15 | } 16 | }; 17 | } 18 | 19 | export default handleDecorator; 20 | -------------------------------------------------------------------------------- /src/decorators/inject.ts: -------------------------------------------------------------------------------- 1 | import WeakMap from '@dojo/shim/WeakMap'; 2 | import { WidgetBase } from './../WidgetBase'; 3 | import { handleDecorator } from './handleDecorator'; 4 | import { beforeProperties } from './beforeProperties'; 5 | import { InjectorItem, RegistryLabel } from './../interfaces'; 6 | 7 | /** 8 | * Map of instances against registered injectors. 9 | */ 10 | const registeredInjectorsMap: WeakMap = new WeakMap(); 11 | 12 | /** 13 | * Defines the contract requires for the get properties function 14 | * used to map the injected properties. 15 | */ 16 | export interface GetProperties { 17 | (payload: any, properties: T): T; 18 | } 19 | 20 | /** 21 | * Defines the inject configuration required for use of the `inject` decorator 22 | */ 23 | export interface InjectConfig { 24 | /** 25 | * The label of the registry injector 26 | */ 27 | name: RegistryLabel; 28 | 29 | /** 30 | * Function that returns propertues to inject using the passed properties 31 | * and the injected payload. 32 | */ 33 | getProperties: GetProperties; 34 | } 35 | 36 | /** 37 | * Decorator retrieves an injector from an available registry using the name and 38 | * calls the `getProperties` function with the payload from the injector 39 | * and current properties with the the injected properties returned. 40 | * 41 | * @param InjectConfig the inject configuration 42 | */ 43 | export function inject({ name, getProperties }: InjectConfig) { 44 | return handleDecorator((target, propertyKey) => { 45 | beforeProperties(function(this: WidgetBase & { own: Function }, properties: any) { 46 | const injectorItem = this.registry.getInjector(name); 47 | if (injectorItem) { 48 | const { injector, invalidator } = injectorItem; 49 | const registeredInjectors = registeredInjectorsMap.get(this) || []; 50 | if (registeredInjectors.length === 0) { 51 | registeredInjectorsMap.set(this, registeredInjectors); 52 | } 53 | if (registeredInjectors.indexOf(injectorItem) === -1) { 54 | this.own( 55 | invalidator.on('invalidate', () => { 56 | this.invalidate(); 57 | }) 58 | ); 59 | registeredInjectors.push(injectorItem); 60 | } 61 | return getProperties(injector(), properties); 62 | } 63 | })(target); 64 | }); 65 | } 66 | 67 | export default inject; 68 | -------------------------------------------------------------------------------- /src/decorators/registry.ts: -------------------------------------------------------------------------------- 1 | import { handleDecorator, DecoratorHandler } from './handleDecorator'; 2 | import { RegistryItem } from '../Registry'; 3 | 4 | export interface RegistryConfig { 5 | [name: string]: RegistryItem; 6 | } 7 | 8 | /** 9 | * Decorator that can be used to register a widget with the calling widgets local registry 10 | */ 11 | export function registry(nameOrConfig: string, loader: RegistryItem): DecoratorHandler; 12 | export function registry(nameOrConfig: RegistryConfig): DecoratorHandler; 13 | export function registry(nameOrConfig: string | RegistryConfig, loader?: RegistryItem) { 14 | return handleDecorator((target, propertyKey) => { 15 | target.addDecorator('afterConstructor', function(this: any) { 16 | if (typeof nameOrConfig === 'string') { 17 | this.registry.define(nameOrConfig, loader); 18 | } else { 19 | Object.keys(nameOrConfig).forEach((name) => { 20 | this.registry.define(name, nameOrConfig[name]); 21 | }); 22 | } 23 | }); 24 | }); 25 | } 26 | 27 | export default registry; 28 | -------------------------------------------------------------------------------- /src/diff.ts: -------------------------------------------------------------------------------- 1 | import { PropertyChangeRecord } from './interfaces'; 2 | import { WIDGET_BASE_TYPE } from './Registry'; 3 | 4 | function isObjectOrArray(value: any): boolean { 5 | return Object.prototype.toString.call(value) === '[object Object]' || Array.isArray(value); 6 | } 7 | 8 | export function always(previousProperty: any, newProperty: any): PropertyChangeRecord { 9 | return { 10 | changed: true, 11 | value: newProperty 12 | }; 13 | } 14 | 15 | export function ignore(previousProperty: any, newProperty: any): PropertyChangeRecord { 16 | return { 17 | changed: false, 18 | value: newProperty 19 | }; 20 | } 21 | 22 | export function reference(previousProperty: any, newProperty: any): PropertyChangeRecord { 23 | return { 24 | changed: previousProperty !== newProperty, 25 | value: newProperty 26 | }; 27 | } 28 | 29 | export function shallow(previousProperty: any, newProperty: any): PropertyChangeRecord { 30 | let changed = false; 31 | 32 | const validOldProperty = previousProperty && isObjectOrArray(previousProperty); 33 | const validNewProperty = newProperty && isObjectOrArray(newProperty); 34 | 35 | if (!validOldProperty || !validNewProperty) { 36 | return { 37 | changed: true, 38 | value: newProperty 39 | }; 40 | } 41 | 42 | const previousKeys = Object.keys(previousProperty); 43 | const newKeys = Object.keys(newProperty); 44 | 45 | if (previousKeys.length !== newKeys.length) { 46 | changed = true; 47 | } else { 48 | changed = newKeys.some((key) => { 49 | return newProperty[key] !== previousProperty[key]; 50 | }); 51 | } 52 | return { 53 | changed, 54 | value: newProperty 55 | }; 56 | } 57 | 58 | export function auto(previousProperty: any, newProperty: any): PropertyChangeRecord { 59 | let result; 60 | if (typeof newProperty === 'function') { 61 | if (newProperty._type === WIDGET_BASE_TYPE) { 62 | result = reference(previousProperty, newProperty); 63 | } else { 64 | result = ignore(previousProperty, newProperty); 65 | } 66 | } else if (isObjectOrArray(newProperty)) { 67 | result = shallow(previousProperty, newProperty); 68 | } else { 69 | result = reference(previousProperty, newProperty); 70 | } 71 | return result; 72 | } 73 | -------------------------------------------------------------------------------- /src/meta/Base.ts: -------------------------------------------------------------------------------- 1 | import { Destroyable } from '@dojo/core/Destroyable'; 2 | import Set from '@dojo/shim/Set'; 3 | import { WidgetMetaBase, WidgetMetaProperties, NodeHandlerInterface, WidgetBaseInterface } from '../interfaces'; 4 | 5 | export class Base extends Destroyable implements WidgetMetaBase { 6 | private _invalidate: () => void; 7 | protected nodeHandler: NodeHandlerInterface; 8 | 9 | private _requestedNodeKeys = new Set(); 10 | 11 | protected _bind: WidgetBaseInterface | undefined; 12 | 13 | constructor(properties: WidgetMetaProperties) { 14 | super(); 15 | 16 | this._invalidate = properties.invalidate; 17 | this.nodeHandler = properties.nodeHandler; 18 | if (properties.bind) { 19 | this._bind = properties.bind; 20 | } 21 | } 22 | 23 | public has(key: string | number): boolean { 24 | return this.nodeHandler.has(key); 25 | } 26 | 27 | protected getNode(key: string | number): Element | undefined { 28 | const stringKey = `${key}`; 29 | const node = this.nodeHandler.get(stringKey); 30 | 31 | if (!node && !this._requestedNodeKeys.has(stringKey)) { 32 | const handle = this.nodeHandler.on(stringKey, () => { 33 | handle.destroy(); 34 | this._requestedNodeKeys.delete(stringKey); 35 | this.invalidate(); 36 | }); 37 | 38 | this.own(handle); 39 | this._requestedNodeKeys.add(stringKey); 40 | } 41 | 42 | return node; 43 | } 44 | 45 | protected invalidate(): void { 46 | this._invalidate(); 47 | } 48 | 49 | public afterRender(): void { 50 | // Do nothing by default. 51 | } 52 | } 53 | 54 | export default Base; 55 | -------------------------------------------------------------------------------- /src/meta/Dimensions.ts: -------------------------------------------------------------------------------- 1 | import { Base } from './Base'; 2 | import { deepAssign } from '@dojo/core/lang'; 3 | 4 | export interface TopLeft { 5 | left: number; 6 | top: number; 7 | } 8 | 9 | export interface BottomRight { 10 | bottom: number; 11 | right: number; 12 | } 13 | 14 | export interface Size { 15 | height: number; 16 | width: number; 17 | } 18 | 19 | export interface DimensionResults { 20 | position: TopLeft & BottomRight; 21 | offset: TopLeft & Size; 22 | size: Size; 23 | scroll: TopLeft & Size; 24 | client: TopLeft & Size; 25 | } 26 | 27 | const defaultDimensions = { 28 | client: { 29 | height: 0, 30 | left: 0, 31 | top: 0, 32 | width: 0 33 | }, 34 | offset: { 35 | height: 0, 36 | left: 0, 37 | top: 0, 38 | width: 0 39 | }, 40 | position: { 41 | bottom: 0, 42 | left: 0, 43 | right: 0, 44 | top: 0 45 | }, 46 | scroll: { 47 | height: 0, 48 | left: 0, 49 | top: 0, 50 | width: 0 51 | }, 52 | size: { 53 | width: 0, 54 | height: 0 55 | } 56 | }; 57 | 58 | export class Dimensions extends Base { 59 | public get(key: string | number): Readonly { 60 | const node = this.getNode(key) as HTMLElement; 61 | 62 | if (!node) { 63 | return deepAssign({}, defaultDimensions); 64 | } 65 | 66 | const boundingDimensions = node.getBoundingClientRect(); 67 | 68 | return { 69 | client: { 70 | height: node.clientHeight, 71 | left: node.clientLeft, 72 | top: node.clientTop, 73 | width: node.clientWidth 74 | }, 75 | offset: { 76 | height: node.offsetHeight, 77 | left: node.offsetLeft, 78 | top: node.offsetTop, 79 | width: node.offsetWidth 80 | }, 81 | position: { 82 | bottom: boundingDimensions.bottom, 83 | left: boundingDimensions.left, 84 | right: boundingDimensions.right, 85 | top: boundingDimensions.top 86 | }, 87 | scroll: { 88 | height: node.scrollHeight, 89 | left: node.scrollLeft, 90 | top: node.scrollTop, 91 | width: node.scrollWidth 92 | }, 93 | size: { 94 | width: boundingDimensions.width, 95 | height: boundingDimensions.height 96 | } 97 | }; 98 | } 99 | } 100 | 101 | export default Dimensions; 102 | -------------------------------------------------------------------------------- /src/meta/Drag.ts: -------------------------------------------------------------------------------- 1 | import { deepAssign } from '@dojo/core/lang'; 2 | import global from '@dojo/shim/global'; 3 | import { assign } from '@dojo/shim/object'; 4 | import WeakMap from '@dojo/shim/WeakMap'; 5 | import { Base } from './Base'; 6 | 7 | export interface DragResults { 8 | /** 9 | * The movement of pointer during the duration of the drag state 10 | */ 11 | delta: Position; 12 | 13 | /** 14 | * Is the DOM node currently in a drag state 15 | */ 16 | isDragging: boolean; 17 | 18 | /** 19 | * A matrix of posistions that represent the start position for the current drag interaction 20 | */ 21 | start?: PositionMatrix; 22 | } 23 | 24 | interface NodeData { 25 | dragResults: DragResults; 26 | invalidate: () => void; 27 | last: PositionMatrix; 28 | start: PositionMatrix; 29 | } 30 | 31 | /** 32 | * An x/y position structure 33 | */ 34 | export interface Position { 35 | x: number; 36 | y: number; 37 | } 38 | 39 | /** 40 | * A matrix of x/y positions 41 | */ 42 | export interface PositionMatrix { 43 | /** 44 | * Client x/y position 45 | */ 46 | client: Position; 47 | 48 | /** 49 | * Offset x/y position 50 | */ 51 | offset: Position; 52 | 53 | /** 54 | * Page x/y position 55 | */ 56 | page: Position; 57 | 58 | /** 59 | * Screen x/y position 60 | */ 61 | screen: Position; 62 | } 63 | 64 | function createNodeData(invalidate: () => void): NodeData { 65 | return { 66 | dragResults: deepAssign({}, emptyResults), 67 | invalidate, 68 | last: createPositionMatrix(), 69 | start: createPositionMatrix() 70 | }; 71 | } 72 | 73 | /** 74 | * Creates an empty position 75 | */ 76 | function createPosition(): Position { 77 | return { x: 0, y: 0 }; 78 | } 79 | 80 | /** 81 | * Create an empty position matrix 82 | */ 83 | function createPositionMatrix(): PositionMatrix { 84 | return { 85 | client: { x: 0, y: 0 }, 86 | offset: { x: 0, y: 0 }, 87 | page: { x: 0, y: 0 }, 88 | screen: { x: 0, y: 0 } 89 | }; 90 | } 91 | 92 | /** 93 | * A frozen empty result object, frozen to ensure that no one downstream modifies it 94 | */ 95 | const emptyResults = Object.freeze({ 96 | delta: Object.freeze(createPosition()), 97 | isDragging: false 98 | }); 99 | 100 | /** 101 | * Return the x/y position matrix for an event 102 | * @param event The pointer event 103 | */ 104 | function getPositionMatrix(event: PointerEvent): PositionMatrix { 105 | return { 106 | client: { 107 | x: event.clientX, 108 | y: event.clientY 109 | }, 110 | offset: { 111 | x: event.offsetX, 112 | y: event.offsetY 113 | }, 114 | page: { 115 | x: event.pageX, 116 | y: event.pageY 117 | }, 118 | screen: { 119 | x: event.screenX, 120 | y: event.screenY 121 | } 122 | }; 123 | } 124 | 125 | /** 126 | * Return the delta position between two positions 127 | * @param start The first position 128 | * @param current The second position 129 | */ 130 | function getDelta(start: PositionMatrix, current: PositionMatrix): Position { 131 | return { 132 | x: current.client.x - start.client.x, 133 | y: current.client.y - start.client.y 134 | }; 135 | } 136 | 137 | /** 138 | * Sets the `touch-action` on nodes so that PointerEvents are always emitted for the node 139 | * @param node The node to init 140 | */ 141 | function initNode(node: HTMLElement): void { 142 | // Ensure that the node has `touch-action` none 143 | node.style.touchAction = 'none'; 144 | // PEP requires an attribute of `touch-action` to be set on the element 145 | node.setAttribute('touch-action', 'none'); 146 | } 147 | 148 | class DragController { 149 | private _nodeMap = new WeakMap(); 150 | private _dragging: HTMLElement | undefined = undefined; 151 | 152 | private _getData(target: HTMLElement): { state: NodeData; target: HTMLElement } | undefined { 153 | if (this._nodeMap.has(target)) { 154 | return { state: this._nodeMap.get(target)!, target }; 155 | } 156 | if (target.parentElement) { 157 | return this._getData(target.parentElement); 158 | } 159 | } 160 | 161 | private _onDragStart = (event: PointerEvent) => { 162 | const { _dragging } = this; 163 | if (!event.isPrimary && _dragging) { 164 | // we have a second touch going on here, while we are dragging, so we aren't really dragging, so we 165 | // will close this down 166 | const state = this._nodeMap.get(_dragging)!; 167 | state.dragResults.isDragging = false; 168 | state.invalidate(); 169 | this._dragging = undefined; 170 | return; 171 | } 172 | if (event.button !== 0) { 173 | // it isn't the primary button that is being clicked, so we will ignore this 174 | return; 175 | } 176 | const data = this._getData(event.target as HTMLElement); 177 | if (data) { 178 | const { state, target } = data; 179 | this._dragging = target; 180 | state.last = state.start = getPositionMatrix(event); 181 | state.dragResults.delta = createPosition(); 182 | state.dragResults.start = deepAssign({}, state.start); 183 | state.dragResults.isDragging = true; 184 | state.invalidate(); 185 | 186 | event.preventDefault(); 187 | event.stopPropagation(); 188 | } // else, we are ignoring the event 189 | }; 190 | 191 | private _onDrag = (event: PointerEvent) => { 192 | const { _dragging } = this; 193 | if (!_dragging) { 194 | return; 195 | } 196 | // state cannot be unset, using ! operator 197 | const state = this._nodeMap.get(_dragging)!; 198 | state.last = getPositionMatrix(event); 199 | state.dragResults.delta = getDelta(state.start, state.last); 200 | if (!state.dragResults.start) { 201 | state.dragResults.start = deepAssign({}, state.start); 202 | } 203 | state.invalidate(); 204 | 205 | event.preventDefault(); 206 | event.stopPropagation(); 207 | }; 208 | 209 | private _onDragStop = (event: PointerEvent) => { 210 | const { _dragging } = this; 211 | if (!_dragging) { 212 | return; 213 | } 214 | // state cannot be unset, using ! operator 215 | const state = this._nodeMap.get(_dragging)!; 216 | state.last = getPositionMatrix(event); 217 | state.dragResults.delta = getDelta(state.start, state.last); 218 | if (!state.dragResults.start) { 219 | state.dragResults.start = deepAssign({}, state.start); 220 | } 221 | state.dragResults.isDragging = false; 222 | state.invalidate(); 223 | this._dragging = undefined; 224 | 225 | event.preventDefault(); 226 | event.stopPropagation(); 227 | }; 228 | 229 | constructor() { 230 | const win: Window = global.window; 231 | win.addEventListener('pointerdown', this._onDragStart); 232 | // Use capture phase, to determine the right node target, as it will be top down versus bottom up 233 | win.addEventListener('pointermove', this._onDrag, true); 234 | win.addEventListener('pointerup', this._onDragStop, true); 235 | } 236 | 237 | public get(node: HTMLElement, invalidate: () => void): DragResults { 238 | const { _nodeMap } = this; 239 | // first time we see a node, we will initialize its state and properties 240 | if (!_nodeMap.has(node)) { 241 | _nodeMap.set(node, createNodeData(invalidate)); 242 | initNode(node); 243 | return emptyResults; 244 | } 245 | 246 | const state = _nodeMap.get(node)!; 247 | // shallow "clone" the results, so no downstream manipulation can occur 248 | const dragResults = assign({}, state.dragResults); 249 | // we are offering up an accurate delta, so we need to take the last event position and move it to the start so 250 | // that our deltas are calculated from the last time they are read 251 | state.start = state.last; 252 | // reset the delta after we have read, as any future reads should have an empty delta 253 | state.dragResults.delta = createPosition(); 254 | // clear the start state 255 | delete state.dragResults.start; 256 | 257 | return dragResults; 258 | } 259 | } 260 | 261 | const controller = new DragController(); 262 | 263 | export class Drag extends Base { 264 | private _boundInvalidate: () => void = this.invalidate.bind(this); 265 | 266 | public get(key: string | number): Readonly { 267 | const node = this.getNode(key) as HTMLElement; 268 | 269 | // if we don't have a reference to the node yet, return an empty set of results 270 | if (!node) { 271 | return emptyResults; 272 | } 273 | 274 | // otherwise we will ask the controller for our results 275 | return controller.get(node, this._boundInvalidate); 276 | } 277 | } 278 | 279 | export default Drag; 280 | -------------------------------------------------------------------------------- /src/meta/Focus.ts: -------------------------------------------------------------------------------- 1 | import { Base } from './Base'; 2 | import { createHandle } from '@dojo/core/lang'; 3 | import global from '@dojo/shim/global'; 4 | 5 | export interface FocusResults { 6 | active: boolean; 7 | containsFocus: boolean; 8 | } 9 | 10 | const defaultResults = { 11 | active: false, 12 | containsFocus: false 13 | }; 14 | 15 | export class Focus extends Base { 16 | private _activeElement: Element | undefined; 17 | 18 | public get(key: string | number): FocusResults { 19 | const node = this.getNode(key); 20 | 21 | if (!node) { 22 | return { ...defaultResults }; 23 | } 24 | 25 | if (!this._activeElement) { 26 | this._activeElement = global.document.activeElement; 27 | this._createListener(); 28 | } 29 | 30 | return { 31 | active: node === this._activeElement, 32 | containsFocus: !!this._activeElement && node.contains(this._activeElement) 33 | }; 34 | } 35 | 36 | public set(key: string | number) { 37 | const node = this.getNode(key); 38 | node && (node as HTMLElement).focus(); 39 | } 40 | 41 | private _onFocusChange = () => { 42 | this._activeElement = global.document.activeElement; 43 | this.invalidate(); 44 | }; 45 | 46 | private _createListener() { 47 | global.document.addEventListener('focusin', this._onFocusChange); 48 | global.document.addEventListener('focusout', this._onFocusChange); 49 | this.own(createHandle(this._removeListener.bind(this))); 50 | } 51 | 52 | private _removeListener() { 53 | global.document.removeEventListener('focusin', this._onFocusChange); 54 | global.document.removeEventListener('focusout', this._onFocusChange); 55 | } 56 | } 57 | 58 | export default Focus; 59 | -------------------------------------------------------------------------------- /src/meta/Intersection.ts: -------------------------------------------------------------------------------- 1 | import global from '@dojo/shim/global'; 2 | import WeakMap from '@dojo/shim/WeakMap'; 3 | import Map from '@dojo/shim/Map'; 4 | import { createHandle } from '@dojo/core/lang'; 5 | import { Base } from './Base'; 6 | 7 | interface ExtendedIntersectionObserverEntry extends IntersectionObserverEntry { 8 | readonly isIntersecting: boolean; 9 | } 10 | 11 | interface IntersectionDetail extends IntersectionGetOptions { 12 | entries: WeakMap; 13 | observer: IntersectionObserver; 14 | } 15 | 16 | export interface IntersectionGetOptions { 17 | root?: string; 18 | rootMargin?: string; 19 | threshold?: number[]; 20 | } 21 | 22 | export interface IntersectionResult { 23 | intersectionRatio: number; 24 | isIntersecting: boolean; 25 | } 26 | 27 | const defaultIntersection: IntersectionResult = Object.freeze({ 28 | intersectionRatio: 0, 29 | isIntersecting: false 30 | }); 31 | 32 | export class Intersection extends Base { 33 | private readonly _details = new Map(); 34 | 35 | /** 36 | * Return an `InteractionResult` for the requested key and options. 37 | * 38 | * @param key The key to return the intersection meta for 39 | * @param options The options for the request 40 | */ 41 | public get(key: string | number, options: IntersectionGetOptions = {}): IntersectionResult { 42 | let rootNode: HTMLElement | undefined; 43 | if (options.root) { 44 | rootNode = this.getNode(options.root) as HTMLElement; 45 | if (!rootNode) { 46 | return defaultIntersection; 47 | } 48 | } 49 | const node = this.getNode(key); 50 | if (!node) { 51 | return defaultIntersection; 52 | } 53 | 54 | let details = this._getDetails(options) || this._createDetails(options, rootNode); 55 | if (!details.entries.get(node)) { 56 | details.entries.set(node, defaultIntersection); 57 | details.observer.observe(node); 58 | } 59 | 60 | return details.entries.get(node) || defaultIntersection; 61 | } 62 | 63 | /** 64 | * Returns true if the node for the key has intersection details 65 | * 66 | * @param key The key to return the intersection meta for 67 | * @param options The options for the request 68 | */ 69 | public has(key: string | number, options?: IntersectionGetOptions): boolean { 70 | const node = this.getNode(key); 71 | const details = this._getDetails(options); 72 | return Boolean(details && node && details.entries.has(node)); 73 | } 74 | 75 | private _createDetails(options: IntersectionGetOptions, rootNode?: HTMLElement): IntersectionDetail { 76 | const entries = new WeakMap(); 77 | const observer = new global.IntersectionObserver(this._onIntersect(entries), { ...options, root: rootNode }); 78 | const details = { observer, entries, ...options }; 79 | 80 | this._details.set(JSON.stringify(options), details); 81 | this.own(createHandle(() => observer.disconnect())); 82 | return details; 83 | } 84 | 85 | private _getDetails(options: IntersectionGetOptions = {}): IntersectionDetail | undefined { 86 | return this._details.get(JSON.stringify(options)); 87 | } 88 | 89 | private _onIntersect = (detailEntries: WeakMap) => { 90 | return (entries: ExtendedIntersectionObserverEntry[]) => { 91 | for (const { intersectionRatio, isIntersecting, target } of entries) { 92 | detailEntries.set(target, { intersectionRatio, isIntersecting }); 93 | } 94 | this.invalidate(); 95 | }; 96 | }; 97 | } 98 | 99 | export default Intersection; 100 | -------------------------------------------------------------------------------- /src/meta/Matches.ts: -------------------------------------------------------------------------------- 1 | import { Base } from './Base'; 2 | 3 | export default class Matches extends Base { 4 | /** 5 | * Determine if the target of a particular `Event` matches the virtual DOM key 6 | * @param key The virtual DOM key 7 | * @param event The event object 8 | */ 9 | public get(key: string | number, event: Event): boolean { 10 | return this.getNode(key) === event.target; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/meta/Resize.ts: -------------------------------------------------------------------------------- 1 | import { Base } from './Base'; 2 | import Map from '@dojo/shim/Map'; 3 | 4 | interface Observer { 5 | observe(node: HTMLElement): void; 6 | } 7 | 8 | declare const ResizeObserver: { 9 | prototype: Observer; 10 | new (callback: (entries: ResizeObserverEntry[]) => any): any; 11 | }; 12 | 13 | interface ResizeObserverEntry { 14 | contentRect: ContentRect; 15 | } 16 | 17 | export interface ContentRect { 18 | readonly bottom: number; 19 | readonly height: number; 20 | readonly left: number; 21 | readonly right: number; 22 | readonly top: number; 23 | readonly width: number; 24 | readonly x: number; 25 | readonly y: number; 26 | } 27 | 28 | export interface PredicateFunction { 29 | (contentRect: ContentRect): boolean; 30 | } 31 | 32 | export interface PredicateFunctions { 33 | [id: string]: PredicateFunction; 34 | } 35 | 36 | export type PredicateResponses = { [id in keyof T]: boolean }; 37 | 38 | export class Resize extends Base { 39 | private _details = new Map(); 40 | 41 | public get( 42 | key: string | number, 43 | predicates = {} as PredicateFunctions 44 | ): PredicateResponses { 45 | const node = this.getNode(key); 46 | 47 | if (!node) { 48 | const defaultResponse: PredicateResponses = {}; 49 | for (let predicateId in predicates) { 50 | defaultResponse[predicateId] = false; 51 | } 52 | return defaultResponse as PredicateResponses; 53 | } 54 | 55 | if (!this._details.has(key)) { 56 | this._details.set(key, {}); 57 | const resizeObserver = new ResizeObserver(([entry]) => { 58 | let predicateChanged = false; 59 | if (Object.keys(predicates).length) { 60 | const { contentRect } = entry; 61 | const previousDetails = this._details.get(key); 62 | let predicateResponses: PredicateResponses = {}; 63 | 64 | for (let predicateId in predicates) { 65 | const response = predicates[predicateId](contentRect); 66 | predicateResponses[predicateId] = response; 67 | if (!predicateChanged && response !== previousDetails![predicateId]) { 68 | predicateChanged = true; 69 | } 70 | } 71 | 72 | this._details.set(key, predicateResponses); 73 | } else { 74 | predicateChanged = true; 75 | } 76 | predicateChanged && this.invalidate(); 77 | }); 78 | resizeObserver.observe(node); 79 | } 80 | 81 | return this._details.get(key) as PredicateResponses; 82 | } 83 | } 84 | 85 | export default Resize; 86 | -------------------------------------------------------------------------------- /src/meta/WebAnimation.ts: -------------------------------------------------------------------------------- 1 | import { Base } from './Base'; 2 | import Map from '@dojo/shim/Map'; 3 | import global from '@dojo/shim/global'; 4 | 5 | /** 6 | * Animation controls are used to control the web animation that has been applied 7 | * to a vdom node. 8 | */ 9 | export interface AnimationControls { 10 | play?: boolean; 11 | onFinish?: () => void; 12 | onCancel?: () => void; 13 | reverse?: boolean; 14 | cancel?: boolean; 15 | finish?: boolean; 16 | playbackRate?: number; 17 | startTime?: number; 18 | currentTime?: number; 19 | } 20 | 21 | /** 22 | * Animation timing properties passed to a new KeyframeEffect. 23 | */ 24 | export interface AnimationTimingProperties { 25 | duration?: number; 26 | delay?: number; 27 | direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse'; 28 | easing?: string; 29 | endDelay?: number; 30 | fill?: 'none' | 'forwards' | 'backwards' | 'both' | 'auto'; 31 | iterations?: number; 32 | iterationStart?: number; 33 | } 34 | 35 | /** 36 | * Animation propertiues that can be passed as vdom property `animate` 37 | */ 38 | export interface AnimationProperties { 39 | id: string; 40 | effects: (() => AnimationKeyFrame | AnimationKeyFrame[]) | AnimationKeyFrame | AnimationKeyFrame[]; 41 | controls?: AnimationControls; 42 | timing?: AnimationTimingProperties; 43 | } 44 | 45 | export type AnimationPropertiesFunction = () => AnimationProperties; 46 | 47 | /** 48 | * Info returned by the `get` function on WebAnimation meta 49 | */ 50 | export interface AnimationInfo { 51 | currentTime: number; 52 | playState: 'idle' | 'pending' | 'running' | 'paused' | 'finished'; 53 | playbackRate: number; 54 | startTime: number; 55 | } 56 | 57 | export interface AnimationPlayer { 58 | player: Animation; 59 | used: boolean; 60 | } 61 | 62 | export class WebAnimations extends Base { 63 | private _animationMap = new Map(); 64 | 65 | private _createPlayer(node: HTMLElement, properties: AnimationProperties): Animation { 66 | const { effects, timing = {} } = properties; 67 | 68 | const fx = typeof effects === 'function' ? effects() : effects; 69 | 70 | const keyframeEffect = new KeyframeEffect(node, fx, timing as AnimationEffectTiming); 71 | 72 | return new Animation(keyframeEffect, global.document.timeline); 73 | } 74 | 75 | private _updatePlayer(player: Animation, controls: AnimationControls) { 76 | const { play, reverse, cancel, finish, onFinish, onCancel, playbackRate, startTime, currentTime } = controls; 77 | 78 | if (playbackRate !== undefined) { 79 | player.playbackRate = playbackRate; 80 | } 81 | 82 | if (reverse) { 83 | player.reverse(); 84 | } 85 | 86 | if (cancel) { 87 | player.cancel(); 88 | } 89 | 90 | if (finish) { 91 | player.finish(); 92 | } 93 | 94 | if (startTime !== undefined) { 95 | player.startTime = startTime; 96 | } 97 | 98 | if (currentTime !== undefined) { 99 | player.currentTime = currentTime; 100 | } 101 | 102 | if (play) { 103 | player.play(); 104 | } else { 105 | player.pause(); 106 | } 107 | 108 | if (onFinish) { 109 | player.onfinish = onFinish.bind(this._bind); 110 | } 111 | 112 | if (onCancel) { 113 | player.oncancel = onCancel.bind(this._bind); 114 | } 115 | } 116 | 117 | animate( 118 | key: string, 119 | animateProperties: 120 | | AnimationProperties 121 | | AnimationPropertiesFunction 122 | | (AnimationProperties | AnimationPropertiesFunction)[] 123 | ) { 124 | const node = this.getNode(key) as HTMLElement; 125 | 126 | if (node) { 127 | if (!Array.isArray(animateProperties)) { 128 | animateProperties = [animateProperties]; 129 | } 130 | animateProperties.forEach((properties) => { 131 | properties = typeof properties === 'function' ? properties() : properties; 132 | 133 | if (properties) { 134 | const { id } = properties; 135 | if (!this._animationMap.has(id)) { 136 | this._animationMap.set(id, { 137 | player: this._createPlayer(node, properties), 138 | used: true 139 | }); 140 | } 141 | 142 | const animation = this._animationMap.get(id); 143 | const { controls = {} } = properties; 144 | 145 | if (animation) { 146 | this._updatePlayer(animation.player, controls); 147 | 148 | this._animationMap.set(id, { 149 | player: animation.player, 150 | used: true 151 | }); 152 | } 153 | } 154 | }); 155 | } 156 | } 157 | 158 | get(id: string): Readonly | undefined { 159 | const animation = this._animationMap.get(id); 160 | if (animation) { 161 | const { currentTime, playState, playbackRate, startTime } = animation.player; 162 | 163 | return { 164 | currentTime, 165 | playState, 166 | playbackRate, 167 | startTime 168 | }; 169 | } 170 | } 171 | 172 | afterRender() { 173 | this._animationMap.forEach((animation, key) => { 174 | if (!animation.used) { 175 | animation.player.cancel(); 176 | this._animationMap.delete(key); 177 | } 178 | animation.used = false; 179 | }); 180 | } 181 | } 182 | 183 | export default WebAnimations; 184 | -------------------------------------------------------------------------------- /src/mixins/Focus.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from './../interfaces'; 2 | import { WidgetBase } from './../WidgetBase'; 3 | import { diffProperty } from './../decorators/diffProperty'; 4 | 5 | export interface FocusProperties { 6 | focus?: (() => boolean); 7 | } 8 | 9 | export interface FocusMixin { 10 | focus: () => void; 11 | shouldFocus: () => boolean; 12 | properties: FocusProperties; 13 | } 14 | 15 | function diffFocus(previousProperty: Function, newProperty: Function) { 16 | const result = newProperty && newProperty(); 17 | return { 18 | changed: result, 19 | value: newProperty 20 | }; 21 | } 22 | 23 | export function FocusMixin>>(Base: T): T & Constructor { 24 | abstract class Focus extends Base { 25 | public abstract properties: FocusProperties; 26 | 27 | private _currentToken = 0; 28 | 29 | private _previousToken = 0; 30 | 31 | @diffProperty('focus', diffFocus) 32 | protected isFocusedReaction() { 33 | this._currentToken++; 34 | } 35 | 36 | public shouldFocus = () => { 37 | const result = this._currentToken !== this._previousToken; 38 | this._previousToken = this._currentToken; 39 | return result; 40 | }; 41 | 42 | public focus() { 43 | this._currentToken++; 44 | this.invalidate(); 45 | } 46 | } 47 | return Focus; 48 | } 49 | 50 | export default FocusMixin; 51 | -------------------------------------------------------------------------------- /src/mixins/Themed.ts: -------------------------------------------------------------------------------- 1 | import { Constructor, WidgetProperties, SupportedClassName } from './../interfaces'; 2 | import { Registry } from './../Registry'; 3 | import { Injector } from './../Injector'; 4 | import { inject } from './../decorators/inject'; 5 | import { WidgetBase } from './../WidgetBase'; 6 | import { handleDecorator } from './../decorators/handleDecorator'; 7 | import { diffProperty } from './../decorators/diffProperty'; 8 | import { shallow } from './../diff'; 9 | 10 | /** 11 | * A lookup object for available class names 12 | */ 13 | export type ClassNames = { 14 | [key: string]: string; 15 | }; 16 | 17 | /** 18 | * A lookup object for available widget classes names 19 | */ 20 | export interface Theme { 21 | [key: string]: object; 22 | } 23 | 24 | /** 25 | * Properties required for the Themed mixin 26 | */ 27 | export interface ThemedProperties extends WidgetProperties { 28 | injectedTheme?: any; 29 | theme?: Theme; 30 | extraClasses?: { [P in keyof T]?: string }; 31 | } 32 | 33 | const THEME_KEY = ' _key'; 34 | 35 | export const INJECTED_THEME_KEY = Symbol('theme'); 36 | 37 | /** 38 | * Interface for the ThemedMixin 39 | */ 40 | export interface ThemedMixin { 41 | theme(classes: SupportedClassName): SupportedClassName; 42 | theme(classes: SupportedClassName[]): SupportedClassName[]; 43 | properties: ThemedProperties; 44 | } 45 | 46 | /** 47 | * Decorator for base css classes 48 | */ 49 | export function theme(theme: {}) { 50 | return handleDecorator((target) => { 51 | target.addDecorator('baseThemeClasses', theme); 52 | }); 53 | } 54 | 55 | /** 56 | * Creates a reverse lookup for the classes passed in via the `theme` function. 57 | * 58 | * @param classes The baseClasses object 59 | * @requires 60 | */ 61 | function createThemeClassesLookup(classes: ClassNames[]): ClassNames { 62 | return classes.reduce( 63 | (currentClassNames, baseClass) => { 64 | Object.keys(baseClass).forEach((key: string) => { 65 | currentClassNames[baseClass[key]] = key; 66 | }); 67 | return currentClassNames; 68 | }, 69 | {} 70 | ); 71 | } 72 | 73 | /** 74 | * Convenience function that is given a theme and an optional registry, the theme 75 | * injector is defined against the registry, returning the theme. 76 | * 77 | * @param theme the theme to set 78 | * @param themeRegistry registry to define the theme injector against. Defaults 79 | * to the global registry 80 | * 81 | * @returns the theme injector used to set the theme 82 | */ 83 | export function registerThemeInjector(theme: any, themeRegistry: Registry): Injector { 84 | const themeInjector = new Injector(theme); 85 | themeRegistry.defineInjector(INJECTED_THEME_KEY, (invalidator) => { 86 | themeInjector.setInvalidator(invalidator); 87 | return () => themeInjector.get(); 88 | }); 89 | return themeInjector; 90 | } 91 | 92 | /** 93 | * Function that returns a class decorated with with Themed functionality 94 | */ 95 | 96 | export function ThemedMixin>>>( 97 | Base: T 98 | ): Constructor> & T { 99 | @inject({ 100 | name: INJECTED_THEME_KEY, 101 | getProperties: (theme: Theme, properties: ThemedProperties): ThemedProperties => { 102 | if (!properties.theme) { 103 | return { theme }; 104 | } 105 | return {}; 106 | } 107 | }) 108 | abstract class Themed extends Base { 109 | public abstract properties: ThemedProperties; 110 | 111 | /** 112 | * The Themed baseClasses 113 | */ 114 | private _registeredBaseTheme: ClassNames | undefined; 115 | 116 | /** 117 | * Registered base theme keys 118 | */ 119 | private _registeredBaseThemeKeys: string[] = []; 120 | 121 | /** 122 | * Reverse lookup of the theme classes 123 | */ 124 | private _baseThemeClassesReverseLookup: ClassNames | undefined; 125 | 126 | /** 127 | * Indicates if classes meta data need to be calculated. 128 | */ 129 | private _recalculateClasses = true; 130 | 131 | /** 132 | * Loaded theme 133 | */ 134 | private _theme: ClassNames = {}; 135 | 136 | public theme(classes: SupportedClassName): SupportedClassName; 137 | public theme(classes: SupportedClassName[]): SupportedClassName[]; 138 | public theme(classes: SupportedClassName | SupportedClassName[]): SupportedClassName | SupportedClassName[] { 139 | if (this._recalculateClasses) { 140 | this._recalculateThemeClasses(); 141 | } 142 | if (Array.isArray(classes)) { 143 | return classes.map((className) => this._getThemeClass(className)); 144 | } 145 | return this._getThemeClass(classes); 146 | } 147 | 148 | /** 149 | * Function fired when `theme` or `extraClasses` are changed. 150 | */ 151 | @diffProperty('theme', shallow) 152 | @diffProperty('extraClasses', shallow) 153 | protected onPropertiesChanged() { 154 | this._recalculateClasses = true; 155 | } 156 | 157 | private _getThemeClass(className: SupportedClassName): SupportedClassName { 158 | if (className === undefined || className === null) { 159 | return className; 160 | } 161 | 162 | const extraClasses = this.properties.extraClasses || ({} as any); 163 | const themeClassName = this._baseThemeClassesReverseLookup![className]; 164 | let resultClassNames: string[] = []; 165 | if (!themeClassName) { 166 | console.warn(`Class name: '${className}' not found in theme`); 167 | return null; 168 | } 169 | 170 | if (extraClasses[themeClassName]) { 171 | resultClassNames.push(extraClasses[themeClassName]); 172 | } 173 | 174 | if (this._theme[themeClassName]) { 175 | resultClassNames.push(this._theme[themeClassName]); 176 | } else { 177 | resultClassNames.push(this._registeredBaseTheme![themeClassName]); 178 | } 179 | return resultClassNames.join(' '); 180 | } 181 | 182 | private _recalculateThemeClasses() { 183 | const { theme = {} } = this.properties; 184 | const baseThemes = this.getDecorator('baseThemeClasses'); 185 | if (!this._registeredBaseTheme) { 186 | this._registeredBaseTheme = baseThemes.reduce((finalBaseTheme, baseTheme) => { 187 | const { [THEME_KEY]: key, ...classes } = baseTheme; 188 | this._registeredBaseThemeKeys.push(key); 189 | return { ...finalBaseTheme, ...classes }; 190 | }, {}); 191 | this._baseThemeClassesReverseLookup = createThemeClassesLookup(baseThemes); 192 | } 193 | 194 | this._theme = this._registeredBaseThemeKeys.reduce((baseTheme, themeKey) => { 195 | return { ...baseTheme, ...theme[themeKey] }; 196 | }, {}); 197 | 198 | this._recalculateClasses = false; 199 | } 200 | } 201 | 202 | return Themed; 203 | } 204 | 205 | export default ThemedMixin; 206 | -------------------------------------------------------------------------------- /src/registerCustomElement.ts: -------------------------------------------------------------------------------- 1 | import { WidgetBase, noBind } from './WidgetBase'; 2 | import { ProjectorMixin } from './mixins/Projector'; 3 | import { from } from '@dojo/shim/array'; 4 | import { w, dom } from './d'; 5 | import global from '@dojo/shim/global'; 6 | import { registerThemeInjector } from './mixins/Themed'; 7 | import { alwaysRender } from './decorators/alwaysRender'; 8 | 9 | export enum CustomElementChildType { 10 | DOJO = 'DOJO', 11 | NODE = 'NODE', 12 | TEXT = 'TEXT' 13 | } 14 | 15 | export function DomToWidgetWrapper(domNode: HTMLElement): any { 16 | @alwaysRender() 17 | class DomToWidgetWrapper extends WidgetBase { 18 | protected render() { 19 | const properties = Object.keys(this.properties).reduce( 20 | (props, key: string) => { 21 | const value = this.properties[key]; 22 | if (key.indexOf('on') === 0) { 23 | key = `__${key}`; 24 | } 25 | props[key] = value; 26 | return props; 27 | }, 28 | {} as any 29 | ); 30 | return dom({ node: domNode, props: properties, diffType: 'dom' }); 31 | } 32 | 33 | static get domNode() { 34 | return domNode; 35 | } 36 | } 37 | 38 | return DomToWidgetWrapper; 39 | } 40 | 41 | export function create(descriptor: any, WidgetConstructor: any): any { 42 | const { attributes, childType, registryFactory } = descriptor; 43 | const attributeMap: any = {}; 44 | 45 | attributes.forEach((propertyName: string) => { 46 | const attributeName = propertyName.toLowerCase(); 47 | attributeMap[attributeName] = propertyName; 48 | }); 49 | 50 | return class extends HTMLElement { 51 | private _projector: any; 52 | private _properties: any = {}; 53 | private _children: any[] = []; 54 | private _eventProperties: any = {}; 55 | private _initialised = false; 56 | 57 | public connectedCallback() { 58 | if (this._initialised) { 59 | return; 60 | } 61 | 62 | const domProperties: any = {}; 63 | const { attributes, properties, events } = descriptor; 64 | 65 | this._properties = { ...this._properties, ...this._attributesToProperties(attributes) }; 66 | 67 | [...attributes, ...properties].forEach((propertyName: string) => { 68 | const value = (this as any)[propertyName]; 69 | const filteredPropertyName = propertyName.replace(/^on/, '__'); 70 | if (value !== undefined) { 71 | this._properties[propertyName] = value; 72 | } 73 | 74 | if (filteredPropertyName !== propertyName) { 75 | domProperties[filteredPropertyName] = { 76 | get: () => this._getProperty(propertyName), 77 | set: (value: any) => this._setProperty(propertyName, value) 78 | }; 79 | } 80 | 81 | domProperties[propertyName] = { 82 | get: () => this._getProperty(propertyName), 83 | set: (value: any) => this._setProperty(propertyName, value) 84 | }; 85 | }); 86 | 87 | events.forEach((propertyName: string) => { 88 | const eventName = propertyName.replace(/^on/, '').toLowerCase(); 89 | const filteredPropertyName = propertyName.replace(/^on/, '__on'); 90 | 91 | domProperties[filteredPropertyName] = { 92 | get: () => this._getEventProperty(propertyName), 93 | set: (value: any) => this._setEventProperty(propertyName, value) 94 | }; 95 | 96 | this._eventProperties[propertyName] = undefined; 97 | this._properties[propertyName] = (...args: any[]) => { 98 | const eventCallback = this._getEventProperty(propertyName); 99 | if (typeof eventCallback === 'function') { 100 | eventCallback(...args); 101 | } 102 | this.dispatchEvent( 103 | new CustomEvent(eventName, { 104 | bubbles: false, 105 | detail: args 106 | }) 107 | ); 108 | }; 109 | }); 110 | 111 | Object.defineProperties(this, domProperties); 112 | 113 | const children = childType === CustomElementChildType.TEXT ? this.childNodes : this.children; 114 | 115 | from(children).forEach((childNode: Node) => { 116 | if (childType === CustomElementChildType.DOJO) { 117 | childNode.addEventListener('dojo-ce-render', () => this._render()); 118 | childNode.addEventListener('dojo-ce-connected', () => this._render()); 119 | this._children.push(DomToWidgetWrapper(childNode as HTMLElement)); 120 | } else { 121 | this._children.push(dom({ node: childNode as HTMLElement, diffType: 'dom' })); 122 | } 123 | }); 124 | 125 | this.addEventListener('dojo-ce-connected', (e: any) => this._childConnected(e)); 126 | 127 | const widgetProperties = this._properties; 128 | const renderChildren = () => this.__children__(); 129 | const Wrapper = class extends WidgetBase { 130 | render() { 131 | return w(WidgetConstructor, widgetProperties, renderChildren()); 132 | } 133 | }; 134 | const registry = registryFactory(); 135 | const themeContext = registerThemeInjector(this._getTheme(), registry); 136 | global.addEventListener('dojo-theme-set', () => themeContext.set(this._getTheme())); 137 | const Projector = ProjectorMixin(Wrapper); 138 | this._projector = new Projector(); 139 | this._projector.setProperties({ registry }); 140 | this._projector.append(this); 141 | 142 | this._initialised = true; 143 | this.dispatchEvent( 144 | new CustomEvent('dojo-ce-connected', { 145 | bubbles: true, 146 | detail: this 147 | }) 148 | ); 149 | } 150 | 151 | private _getTheme() { 152 | if (global && global.dojoce && global.dojoce.theme) { 153 | return global.dojoce.themes[global.dojoce.theme]; 154 | } 155 | } 156 | 157 | private _childConnected(e: any) { 158 | const node = e.detail; 159 | if (node.parentNode === this) { 160 | const exists = this._children.some((child) => child.domNode === node); 161 | if (!exists) { 162 | node.addEventListener('dojo-ce-render', () => this._render()); 163 | this._children.push(DomToWidgetWrapper(node)); 164 | this._render(); 165 | } 166 | } 167 | } 168 | 169 | private _render() { 170 | if (this._projector) { 171 | this._projector.invalidate(); 172 | this.dispatchEvent( 173 | new CustomEvent('dojo-ce-render', { 174 | bubbles: false, 175 | detail: this 176 | }) 177 | ); 178 | } 179 | } 180 | 181 | public __properties__() { 182 | return { ...this._properties, ...this._eventProperties }; 183 | } 184 | 185 | public __children__() { 186 | if (childType === CustomElementChildType.DOJO) { 187 | return this._children.filter((Child) => Child.domNode.isWidget).map((Child: any) => { 188 | const { domNode } = Child; 189 | return w(Child, { ...domNode.__properties__() }, [...domNode.__children__()]); 190 | }); 191 | } else { 192 | return this._children; 193 | } 194 | } 195 | 196 | public attributeChangedCallback(name: string, oldValue: string | null, value: string | null) { 197 | const propertyName = attributeMap[name]; 198 | this._setProperty(propertyName, value); 199 | } 200 | 201 | private _setEventProperty(propertyName: string, value: any) { 202 | this._eventProperties[propertyName] = value; 203 | } 204 | 205 | private _getEventProperty(propertyName: string) { 206 | return this._eventProperties[propertyName]; 207 | } 208 | 209 | private _setProperty(propertyName: string, value: any) { 210 | if (typeof value === 'function') { 211 | value[noBind] = true; 212 | } 213 | this._properties[propertyName] = value; 214 | this._render(); 215 | } 216 | 217 | private _getProperty(propertyName: string) { 218 | return this._properties[propertyName]; 219 | } 220 | 221 | private _attributesToProperties(attributes: string[]) { 222 | return attributes.reduce((properties: any, propertyName: string) => { 223 | const attributeName = propertyName.toLowerCase(); 224 | const value = this.getAttribute(attributeName); 225 | if (value !== null) { 226 | properties[propertyName] = value; 227 | } 228 | return properties; 229 | }, {}); 230 | } 231 | 232 | static get observedAttributes() { 233 | return Object.keys(attributeMap); 234 | } 235 | 236 | public get isWidget() { 237 | return true; 238 | } 239 | }; 240 | } 241 | 242 | export function register(WidgetConstructor: any): void { 243 | const descriptor = WidgetConstructor.prototype && WidgetConstructor.prototype.__customElementDescriptor; 244 | 245 | if (!descriptor) { 246 | throw new Error( 247 | 'Cannot get descriptor for Custom Element, have you added the @customElement decorator to your Widget?' 248 | ); 249 | } 250 | 251 | global.customElements.define(descriptor.tagName, create(descriptor, WidgetConstructor)); 252 | } 253 | 254 | export default register; 255 | -------------------------------------------------------------------------------- /src/tsx.ts: -------------------------------------------------------------------------------- 1 | import { v, w } from './d'; 2 | import { Constructor, DNode } from './interfaces'; 3 | import { WNode, VNodeProperties } from './interfaces'; 4 | 5 | declare global { 6 | namespace JSX { 7 | type Element = WNode; 8 | interface ElementAttributesProperty { 9 | properties: {}; 10 | } 11 | interface IntrinsicElements { 12 | [key: string]: VNodeProperties; 13 | } 14 | } 15 | } 16 | 17 | export const REGISTRY_ITEM = Symbol('Identifier for an item from the Widget Registry.'); 18 | 19 | export class FromRegistry

{ 20 | static type = REGISTRY_ITEM; 21 | properties: P = {} as P; 22 | name: string | undefined; 23 | } 24 | 25 | export function fromRegistry

(tag: string): Constructor> { 26 | return class extends FromRegistry

{ 27 | properties: P = {} as P; 28 | static type = REGISTRY_ITEM; 29 | name = tag; 30 | }; 31 | } 32 | 33 | function spreadChildren(children: any[], child: any): any[] { 34 | if (Array.isArray(child)) { 35 | return child.reduce(spreadChildren, children); 36 | } else { 37 | return [...children, child]; 38 | } 39 | } 40 | 41 | export function tsx(tag: any, properties = {}, ...children: any[]): DNode { 42 | children = children.reduce(spreadChildren, []); 43 | properties = properties === null ? {} : properties; 44 | if (typeof tag === 'string') { 45 | return v(tag, properties, children); 46 | } else if (tag.type === REGISTRY_ITEM) { 47 | const registryItem = new tag(); 48 | return w(registryItem.name, properties, children); 49 | } else { 50 | return w(tag, properties, children); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/benchmark/app/App.ts: -------------------------------------------------------------------------------- 1 | import { DNode } from '../../../src/interfaces'; 2 | import { WidgetBase } from '../../../src/WidgetBase'; 3 | import { v, w } from '../../../src/d'; 4 | 5 | import { Row } from './Row'; 6 | import { Buttons, ButtonConfig } from './Buttons'; 7 | import { Store } from './Store'; 8 | 9 | export class App extends WidgetBase { 10 | private _store: Store = new Store(); 11 | 12 | private _run = () => { 13 | this._store.run(); 14 | this.invalidate(); 15 | }; 16 | 17 | private _add = () => { 18 | this._store.add(); 19 | this.invalidate(); 20 | }; 21 | 22 | private _update = () => { 23 | this._store.update(); 24 | this.invalidate(); 25 | }; 26 | 27 | private _select = (id: number) => { 28 | this._store.select(id); 29 | this.invalidate(); 30 | }; 31 | 32 | private _delete = (id: number) => { 33 | this._store.delete(id); 34 | this.invalidate(); 35 | }; 36 | 37 | private _runLots = () => { 38 | this._store.runLots(); 39 | this.invalidate(); 40 | }; 41 | 42 | private _clear = () => { 43 | this._store.clear(); 44 | this.invalidate(); 45 | }; 46 | 47 | private _swapRows = () => { 48 | this._store.swapRows(); 49 | this.invalidate(); 50 | }; 51 | 52 | private _buttonConfigs: ButtonConfig[] = [ 53 | { id: 'run', label: 'Create 1,000 rows', onClick: this._run }, 54 | { id: 'runlots', label: 'Create 10,000 rows', onClick: this._runLots }, 55 | { id: 'add', label: 'Append 1,000 rows', onClick: this._add }, 56 | { id: 'update', label: 'Update every 10th row', onClick: this._update }, 57 | { id: 'clear', label: 'Clear', onClick: this._clear }, 58 | { id: 'swaprows', label: 'Swap Rows', onClick: this._swapRows } 59 | ]; 60 | 61 | protected render(): DNode { 62 | const { _select, _delete, _store } = this; 63 | const rows = _store.data.map(({ id, label }, index) => { 64 | return w(Row, { 65 | id, 66 | key: id, 67 | label, 68 | onRowSelected: _select, 69 | onRowDeleted: _delete, 70 | selected: id === _store.selected 71 | }); 72 | }); 73 | 74 | return v('div', { key: 'root', classes: ['container'] }, [ 75 | w(Buttons, { buttonConfigs: this._buttonConfigs }), 76 | v('table', { classes: ['table', 'table-hover', 'table-striped', 'test-data'] }, [v('tbody', rows)]), 77 | v('span', { classes: ['preloadicon', 'glyphicon', 'glyphicon-remove'] }) 78 | ]); 79 | } 80 | } 81 | 82 | export default App; 83 | -------------------------------------------------------------------------------- /tests/benchmark/app/Button.ts: -------------------------------------------------------------------------------- 1 | import { WidgetBase } from '../../../src/WidgetBase'; 2 | import { v } from '../../../src/d'; 3 | import { DNode } from '../../../src/interfaces'; 4 | 5 | export interface ButtonProperties { 6 | id: string; 7 | label: string; 8 | onClick: () => void; 9 | } 10 | 11 | export class Button extends WidgetBase { 12 | protected render(): DNode { 13 | const { id, label, onClick } = this.properties; 14 | 15 | return v('div', { classes: ['col-sm-6', 'smallpad'] }, [ 16 | v( 17 | 'button', 18 | { 19 | id, 20 | classes: ['btn', 'btn-primary', 'btn-block'], 21 | onclick: onClick 22 | }, 23 | [label] 24 | ) 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/benchmark/app/Buttons.ts: -------------------------------------------------------------------------------- 1 | import { DNode } from '../../../src/interfaces'; 2 | import { WidgetBase } from '../../../src/WidgetBase'; 3 | import { v, w } from '../../../src/d'; 4 | 5 | import { Button } from './Button'; 6 | 7 | export interface ButtonConfig { 8 | id: string; 9 | label: string; 10 | onClick: () => void; 11 | } 12 | 13 | export interface ButtonsProperties { 14 | buttonConfigs: ButtonConfig[]; 15 | } 16 | 17 | export class Buttons extends WidgetBase { 18 | protected render(): DNode { 19 | const { buttonConfigs } = this.properties; 20 | 21 | return v('div', { classes: ['jumbotron'] }, [ 22 | v('div', { classes: ['row'] }, [ 23 | v('div', { classes: ['col-md-6'] }, [v('h1', ['Dojo2 v0.2.0'])]), 24 | v( 25 | 'div', 26 | { classes: ['col-md-6'] }, 27 | buttonConfigs.map(({ id, label, onClick }) => { 28 | return w(Button, { key: id, id, label, onClick }); 29 | }) 30 | ) 31 | ]) 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/benchmark/app/Row.ts: -------------------------------------------------------------------------------- 1 | import { DNode } from '../../../src/interfaces'; 2 | import { WidgetBase } from '../../../src/WidgetBase'; 3 | import { v } from '../../../src/d'; 4 | 5 | export interface RowProperties { 6 | onRowSelected: (id: number) => void; 7 | onRowDeleted: (id: number) => void; 8 | selected: boolean; 9 | label: string; 10 | id: number; 11 | } 12 | 13 | export class Row extends WidgetBase { 14 | private _onDelete() { 15 | const { onRowDeleted, id } = this.properties; 16 | onRowDeleted(id); 17 | } 18 | 19 | private _onClick() { 20 | const { onRowSelected, id } = this.properties; 21 | onRowSelected(id); 22 | } 23 | 24 | protected render(): DNode { 25 | const { id, selected, label } = this.properties; 26 | 27 | return v( 28 | 'tr', 29 | { 30 | classes: [selected ? 'danger' : null] 31 | }, 32 | [ 33 | v('td', { classes: ['col-md-1'] }, [`${id}`]), 34 | v('td', { classes: ['col-md-4'] }, [v('a', { onclick: this._onClick }, [label])]), 35 | v('td', { classes: ['col-md-1'] }, [ 36 | v('a', { onclick: this._onDelete }, [ 37 | v('span', { 38 | 'aria-hidden': true, 39 | classes: ['glyphicon', 'glyphicon-remove'] 40 | }) 41 | ]) 42 | ]), 43 | v('td', { classes: ['col-md-6'] }) 44 | ] 45 | ); 46 | } 47 | } 48 | 49 | export default Row; 50 | -------------------------------------------------------------------------------- /tests/benchmark/app/Store.ts: -------------------------------------------------------------------------------- 1 | import { findIndex } from '@dojo/shim/array'; 2 | 3 | function random(max: number) { 4 | return Math.round(Math.random() * 1000) % max; 5 | } 6 | 7 | export interface Data { 8 | id: number; 9 | label: string; 10 | } 11 | 12 | const adjectives = [ 13 | 'pretty', 14 | 'large', 15 | 'big', 16 | 'small', 17 | 'tall', 18 | 'short', 19 | 'long', 20 | 'handsome', 21 | 'plain', 22 | 'quaint', 23 | 'clean', 24 | 'elegant', 25 | 'easy', 26 | 'angry', 27 | 'crazy', 28 | 'helpful', 29 | 'mushy', 30 | 'odd', 31 | 'unsightly', 32 | 'adorable', 33 | 'important', 34 | 'inexpensive', 35 | 'cheap', 36 | 'expensive', 37 | 'fancy' 38 | ]; 39 | 40 | const colours = ['red', 'yellow', 'blue', 'green', 'pink', 'brown', 'purple', 'brown', 'white', 'black', 'orange']; 41 | 42 | const nouns = [ 43 | 'table', 44 | 'chair', 45 | 'house', 46 | 'bbq', 47 | 'desk', 48 | 'car', 49 | 'pony', 50 | 'cookie', 51 | 'sandwich', 52 | 'burger', 53 | 'pizza', 54 | 'mouse', 55 | 'keyboard' 56 | ]; 57 | 58 | export class Store { 59 | private _data: Data[] = []; 60 | private _selected: number | undefined; 61 | private _id = 1; 62 | 63 | public get data(): Data[] { 64 | return this._data; 65 | } 66 | 67 | public get selected(): number | undefined { 68 | return this._selected; 69 | } 70 | 71 | private _buildData(count: number = 1000): Data[] { 72 | let data = []; 73 | for (let i = 0; i < count; i++) { 74 | const adjective = adjectives[random(adjectives.length)]; 75 | const colour = colours[random(colours.length)]; 76 | const noun = nouns[random(nouns.length)]; 77 | const label = `${adjective} ${colour} ${noun}`; 78 | data.push({ id: this._id, label }); 79 | this._id++; 80 | } 81 | return data; 82 | } 83 | 84 | public updateData(mod: number = 10): void { 85 | for (let i = 0; i < this._data.length; i += 10) { 86 | const data = this._data[i]; 87 | this._data[i] = { ...data, label: `${data.label} !!!` }; 88 | } 89 | } 90 | 91 | public delete(id: number): void { 92 | const idx = findIndex(this._data, (item) => item.id === id); 93 | this._data.splice(idx, 1); 94 | } 95 | 96 | public run(): void { 97 | this._data = this._buildData(); 98 | this._selected = undefined; 99 | } 100 | 101 | public add(): void { 102 | this._data = [...this._data, ...this._buildData()]; 103 | } 104 | 105 | public update(): void { 106 | this.updateData(); 107 | } 108 | 109 | public select(id: number) { 110 | this._selected = id; 111 | } 112 | 113 | public runLots() { 114 | this._data = this._buildData(10000); 115 | this._selected = undefined; 116 | } 117 | 118 | public clear() { 119 | this._data = []; 120 | this._selected = undefined; 121 | } 122 | 123 | public swapRows() { 124 | if (this._data.length > 10) { 125 | const row = this._data[4]; 126 | this._data[4] = this._data[9]; 127 | this._data[9] = row; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/benchmark/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dojo 2 5 | 6 | 7 | 8 |

9 | 10 | 11 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/benchmark/app/main.css: -------------------------------------------------------------------------------- 1 | /* Put your styles and imports here */ 2 | -------------------------------------------------------------------------------- /tests/benchmark/app/main.ts: -------------------------------------------------------------------------------- 1 | import { ProjectorMixin } from '../../../src/mixins/Projector'; 2 | 3 | import App from './App'; 4 | 5 | const root = document.getElementById('main') || undefined; 6 | 7 | const Projector = ProjectorMixin(App); 8 | const projector = new Projector(); 9 | 10 | projector.append(root); 11 | -------------------------------------------------------------------------------- /tests/benchmark/runner/css/currentStyle.css: -------------------------------------------------------------------------------- 1 | @import url("./../../../../../node_modules/bootstrap/dist/css/bootstrap.min.css"); 2 | @import url("main.css"); 3 | -------------------------------------------------------------------------------- /tests/benchmark/runner/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 10px 0 0 0; 3 | margin: 0; 4 | overflow-y: scroll; 5 | } 6 | #duration { 7 | padding-top: 0px; 8 | } 9 | .jumbotron { 10 | padding-top:10px; 11 | padding-bottom:10px; 12 | } 13 | .test-data a { 14 | display: block; 15 | } 16 | .preloadicon { 17 | position: absolute; 18 | top:-20px; 19 | left:-20px; 20 | } 21 | .col-sm-6.smallpad { 22 | padding: 5px; 23 | } 24 | .jumbotron .row h1 { 25 | font-size: 40px; 26 | } 27 | -------------------------------------------------------------------------------- /tests/benchmark/runner/css/useMinimalCss.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | h1 { 27 | font-family: Arial; 28 | font-size: 28px; 29 | } 30 | /* HTML5 display-role reset for older browsers */ 31 | article, aside, details, figcaption, figure, 32 | footer, header, hgroup, menu, nav, section { 33 | display: block; 34 | } 35 | body { 36 | line-height: 1; 37 | } 38 | ol, ul { 39 | list-style: none; 40 | } 41 | blockquote, q { 42 | quotes: none; 43 | } 44 | blockquote:before, blockquote:after, 45 | q:before, q:after { 46 | content: ''; 47 | content: none; 48 | } 49 | table { 50 | border-collapse: collapse; 51 | border-spacing: 0; 52 | } 53 | 54 | .jumbotron { 55 | background-color: #7b7; 56 | } 57 | .btn { 58 | width: 200px; 59 | } 60 | .table { 61 | table-layout: fixed; 62 | font-family: Arial; 63 | font-size: 15px; 64 | } 65 | .table tr { 66 | height: 20px; 67 | border-top: 1px solid #000; 68 | border-bottom: 1px solid #000; 69 | } 70 | .table td.col-md-1 { 71 | width: 50px; 72 | } 73 | .table td.col-md-4 { 74 | width: 350px; 75 | } 76 | .table td.col-md-6 { 77 | width: 400px; 78 | } 79 | .glyphicon-remove:after { 80 | content: 'x' 81 | } 82 | tr.danger td { 83 | background-color: #faa; 84 | } 85 | -------------------------------------------------------------------------------- /tests/benchmark/runner/css/useOriginalBootstrap.css: -------------------------------------------------------------------------------- 1 | @import url("/css/bootstrap/dist/css/bootstrap.min.css"); 2 | @import url("/css/main.css"); 3 | -------------------------------------------------------------------------------- /tests/benchmark/runner/process-benchmark-results.ts: -------------------------------------------------------------------------------- 1 | export function processBenchmarkResults() { 2 | const benchmarkResultsPath = process.cwd() + '/benchmark-results'; 3 | 4 | const files = [ 5 | '01_run1k.json', 6 | '02_replace1k.json', 7 | '03_update10th1k.json', 8 | '04_select1k.json', 9 | '05_swap1k.json', 10 | '06_remove-one-1k.json', 11 | '07_create10k.json', 12 | '08_create1k-after10k.json', 13 | '09_clear10k.json', 14 | '21_ready-memory.json', 15 | '22_run-memory.json', 16 | '23_update5-memory.json', 17 | '24_run5-memory.json', 18 | '25_run-clear-memory.json', 19 | '30_startup.json' 20 | ]; 21 | 22 | const results = files.map((file) => { 23 | const vanillaResult = require(`${benchmarkResultsPath}/vanillajs-non-keyed_${file}`); 24 | const dojoResult = require(`${benchmarkResultsPath}/dojo2-v0.2.0-non-keyed_${file}`); 25 | 26 | return { 27 | vanillaResult, 28 | dojoResult 29 | }; 30 | }); 31 | 32 | console.dir(results, { colors: true }); 33 | console.log(' ---- \n'); 34 | 35 | results.forEach(({ vanillaResult, dojoResult }) => { 36 | const percentSlower = (dojoResult.median - vanillaResult.median) / vanillaResult.median * 100; 37 | console.log( 38 | `${vanillaResult.benchmark} - vanilla: ${vanillaResult.median}. dojo: ${dojoResult.median} (${Math.round( 39 | percentSlower 40 | )}% slower)` 41 | ); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /tests/benchmark/runner/src/benchmarkCli.ts: -------------------------------------------------------------------------------- 1 | import { config } from './common'; 2 | import * as yargs from 'yargs'; 3 | import * as fs from 'fs'; 4 | import { runBench } from './benchmarkRunner'; 5 | 6 | let args = yargs(process.argv) 7 | .usage( 8 | '$0 [--framework Framework1,Framework2,...] [--benchmark Benchmark1,Benchmark2,...] [--count n] [--exitOnError]' 9 | ) 10 | .help('help') 11 | .default('check', 'false') 12 | .default('exitOnError', 'false') 13 | .default('count', config.REPEAT_RUN) 14 | .boolean('headless') 15 | .array('framework') 16 | .array('benchmark').argv; 17 | 18 | let runBenchmarks = args.benchmark && args.benchmark.length > 0 ? args.benchmark : ['']; 19 | let runFrameworks = args.framework && args.framework.length > 0 ? args.framework : ['']; 20 | let count = Number(args.count); 21 | 22 | config.REPEAT_RUN = count; 23 | 24 | let dir = args.check === 'true' ? 'results_check' : 'benchmark-results'; 25 | let exitOnError = args.exitOnError === 'true'; 26 | 27 | config.EXIT_ON_ERROR = exitOnError; 28 | 29 | if (!fs.existsSync(dir)) { 30 | fs.mkdirSync(dir); 31 | } 32 | 33 | if (args.help) { 34 | yargs.showHelp(); 35 | } else { 36 | runBench(runFrameworks, runBenchmarks, dir, count); 37 | } 38 | -------------------------------------------------------------------------------- /tests/benchmark/runner/src/common.ts: -------------------------------------------------------------------------------- 1 | export interface JSONResult { 2 | framework: string; 3 | benchmark: string; 4 | type: string; 5 | min: number; 6 | max: number; 7 | mean: number; 8 | geometricMean: number; 9 | standardDeviation: number; 10 | median: number; 11 | values: Array; 12 | } 13 | 14 | export let config = { 15 | HEADLESS: false, 16 | REPEAT_RUN: 20, 17 | DROP_WORST_RUN: 0, 18 | WARMUP_COUNT: 5, 19 | TIMEOUT: 60 * 1000, 20 | LOG_PROGRESS: true, 21 | LOG_DETAILS: false, 22 | LOG_DEBUG: false, 23 | EXIT_ON_ERROR: false 24 | }; 25 | 26 | export interface FrameworkData { 27 | name: string; 28 | uri: string; 29 | keyed: boolean; 30 | useShadowRoot: boolean | undefined; 31 | } 32 | 33 | interface Options { 34 | uri: string | null; 35 | useShadowRoot?: boolean; 36 | } 37 | 38 | function f(name: string, keyed: boolean, options: Options = { uri: null, useShadowRoot: false }): FrameworkData { 39 | return { name, keyed, uri: options.uri ? options.uri : name, useShadowRoot: options.useShadowRoot }; 40 | } 41 | 42 | export let frameworks = [ 43 | f('dojo2-v0.2.0-non-keyed', false, { uri: '_build/tests/benchmark/app' }), 44 | f('vanillajs-non-keyed', false, { uri: '_build/tests/benchmark/vanillajs-non-keyed' }) 45 | ]; 46 | -------------------------------------------------------------------------------- /tests/benchmark/runner/src/createResultJS.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import Map from '@dojo/shim/Map'; 3 | import { frameworks, FrameworkData } from './common'; 4 | import { benchmarks, fileName } from './benchmarks'; 5 | 6 | let frameworkMap = new Map(); 7 | frameworks.map((f) => frameworkMap.set(f.name, f)); 8 | 9 | let resultJS = 'export let results=['; 10 | 11 | frameworks.forEach((framework, fIdx) => { 12 | benchmarks.forEach((benchmark, bIdx) => { 13 | let name = `${fileName(framework, benchmark)}`; 14 | let file = './results/' + name; 15 | if (fs.existsSync(file)) { 16 | let data = fs.readFileSync(file, { 17 | encoding: 'utf-8' 18 | }); 19 | resultJS += '\n' + data + ','; 20 | } else { 21 | console.log('MISSING FILE', file); 22 | } 23 | }); 24 | }); 25 | 26 | resultJS += '];\n'; 27 | resultJS += 'export let frameworks = ' + JSON.stringify(frameworks) + ';\n'; 28 | resultJS += 'export let benchmarks = ' + JSON.stringify(benchmarks) + ';\n'; 29 | 30 | fs.writeFileSync('../webdriver-ts-results/src/results.ts', resultJS, { encoding: 'utf-8' }); 31 | -------------------------------------------------------------------------------- /tests/benchmark/runner/src/createResultTable.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import Map from '@dojo/shim/Map'; 3 | import { endsWith } from '@dojo/shim/string'; 4 | import { JSONResult, frameworks, FrameworkData } from './common'; 5 | import { BenchmarkType, Benchmark, benchmarks } from './benchmarks'; 6 | 7 | const dots = require('dot').process({ 8 | path: './' 9 | }); 10 | 11 | let frameworkMap = new Map(); 12 | frameworks.map((f) => frameworkMap.set(f.name, f)); 13 | 14 | let results: Map> = new Map(); 15 | 16 | fs 17 | .readdirSync('./results') 18 | .filter((file) => endsWith(file, '.json')) 19 | .forEach((name) => { 20 | let data = JSON.parse( 21 | fs.readFileSync('./results/' + name, { 22 | encoding: 'utf-8' 23 | }) 24 | ); 25 | 26 | if (!frameworkMap.has(data.framework)) { 27 | console.log( 28 | 'WARN: No entry in commons.ts for ' + data.framework + '. Data will not appear in result table.' 29 | ); 30 | } else { 31 | const dataFrameworkResult = results.get(data.framework); 32 | 33 | if (dataFrameworkResult) { 34 | dataFrameworkResult.set(data.benchmark, data); 35 | } else { 36 | results.set(data.framework, new Map()); 37 | } 38 | } 39 | }); 40 | 41 | let cpuBenchmarks = benchmarks.filter( 42 | (benchmark) => benchmark.type === BenchmarkType.CPU || benchmark.type === BenchmarkType.STARTUP 43 | ); 44 | let memBenchmarks = benchmarks.filter((benchmark) => benchmark.type === BenchmarkType.MEM); 45 | let cpuBenchmarkCount = cpuBenchmarks.length; 46 | 47 | let getValue = (framework: string, benchmark: string) => { 48 | const frameworkResult = results.get(framework); 49 | 50 | if (frameworkResult) { 51 | return frameworkResult.get(benchmark); 52 | } 53 | }; 54 | 55 | function color(factor: number): string { 56 | if (factor < 2.0) { 57 | let a = factor - 1.0; 58 | let r = (1.0 - a) * 99 + a * 255; 59 | let g = (1.0 - a) * 191 + a * 236; 60 | let b = (1.0 - a) * 124 + a * 132; 61 | return `rgb(${r.toFixed(0)}, ${g.toFixed(0)}, ${b.toFixed(0)})`; 62 | } else { 63 | let a = Math.min((factor - 2.0) / 2.0, 1.0); 64 | let r = (1.0 - a) * 255 + a * 249; 65 | let g = (1.0 - a) * 236 + a * 105; 66 | let b = (1.0 - a) * 132 + a * 108; 67 | return `rgb(${r.toFixed(0)}, ${g.toFixed(0)}, ${b.toFixed(0)})`; 68 | } 69 | } 70 | 71 | interface TestData { 72 | mean: string; 73 | deviation: string; 74 | factor: string; 75 | styleClass: string; 76 | } 77 | 78 | class BenchResultList { 79 | tests: Array; 80 | name: string; 81 | description: string; 82 | 83 | constructor(benchmark: Benchmark) { 84 | this.tests = []; 85 | this.name = benchmark.label; 86 | this.description = benchmark.description; 87 | } 88 | } 89 | 90 | interface FrameworkPredicate { 91 | (framework: FrameworkData): boolean; 92 | } 93 | 94 | let generateBenchData = ( 95 | benchmarks: Array, 96 | frameworkPredicate: FrameworkPredicate, 97 | referenceName: string 98 | ) => { 99 | let benches: Array = []; 100 | 101 | let filteredFrameworks = frameworks.filter((f) => frameworkPredicate(f)).slice(); 102 | 103 | let sortedFrameworks = filteredFrameworks.sort((a: FrameworkData, b: FrameworkData) => { 104 | if (a.name === referenceName) { 105 | if (b.name === referenceName) { 106 | return 0; 107 | } 108 | console.log('found reference name', referenceName); 109 | return 1; 110 | } else if (b.name === referenceName) { 111 | if (a.name === referenceName) { 112 | return 0; 113 | } 114 | console.log('found reference name', referenceName); 115 | return -1; 116 | } else { 117 | if (a.name < b.name) { 118 | return -1; 119 | } else if (a.name === b.name) { 120 | return 0; 121 | } else { 122 | return 1; 123 | } 124 | } 125 | }); 126 | 127 | let frameworkNames = sortedFrameworks.map((framework) => framework.name.replace('-v', ' v')); // .replace(/-keyed$|-non-keyed$/, '')) 128 | let factors = sortedFrameworks.map((f) => 1.0); 129 | 130 | benchmarks.forEach((benchmark) => { 131 | let bench = new BenchResultList(benchmark); 132 | 133 | let values: Array = []; 134 | sortedFrameworks.forEach((framework) => { 135 | if (frameworkPredicate(framework)) { 136 | const frameworkValue = getValue(framework.name, benchmark.id); 137 | if (frameworkValue) { 138 | values.push(frameworkValue); 139 | } 140 | } 141 | }); 142 | 143 | let sorted = values 144 | .filter((value) => value) 145 | .map((data) => { 146 | return data.mean; 147 | }) 148 | .sort((a, b) => a - b); 149 | 150 | let min = 1.0; 151 | 152 | if (sorted.length) { 153 | min = sorted[0]; 154 | } 155 | 156 | values.forEach(function(value, idx) { 157 | if (value) { 158 | try { 159 | let factor: number; 160 | if (benchmark.type === BenchmarkType.CPU || benchmark.type === BenchmarkType.STARTUP) { 161 | // Clamp to 1 fps 162 | factor = Math.max(16, value.mean) / Math.max(16, min); 163 | factors[idx] = factors[idx] * factor; 164 | } else { 165 | factor = value.mean / min; 166 | } 167 | 168 | bench.tests.push({ 169 | mean: value.mean.toFixed(2), 170 | deviation: value.standardDeviation.toFixed(2), 171 | factor: factor.toFixed(2), 172 | styleClass: color(factor) 173 | }); 174 | } catch (err) { 175 | console.log(`error in ${benchmark} ${JSON.stringify(value)}`, err); 176 | } 177 | } else { 178 | bench.tests.push(null); 179 | } 180 | }); 181 | benches.push(bench); 182 | }); 183 | let geomMeans = factors.map((f) => { 184 | let value = Math.pow(f, 1 / cpuBenchmarkCount); 185 | console.log('cpuBenchmarkCount', cpuBenchmarkCount, f); 186 | return { value: value.toPrecision(3), styleClass: color(value) }; 187 | }); 188 | return { 189 | frameworks: frameworkNames, 190 | benches, 191 | geomMeans 192 | }; 193 | }; 194 | 195 | function frameworkPredicateKeyed(keyed: boolean): FrameworkPredicate { 196 | return (framework: FrameworkData) => { 197 | return framework.keyed === keyed; 198 | }; 199 | } 200 | let cpubenchesNonKeyed = generateBenchData(cpuBenchmarks, frameworkPredicateKeyed(false), 'vanillajs-non-keyed'); 201 | let membenchesNonKeyed = generateBenchData(memBenchmarks, frameworkPredicateKeyed(false), 'vanillajs-non-keyed'); 202 | let cpubenchesKeyed = generateBenchData(cpuBenchmarks, frameworkPredicateKeyed(true), 'vanillajs-keyed'); // react 203 | let membenchesKeyed = generateBenchData(memBenchmarks, frameworkPredicateKeyed(true), 'vanillajs-keyed'); 204 | 205 | fs.writeFileSync( 206 | './table.html', 207 | dots.table({ 208 | data: [ 209 | { 210 | label: 'Keyed results', 211 | description: `Keyed implementations create an association between the domain data and a dom element 212 | by assigning a 'key'. If data changes the dom element with that key will be updated. 213 | In consequence inserting or deleting an element in the data array causes a corresponding change to the dom. 214 | `, 215 | cpubenches: cpubenchesKeyed, 216 | membenches: membenchesKeyed 217 | }, 218 | { 219 | label: 'Non keyed results', 220 | description: `Non keyed implementations are allowed to reuse existing dom elements. 221 | In consequence inserting or deleting an element in the data array might append after or delete the last table row 222 | and update the contents of all elements after the inserting or deletion index. 223 | This can perform better, but can cause problems if dom state is modified externally. 224 | `, 225 | cpubenches: cpubenchesNonKeyed, 226 | membenches: membenchesNonKeyed 227 | } 228 | ] 229 | }), 230 | { 231 | encoding: 'utf8' 232 | } 233 | ); 234 | -------------------------------------------------------------------------------- /tests/benchmark/runner/src/webdriverAccess.ts: -------------------------------------------------------------------------------- 1 | import { By, WebDriver, WebElement, Condition } from 'selenium-webdriver'; 2 | import { config } from './common'; 3 | 4 | interface PathPart { 5 | tagName: string; 6 | index: number; 7 | } 8 | 9 | let useShadowRoot = false; 10 | 11 | export function setUseShadowRoot(val: boolean | undefined) { 12 | useShadowRoot = Boolean(val); 13 | } 14 | 15 | function convertPath(path: string): Array { 16 | let parts = path.split(/\//).filter((v) => !!v); 17 | let res: Array = []; 18 | for (let part of parts) { 19 | let components = part.split(/\[|]/).filter((v) => !!v); 20 | let tagName = components[0]; 21 | let index = 0; 22 | if (components.length === 2) { 23 | index = Number(components[1]); 24 | if (!index) { 25 | console.log("Index can't be parsed", components[1]); 26 | throw "Index can't be parsed " + components[1]; 27 | } 28 | } else { 29 | index = 1; 30 | } 31 | res.push({ tagName, index }); 32 | } 33 | return res; 34 | } 35 | 36 | // Fake findByXPath for simple XPath expressions to allow usage with shadow dom 37 | async function findByXPath(node: WebElement, path: string): Promise { 38 | let paths = convertPath(path); 39 | let n = node; 40 | try { 41 | for (let p of paths) { 42 | let elems = await n.findElements(By.css(p.tagName + ':nth-child(' + p.index + ')')); 43 | 44 | if (elems === null || elems.length === 0) { 45 | console.log('not found'); 46 | return null; 47 | } 48 | 49 | n = elems[0]; 50 | } 51 | } catch (e) { 52 | // Can happen for StaleElementReferenceError 53 | return null; 54 | } 55 | 56 | return n; 57 | } 58 | 59 | function waitForCondition(driver: WebDriver) { 60 | return async function( 61 | text: string, 62 | fn: (driver: WebDriver) => Promise, 63 | timeout: number 64 | ): Promise { 65 | return await driver.wait(new Condition(text, fn), timeout); 66 | }; 67 | } 68 | 69 | // driver.findElement(By.xpath("//tbody/tr[1]/td[1]")).getText().then(...) can throw a stale element error: 70 | // thus we're using a safer way here: 71 | export async function testTextContains(driver: WebDriver, xpath: string, text: string, timeout = config.TIMEOUT) { 72 | return waitForCondition(driver)( 73 | `testTextContains ${xpath} ${text}`, 74 | async function(driver): Promise { 75 | try { 76 | const shadowRootElm = await shadowRoot(driver); 77 | const elm = await findByXPath(shadowRootElm, xpath); 78 | if (elm === null) { 79 | return false; 80 | } 81 | let v = await elm.getText(); 82 | return v && v.indexOf(text) > -1; 83 | } catch (err) { 84 | console.log( 85 | 'ignoring error in testTextContains for xpath = ' + xpath + ' text = ' + text, 86 | err.toString().split('\n')[0] 87 | ); 88 | } 89 | }, 90 | timeout 91 | ); 92 | } 93 | 94 | export function testTextNotContained(driver: WebDriver, xpath: string, text: string, timeout = config.TIMEOUT) { 95 | return waitForCondition(driver)( 96 | `testTextNotContained ${xpath} ${text}`, 97 | async function(driver) { 98 | try { 99 | const shadowRootElm = await shadowRoot(driver); 100 | const elem = await findByXPath(shadowRootElm, xpath); 101 | if (elem === null) { 102 | return false; 103 | } 104 | let v = await elem.getText(); 105 | return Boolean(v && v.indexOf(text) === -1); 106 | } catch (err) { 107 | console.log( 108 | 'ignoring error in testTextNotContained for xpath = ' + xpath + ' text = ' + text, 109 | err.toString().split('\n')[0] 110 | ); 111 | return false; 112 | } 113 | }, 114 | timeout 115 | ); 116 | } 117 | 118 | export function testClassContains(driver: WebDriver, xpath: string, text: string, timeout = config.TIMEOUT) { 119 | return waitForCondition(driver)( 120 | `testClassContains ${xpath} ${text}`, 121 | async function(driver) { 122 | try { 123 | const shadowRootElm = await shadowRoot(driver); 124 | const elem = await findByXPath(shadowRootElm, xpath); 125 | if (elem === null) { 126 | return false; 127 | } 128 | let v = await elem.getAttribute('class'); 129 | return Boolean(v && v.indexOf(text) > -1); 130 | } catch (err) { 131 | console.log( 132 | 'ignoring error in testClassContains for xpath = ' + xpath + ' text = ' + text, 133 | err.toString().split('\n')[0] 134 | ); 135 | return false; 136 | } 137 | }, 138 | timeout 139 | ); 140 | } 141 | 142 | export function testElementLocatedByXpath(driver: WebDriver, xpath: string, timeout = config.TIMEOUT) { 143 | return waitForCondition(driver)( 144 | `testElementLocatedByXpath ${xpath}`, 145 | async function(driver) { 146 | try { 147 | const shadowRootElm = await shadowRoot(driver); 148 | const elem = await findByXPath(shadowRootElm, xpath); 149 | return elem ? true : false; 150 | } catch (err) { 151 | console.log('ignoring error in testElementLocatedByXpath for xpath = ' + xpath, err.toString()); 152 | return false; 153 | } 154 | }, 155 | timeout 156 | ); 157 | } 158 | 159 | export function testElementNotLocatedByXPath(driver: WebDriver, xpath: string, timeout = config.TIMEOUT) { 160 | return waitForCondition(driver)( 161 | `testElementNotLocatedByXPath ${xpath}`, 162 | async function(driver) { 163 | try { 164 | const shadowRootElm = await shadowRoot(driver); 165 | const elem = await findByXPath(shadowRootElm, xpath); 166 | return elem ? false : true; 167 | } catch (err) { 168 | console.log( 169 | 'ignoring error in testElementNotLocatedByXPath for xpath = ' + xpath, 170 | err.toString().split('\n')[0] 171 | ); 172 | return false; 173 | } 174 | }, 175 | timeout 176 | ); 177 | } 178 | 179 | export function testElementLocatedById(driver: WebDriver, id: string, timeout = config.TIMEOUT) { 180 | return waitForCondition(driver)( 181 | `testElementLocatedById ${id}`, 182 | async function(driver) { 183 | try { 184 | await shadowRoot(driver); 185 | return true; 186 | } catch (err) { 187 | console.log('ignoring error in testElementLocatedById for id = ' + id, err.toString().split('\n')[0]); 188 | return false; 189 | } 190 | }, 191 | timeout 192 | ); 193 | } 194 | 195 | async function retry( 196 | retryCount: number, 197 | driver: WebDriver, 198 | fun: (driver: WebDriver, retryCount: number) => Promise 199 | ): Promise { 200 | for (let i = 0; i < retryCount; i++) { 201 | try { 202 | return fun(driver, i); 203 | } catch (err) { 204 | console.log('retry failed'); 205 | } 206 | } 207 | } 208 | 209 | // Stale element prevention. For aurelia even after a testElementLocatedById clickElementById for the same id can fail 210 | // No idea how that can be explained 211 | export function clickElementById(driver: WebDriver, id: string) { 212 | return retry(5, driver, async function(driver) { 213 | let elem = await shadowRoot(driver); 214 | elem = await elem.findElement(By.id(id)); 215 | await elem.click(); 216 | }); 217 | } 218 | 219 | export function clickElementByXPath(driver: WebDriver, xpath: string) { 220 | return retry(5, driver, async function(driver, count) { 221 | if (count > 1 && config.LOG_DETAILS) { 222 | console.log('clickElementByXPath ', xpath, ' attempt #', count); 223 | } 224 | const shadowRootElm = await shadowRoot(driver); 225 | const elem = await findByXPath(shadowRootElm, xpath); 226 | if (elem) { 227 | await elem.click(); 228 | } 229 | }); 230 | } 231 | 232 | export async function getTextByXPath(driver: WebDriver, xpath: string): Promise { 233 | return await retry(5, driver, async function(driver, count) { 234 | if (count > 1 && config.LOG_DETAILS) { 235 | console.log('getTextByXPath ', xpath, ' attempt #', count); 236 | } 237 | const shadowRootElm = await shadowRoot(driver); 238 | const elem = await findByXPath(shadowRootElm, xpath); 239 | if (elem) { 240 | return await elem.getText(); 241 | } 242 | }); 243 | } 244 | 245 | async function shadowRoot(driver: WebDriver): Promise { 246 | return useShadowRoot 247 | ? ((await driver.executeScript('return document.querySelector("main-element").shadowRoot')) as WebElement) 248 | : await driver.findElement(By.tagName('body')); 249 | } 250 | -------------------------------------------------------------------------------- /tests/benchmark/vanillajs-non-keyed/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VanillaJS 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 |

VanillaJS

15 |
16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 | 43 |
44 | 45 |
46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /tests/functional/Drag.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import { DragResults } from '../../src/meta/Drag'; 4 | import Test from 'intern/lib/Test'; 5 | 6 | function getPage(test: Test) { 7 | return test.remote.get('_build/tests/functional/meta/Drag.html').setFindTimeout(5000); 8 | } 9 | 10 | registerSuite('Drag', { 11 | 'touch drag'() { 12 | if (!this.remote.session.capabilities.touchEnabled) { 13 | this.skip('Not touch enabled device'); 14 | } 15 | return getPage(this) 16 | .findById('results') 17 | .pressFinger(50, 50) 18 | .sleep(100) 19 | .moveFinger(100, 100) 20 | .sleep(100) 21 | .findById('results') 22 | .getVisibleText() 23 | .then((text) => { 24 | const result: DragResults = JSON.parse(text); 25 | assert.isTrue(result.isDragging, 'should be in a drag state'); 26 | assert.deepEqual(result.delta, { x: 50, y: 50 }, 'should have dragged expected distance'); 27 | }) 28 | .releaseFinger(100, 100) 29 | .sleep(50) 30 | .findById('results') 31 | .getVisibleText() 32 | .then((text) => { 33 | const result: DragResults = JSON.parse(text); 34 | assert.isFalse(result.isDragging, 'should be no longer dragging'); 35 | assert.deepEqual(result.delta, { x: 0, y: 0 }, 'should not have moved further'); 36 | }); 37 | }, 38 | 39 | 'mouse drag'() { 40 | const { browser, browserName, mouseEnabled } = this.remote.session.capabilities; 41 | if (!mouseEnabled || browser === 'iPhone' || browser === 'iPad') { 42 | this.skip('Not mouse enabled device'); 43 | } 44 | if (browserName === 'MicrosoftEdge') { 45 | this.skip('For some reason, findById not working on Edge ATM.'); 46 | } 47 | if (browserName === 'internet explorer') { 48 | this.skip('Dragging is not working on Internet Explorer.'); 49 | } 50 | return getPage(this) 51 | .findById('results') 52 | .moveMouseTo(50, 50) 53 | .pressMouseButton() 54 | .sleep(100) 55 | .moveMouseTo(100, 100) 56 | .sleep(100) 57 | .getVisibleText() 58 | .then((text) => { 59 | const result: DragResults = JSON.parse(text); 60 | assert.isTrue(result.isDragging, 'should be in a drag state'); 61 | assert.deepEqual(result.delta, { x: 50, y: 50 }, 'should have dragged expected distance'); 62 | }) 63 | .releaseMouseButton() 64 | .sleep(50) 65 | .getVisibleText() 66 | .then((text) => { 67 | const result: DragResults = JSON.parse(text); 68 | assert.isFalse(result.isDragging, 'should be no longer dragging'); 69 | assert.deepEqual(result.delta, { x: 0, y: 0 }, 'should have dragged expected distance'); 70 | }); 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /tests/functional/all.ts: -------------------------------------------------------------------------------- 1 | import './Drag'; 2 | -------------------------------------------------------------------------------- /tests/functional/meta/Drag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Drag Test 5 | 6 | 7 | 8 | 9 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/functional/meta/Drag.ts: -------------------------------------------------------------------------------- 1 | import { v } from '../../../src/d'; 2 | import WidgetBase from '../../../src/WidgetBase'; 3 | import Projector from '../../../src/mixins/Projector'; 4 | import Drag from '../../../src/meta/Drag'; 5 | 6 | class DragExample extends WidgetBase { 7 | render() { 8 | const dragResults = this.meta(Drag).get('root'); 9 | return v( 10 | 'div', 11 | { 12 | key: 'root', 13 | styles: { 14 | 'background-color': dragResults.isDragging ? 'green' : 'white', 15 | border: '1px solid black', 16 | color: dragResults.isDragging ? 'white' : 'black', 17 | height: '400px', 18 | 'user-select': 'none', 19 | width: '200px' 20 | } 21 | }, 22 | [v('pre', { id: 'results' }, [JSON.stringify(dragResults, null, ' ')])] 23 | ); 24 | } 25 | } 26 | 27 | const projector = new (Projector(DragExample))(); 28 | 29 | projector.append(); 30 | -------------------------------------------------------------------------------- /tests/run.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Intern suite 6 | 7 | 8 | 9 | Redirecting to Intern client 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/support/loadCustomElements.ts: -------------------------------------------------------------------------------- 1 | intern.registerPlugin('custom-elements', async function() { 2 | const scripts = ['./node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js']; 3 | if (window.customElements) { 4 | scripts.unshift('./node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'); 5 | } 6 | 7 | await intern.loadScript(scripts); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/support/loadJsdom.ts: -------------------------------------------------------------------------------- 1 | import * as jsdom from 'jsdom'; 2 | import global from '@dojo/shim/global'; 3 | 4 | /* In order to have the tests work under Node.js, we need to load JSDom and polyfill 5 | * requestAnimationFrame and create a fake document.activeElement getter */ 6 | 7 | /* Create a basic document */ 8 | const doc = jsdom.jsdom(` 9 | 10 | 11 | 12 | 13 | 14 | `); 15 | 16 | /* Assign it to the global namespace */ 17 | global.document = doc; 18 | 19 | /* Assign a global window as well */ 20 | global.window = doc.defaultView; 21 | 22 | /* Needed for Pointer Event Polyfill's incorrect Element detection */ 23 | global.Element = function() {}; 24 | 25 | /* Polyfill requestAnimationFrame - this can never be called an *actual* polyfill */ 26 | global.requestAnimationFrame = (cb: (...args: any[]) => {}) => { 27 | setImmediate(cb); 28 | // return something at least! 29 | return true; 30 | }; 31 | 32 | global.cancelAnimationFrame = () => {}; 33 | global.IntersectionObserver = () => {}; 34 | 35 | global.fakeActiveElement = () => {}; 36 | Object.defineProperty(doc, 'activeElement', { 37 | get: () => { 38 | return global.fakeActiveElement(); 39 | } 40 | }); 41 | 42 | console.log('Loaded JSDOM...'); 43 | -------------------------------------------------------------------------------- /tests/support/nls/fr/greetings.ts: -------------------------------------------------------------------------------- 1 | const messages = { 2 | hello: 'Bonjour', 3 | goodbye: 'Au revoir', 4 | welcome: 'Bienvenue, {name}!' 5 | }; 6 | export default messages; 7 | -------------------------------------------------------------------------------- /tests/support/nls/greetings.ts: -------------------------------------------------------------------------------- 1 | import fr from './fr/greetings'; 2 | 3 | export default { 4 | locales: { 5 | fr: () => fr 6 | }, 7 | messages: { 8 | hello: 'Hello', 9 | goodbye: 'Goodbye', 10 | welcome: 'Welcome, {name}!' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /tests/support/sendEvent.ts: -------------------------------------------------------------------------------- 1 | import has, { add as hasAdd } from '@dojo/core/has'; 2 | import { deepAssign } from '@dojo/core/lang'; 3 | import global from '@dojo/shim/global'; 4 | import { assign } from '@dojo/shim/object'; 5 | import { spy } from 'sinon'; 6 | 7 | hasAdd('customevent-constructor', () => { 8 | try { 9 | new global.window.CustomEvent('foo'); 10 | return true; 11 | } catch (e) { 12 | return false; 13 | } 14 | }); 15 | 16 | export type EventClass = 17 | | 'AnimationEvent' 18 | | 'AudioProcessingEvent' 19 | | 'BeforeInputEvent' 20 | | 'BeforeUnloadEvent' 21 | | 'BlobEvent' 22 | | 'ClipboardEvent' 23 | | 'CloseEvent' 24 | | 'CompositionEvent' 25 | | 'CSSFontFaceLoadEvent' 26 | | 'CustomEvent' 27 | | 'DeviceLightEvent' 28 | | 'DeviceMotionEvent' 29 | | 'DeviceOrientationEvent' 30 | | 'DeviceProximityEvent' 31 | | 'DOMTransactionEvent' 32 | | 'DragEvent' 33 | | 'EditingBeforeInputEvent' 34 | | 'ErrorEvent' 35 | | 'FetchEvent' 36 | | 'FocusEvent' 37 | | 'GamepadEvent' 38 | | 'HashChangeEvent' 39 | | 'IDBVersionChangeEvent' 40 | | 'InputEvent' 41 | | 'KeyboardEvent' 42 | | 'MediaStreamEvent' 43 | | 'MessageEvent' 44 | | 'MouseEvent' 45 | | 'MutationEvent' 46 | | 'OfflineAudioCompletionEvent' 47 | | 'PageTransitionEvent' 48 | | 'PointerEvent' 49 | | 'PopStateEvent' 50 | | 'ProgressEvent' 51 | | 'RelatedEvent' 52 | | 'RTCDataChannelEvent' 53 | | 'RTCIdentityErrorEvent' 54 | | 'RTCIdentityEvent' 55 | | 'RTCPeerConnectionIceEvent' 56 | | 'SensorEvent' 57 | | 'StorageEvent' 58 | | 'SVGEvent' 59 | | 'SVGZoomEvent' 60 | | 'TimeEvent' 61 | | 'TouchEvent' 62 | | 'TrackEvent' 63 | | 'TransitionEvent' 64 | | 'UIEvent' 65 | | 'UserProximityEvent' 66 | | 'WebGLContextEvent' 67 | | 'WheelEvent'; 68 | 69 | export interface SendEventOptions { 70 | /** 71 | * The event class to use to create the event, defaults to `CustomEvent` 72 | */ 73 | eventClass?: EventClass; 74 | 75 | /** 76 | * An object which is used to initialise the event 77 | */ 78 | eventInit?: I; 79 | 80 | /** 81 | * A CSS selector string, used to query the target to identify the element to 82 | * dispatch the event to 83 | */ 84 | selector?: string; 85 | } 86 | 87 | export interface EventInitializer { 88 | (type: string, bubbles: boolean, cancelable: boolean, detail: any): void; 89 | } 90 | 91 | /** 92 | * Create and dispatch an event to an element 93 | * @param type The event type to dispatch 94 | * @param options A map of options to configure the event 95 | */ 96 | export default function sendEvent( 97 | target: Element, 98 | type: string, 99 | options?: SendEventOptions 100 | ): Event { 101 | function dispatchEvent(target: Element, event: Event) { 102 | let error: Error | undefined; 103 | 104 | function catcher(e: ErrorEvent) { 105 | e.preventDefault(); 106 | error = e.error; 107 | return true; 108 | } 109 | 110 | window.addEventListener('error', catcher); 111 | target.dispatchEvent(event); 112 | window.removeEventListener('error', catcher); 113 | if (error) { 114 | throw error; 115 | } 116 | } 117 | 118 | const { eventClass = 'CustomEvent', eventInit = {} as EventInit, selector = '' } = options || {}; 119 | let event: CustomEvent; 120 | assign(eventInit, { 121 | bubbles: 'bubbles' in eventInit ? eventInit.bubbles : true, 122 | cancelable: 'cancelable' in eventInit ? eventInit.cancelable : true 123 | }); 124 | const { bubbles, cancelable, ...initProps } = eventInit; 125 | if (has('customevent-constructor')) { 126 | const ctorName = eventClass in window ? eventClass : 'CustomEvent'; 127 | event = new ((window as any)[ctorName] as typeof CustomEvent)(type, eventInit); 128 | } else { 129 | /* because the arity varies too greatly to be able to properly call all the event types, we will 130 | * only support CustomEvent for those platforms that don't support event constructors, which is 131 | * essentially IE11 */ 132 | event = document.createEvent('CustomEvent'); 133 | (event as CustomEvent).initCustomEvent(type, bubbles!, cancelable!, {}); 134 | } 135 | try { 136 | deepAssign(event, initProps); 137 | } catch (e) { 138 | /* swallowing assignment errors when trying to overwrite native event properties */ 139 | } 140 | 141 | spy(event, 'stopPropagation'); 142 | 143 | if (selector) { 144 | const selectorTarget = target.querySelector(selector); 145 | if (selectorTarget) { 146 | dispatchEvent(selectorTarget, event); 147 | } else { 148 | throw new Error(`Cannot resolve to an element with selector "${selector}"`); 149 | } 150 | } else { 151 | dispatchEvent(target, event); 152 | } 153 | 154 | return event; 155 | } 156 | -------------------------------------------------------------------------------- /tests/support/styles/baseTheme3.css.ts: -------------------------------------------------------------------------------- 1 | export const class1 = 'overriddenBaseClass1'; 2 | -------------------------------------------------------------------------------- /tests/support/styles/extraClasses1.css.ts: -------------------------------------------------------------------------------- 1 | export const class1 = 'override1Class1'; 2 | -------------------------------------------------------------------------------- /tests/support/styles/extraClasses2.css.ts: -------------------------------------------------------------------------------- 1 | export const class1 = 'override2Class1'; 2 | -------------------------------------------------------------------------------- /tests/support/styles/testWidget1.css.ts: -------------------------------------------------------------------------------- 1 | export const class1 = 'baseClass1'; 2 | export const class2 = 'baseClass2'; 3 | -------------------------------------------------------------------------------- /tests/support/styles/testWidget1Theme1.css.ts: -------------------------------------------------------------------------------- 1 | export const class1 = 'theme1Class1'; 2 | -------------------------------------------------------------------------------- /tests/support/styles/testWidget1Theme2.css.ts: -------------------------------------------------------------------------------- 1 | export const class1 = 'theme2Class1'; 2 | -------------------------------------------------------------------------------- /tests/support/styles/testWidget1Theme3.css.ts: -------------------------------------------------------------------------------- 1 | export const class1 = 'testTheme3Class1 testTheme3AdjoinedClass1'; 2 | -------------------------------------------------------------------------------- /tests/support/styles/testWidget2.css.ts: -------------------------------------------------------------------------------- 1 | export const class3 = 'baseClass3'; 2 | export const class4 = 'baseClass4'; 3 | -------------------------------------------------------------------------------- /tests/support/styles/testWidget2Theme1.css.ts: -------------------------------------------------------------------------------- 1 | export const class3 = 'theme1Class3'; 2 | -------------------------------------------------------------------------------- /tests/support/styles/testWidget2Theme2.css.ts: -------------------------------------------------------------------------------- 1 | export const class4 = 'theme2Class4'; 2 | -------------------------------------------------------------------------------- /tests/support/styles/testWidget2Theme3.css.ts: -------------------------------------------------------------------------------- 1 | export const class3 = 'testTheme3Class3 testTheme3AdjoinedClass3'; 2 | -------------------------------------------------------------------------------- /tests/support/styles/theme1.css.ts: -------------------------------------------------------------------------------- 1 | import * as testWidget1Theme from './testWidget1Theme1.css'; 2 | import * as testWidget2Theme from './testWidget2Theme1.css'; 3 | 4 | const theme1 = { 5 | testPath1: testWidget1Theme, 6 | testPath2: testWidget2Theme 7 | }; 8 | 9 | export default theme1; 10 | -------------------------------------------------------------------------------- /tests/support/styles/theme2.css.ts: -------------------------------------------------------------------------------- 1 | import * as testWidget1Theme from './testWidget1Theme2.css'; 2 | import * as testWidget2Theme from './testWidget2Theme2.css'; 3 | 4 | const theme2 = { 5 | testPath1: testWidget1Theme, 6 | testPath2: testWidget2Theme 7 | }; 8 | 9 | export default theme2; 10 | -------------------------------------------------------------------------------- /tests/support/styles/theme3.css.ts: -------------------------------------------------------------------------------- 1 | import * as testWidget1Theme from './testWidget1Theme3.css'; 2 | import * as testWidget2Theme from './testWidget2Theme3.css'; 3 | 4 | const theme3 = { 5 | testPath1: testWidget1Theme, 6 | testPath2: testWidget2Theme 7 | }; 8 | 9 | export default theme3; 10 | -------------------------------------------------------------------------------- /tests/support/util.ts: -------------------------------------------------------------------------------- 1 | import global from '@dojo/shim/global'; 2 | import Promise from '@dojo/shim/Promise'; 3 | import loadCldrData from '@dojo/i18n/cldr/load'; 4 | import { systemLocale } from '@dojo/i18n/i18n'; 5 | import likelySubtags from './likelySubtags'; 6 | import { stub, SinonStub } from 'sinon'; 7 | 8 | /** 9 | * Load into Globalize.js all CLDR data for the specified locales. 10 | */ 11 | export function fetchCldrData(): Promise { 12 | return Promise.all([ 13 | // this weird dummy load is needed by i18n right now 14 | loadCldrData({ 15 | main: { 16 | [systemLocale]: {} 17 | } 18 | }), 19 | loadCldrData(likelySubtags) 20 | ]); 21 | } 22 | 23 | export function createResolvers() { 24 | let rAFStub: SinonStub; 25 | let rICStub: SinonStub; 26 | 27 | function resolveRAF() { 28 | for (let i = 0; i < rAFStub.callCount; i++) { 29 | rAFStub.getCall(i).callArg(0); 30 | } 31 | rAFStub.resetHistory(); 32 | } 33 | 34 | function resolveRIC() { 35 | for (let i = 0; i < rICStub.callCount; i++) { 36 | rICStub.getCall(i).callArg(0); 37 | } 38 | rICStub.resetHistory(); 39 | } 40 | 41 | return { 42 | resolve() { 43 | resolveRAF(); 44 | resolveRIC(); 45 | }, 46 | stub() { 47 | rAFStub = stub(global, 'requestAnimationFrame').returns(1); 48 | if (global.requestIdleCallback) { 49 | rICStub = stub(global, 'requestIdleCallback').returns(1); 50 | } else { 51 | rICStub = stub(global, 'setTimeout').returns(1); 52 | } 53 | }, 54 | restore() { 55 | rAFStub.restore(); 56 | rICStub.restore(); 57 | } 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /tests/unit/Container.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import { v, w } from '../../src/d'; 4 | import { WidgetBase } from '../../src/WidgetBase'; 5 | import { Container } from './../../src/Container'; 6 | import { Registry } from './../../src/Registry'; 7 | import { ProjectorMixin } from './../../src/mixins/Projector'; 8 | 9 | interface TestWidgetProperties { 10 | foo: string; 11 | boo: number; 12 | } 13 | 14 | class TestWidget extends WidgetBase { 15 | render() { 16 | assertRender(this.properties); 17 | return v('test', this.properties); 18 | } 19 | } 20 | 21 | let childrenCalled = false; 22 | let propertiesCalled = false; 23 | let assertRender = (properties: any) => {}; 24 | 25 | function getProperties(toInject: any, properties: any) { 26 | propertiesCalled = true; 27 | return properties; 28 | } 29 | 30 | const registry = new Registry(); 31 | const injector = () => () => ({}); 32 | registry.defineInjector('test-state-1', injector); 33 | registry.define('test-widget', TestWidget); 34 | 35 | registerSuite('mixins/Container', { 36 | beforeEach() { 37 | childrenCalled = false; 38 | propertiesCalled = false; 39 | assertRender = (properties: any) => {}; 40 | }, 41 | 42 | tests: { 43 | 'container with no default mappers'() { 44 | assertRender = (properties: any) => { 45 | properties.getChildren(); 46 | properties.getProperties(); 47 | assert.isTrue(childrenCalled); 48 | assert.isTrue(propertiesCalled); 49 | assert.deepEqual(properties.properties, { foo: 'bar', registry }); 50 | assert.deepEqual(properties.children, []); 51 | }; 52 | const TestWidgetContainer = Container(TestWidget, 'test-state-1', { getProperties }); 53 | const widget = new TestWidgetContainer(); 54 | widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); 55 | widget.__setProperties__({ foo: 'bar' }); 56 | widget.__setChildren__([]); 57 | widget.__render__(); 58 | }, 59 | 'container with custom getProperties mapper only'() { 60 | const child = v('sub-widget'); 61 | assertRender = (properties: any) => { 62 | const children = properties.getChildren(); 63 | properties.getProperties(); 64 | assert.isFalse(childrenCalled); 65 | assert.isTrue(propertiesCalled); 66 | assert.deepEqual(children, []); 67 | assert.deepEqual(properties.properties, { foo: 'bar', registry }); 68 | assert.lengthOf(properties.children, 1); 69 | assert.deepEqual(properties.children[0], child); 70 | }; 71 | const TestWidgetContainer = Container(TestWidget, 'test-state-1', { getProperties }); 72 | const widget = new TestWidgetContainer(); 73 | widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); 74 | widget.__setProperties__({ foo: 'bar' }); 75 | widget.__setChildren__([child]); 76 | widget.__render__(); 77 | }, 78 | 'container for registry item'() { 79 | assertRender = (properties: any) => { 80 | const calculatedChildren = properties.getChildren(); 81 | const calculatedProperties = properties.getProperties(); 82 | assert.isFalse(childrenCalled); 83 | assert.isFalse(propertiesCalled); 84 | assert.deepEqual(calculatedProperties, {}); 85 | assert.deepEqual(calculatedChildren, []); 86 | assert.deepEqual(properties.properties, { foo: 'bar' }); 87 | assert.deepEqual(properties.children, []); 88 | }; 89 | 90 | const TestWidgetContainer = Container('test-widget', 'test-state-1', { getProperties }); 91 | const widget = new TestWidgetContainer(); 92 | const renderResult: any = widget.__render__(); 93 | 94 | assert.strictEqual(renderResult.widgetConstructor, 'test-widget'); 95 | }, 96 | 'Container should always render but not invalidate parent when properties have not changed'() { 97 | class TestInvalidate { 98 | invalidator: any; 99 | } 100 | const testInvalidate = new TestInvalidate(); 101 | const testInvalidateInjector = (invalidator: any) => { 102 | testInvalidate.invalidator = invalidator; 103 | return () => {}; 104 | }; 105 | let renderCount = 0; 106 | registry.defineInjector('test-always-render', testInvalidateInjector); 107 | class Child extends WidgetBase<{ foo: string }> {} 108 | class ContainerClass extends Container(Child, 'test-always-render', { getProperties }) { 109 | render() { 110 | renderCount++; 111 | return super.render(); 112 | } 113 | } 114 | class Parent extends ProjectorMixin(WidgetBase) { 115 | render() { 116 | return w(ContainerClass, {}); 117 | } 118 | } 119 | const projector = new Parent(); 120 | projector.setProperties({ registry }); 121 | projector.async = false; 122 | projector.append(); 123 | renderCount = 0; 124 | 125 | testInvalidate.invalidator(); 126 | assert.strictEqual(renderCount, 1); 127 | 128 | testInvalidate.invalidator(); 129 | assert.strictEqual(renderCount, 2); 130 | 131 | testInvalidate.invalidator(); 132 | assert.strictEqual(renderCount, 3); 133 | } 134 | } 135 | }); 136 | -------------------------------------------------------------------------------- /tests/unit/Injector.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import { stub } from 'sinon'; 4 | 5 | import { Injector } from './../../src/Injector'; 6 | 7 | registerSuite('Injector', { 8 | get() { 9 | const payload = {}; 10 | const injector = new Injector(payload); 11 | assert.strictEqual(injector.get(), payload); 12 | }, 13 | set() { 14 | const payload = {}; 15 | const injector = new Injector(payload); 16 | const invalidatorStub = stub(); 17 | injector.setInvalidator(invalidatorStub); 18 | assert.strictEqual(injector.get(), payload); 19 | injector.set({}); 20 | assert.isTrue(invalidatorStub.calledOnce); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /tests/unit/NodeHandler.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import { stub, SinonStub } from 'sinon'; 4 | import NodeHandler, { NodeEventType } from '../../src/NodeHandler'; 5 | 6 | const elementStub: SinonStub = stub(); 7 | const widgetStub: SinonStub = stub(); 8 | const projectorStub: SinonStub = stub(); 9 | let nodeHandler: NodeHandler; 10 | let element: HTMLElement; 11 | 12 | registerSuite('NodeHandler', { 13 | beforeEach() { 14 | nodeHandler = new NodeHandler(); 15 | element = document.createElement('div'); 16 | }, 17 | 18 | tests: { 19 | 'add populates nodehandler map'() { 20 | nodeHandler.add(element, 'foo'); 21 | assert.isTrue(nodeHandler.has('foo')); 22 | }, 23 | 'has returns undefined when element does not exist'() { 24 | assert.isFalse(nodeHandler.has('foo')); 25 | }, 26 | 'get returns elements that have been added'() { 27 | nodeHandler.add(element, 'foo'); 28 | assert.equal(nodeHandler.get('foo'), element); 29 | }, 30 | 'clear removes nodes from map'() { 31 | nodeHandler.add(element, 'foo'); 32 | assert.isTrue(nodeHandler.has('foo')); 33 | nodeHandler.clear(); 34 | assert.isFalse(nodeHandler.has('foo')); 35 | }, 36 | events: { 37 | beforeEach() { 38 | elementStub.resetHistory(); 39 | widgetStub.resetHistory(); 40 | projectorStub.resetHistory(); 41 | 42 | nodeHandler.on('foo', elementStub); 43 | nodeHandler.on(NodeEventType.Widget, widgetStub); 44 | nodeHandler.on(NodeEventType.Projector, projectorStub); 45 | }, 46 | 47 | tests: { 48 | 'add emits event when element added'() { 49 | nodeHandler.add(element, 'foo'); 50 | 51 | assert.isTrue(elementStub.calledOnce); 52 | assert.isTrue(widgetStub.notCalled); 53 | assert.isTrue(projectorStub.notCalled); 54 | }, 55 | 'add root emits Widget'() { 56 | nodeHandler.addRoot(); 57 | 58 | assert.isTrue(widgetStub.calledOnce); 59 | assert.isTrue(projectorStub.notCalled); 60 | }, 61 | 'add root without a key emits Widget event only'() { 62 | nodeHandler.addRoot(); 63 | 64 | assert.isTrue(widgetStub.calledOnce); 65 | assert.isTrue(elementStub.notCalled); 66 | assert.isTrue(projectorStub.notCalled); 67 | }, 68 | 'add projector emits Projector event'() { 69 | nodeHandler.addProjector(); 70 | 71 | assert.isTrue(widgetStub.notCalled); 72 | assert.isTrue(elementStub.notCalled); 73 | assert.isTrue(projectorStub.calledOnce); 74 | } 75 | } 76 | } 77 | } 78 | }); 79 | -------------------------------------------------------------------------------- /tests/unit/all.ts: -------------------------------------------------------------------------------- 1 | import './Container'; 2 | import './WidgetBase'; 3 | import './Registry'; 4 | import './d'; 5 | import './decorators/all'; 6 | import './mixins/all'; 7 | import './diff'; 8 | import './RegistryHandler'; 9 | import './Injector'; 10 | import './tsx'; 11 | import './tsxIntegration'; 12 | import './NodeHandler'; 13 | import './meta/all'; 14 | import './vdom'; 15 | import './registerCustomElement'; 16 | -------------------------------------------------------------------------------- /tests/unit/decorators/afterRender.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | 4 | import { DNode } from './../../../src/interfaces'; 5 | import { afterRender } from './../../../src/decorators/afterRender'; 6 | import { WidgetBase } from './../../../src/WidgetBase'; 7 | 8 | registerSuite('decorators/afterRender', { 9 | decorator() { 10 | let afterRenderCount = 1; 11 | class TestWidget extends WidgetBase { 12 | @afterRender() 13 | firstAfterRender(result: DNode): DNode { 14 | assert.strictEqual(afterRenderCount++, 1); 15 | return result; 16 | } 17 | 18 | @afterRender() 19 | secondAfterRender(result: DNode): DNode { 20 | assert.strictEqual(afterRenderCount++, 2); 21 | return result; 22 | } 23 | } 24 | 25 | class ExtendedTestWidget extends TestWidget { 26 | @afterRender() 27 | thirdAfterRender(result: DNode): DNode { 28 | assert.strictEqual(afterRenderCount, 3); 29 | return result; 30 | } 31 | } 32 | 33 | const widget = new ExtendedTestWidget(); 34 | widget.__render__(); 35 | assert.strictEqual(afterRenderCount, 3); 36 | }, 37 | 'non decorator'() { 38 | let afterRenderCount = 1; 39 | class TestWidget extends WidgetBase { 40 | constructor() { 41 | super(); 42 | afterRender()(this, 'firstAfterRender'); 43 | afterRender()(this, 'secondAfterRender'); 44 | } 45 | 46 | firstAfterRender(result: DNode): DNode { 47 | assert.strictEqual(afterRenderCount++, 1); 48 | return result; 49 | } 50 | 51 | secondAfterRender(result: DNode): DNode { 52 | assert.strictEqual(afterRenderCount++, 2); 53 | return result; 54 | } 55 | } 56 | 57 | class ExtendedTestWidget extends TestWidget { 58 | constructor() { 59 | super(); 60 | afterRender(this.thirdAfterRender)(this); 61 | } 62 | 63 | thirdAfterRender(result: DNode): DNode { 64 | assert.strictEqual(afterRenderCount, 3); 65 | return result; 66 | } 67 | } 68 | 69 | const widget = new ExtendedTestWidget(); 70 | widget.__render__(); 71 | assert.strictEqual(afterRenderCount, 3); 72 | }, 73 | 'class level decorator'() { 74 | let afterRenderCount = 0; 75 | 76 | @afterRender(function(node: any) { 77 | afterRenderCount++; 78 | return node; 79 | }) 80 | class TestWidget extends WidgetBase {} 81 | 82 | const widget = new TestWidget(); 83 | widget.__render__(); 84 | assert.strictEqual(afterRenderCount, 1); 85 | }, 86 | 'class level without decorator'() { 87 | let afterRenderCount = 0; 88 | 89 | function afterRenderFn(node: any) { 90 | afterRenderCount++; 91 | 92 | return node; 93 | } 94 | 95 | class TestWidget extends WidgetBase { 96 | constructor() { 97 | super(); 98 | afterRender(afterRenderFn)(this); 99 | } 100 | } 101 | 102 | const widget = new TestWidget(); 103 | widget.__render__(); 104 | assert.strictEqual(afterRenderCount, 1); 105 | } 106 | }); 107 | -------------------------------------------------------------------------------- /tests/unit/decorators/all.ts: -------------------------------------------------------------------------------- 1 | import './afterRender'; 2 | import './alwaysRender'; 3 | import './beforeProperties'; 4 | import './beforeRender'; 5 | import './customElement'; 6 | import './diffProperty'; 7 | import './inject'; 8 | import './registry'; 9 | -------------------------------------------------------------------------------- /tests/unit/decorators/alwaysRender.ts: -------------------------------------------------------------------------------- 1 | const { describe, it } = intern.getInterface('bdd'); 2 | const { assert } = intern.getPlugin('chai'); 3 | 4 | import { WidgetBase } from './../../../src/WidgetBase'; 5 | import { w } from './../../../src/d'; 6 | import { ProjectorMixin } from './../../../src/mixins/Projector'; 7 | import { alwaysRender } from './../../../src/decorators/alwaysRender'; 8 | 9 | describe('decorators/alwaysRender', () => { 10 | it('Widgets should always render', () => { 11 | let renderCount = 0; 12 | 13 | @alwaysRender() 14 | class Widget extends WidgetBase { 15 | render() { 16 | renderCount++; 17 | return super.render(); 18 | } 19 | } 20 | 21 | class Parent extends ProjectorMixin(WidgetBase) { 22 | render() { 23 | return w(Widget, {}); 24 | } 25 | } 26 | 27 | const projector = new Parent(); 28 | projector.async = false; 29 | projector.setProperties({}); 30 | projector.append(); 31 | assert.strictEqual(renderCount, 1); 32 | 33 | projector.invalidate(); 34 | assert.strictEqual(renderCount, 2); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/unit/decorators/beforeProperties.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | 4 | import { beforeProperties } from './../../../src/decorators/beforeProperties'; 5 | import { WidgetBase } from './../../../src/WidgetBase'; 6 | import { WidgetProperties } from './../../../src/interfaces'; 7 | 8 | registerSuite('decorators/beforeProperties', { 9 | beforeProperties() { 10 | function before(properties: WidgetProperties): WidgetProperties { 11 | return { key: 'foo' }; 12 | } 13 | 14 | @beforeProperties(before) 15 | class TestWidget extends WidgetBase {} 16 | const widget = new TestWidget(); 17 | widget.__setProperties__({}); 18 | assert.strictEqual(widget.properties.key, 'foo'); 19 | }, 20 | 'multiple beforeProperties decorators'() { 21 | function beforeOne(properties: WidgetProperties): WidgetProperties { 22 | return { key: 'foo' }; 23 | } 24 | function beforeTwo(properties: any): any { 25 | return { other: 'bar' }; 26 | } 27 | 28 | @beforeProperties(beforeOne) 29 | @beforeProperties(beforeTwo) 30 | class TestWidget extends WidgetBase {} 31 | const widget = new TestWidget(); 32 | widget.__setProperties__({}); 33 | assert.strictEqual(widget.properties.key, 'foo'); 34 | assert.strictEqual(widget.properties.other, 'bar'); 35 | }, 36 | 'beforeProperties on class method'() { 37 | class TestWidget extends WidgetBase { 38 | @beforeProperties() 39 | before(properties: WidgetProperties): WidgetProperties { 40 | return { key: 'foo' }; 41 | } 42 | } 43 | const widget = new TestWidget(); 44 | widget.__setProperties__({}); 45 | assert.strictEqual(widget.properties.key, 'foo'); 46 | }, 47 | 'programmatic beforeProperties'() { 48 | function beforeOne(properties: WidgetProperties): WidgetProperties { 49 | return { key: 'foo' }; 50 | } 51 | function beforeTwo(properties: any): any { 52 | return { other: 'bar' }; 53 | } 54 | 55 | class TestWidget extends WidgetBase { 56 | constructor() { 57 | super(); 58 | beforeProperties(beforeOne)(this); 59 | beforeProperties(beforeTwo)(this); 60 | } 61 | } 62 | const widget = new TestWidget(); 63 | widget.__setProperties__({}); 64 | assert.strictEqual(widget.properties.key, 'foo'); 65 | assert.strictEqual(widget.properties.other, 'bar'); 66 | } 67 | }); 68 | -------------------------------------------------------------------------------- /tests/unit/decorators/beforeRender.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import { stub, SinonStub } from 'sinon'; 4 | 5 | import { v } from './../../../src/d'; 6 | import { DNode, Render } from './../../../src/interfaces'; 7 | import { beforeRender } from './../../../src/decorators/beforeRender'; 8 | import { WidgetBase } from './../../../src/WidgetBase'; 9 | 10 | let consoleStub: SinonStub; 11 | 12 | registerSuite('decorators/beforeRender', { 13 | beforeEach() { 14 | consoleStub = stub(console, 'warn'); 15 | }, 16 | afterEach() { 17 | consoleStub.restore(); 18 | }, 19 | 20 | tests: { 21 | decorator() { 22 | let beforeRenderCount = 1; 23 | type RenderFunction = () => DNode; 24 | class TestWidget extends WidgetBase { 25 | @beforeRender() 26 | firstAfterRender(renderFunction: RenderFunction, properties: any, children: DNode[]): RenderFunction { 27 | assert.strictEqual(beforeRenderCount++, 1); 28 | return () => { 29 | const rendered = renderFunction(); 30 | const clonedProperties = { ...properties }; 31 | return v('bar', clonedProperties, [rendered, ...children]); 32 | }; 33 | } 34 | 35 | @beforeRender() 36 | secondAfterRender(renderFunction: RenderFunction, properties: any, children: DNode[]): RenderFunction { 37 | assert.strictEqual(beforeRenderCount++, 2); 38 | return () => { 39 | const rendered = renderFunction(); 40 | properties.bar = 'foo'; 41 | return v('qux', properties, [rendered]); 42 | }; 43 | } 44 | } 45 | 46 | class ExtendedTestWidget extends TestWidget { 47 | @beforeRender() 48 | thirdAfterRender(renderFunction: RenderFunction, properties: any, children: DNode[]): RenderFunction { 49 | assert.strictEqual(beforeRenderCount, 3); 50 | return renderFunction; 51 | } 52 | 53 | render() { 54 | return v('foo', this.children); 55 | } 56 | } 57 | 58 | const widget = new ExtendedTestWidget(); 59 | widget.__setChildren__([v('baz', { baz: 'qux' })]); 60 | widget.__setProperties__({ foo: 'bar' }); 61 | const qux: any = widget.__render__(); 62 | assert.equal(qux.tag, 'qux'); 63 | assert.equal(qux.properties.bar, 'foo'); 64 | assert.equal(qux.properties.foo, 'bar'); 65 | assert.lengthOf(qux.children, 1); 66 | const bar = qux.children[0]; 67 | assert.equal(bar.tag, 'bar'); 68 | assert.deepEqual(bar.properties.foo, 'bar'); 69 | assert.lengthOf(bar.children, 2); 70 | const foo = bar.children[0]; 71 | assert.equal(foo.tag, 'foo'); 72 | assert.lengthOf(foo.children, 1); 73 | const baz1 = foo.children[0]; 74 | assert.equal(baz1.tag, 'baz'); 75 | assert.deepEqual(baz1.properties.baz, 'qux'); 76 | assert.isUndefined(baz1.children); 77 | const baz2 = bar.children[1]; 78 | assert.equal(baz2.tag, 'baz'); 79 | assert.deepEqual(baz2.properties.baz, 'qux'); 80 | assert.isUndefined(baz2.children); 81 | }, 82 | 'class level decorator'() { 83 | let beforeRenderCount = 0; 84 | 85 | @beforeRender(function(renderFunc: Render) { 86 | beforeRenderCount++; 87 | return renderFunc; 88 | }) 89 | class TestWidget extends WidgetBase {} 90 | 91 | const widget = new TestWidget(); 92 | widget.__render__(); 93 | assert.strictEqual(beforeRenderCount, 1); 94 | }, 95 | 'Use previous render function when a beforeRender does not return a function'() { 96 | class TestWidget extends WidgetBase { 97 | @beforeRender() 98 | protected firstBeforeRender(renderFunc: Render) { 99 | return () => 'first render'; 100 | } 101 | 102 | @beforeRender() 103 | protected secondBeforeRender(renderFunc: Render) {} 104 | } 105 | 106 | const widget = new TestWidget(); 107 | const renderResult = widget.__render__(); 108 | assert.strictEqual(renderResult, 'first render'); 109 | assert.isTrue(consoleStub.calledOnce); 110 | assert.isTrue( 111 | consoleStub.calledWith('Render function not returned from beforeRender, using previous render') 112 | ); 113 | } 114 | } 115 | }); 116 | -------------------------------------------------------------------------------- /tests/unit/decorators/customElement.ts: -------------------------------------------------------------------------------- 1 | const { describe, it } = intern.getInterface('bdd'); 2 | const { assert } = intern.getPlugin('chai'); 3 | 4 | import { customElement } from '../../../src/decorators/customElement'; 5 | import { WidgetBase } from '../../../src/WidgetBase'; 6 | import { CustomElementChildType } from '../../../src/registerCustomElement'; 7 | 8 | interface CustomElementWidgetProperties { 9 | label: string; 10 | labelSuffix: string; 11 | onClick: () => void; 12 | } 13 | 14 | function registryFactory() { 15 | return {} as any; 16 | } 17 | 18 | @customElement({ 19 | tag: 'custom-element', 20 | attributes: ['key', 'label', 'labelSuffix'], 21 | properties: ['label'], 22 | events: ['onClick'], 23 | registryFactory 24 | }) 25 | export class CustomElementWidget extends WidgetBase {} 26 | 27 | describe('@customElement', () => { 28 | it('Should add the descriptor to the widget prototype', () => { 29 | assert.deepEqual((CustomElementWidget.prototype as any).__customElementDescriptor, { 30 | tagName: 'custom-element', 31 | attributes: ['key', 'label', 'labelSuffix'], 32 | properties: ['label'], 33 | events: ['onClick'], 34 | childType: CustomElementChildType.DOJO, 35 | registryFactory 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/unit/decorators/inject.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | 4 | import { inject } from './../../../src/decorators/inject'; 5 | import { WidgetBase } from './../../../src/WidgetBase'; 6 | import { Registry } from './../../../src/Registry'; 7 | import { WidgetProperties } from './../../../src/interfaces'; 8 | 9 | let injectorOne = () => () => ({ foo: 'bar' }); 10 | let injectorTwo = () => () => ({ bar: 'foo' }); 11 | let registry: Registry; 12 | 13 | registerSuite('decorators/inject', { 14 | beforeEach() { 15 | registry = new Registry(); 16 | injectorOne = () => () => ({ foo: 'bar' }); 17 | injectorTwo = () => () => ({ bar: 'foo' }); 18 | registry.defineInjector('inject-one', injectorOne); 19 | registry.defineInjector('inject-two', injectorTwo); 20 | }, 21 | 22 | tests: { 23 | beforeProperties() { 24 | function getProperties(payload: any, properties: WidgetProperties): WidgetProperties { 25 | return payload; 26 | } 27 | 28 | @inject({ name: 'inject-one', getProperties }) 29 | class TestWidget extends WidgetBase {} 30 | const widget = new TestWidget(); 31 | widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); 32 | widget.__setProperties__({}); 33 | 34 | assert.strictEqual(widget.properties.foo, 'bar'); 35 | }, 36 | 'multiple injectors'() { 37 | function getPropertiesOne(payload: any, properties: WidgetProperties): WidgetProperties { 38 | return payload; 39 | } 40 | function getPropertiesTwo(payload: any, properties: WidgetProperties): WidgetProperties { 41 | return payload; 42 | } 43 | 44 | @inject({ name: 'inject-one', getProperties: getPropertiesOne }) 45 | @inject({ name: 'inject-two', getProperties: getPropertiesTwo }) 46 | class TestWidget extends WidgetBase {} 47 | const widget = new TestWidget(); 48 | widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); 49 | widget.__setProperties__({}); 50 | assert.strictEqual(widget.properties.foo, 'bar'); 51 | assert.strictEqual(widget.properties.bar, 'foo'); 52 | }, 53 | 'programmatic registration'() { 54 | function getPropertiesOne(payload: any, properties: WidgetProperties): WidgetProperties { 55 | return payload; 56 | } 57 | function getPropertiesTwo(payload: any, properties: WidgetProperties): WidgetProperties { 58 | return payload; 59 | } 60 | 61 | class TestWidget extends WidgetBase { 62 | constructor() { 63 | super(); 64 | inject({ name: 'inject-one', getProperties: getPropertiesOne })(this); 65 | inject({ name: 'inject-two', getProperties: getPropertiesTwo })(this); 66 | } 67 | } 68 | const widget = new TestWidget(); 69 | widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); 70 | widget.__setProperties__({}); 71 | assert.strictEqual(widget.properties.foo, 'bar'); 72 | assert.strictEqual(widget.properties.bar, 'foo'); 73 | }, 74 | 'invalidate listeners are removed when widget is destroyed'() { 75 | class TestInvalidate { 76 | invalidator: any; 77 | } 78 | const testInvalidate = new TestInvalidate(); 79 | const testInvalidateInjector = (invalidator: any) => { 80 | testInvalidate.invalidator = invalidator; 81 | return () => {}; 82 | }; 83 | registry.defineInjector('invalidate-test', testInvalidateInjector); 84 | function getProperties(payload: any, properties: WidgetProperties): WidgetProperties { 85 | return payload; 86 | } 87 | let invalidateCounter = 0; 88 | @inject({ name: 'invalidate-test', getProperties: getProperties }) 89 | class TestWidget extends WidgetBase { 90 | destroy() { 91 | super.destroy(); 92 | } 93 | invalidate() { 94 | invalidateCounter++; 95 | super.invalidate(); 96 | } 97 | } 98 | const widget = new TestWidget(); 99 | widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); 100 | widget.__setProperties__({}); 101 | testInvalidate.invalidator(); 102 | assert.strictEqual(invalidateCounter, 2); 103 | testInvalidate.invalidator(); 104 | assert.strictEqual(invalidateCounter, 3); 105 | widget.destroy(); 106 | testInvalidate.invalidator(); 107 | assert.strictEqual(invalidateCounter, 3); 108 | } 109 | } 110 | }); 111 | -------------------------------------------------------------------------------- /tests/unit/decorators/registry.ts: -------------------------------------------------------------------------------- 1 | const { describe, it } = intern.getInterface('bdd'); 2 | import { v, w } from '../../../src/d'; 3 | const { assert } = intern.getPlugin('chai'); 4 | 5 | import { registry } from './../../../src/decorators/registry'; 6 | import { WidgetBase } from './../../../src/WidgetBase'; 7 | import ProjectorMixin from './../../../src/mixins/Projector'; 8 | 9 | export class Widget1 extends WidgetBase { 10 | protected render() { 11 | return v('span', { classes: ['widget1'] }); 12 | } 13 | } 14 | 15 | export class Widget2 extends WidgetBase { 16 | protected render() { 17 | return v('span', { classes: ['widget2'] }); 18 | } 19 | } 20 | 21 | describe('decorators/registry', () => { 22 | it('should use the single entry decorator format to register reg-widget-1', () => { 23 | @registry('reg-widget-1', Widget1) 24 | class TestWidget1 extends WidgetBase { 25 | render() { 26 | return w('reg-widget-1', {}); 27 | } 28 | } 29 | 30 | const Projector = ProjectorMixin(TestWidget1); 31 | const projector = new Projector(); 32 | projector.async = false; 33 | 34 | const root = document.createElement('div'); 35 | projector.append(root); 36 | 37 | assert.strictEqual(root.querySelectorAll('.widget1').length, 1); 38 | assert.strictEqual(root.querySelectorAll('.widget2').length, 0); 39 | }); 40 | 41 | it('should use the registry config format to register multiple widgets', () => { 42 | @registry({ 43 | 'reg-widget-1': Widget1, 44 | 'reg-widget-2': Widget2 45 | }) 46 | class TestWidget2 extends WidgetBase { 47 | render() { 48 | return [w('reg-widget-1', {}), w('reg-widget-2', {})]; 49 | } 50 | } 51 | 52 | const Projector = ProjectorMixin(TestWidget2); 53 | const projector = new Projector(); 54 | projector.async = false; 55 | 56 | const root = document.createElement('div'); 57 | projector.append(root); 58 | 59 | assert.strictEqual(root.querySelectorAll('.widget1').length, 1); 60 | assert.strictEqual(root.querySelectorAll('.widget2').length, 1); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/unit/diff.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import * as diff from '../../src/diff'; 4 | import WidgetBase from '../../src/WidgetBase'; 5 | 6 | registerSuite('diff', { 7 | always() { 8 | const foo = {}; 9 | const result = diff.always(foo, foo); 10 | assert.equal(result.value, foo); 11 | assert.isTrue(result.changed); 12 | }, 13 | ignore() { 14 | const result = diff.ignore('foo', 'bar'); 15 | assert.equal(result.value, 'bar'); 16 | assert.isFalse(result.changed); 17 | }, 18 | reference() { 19 | const foo = { 20 | bar: 'bar' 21 | }; 22 | const bar = { 23 | bar: 'bar' 24 | }; 25 | const result = diff.reference(foo, bar); 26 | assert.equal(result.value, bar); 27 | assert.isTrue(result.changed); 28 | }, 29 | shallow: { 30 | object() { 31 | const foo = { 32 | bar: 'bar' 33 | }; 34 | const bar = { 35 | bar: 'bar' 36 | }; 37 | let result = diff.shallow(foo, bar); 38 | assert.equal(result.value, bar); 39 | assert.isFalse(result.changed); 40 | 41 | bar.bar = 'qux'; 42 | result = diff.shallow(foo, bar); 43 | assert.equal(result.value, bar); 44 | assert.isTrue(result.changed); 45 | 46 | const baz = { 47 | bar: 'bar', 48 | baz: 'baz' 49 | }; 50 | result = diff.shallow(foo, baz); 51 | assert.equal(result.value, baz); 52 | assert.isTrue(result.changed); 53 | 54 | result = diff.shallow('foo', baz); 55 | assert.equal(result.value, baz); 56 | assert.isTrue(result.changed); 57 | }, 58 | array() { 59 | const foo = [1, 2, 3]; 60 | const bar = [1, 2, 3]; 61 | let result = diff.shallow(foo, bar); 62 | assert.equal(result.value, bar); 63 | assert.isFalse(result.changed); 64 | 65 | const qux = [1, 3, 2]; 66 | result = diff.shallow(foo, qux); 67 | assert.equal(result.value, qux); 68 | assert.isTrue(result.changed); 69 | } 70 | }, 71 | auto: { 72 | 'widget constructor'() { 73 | class Foo extends WidgetBase {} 74 | class Bar extends WidgetBase {} 75 | let result = diff.auto(Foo, Bar); 76 | assert.equal(result.value, Bar); 77 | assert.isTrue(result.changed); 78 | }, 79 | function() { 80 | const foo = () => {}; 81 | const bar = () => {}; 82 | let result = diff.auto(foo, bar); 83 | assert.equal(result.value, bar); 84 | assert.isFalse(result.changed); 85 | }, 86 | object() { 87 | const foo = { 88 | bar: 'bar' 89 | }; 90 | const bar = { 91 | bar: 'bar' 92 | }; 93 | let result = diff.auto(foo, bar); 94 | assert.equal(result.value, bar); 95 | assert.isFalse(result.changed); 96 | 97 | bar.bar = 'qux'; 98 | result = diff.auto(foo, bar); 99 | assert.equal(result.value, bar); 100 | assert.isTrue(result.changed); 101 | }, 102 | other() { 103 | const foo = new Date(); 104 | let result = diff.auto(foo, foo); 105 | assert.equal(result.value, foo); 106 | assert.isFalse(result.changed); 107 | 108 | const bar = new Date(); 109 | result = diff.auto(foo, bar); 110 | assert.equal(result.value, bar); 111 | assert.isTrue(result.changed); 112 | } 113 | } 114 | }); 115 | -------------------------------------------------------------------------------- /tests/unit/meta/Dimensions.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import global from '@dojo/shim/global'; 4 | import { spy, stub } from 'sinon'; 5 | import Dimensions from '../../../src/meta/Dimensions'; 6 | import NodeHandler from '../../../src/NodeHandler'; 7 | import WidgetBase from '../../../src/WidgetBase'; 8 | 9 | let rAF: any; 10 | const bindInstance = new WidgetBase(); 11 | const defaultDimensions = { 12 | client: { 13 | height: 0, 14 | left: 0, 15 | top: 0, 16 | width: 0 17 | }, 18 | offset: { 19 | height: 0, 20 | left: 0, 21 | top: 0, 22 | width: 0 23 | }, 24 | position: { 25 | bottom: 0, 26 | left: 0, 27 | right: 0, 28 | top: 0 29 | }, 30 | scroll: { 31 | height: 0, 32 | left: 0, 33 | top: 0, 34 | width: 0 35 | }, 36 | size: { 37 | height: 0, 38 | width: 0 39 | } 40 | }; 41 | 42 | function resolveRAF() { 43 | for (let i = 0; i < rAF.callCount; i++) { 44 | rAF.getCall(i).args[0](); 45 | } 46 | rAF.resetHistory(); 47 | } 48 | 49 | registerSuite('meta - Dimensions', { 50 | beforeEach() { 51 | rAF = stub(global, 'requestAnimationFrame'); 52 | }, 53 | 54 | afterEach() { 55 | rAF.restore(); 56 | }, 57 | 58 | tests: { 59 | 'Will return default dimensions if node not loaded'() { 60 | const nodeHandler = new NodeHandler(); 61 | 62 | const dimensions = new Dimensions({ 63 | invalidate: () => {}, 64 | nodeHandler, 65 | bind: bindInstance 66 | }); 67 | 68 | assert.deepEqual(dimensions.get('foo'), defaultDimensions); 69 | }, 70 | 'Will accept a number key'() { 71 | const nodeHandler = new NodeHandler(); 72 | 73 | const dimensions = new Dimensions({ 74 | invalidate: () => {}, 75 | nodeHandler, 76 | bind: bindInstance 77 | }); 78 | 79 | assert.deepEqual(dimensions.get(1234), defaultDimensions); 80 | }, 81 | 'Will create event listener for node if not yet loaded'() { 82 | const nodeHandler = new NodeHandler(); 83 | const onSpy = spy(nodeHandler, 'on'); 84 | 85 | const dimensions = new Dimensions({ 86 | invalidate: () => {}, 87 | nodeHandler, 88 | bind: bindInstance 89 | }); 90 | 91 | dimensions.get('foo'); 92 | assert.isTrue(onSpy.calledOnce); 93 | assert.isTrue(onSpy.firstCall.calledWith('foo')); 94 | }, 95 | 'Will call invalidate when awaited node is available'() { 96 | const nodeHandler = new NodeHandler(); 97 | const onSpy = spy(nodeHandler, 'on'); 98 | const invalidateStub = stub(); 99 | 100 | const dimensions = new Dimensions({ 101 | invalidate: invalidateStub, 102 | nodeHandler, 103 | bind: bindInstance 104 | }); 105 | 106 | dimensions.get('foo'); 107 | assert.isTrue(onSpy.calledOnce); 108 | assert.isTrue(onSpy.firstCall.calledWith('foo')); 109 | 110 | const element = document.createElement('div'); 111 | document.body.appendChild(element); 112 | const getRectSpy = spy(element, 'getBoundingClientRect'); 113 | 114 | nodeHandler.add(element, 'foo'); 115 | 116 | resolveRAF(); 117 | assert.isTrue(invalidateStub.calledOnce); 118 | 119 | onSpy.resetHistory(); 120 | dimensions.get('foo'); 121 | 122 | assert.isFalse(onSpy.called); 123 | assert.isTrue(getRectSpy.calledOnce); 124 | document.body.removeChild(element); 125 | }, 126 | 'Will return element dimensions if node is loaded'() { 127 | const nodeHandler = new NodeHandler(); 128 | 129 | const client = { clientLeft: 1, clientTop: 2, clientWidth: 3, clientHeight: 4 }; 130 | const offset = { offsetHeight: 10, offsetLeft: 10, offsetTop: 10, offsetWidth: 10 }; 131 | const scroll = { scrollHeight: 10, scrollLeft: 10, scrollTop: 10, scrollWidth: 10 }; 132 | const position = { bottom: 10, left: 10, right: 10, top: 10 }; 133 | const size = { width: 10, height: 10 }; 134 | 135 | const element = { 136 | ...offset, 137 | ...scroll, 138 | ...client, 139 | getBoundingClientRect: stub().returns({ 140 | ...position, 141 | ...size 142 | }) 143 | }; 144 | 145 | nodeHandler.add(element as any, 'foo'); 146 | 147 | const dimensions = new Dimensions({ 148 | invalidate: () => {}, 149 | nodeHandler, 150 | bind: bindInstance 151 | }); 152 | 153 | assert.deepEqual(dimensions.get('foo'), { 154 | offset: { height: 10, left: 10, top: 10, width: 10 }, 155 | scroll: { height: 10, left: 10, top: 10, width: 10 }, 156 | position, 157 | size, 158 | client: { height: 4, left: 1, top: 2, width: 3 } 159 | }); 160 | } 161 | } 162 | }); 163 | -------------------------------------------------------------------------------- /tests/unit/meta/Focus.ts: -------------------------------------------------------------------------------- 1 | const { assert } = intern.getPlugin('chai'); 2 | const { afterEach, beforeEach, describe, it } = intern.getInterface('bdd'); 3 | import global from '@dojo/shim/global'; 4 | import * as sinon from 'sinon'; 5 | import Focus from '../../../src/meta/Focus'; 6 | import NodeHandler from '../../../src/NodeHandler'; 7 | import WidgetBase from '../../../src/WidgetBase'; 8 | 9 | describe('meta - Focus', () => { 10 | const bindInstance = new WidgetBase(); 11 | const defaultFocus = { 12 | active: false, 13 | containsFocus: false 14 | }; 15 | let element: HTMLElement; 16 | let activeGetter: any; 17 | let focus: any; 18 | let nodeHandler: any; 19 | let invalidateStub: any; 20 | const isNode = typeof global.fakeActiveElement === 'function'; 21 | 22 | beforeEach((test) => { 23 | invalidateStub = sinon.stub(); 24 | nodeHandler = new NodeHandler(); 25 | focus = new Focus({ 26 | invalidate: invalidateStub, 27 | nodeHandler, 28 | bind: bindInstance 29 | }); 30 | 31 | element = document.createElement('button'); 32 | document.body.appendChild(element); 33 | 34 | if (isNode) { 35 | activeGetter = sinon.stub(global, 'fakeActiveElement').returns(element); 36 | } 37 | }); 38 | 39 | afterEach(() => { 40 | focus.destroy(); 41 | nodeHandler.destroy(); 42 | if (document.body.contains(element)) { 43 | document.body.removeChild(element); 44 | } 45 | if (isNode) { 46 | activeGetter.restore(); 47 | } 48 | }); 49 | 50 | describe('get', () => { 51 | it('will return default dimensions if a node is not loaded', () => { 52 | assert.deepEqual(focus.get('foo'), defaultFocus); 53 | }); 54 | it('will accept a number key', () => { 55 | assert.deepEqual(focus.get(1234), defaultFocus); 56 | }); 57 | it('will return true/true for an element with focus', (test) => { 58 | nodeHandler.add(element, 'root'); 59 | element.focus(); 60 | 61 | const focusResults = focus.get('root'); 62 | assert.equal(focusResults.active, true); 63 | assert.equal(focusResults.containsFocus, true); 64 | }); 65 | it('will return false/true for an element containing focus', () => { 66 | const containingEl = document.createElement('div'); 67 | containingEl.appendChild(element); 68 | document.body.appendChild(containingEl); 69 | nodeHandler.add(containingEl, 'root'); 70 | element.focus(); 71 | 72 | const focusResults = focus.get('root'); 73 | assert.equal(focusResults.active, false); 74 | assert.equal(focusResults.containsFocus, true); 75 | document.body.removeChild(containingEl); 76 | }); 77 | it('will return false/false for an element without focus', () => { 78 | const rootEl = document.createElement('div'); 79 | nodeHandler.add(rootEl, 'root'); 80 | element.focus(); 81 | 82 | const focusResults = focus.get('root'); 83 | assert.equal(focusResults.active, false); 84 | assert.equal(focusResults.containsFocus, false); 85 | }); 86 | it('will only query activeElement once for multiple requests', (test) => { 87 | if (!isNode) { 88 | test.skip('test requires activeElement stub'); 89 | } 90 | nodeHandler.add(element, 'root'); 91 | 92 | let focusResults = focus.get('root'); 93 | assert.isTrue(activeGetter.calledOnce, 'activeElement called on first .get()'); 94 | assert.equal(focusResults.active, true); 95 | 96 | focusResults = focus.get('root'); 97 | assert.isTrue(activeGetter.calledOnce, 'cached value used on second .get()'); 98 | assert.equal(focusResults.active, true); 99 | }); 100 | it('will invalidate on focus events', () => { 101 | const focusEvent = global.document.createEvent('Event'); 102 | focusEvent.initEvent('focusin', true, true); 103 | nodeHandler.add(element, 'root'); 104 | 105 | focus.get('root'); 106 | global.document.dispatchEvent(focusEvent); 107 | assert.isTrue(invalidateStub.calledOnce); 108 | }); 109 | it('will invalidate on blur events', () => { 110 | const blurEvent = global.document.createEvent('Event'); 111 | blurEvent.initEvent('focusout', true, true); 112 | nodeHandler.add(element, 'root'); 113 | 114 | focus.get('root'); 115 | global.document.dispatchEvent(blurEvent); 116 | assert.isTrue(invalidateStub.calledOnce); 117 | }); 118 | it('updates the saved activeElement value on focus events', (test) => { 119 | if (!isNode) { 120 | test.skip('test requires activeElement stub'); 121 | } 122 | const child = document.createElement('span'); 123 | const focusEvent = global.document.createEvent('Event'); 124 | focusEvent.initEvent('focusin', true, true); 125 | element.appendChild(child); 126 | nodeHandler.add(element, 'root'); 127 | 128 | let focusResults = focus.get('root'); 129 | assert.isTrue(activeGetter.calledOnce, 'activeElement called on first .get()'); 130 | assert.equal(focusResults.active, true); 131 | assert.equal(focusResults.containsFocus, true); 132 | 133 | activeGetter.restore(); 134 | activeGetter = sinon.stub(global, 'fakeActiveElement').returns(child); 135 | global.document.dispatchEvent(focusEvent); 136 | assert.isTrue(activeGetter.calledOnce, 'activeElement called after focus event'); 137 | 138 | activeGetter.resetHistory(); 139 | focusResults = focus.get('root'); 140 | assert.isFalse(activeGetter.called, 'activeElement not called on second .get()'); 141 | assert.equal(focusResults.active, false); 142 | assert.equal(focusResults.containsFocus, true); 143 | }); 144 | it('removes the focus and blur listeners when the meta is destroyed', (test) => { 145 | if (!isNode) { 146 | test.skip('test requires activeElement stub'); 147 | } 148 | const focusEvent = global.document.createEvent('Event'); 149 | const blurEvent = global.document.createEvent('Event'); 150 | focusEvent.initEvent('focusin', true, true); 151 | blurEvent.initEvent('focusout', true, true); 152 | nodeHandler.add(element, 'root'); 153 | 154 | focus.get('root'); 155 | global.document.dispatchEvent(focusEvent); 156 | assert.isTrue(activeGetter.called, 'focus handler calls activeElement'); 157 | activeGetter.resetHistory(); 158 | global.document.dispatchEvent(blurEvent); 159 | assert.isTrue(activeGetter.called, 'blur handler calls activeElement'); 160 | 161 | focus.destroy(); 162 | activeGetter.resetHistory(); 163 | global.document.dispatchEvent(focusEvent); 164 | global.document.dispatchEvent(blurEvent); 165 | assert.isFalse(activeGetter.called, 'focus and blur handlers removed'); 166 | }); 167 | }); 168 | 169 | describe('set', () => { 170 | it('sets focus on the node', () => { 171 | const setFocus = sinon.stub(); 172 | sinon.stub(element, 'focus').callsFake(setFocus); 173 | nodeHandler.add(element, 'root'); 174 | 175 | focus.set('root'); 176 | assert.isTrue(setFocus.calledOnce); 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /tests/unit/meta/Matches.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | 4 | import sendEvent from '../../support/sendEvent'; 5 | import { createResolvers } from './../../support/util'; 6 | import { v } from '../../../src/d'; 7 | import { ProjectorMixin } from '../../../src/mixins/Projector'; 8 | import { WidgetBase } from '../../../src/WidgetBase'; 9 | import { ThemedMixin } from '../../../src/mixins/Themed'; 10 | 11 | import Matches from '../../../src/meta/Matches'; 12 | 13 | const resolvers = createResolvers(); 14 | 15 | registerSuite('support/meta/Matches', { 16 | beforeEach() { 17 | resolvers.stub(); 18 | }, 19 | 20 | afterEach() { 21 | resolvers.restore(); 22 | }, 23 | 24 | tests: { 25 | 'node matches'() { 26 | const results: boolean[] = []; 27 | 28 | class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { 29 | private _onclick(evt: MouseEvent) { 30 | results.push(this.meta(Matches).get('root', evt)); 31 | } 32 | 33 | render() { 34 | return v('div', { 35 | innerHTML: 'hello world', 36 | key: 'root', 37 | onclick: this._onclick 38 | }); 39 | } 40 | } 41 | 42 | const div = document.createElement('div'); 43 | 44 | document.body.appendChild(div); 45 | 46 | const widget = new TestWidget(); 47 | widget.append(div); 48 | 49 | resolvers.resolve(); 50 | resolvers.resolve(); 51 | 52 | sendEvent(div.firstChild as Element, 'click'); 53 | 54 | assert.deepEqual(results, [true], 'should have been called and the target matched'); 55 | 56 | document.body.removeChild(div); 57 | }, 58 | 59 | 'node matches with number key'() { 60 | const results: boolean[] = []; 61 | 62 | class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { 63 | private _onclick(evt: MouseEvent) { 64 | results.push(this.meta(Matches).get(1234, evt)); 65 | } 66 | 67 | render() { 68 | return v('div', { 69 | innerHTML: 'hello world', 70 | key: 1234, 71 | onclick: this._onclick 72 | }); 73 | } 74 | } 75 | 76 | const div = document.createElement('div'); 77 | 78 | document.body.appendChild(div); 79 | 80 | const widget = new TestWidget(); 81 | widget.append(div); 82 | 83 | resolvers.resolve(); 84 | resolvers.resolve(); 85 | 86 | sendEvent(div.firstChild as Element, 'click'); 87 | 88 | assert.deepEqual(results, [true], 'should have been called and the target matched'); 89 | 90 | document.body.removeChild(div); 91 | }, 92 | 93 | 'node does not match'() { 94 | const results: boolean[] = []; 95 | 96 | class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { 97 | private _onclick(evt: MouseEvent) { 98 | results.push(this.meta(Matches).get('root', evt)); 99 | } 100 | 101 | render() { 102 | return v( 103 | 'div', 104 | { 105 | key: 'root', 106 | onclick: this._onclick 107 | }, 108 | [ 109 | v('div', { 110 | innerHTML: 'Hello World', 111 | root: 'child' 112 | }) 113 | ] 114 | ); 115 | } 116 | } 117 | 118 | const div = document.createElement('div'); 119 | 120 | document.body.appendChild(div); 121 | 122 | const widget = new TestWidget(); 123 | widget.append(div); 124 | 125 | resolvers.resolve(); 126 | resolvers.resolve(); 127 | 128 | sendEvent(div.firstChild!.firstChild as Element, 'click', { 129 | eventInit: { 130 | bubbles: true 131 | } 132 | }); 133 | 134 | assert.deepEqual(results, [false], 'should have been called and the target not matching'); 135 | 136 | document.body.removeChild(div); 137 | }, 138 | 139 | 'node only exists on some renders'() { 140 | const results: boolean[] = []; 141 | 142 | class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { 143 | private _renderSecond = false; 144 | private _onclick(evt: MouseEvent) { 145 | results.push(this.meta(Matches).get('child1', evt)); 146 | results.push(this.meta(Matches).get('child2', evt)); 147 | this._renderSecond = true; 148 | this.invalidate(); 149 | } 150 | 151 | render() { 152 | return v( 153 | 'div', 154 | { 155 | key: 'root', 156 | onclick: this._onclick 157 | }, 158 | [ 159 | v('div', { 160 | innerHTML: this._renderSecond ? 'child2' : 'child1', 161 | key: this._renderSecond ? 'child2' : 'child1' 162 | }) 163 | ] 164 | ); 165 | } 166 | } 167 | 168 | const div = document.createElement('div'); 169 | 170 | document.body.appendChild(div); 171 | 172 | const widget = new TestWidget(); 173 | widget.append(div); 174 | 175 | resolvers.resolve(); 176 | resolvers.resolve(); 177 | 178 | sendEvent(div.firstChild!.firstChild as Element, 'click', { 179 | eventInit: { 180 | bubbles: true 181 | } 182 | }); 183 | 184 | resolvers.resolve(); 185 | 186 | sendEvent(div.firstChild!.firstChild as Element, 'click', { 187 | eventInit: { 188 | bubbles: true 189 | } 190 | }); 191 | 192 | assert.deepEqual(results, [true, false, false, true], 'should have been called twice and keys changed'); 193 | 194 | document.body.removeChild(div); 195 | } 196 | } 197 | }); 198 | -------------------------------------------------------------------------------- /tests/unit/meta/Resize.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import global from '@dojo/shim/global'; 4 | import { stub, SinonStub } from 'sinon'; 5 | import Resize, { ContentRect } from '../../../src/meta/Resize'; 6 | import NodeHandler from '../../../src/NodeHandler'; 7 | import WidgetBase from '../../../src/WidgetBase'; 8 | 9 | let resizeObserver: any; 10 | let resizeCallback: ([]: any[]) => void; 11 | const bindInstance = new WidgetBase(); 12 | let isFoo: SinonStub; 13 | let isBar: SinonStub; 14 | 15 | registerSuite('meta - Resize', { 16 | beforeEach() { 17 | isFoo = stub(); 18 | isBar = stub(); 19 | resizeObserver = stub().callsFake(function(callback: any) { 20 | const observer = { 21 | observe: stub() 22 | }; 23 | resizeCallback = callback; 24 | return observer; 25 | }); 26 | 27 | global.ResizeObserver = resizeObserver; 28 | }, 29 | 30 | afterEach() { 31 | isFoo.reset(); 32 | isBar.reset(); 33 | resizeObserver.reset(); 34 | global.ResizeObserver = undefined; 35 | }, 36 | 37 | tests: { 38 | 'Will return predicates defaulted to false if node not loaded'() { 39 | const nodeHandler = new NodeHandler(); 40 | 41 | const resize = new Resize({ 42 | invalidate: () => {}, 43 | nodeHandler, 44 | bind: bindInstance 45 | }); 46 | 47 | assert.deepEqual(resize.get('foo', { isFoo, isBar }), { isFoo: false, isBar: false }); 48 | assert.isFalse(isFoo.called); 49 | assert.isFalse(isBar.called); 50 | }, 51 | 'Will create a new ResizeObserver when node exists'() { 52 | const nodeHandler = new NodeHandler(); 53 | const element = document.createElement('div'); 54 | document.body.appendChild(element); 55 | nodeHandler.add(element, 'foo'); 56 | 57 | const resize = new Resize({ 58 | invalidate: () => {}, 59 | nodeHandler, 60 | bind: bindInstance 61 | }); 62 | 63 | resize.get('foo', { isFoo, isBar }); 64 | assert.isTrue(resizeObserver.calledOnce); 65 | }, 66 | 'Will call predicates when resize event is observed'() { 67 | const nodeHandler = new NodeHandler(); 68 | const element = document.createElement('div'); 69 | document.body.appendChild(element); 70 | nodeHandler.add(element, 'foo'); 71 | 72 | const resize = new Resize({ 73 | invalidate: () => {}, 74 | nodeHandler, 75 | bind: bindInstance 76 | }); 77 | 78 | const contentRect: Partial = { 79 | width: 10 80 | }; 81 | 82 | resize.get('foo', { isFoo, isBar }); 83 | resizeCallback([{ contentRect }]); 84 | 85 | assert.isTrue(isFoo.firstCall.calledWith(contentRect)); 86 | assert.isTrue(isBar.firstCall.calledWith(contentRect)); 87 | }, 88 | 'Will only set up one observer per widget per key'() { 89 | const nodeHandler = new NodeHandler(); 90 | const element = document.createElement('div'); 91 | document.body.appendChild(element); 92 | nodeHandler.add(element, 'foo'); 93 | 94 | const resize = new Resize({ 95 | invalidate: () => {}, 96 | nodeHandler, 97 | bind: bindInstance 98 | }); 99 | 100 | resize.get('foo', { isFoo }); 101 | resize.get('foo', { isBar }); 102 | assert.isTrue(resizeObserver.calledOnce); 103 | }, 104 | 'Will call invalidate when predicates have changed'() { 105 | const nodeHandler = new NodeHandler(); 106 | const invalidate = stub(); 107 | const element = document.createElement('div'); 108 | document.body.appendChild(element); 109 | nodeHandler.add(element, 'foo'); 110 | 111 | const resize = new Resize({ 112 | invalidate, 113 | nodeHandler, 114 | bind: bindInstance 115 | }); 116 | 117 | const contentRect: Partial = { 118 | width: 10 119 | }; 120 | 121 | isFoo.onFirstCall().returns(false); 122 | isFoo.onSecondCall().returns(true); 123 | 124 | resize.get('foo', { isFoo, isBar }); 125 | 126 | resizeCallback([{ contentRect }]); 127 | resizeCallback([{ contentRect }]); 128 | 129 | const predicates = resize.get('foo', { isFoo, isBar }); 130 | 131 | assert.isTrue(invalidate.calledTwice); 132 | assert.isTrue(predicates.isFoo); 133 | }, 134 | 'Will invalidate given no predicates'() { 135 | const nodeHandler = new NodeHandler(); 136 | const invalidate = stub(); 137 | const element = document.createElement('div'); 138 | document.body.appendChild(element); 139 | nodeHandler.add(element, 'foo'); 140 | 141 | const resize = new Resize({ 142 | invalidate, 143 | nodeHandler, 144 | bind: bindInstance 145 | }); 146 | 147 | const contentRect: Partial = { 148 | width: 10 149 | }; 150 | 151 | resize.get('foo'); 152 | resizeCallback([{ contentRect }]); 153 | assert.isTrue(invalidate.called); 154 | } 155 | } 156 | }); 157 | -------------------------------------------------------------------------------- /tests/unit/meta/all.ts: -------------------------------------------------------------------------------- 1 | import './meta'; 2 | import './Dimensions'; 3 | import './Drag'; 4 | import './Focus'; 5 | import './Intersection'; 6 | import './Matches'; 7 | import './Resize'; 8 | import './WebAnimation'; 9 | -------------------------------------------------------------------------------- /tests/unit/meta/meta.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import { Base as MetaBase } from '../../../src/meta/Base'; 4 | import { stub, spy } from 'sinon'; 5 | import { createResolvers } from './../../support/util'; 6 | import NodeHandler, { NodeEventType } from '../../../src/NodeHandler'; 7 | import { v } from '../../../src/d'; 8 | import { ProjectorMixin } from '../../../src/mixins/Projector'; 9 | import { WidgetBase } from '../../../src/WidgetBase'; 10 | 11 | const resolvers = createResolvers(); 12 | const bindInstance = new WidgetBase(); 13 | 14 | registerSuite('meta base', { 15 | beforeEach() { 16 | resolvers.stub(); 17 | }, 18 | afterEach() { 19 | resolvers.restore(); 20 | }, 21 | 22 | tests: { 23 | 'has checks nodehandler for nodes'() { 24 | const nodeHandler = new NodeHandler(); 25 | const element = document.createElement('div'); 26 | nodeHandler.add(element, 'foo'); 27 | const meta = new MetaBase({ 28 | invalidate: () => {}, 29 | nodeHandler, 30 | bind: bindInstance 31 | }); 32 | 33 | assert.isTrue(meta.has('foo')); 34 | assert.isFalse(meta.has('bar')); 35 | }, 36 | 'get node returns element from nodehandler'() { 37 | const nodeHandler = new NodeHandler(); 38 | const invalidate = stub(); 39 | const element = document.createElement('div'); 40 | nodeHandler.add(element, 'foo'); 41 | 42 | class MyMeta extends MetaBase { 43 | callGetNode(key: string) { 44 | return this.getNode(key); 45 | } 46 | } 47 | 48 | const meta = new MyMeta({ 49 | invalidate, 50 | nodeHandler, 51 | bind: bindInstance 52 | }); 53 | 54 | const node = meta.callGetNode('foo'); 55 | assert.equal(node, element); 56 | }, 57 | 'Will create event listener for node if not yet loaded'() { 58 | const nodeHandler = new NodeHandler(); 59 | const invalidate = stub(); 60 | const onSpy = spy(nodeHandler, 'on'); 61 | 62 | class MyMeta extends MetaBase { 63 | callGetNode(key: string) { 64 | return this.getNode(key); 65 | } 66 | } 67 | 68 | const meta = new MyMeta({ 69 | invalidate, 70 | nodeHandler, 71 | bind: bindInstance 72 | }); 73 | 74 | meta.callGetNode('foo'); 75 | assert.isTrue(onSpy.calledOnce); 76 | assert.isTrue(onSpy.firstCall.calledWith('foo')); 77 | }, 78 | 'Will call invalidate when awaited node is available'() { 79 | const nodeHandler = new NodeHandler(); 80 | const onSpy = spy(nodeHandler, 'on'); 81 | const invalidate = stub(); 82 | 83 | class MyMeta extends MetaBase { 84 | callGetNode(key: string) { 85 | return this.getNode(key); 86 | } 87 | } 88 | 89 | const meta = new MyMeta({ 90 | invalidate, 91 | nodeHandler, 92 | bind: bindInstance 93 | }); 94 | 95 | meta.callGetNode('foo'); 96 | assert.isTrue(onSpy.calledOnce); 97 | assert.isTrue(onSpy.firstCall.calledWith('foo')); 98 | 99 | const element = document.createElement('div'); 100 | 101 | nodeHandler.add(element, 'foo'); 102 | 103 | resolvers.resolve(); 104 | assert.isTrue(invalidate.calledOnce); 105 | 106 | onSpy.resetHistory(); 107 | meta.callGetNode('foo'); 108 | 109 | assert.isFalse(onSpy.called); 110 | }, 111 | 'Will not add a second callback handle if one already exists'() { 112 | const nodeHandler = new NodeHandler(); 113 | const onSpy = spy(nodeHandler, 'on'); 114 | const invalidate = stub(); 115 | 116 | class MyMeta extends MetaBase { 117 | callGetNode(key: string) { 118 | return this.getNode(key); 119 | } 120 | } 121 | 122 | const meta = new MyMeta({ 123 | invalidate, 124 | nodeHandler, 125 | bind: bindInstance 126 | }); 127 | 128 | meta.callGetNode('foo'); 129 | assert.isTrue(onSpy.calledOnce); 130 | assert.isTrue(onSpy.firstCall.calledWith('foo')); 131 | onSpy.resetHistory(); 132 | meta.callGetNode('foo'); 133 | assert.isTrue(onSpy.notCalled); 134 | assert.isTrue(invalidate.notCalled); 135 | }, 136 | 'invalidate calls passed in invalidate function'() { 137 | const nodeHandler = new NodeHandler(); 138 | const invalidate = stub(); 139 | 140 | class MyMeta extends MetaBase { 141 | callInvalidate() { 142 | this.invalidate(); 143 | } 144 | } 145 | 146 | const meta = new MyMeta({ 147 | invalidate, 148 | nodeHandler, 149 | bind: bindInstance 150 | }); 151 | 152 | meta.callInvalidate(); 153 | resolvers.resolve(); 154 | assert.isTrue(invalidate.calledOnce); 155 | }, 156 | 'integration with single root node'() { 157 | class MyMeta extends MetaBase { 158 | callGetNode(key: string) { 159 | return this.getNode(key); 160 | } 161 | getNodeHandler() { 162 | return this.nodeHandler; 163 | } 164 | } 165 | 166 | class TestWidget extends ProjectorMixin(WidgetBase) { 167 | render() { 168 | return v('div', { key: 'foo' }, [v('div', { key: 'bar' }, ['hello world'])]); 169 | } 170 | 171 | getMeta() { 172 | return this.meta(MyMeta); 173 | } 174 | } 175 | 176 | const widget = new TestWidget(); 177 | const meta = widget.getMeta(); 178 | 179 | const nodeHandler = meta.getNodeHandler(); 180 | const onFoo = stub(); 181 | const onBar = stub(); 182 | const onWidget = stub(); 183 | 184 | nodeHandler.on('foo', onFoo); 185 | nodeHandler.on('bar', onBar); 186 | nodeHandler.on(NodeEventType.Widget, onWidget); 187 | 188 | const div = document.createElement('div'); 189 | widget.append(div); 190 | resolvers.resolve(); 191 | 192 | assert.isTrue(meta.has('foo'), '1'); 193 | assert.isTrue(meta.has('bar'), '2'); 194 | assert.isTrue(onFoo.calledOnce, '3'); 195 | assert.isTrue(onBar.calledOnce, '4'); 196 | assert.isTrue(onWidget.calledOnce, '5'); 197 | assert.isTrue(onFoo.calledBefore(onWidget), '6'); 198 | }, 199 | 'integration with multiple root node'() { 200 | class MyMeta extends MetaBase { 201 | callGetNode(key: string) { 202 | return this.getNode(key); 203 | } 204 | getNodeHandler() { 205 | return this.nodeHandler; 206 | } 207 | } 208 | 209 | class TestWidget extends ProjectorMixin(WidgetBase) { 210 | render() { 211 | return [v('div', { key: 'foo' }), v('div', { key: 'bar' })]; 212 | } 213 | 214 | getMeta() { 215 | return this.meta(MyMeta); 216 | } 217 | } 218 | 219 | const widget = new TestWidget(); 220 | const meta = widget.getMeta(); 221 | 222 | const nodeHandler = meta.getNodeHandler(); 223 | const onFoo = stub(); 224 | const onBar = stub(); 225 | const onWidget = stub(); 226 | 227 | nodeHandler.on('foo', onFoo); 228 | nodeHandler.on('bar', onBar); 229 | nodeHandler.on(NodeEventType.Widget, onWidget); 230 | 231 | const div = document.createElement('div'); 232 | widget.append(div); 233 | resolvers.resolve(); 234 | 235 | assert.isTrue(meta.has('foo')); 236 | assert.isTrue(meta.has('bar')); 237 | assert.isTrue(onFoo.calledOnce); 238 | assert.isTrue(onBar.calledOnce); 239 | assert.isTrue(onWidget.calledOnce); 240 | assert.isTrue(onFoo.calledBefore(onWidget)); 241 | } 242 | } 243 | }); 244 | -------------------------------------------------------------------------------- /tests/unit/mixins/Focus.ts: -------------------------------------------------------------------------------- 1 | const { describe, it } = intern.getInterface('bdd'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import { WidgetBase } from '../../../src/WidgetBase'; 4 | import Focus from '../../../src/mixins/Focus'; 5 | 6 | class Foo extends Focus(WidgetBase) {} 7 | 8 | describe('Focus Mixin', () => { 9 | it('should allow once focus when focus property returns true', () => { 10 | const widget = new Foo(); 11 | widget.__setProperties__({ focus: () => true }); 12 | assert.isTrue(widget.shouldFocus()); 13 | assert.isFalse(widget.shouldFocus()); 14 | widget.focus(); 15 | assert.isTrue(widget.shouldFocus()); 16 | assert.isFalse(widget.shouldFocus()); 17 | }); 18 | 19 | it('should not focus when focus property returns false', () => { 20 | const widget = new Foo(); 21 | widget.__setProperties__({ focus: () => false }); 22 | assert.isFalse(widget.shouldFocus()); 23 | widget.focus(); 24 | assert.isTrue(widget.shouldFocus()); 25 | assert.isFalse(widget.shouldFocus()); 26 | widget.__setProperties__({ focus: () => true }); 27 | assert.isTrue(widget.shouldFocus()); 28 | assert.isFalse(widget.shouldFocus()); 29 | }); 30 | 31 | it('should not focus when is not passed', () => { 32 | const widget = new Foo(); 33 | widget.__setProperties__({}); 34 | assert.isFalse(widget.shouldFocus()); 35 | widget.focus(); 36 | assert.isTrue(widget.shouldFocus()); 37 | assert.isFalse(widget.shouldFocus()); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/unit/mixins/all.ts: -------------------------------------------------------------------------------- 1 | import './Focus'; 2 | import './Themed'; 3 | import './Projector'; 4 | import './I18n'; 5 | -------------------------------------------------------------------------------- /tests/unit/tsx.ts: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import { WidgetBase } from './../../src/WidgetBase'; 4 | import { VNode, WidgetProperties, WNode } from '../../src/interfaces'; 5 | import { tsx, fromRegistry, REGISTRY_ITEM } from '../../src/tsx'; 6 | import { VNODE } from './../../src/d'; 7 | 8 | registerSuite('tsx', { 9 | 'create a registry wrapper'() { 10 | const RegistryWrapper = fromRegistry('tag'); 11 | assert.strictEqual((RegistryWrapper as any).type, REGISTRY_ITEM); 12 | const registryWrapper = new RegistryWrapper(); 13 | assert.strictEqual(registryWrapper.name, 'tag'); 14 | // These will always be undefined but show the type inference of properties. 15 | registryWrapper.properties = {}; 16 | assert.isUndefined(registryWrapper.properties.key); 17 | }, 18 | tsx: { 19 | 'tsx generate a VNode'() { 20 | const node: VNode = tsx('div', { hello: 'world' }, ['child']) as VNode; 21 | assert.deepEqual(node.tag, 'div'); 22 | assert.deepEqual(node.properties, { hello: 'world' }); 23 | assert.deepEqual(node.children, ['child']); 24 | assert.strictEqual(node.type, VNODE); 25 | }, 26 | 'tsx generate a WNode'() { 27 | const node: WNode = tsx(WidgetBase, { hello: 'world' }, ['child']) as WNode; 28 | assert.deepEqual(node.widgetConstructor, WidgetBase); 29 | assert.deepEqual(node.properties, { hello: 'world' } as any); 30 | assert.deepEqual(node.children, ['child']); 31 | }, 32 | 'tsx generate a WNode from a RegistryWrapper'() { 33 | const RegistryWrapper = fromRegistry('tag'); 34 | const node: WNode = tsx(RegistryWrapper, { hello: 'world' }, ['child']) as WNode; 35 | assert.deepEqual(node.widgetConstructor, 'tag'); 36 | assert.deepEqual(node.properties, { hello: 'world' } as any); 37 | assert.deepEqual(node.children, ['child']); 38 | }, 39 | 'children arrays are spread correctly'() { 40 | const node: VNode = tsx('div', { hello: 'world' }, ['child', ['child-2', ['child-3']]]) as VNode; 41 | assert.deepEqual(node.tag, 'div'); 42 | assert.deepEqual(node.properties, { hello: 'world' }); 43 | assert.deepEqual(node.children, ['child', 'child-2', 'child-3']); 44 | assert.strictEqual(node.type, VNODE); 45 | }, 46 | 'defaults properties to empty object'() { 47 | const node: VNode = tsx('div') as VNode; 48 | assert.deepEqual(node.tag, 'div'); 49 | assert.deepEqual(node.properties, {}); 50 | assert.deepEqual(node.children, []); 51 | assert.strictEqual(node.type, VNODE); 52 | }, 53 | 'defaults `null` properties to empty object'() { 54 | const node: VNode = tsx('div', null as any) as VNode; 55 | assert.deepEqual(node.tag, 'div'); 56 | assert.deepEqual(node.properties, {}); 57 | assert.deepEqual(node.children, []); 58 | assert.strictEqual(node.type, VNODE); 59 | } 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /tests/unit/tsxIntegration.tsx: -------------------------------------------------------------------------------- 1 | const { registerSuite } = intern.getInterface('object'); 2 | const { assert } = intern.getPlugin('chai'); 3 | import { WidgetBase } from '../../src/WidgetBase'; 4 | import { Registry } from '../../src/Registry'; 5 | import { WidgetProperties, WNode } from '../../src/interfaces'; 6 | import { tsx, fromRegistry } from './../../src/tsx'; 7 | 8 | const registry = new Registry(); 9 | 10 | registerSuite('tsx integration', { 11 | 'can use tsx'() { 12 | interface FooProperties extends WidgetProperties { 13 | hello: string; 14 | } 15 | class Foo extends WidgetBase { 16 | render() { 17 | const { hello } = this.properties; 18 | return ( 19 |
20 |
{hello}
21 |
22 | ); 23 | } 24 | } 25 | class Bar extends WidgetBase { 26 | render() { 27 | return ; 28 | } 29 | } 30 | 31 | class Qux extends WidgetBase { 32 | render() { 33 | const LazyFoo = fromRegistry('LazyFoo'); 34 | return ; 35 | } 36 | } 37 | 38 | const bar = new Bar(); 39 | bar.__setCoreProperties__({ bind: bar, baseRegistry: registry }); 40 | bar.__setProperties__({ registry }); 41 | const barRender = bar.__render__() as WNode; 42 | assert.deepEqual(barRender.properties, { hello: 'world' } as any); 43 | assert.strictEqual(barRender.widgetConstructor, Foo); 44 | assert.lengthOf(barRender.children, 0); 45 | 46 | const qux = new Qux(); 47 | qux.__setCoreProperties__({ bind: qux, baseRegistry: registry }); 48 | qux.__setProperties__({ registry }); 49 | const firstQuxRender = qux.__render__() as WNode; 50 | assert.strictEqual(firstQuxRender.widgetConstructor, 'LazyFoo'); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /tests/unit/waitFor.ts: -------------------------------------------------------------------------------- 1 | export async function waitFor( 2 | callback: () => boolean, 3 | message: string = 'timed out waiting for something to happen', 4 | timeout = 1000 5 | ) { 6 | const startTime = new Date().valueOf() / 1000; 7 | 8 | return new Promise((resolve, reject) => { 9 | function check() { 10 | const now = new Date().valueOf() / 1000; 11 | 12 | if (now - startTime > timeout) { 13 | reject(new Error(message)); 14 | return; 15 | } 16 | 17 | if (callback()) { 18 | resolve(); 19 | } else { 20 | setTimeout(check, 10); 21 | } 22 | } 23 | 24 | check(); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1.5", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "jsx": "react", 8 | "jsxFactory": "tsx", 9 | "lib": [ 10 | "dom", 11 | "es5", 12 | "es2015.iterable", 13 | "es2015.promise", 14 | "es2015.symbol", 15 | "es2015.symbol.wellknown" 16 | ], 17 | "module": "umd", 18 | "moduleResolution": "node", 19 | "noUnusedLocals": true, 20 | "importHelpers": true, 21 | "downlevelIteration": true, 22 | "outDir": "_build/", 23 | "removeComments": false, 24 | "sourceMap": true, 25 | "strict": true, 26 | "target": "es5", 27 | "types": [ "intern", "web-animations-js" ] 28 | }, 29 | "include": [ 30 | "./src/**/*.ts", 31 | "./src/**/*.tsx", 32 | "./tests/**/*.ts", 33 | "./tests/**/*.tsx" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": false, 4 | "ban": [], 5 | "class-name": true, 6 | "comment-format": [ true, "check-space" ], 7 | "curly": true, 8 | "eofline": true, 9 | "forin": false, 10 | "indent": [ true, "tabs" ], 11 | "interface-name": [ true, "never-prefix" ], 12 | "jsdoc-format": true, 13 | "label-position": true, 14 | "max-line-length": 120, 15 | "member-access": false, 16 | "member-ordering": false, 17 | "no-any": false, 18 | "no-arg": true, 19 | "no-bitwise": false, 20 | "no-consecutive-blank-lines": true, 21 | "no-console": false, 22 | "no-construct": false, 23 | "no-debugger": true, 24 | "no-duplicate-variable": true, 25 | "no-empty": false, 26 | "no-eval": true, 27 | "no-inferrable-types": [ true, "ignore-params" ], 28 | "no-shadowed-variable": false, 29 | "no-string-literal": false, 30 | "no-switch-case-fall-through": false, 31 | "no-trailing-whitespace": true, 32 | "no-unused-expression": false, 33 | "no-use-before-declare": false, 34 | "no-var-keyword": true, 35 | "no-var-requires": false, 36 | "object-literal-sort-keys": false, 37 | "one-line": [ true, "check-open-brace", "check-whitespace" ], 38 | "radix": true, 39 | "trailing-comma": [ true, { 40 | "multiline": "never", 41 | "singleline": "never" 42 | } ], 43 | "triple-equals": [ true, "allow-null-check" ], 44 | "typedef": false, 45 | "typedef-whitespace": [ true, { 46 | "call-signature": "nospace", 47 | "index-signature": "nospace", 48 | "parameter": "nospace", 49 | "property-declaration": "nospace", 50 | "variable-declaration": "nospace" 51 | }, { 52 | "call-signature": "onespace", 53 | "index-signature": "onespace", 54 | "parameter": "onespace", 55 | "property-declaration": "onespace", 56 | "variable-declaration": "onespace" 57 | } ], 58 | "variable-name": [ true, "check-format", "allow-leading-underscore", "ban-keywords", "allow-pascal-case" ] 59 | } 60 | } 61 | --------------------------------------------------------------------------------