├── .config ├── karma.conf.js ├── tsconfig.commonjs.json └── tsconfig.es2015.json ├── .coveralls.yml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── package.json ├── src ├── dom-driver │ ├── DomSources │ │ ├── ElementDomSource.ts │ │ ├── EventDelegator.ts │ │ ├── MotorcycleDomSource.ts │ │ ├── common.ts │ │ ├── createEventStream.ts │ │ ├── elementMap.ts │ │ ├── index.ts │ │ ├── isInScope.ts │ │ ├── namespaceParsers.ts │ │ └── shouldUseCapture.ts │ ├── api-wrappers │ │ ├── elements.ts │ │ ├── events.ts │ │ ├── index.ts │ │ ├── query.ts │ │ └── useCapture.ts │ ├── index.ts │ ├── makeDomDriver.ts │ ├── mockDomSource.ts │ └── vNodeWrapper.ts ├── index.ts ├── modules │ ├── IsolateModule.ts │ ├── attributes.ts │ ├── class.ts │ ├── dataset.ts │ ├── hero.ts │ ├── index.ts │ ├── props.ts │ └── style.ts ├── types │ ├── DomSource.ts │ ├── Events.ts │ ├── index.ts │ ├── tagNames.ts │ └── virtual-dom.ts └── virtual-dom │ ├── MotorcycleVNode.ts │ ├── helpers │ ├── h.ts │ ├── hasCssSelector.ts │ ├── hyperscript.ts │ ├── index.ts │ └── svg.ts │ ├── htmldomapi.ts │ ├── index.ts │ ├── init.ts │ ├── is.ts │ └── util.ts ├── test ├── driver │ ├── MotorcycleDomSource.ts │ ├── integration │ │ ├── events.ts │ │ ├── isolation.ts │ │ └── rendering.ts │ ├── issue-105.ts │ ├── mockDomSource.ts │ └── vNodeWrapper.ts ├── helpers │ ├── createRenderTarget.ts │ ├── fake-raf.ts │ ├── interval.ts │ └── shuffle.ts ├── modules │ ├── IsolateModule.ts │ ├── dataset.ts │ └── style.ts └── virtual-dom │ ├── core.ts │ ├── hasCssSelector.ts │ └── parseSelector.ts ├── tsconfig.json └── tslint.json /.config/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | const options = 3 | { 4 | basePath: '../', 5 | 6 | files: [ 7 | 'src/**/*.ts', 8 | 'test/**/*.ts', 9 | ], 10 | 11 | frameworks: [ 12 | 'mocha', 13 | 'karma-typescript', 14 | ], 15 | 16 | preprocessors: { 17 | '**/*.ts': ['karma-typescript'], 18 | }, 19 | 20 | reporters: [], 21 | 22 | customLaunchers: { 23 | Chrome_travis_ci: { 24 | base: 'Chrome', 25 | flags: ['--no-sandbox'] 26 | } 27 | }, 28 | 29 | browsers: [], 30 | 31 | coverageReporter: { 32 | type: 'lcov', 33 | dir: 'coverage' 34 | }, 35 | 36 | karmaTypescriptConfig: { 37 | tsconfig: 'tsconfig.json', 38 | reports: { 39 | "html": "coverage", 40 | "lcovonly": "coverage", 41 | } 42 | } 43 | } 44 | 45 | if (process.env.UNIT) 46 | options.browsers.push('Chrome') 47 | 48 | if (process.env.TRAVIS) { 49 | options.browsers.push('Chrome_travis_ci', 'Firefox') 50 | options.reporters.push('coverage', 'coveralls') 51 | } 52 | 53 | if (options.browsers.length === 0) 54 | options.browsers.push('Chrome', 'Firefox') 55 | 56 | if (options.reporters.length === 0) 57 | options.reports.push('progress', 'karma-typescript') 58 | 59 | config.set(options); 60 | } 61 | -------------------------------------------------------------------------------- /.config/tsconfig.commonjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": true, 5 | "outDir": "../lib/commonjs", 6 | "types": [] 7 | }, 8 | "include": [ 9 | "../src/index.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.config/tsconfig.es2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es2015", 5 | "noImplicitAny": true, 6 | "outDir": "../lib/es2015", 7 | "types": [] 8 | }, 9 | "include": [ 10 | "../src/index.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: ekDEGXyAyXcFPiAfMX7lCfXRYHGehle0q 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 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 | insert_final_newline = false 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | **Code to reproduce the issue:** 8 | 9 | 10 | **Expected behavior:** 11 | 12 | 13 | **Actual behavior:** 14 | 15 | 16 | **Versions of packages used:** 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | - [ ] I added new tests for the issue I fixed or the feature I built 7 | - [ ] I ran `npm test` for the package I'm modifying 8 | - [ ] I used `npm run commit` instead of `git commit` 9 | 10 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # generated files 41 | lib 42 | .tmp 43 | test/bundle.js 44 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/motorcyclejs/dom/4892a8623eaef657064b6a284f8bfd7aef57c02b/.npmignore -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | dist: trusty 4 | sudo: false 5 | 6 | addons: 7 | firefox: latest 8 | 9 | node_js: 10 | - 7 11 | 12 | before_install: 13 | - export CHROME_BIN=chromium-browser 14 | - export DISPLAY=:99.0 15 | - sh -e /etc/init.d/xvfb start 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.fontFamily": "'Fira Code Medium', 'Courier New', monospace, 'Droid Sans Fallback'", 3 | 4 | "editor.fontLigatures": true, 5 | 6 | "editor.rulers": [80, 160], 7 | 8 | "editor.wordWrap": "wordWrapColumn", 9 | 10 | "editor.wordWrapColumn": 120, 11 | 12 | "editor.wrappingIndent": "indent", 13 | 14 | "editor.tabSize": 2, 15 | 16 | "editor.insertSpaces": true, 17 | 18 | "editor.formatOnType": true, 19 | 20 | "typescript.tsdk": "node_modules/typescript/lib" 21 | } 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [6.7.0](https://github.com/motorcyclejs/dom/compare/v6.6.0...v6.7.0) (2016-12-21) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * **MotorcycleDomSource:** fix selecting of elements with no prior select() ([#121](https://github.com/motorcyclejs/dom/issues/121)) ([f548609](https://github.com/motorcyclejs/dom/commit/f548609)) 8 | 9 | 10 | 11 | 12 | # [6.6.0](https://github.com/motorcyclejs/dom/compare/v6.5.0...v6.6.0) (2016-12-20) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **MotorcycleDomSource:** correctly simulate events ([#118](https://github.com/motorcyclejs/dom/issues/118)) ([9bd15be](https://github.com/motorcyclejs/dom/commit/9bd15be)) 18 | 19 | 20 | 21 | 22 | # [6.5.0](https://github.com/motorcyclejs/dom/compare/v6.4.0...v6.5.0) (2016-12-20) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **events:** fix event delegation ([#117](https://github.com/motorcyclejs/dom/issues/117)) ([10f6fb4](https://github.com/motorcyclejs/dom/commit/10f6fb4)) 28 | 29 | 30 | 31 | 32 | # [6.4.0](https://github.com/motorcyclejs/dom/compare/v6.3.0...v6.4.0) (2016-12-20) 33 | 34 | 35 | ### Features 36 | 37 | * **api-wrappers:** implement functional api wrappers for DomSource ([9ad262b](https://github.com/motorcyclejs/dom/commit/9ad262b)) 38 | 39 | 40 | 41 | 42 | # [6.3.0](https://github.com/motorcyclejs/dom/compare/v6.2.0...v6.3.0) (2016-12-20) 43 | 44 | 45 | ### Features 46 | 47 | * **hasCssSelector:** implement hasCssSelector function ([05cc55f](https://github.com/motorcyclejs/dom/commit/05cc55f)) 48 | 49 | 50 | 51 | 52 | # [6.2.0](https://github.com/motorcyclejs/dom/compare/v6.1.0...v6.2.0) (2016-12-14) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * **virtual-dom:** fix test node patching errors ([#110](https://github.com/motorcyclejs/dom/issues/110)) ([68f1a41](https://github.com/motorcyclejs/dom/commit/68f1a41)) 58 | 59 | 60 | 61 | 62 | # [6.1.0](https://github.com/motorcyclejs/dom/compare/v6.0.0...v6.1.0) (2016-12-13) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * **hyperscript:** include missing img tag ([3cd90fb](https://github.com/motorcyclejs/dom/commit/3cd90fb)) 68 | 69 | 70 | 71 | 72 | # [6.0.0](https://github.com/motorcyclejs/dom/compare/v5.0.0...v6.0.0) (2016-12-09) 73 | 74 | 75 | ### Features 76 | 77 | * **dom:** complete reimplementation of dom driver on top of snabbdom fork ([#108](https://github.com/motorcyclejs/dom/issues/108)) ([2ae7b8b](https://github.com/motorcyclejs/dom/commit/2ae7b8b)) 78 | 79 | 80 | ### BREAKING CHANGES 81 | 82 | * dom: VNode shape no longer has .sel, but .tagName, .className, and .id. 83 | Events are no longer mutated to point to a different currentTarget. 84 | Parent elements will receive non-bubbling events originating from child elements. 85 | 86 | 87 | 88 | 89 | # [5.0.0](https://github.com/motorcyclejs/dom/compare/v4.2.0...v5.0.0) (2016-12-01) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * **MainDomSource:** fix incorrect usage of `this` ([cfb6eed](https://github.com/motorcyclejs/dom/commit/cfb6eed)) 95 | 96 | 97 | ### Features 98 | 99 | * **driver:** remove transposition and refactor ([c04bf15](https://github.com/motorcyclejs/dom/commit/c04bf15)) 100 | * **makeHTMLDriver:** remove html driver ([91aaa92](https://github.com/motorcyclejs/dom/commit/91aaa92)) 101 | 102 | 103 | ### BREAKING CHANGES 104 | 105 | * driver: removed transposition, remove makeHTMLDriver entirely. Rename 106 | makeDOMDriver to makeDomDriver. 107 | 108 | 109 | 110 | 111 | # [4.2.0](https://github.com/motorcyclejs/dom/compare/v4.1.0...v4.2.0) (2016-11-19) 112 | 113 | 114 | ### Bug Fixes 115 | 116 | * **EventDelegator:** fix obscure cases where events are passed multiple times ([f2171e6](https://github.com/motorcyclejs/dom/commit/f2171e6)) 117 | * **EventDelegator:** make destination to mimic bubbling ([75ae10e](https://github.com/motorcyclejs/dom/commit/75ae10e)) 118 | * **EventDelegator:** make sure destinations match ([f64a63b](https://github.com/motorcyclejs/dom/commit/f64a63b)) 119 | 120 | 121 | 122 | 123 | # [4.1.0](https://github.com/motorcyclejs/dom/compare/v4.0.0...v4.1.0) (2016-11-19) 124 | 125 | 126 | ### Bug Fixes 127 | 128 | * **events:** fix subtle bug ([c909727](https://github.com/motorcyclejs/dom/commit/c909727)) 129 | 130 | 131 | 132 | 133 | # [4.0.0](https://github.com/motorcyclejs/dom/compare/v3.3.0...v4.0.0) (2016-11-19) 134 | 135 | 136 | ### Features 137 | 138 | * **classes:** use classes module to avoid extra rerendering ([15bc632](https://github.com/motorcyclejs/dom/commit/15bc632)) 139 | 140 | 141 | 142 | 143 | # [3.3.0](https://github.com/motorcyclejs/dom/compare/v3.2.0...v3.3.0) (2016-11-17) 144 | 145 | 146 | 147 | 148 | # [3.2.0](https://github.com/motorcyclejs/dom/compare/v3.1.0...v3.2.0) (2016-11-16) 149 | 150 | 151 | ### Bug Fixes 152 | 153 | * **ElementFinder:** use local matchesSelector function ([8d4085a](https://github.com/motorcyclejs/dom/commit/8d4085a)) 154 | * **EventDelegator:** update to use local matchesSelector ([b381c35](https://github.com/motorcyclejs/dom/commit/b381c35)) 155 | 156 | 157 | ### Features 158 | 159 | * **DOMSource:** add document window and body dom sources ([c989dfa](https://github.com/motorcyclejs/dom/commit/c989dfa)) 160 | 161 | 162 | 163 | 164 | # [3.1.0](https://github.com/motorcyclejs/dom/compare/v3.0.0...v3.1.0) (2016-11-14) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * **dom:** fix errors around most.js Stream instances ([1a2ae9d](https://github.com/motorcyclejs/dom/commit/1a2ae9d)) 170 | * **DOM:** fix for failing tests ([b32fe5e](https://github.com/motorcyclejs/dom/commit/b32fe5e)) 171 | 172 | 173 | ### Features 174 | 175 | * **DOMSource:** have interface for DOMSource for other sources to implement ([8a428f2](https://github.com/motorcyclejs/dom/commit/8a428f2)) 176 | 177 | 178 | 179 | 180 | # [3.0.0](https://github.com/motorcyclejs/dom/compare/v2.0.1...v3.0.0) (2016-08-14) 181 | 182 | 183 | ### Features 184 | 185 | * **dom:** rewrite in TypeScript ([baa2588](https://github.com/motorcyclejs/dom/commit/baa2588)) 186 | 187 | 188 | ### BREAKING CHANGES 189 | 190 | * dom: before: DOMSource.elements -> Stream 191 | 192 | after: DOMSource.elements() -> Stream 193 | 194 | 195 | 196 | 197 | ## [2.0.1](https://github.com/motorcyclejs/dom/compare/v2.0.0...v2.0.1) (2016-06-14) 198 | 199 | 200 | ### Bug Fixes 201 | 202 | * **DOMSource:** fix typo in dispose ([c39dad9](https://github.com/motorcyclejs/dom/commit/c39dad9)) 203 | * **isolate:** fix isolate import in tests ([90505a7](https://github.com/motorcyclejs/dom/commit/90505a7)) 204 | 205 | 206 | 207 | 208 | # [2.0.0](https://github.com/motorcyclejs/dom/compare/v1.4.0...v2.0.0) (2016-05-17) 209 | 210 | 211 | ### Reverts 212 | 213 | * **release:** undo poorly done release ([29a8e9c](https://github.com/motorcyclejs/dom/commit/29a8e9c)) 214 | * **release:** undo poorly done release v2 ([e857fb0](https://github.com/motorcyclejs/dom/commit/e857fb0)) 215 | 216 | 217 | 218 | 219 | # [1.4.0](https://github.com/motorcyclejs/dom/compare/v1.3.0...v1.4.0) (2016-03-30) 220 | 221 | 222 | ### Bug Fixes 223 | 224 | * dataset module has not yet been publised to npm ([07e4b47](https://github.com/motorcyclejs/dom/commit/07e4b47)) 225 | * **issue-89:** hopefully help fix fiddly test ([2eb6afb](https://github.com/motorcyclejs/dom/commit/2eb6afb)) 226 | 227 | 228 | ### Features 229 | 230 | * **mockDOMSource:** update to allow for multiple .select()s ([9a47a30](https://github.com/motorcyclejs/dom/commit/9a47a30)) 231 | * **modules:** remove local version of modules in favor of fixed snabbdom versions ([c1864b2](https://github.com/motorcyclejs/dom/commit/c1864b2)) 232 | 233 | 234 | 235 | 236 | # [1.3.0](https://github.com/motorcyclejs/dom/compare/v1.2.1...v1.3.0) (2016-03-15) 237 | 238 | 239 | ### Features 240 | 241 | * add new event types that don't bubble ([e62092e](https://github.com/motorcyclejs/dom/commit/e62092e)) 242 | * **makeDOMDriver:** add option to specify your own error handling function ([80717f8](https://github.com/motorcyclejs/dom/commit/80717f8)) 243 | 244 | 245 | 246 | 247 | ## [1.2.1](https://github.com/motorcyclejs/dom/compare/v1.2.0...v1.2.1) (2016-02-23) 248 | 249 | 250 | ### Bug Fixes 251 | 252 | * **select:** adjust select() semantics to match more css selectors properly ([362cab6](https://github.com/motorcyclejs/dom/commit/362cab6)), closes [#80](https://github.com/motorcyclejs/dom/issues/80) 253 | 254 | 255 | 256 | 257 | # [1.2.0](https://github.com/motorcyclejs/dom/compare/v1.1.0...v1.2.0) (2016-02-19) 258 | 259 | 260 | ### Bug Fixes 261 | 262 | * fix all failing tests of new test suite ([7107cb8](https://github.com/motorcyclejs/dom/commit/7107cb8)) 263 | 264 | 265 | 266 | 267 | # [1.1.0](https://github.com/motorcyclejs/dom/compare/v1.0.3...v1.1.0) (2016-02-07) 268 | 269 | 270 | ### Features 271 | 272 | * update event-delegation model ([2543bea](https://github.com/motorcyclejs/dom/commit/2543bea)), closes [#68](https://github.com/motorcyclejs/dom/issues/68) 273 | * **events:** use [@most](https://github.com/most)/dom-event instead of local fromEvent ([daec57d](https://github.com/motorcyclejs/dom/commit/daec57d)), closes [#69](https://github.com/motorcyclejs/dom/issues/69) 274 | 275 | 276 | 277 | 278 | ## [1.0.3](https://github.com/motorcyclejs/dom/compare/v1.0.2...v1.0.3) (2015-12-30) 279 | 280 | 281 | 282 | 283 | ## [1.0.2](https://github.com/motorcyclejs/dom/compare/v1.0.1...v1.0.2) (2015-12-30) 284 | 285 | 286 | ### Bug Fixes 287 | 288 | * polyfill raf for snabbom ([eb17a5d](https://github.com/motorcyclejs/dom/commit/eb17a5d)) 289 | 290 | 291 | 292 | 293 | ## [1.0.1](https://github.com/motorcyclejs/dom/compare/v1.0.0...v1.0.1) (2015-12-30) 294 | 295 | 296 | 297 | 298 | # [1.0.0](https://github.com/motorcyclejs/dom/compare/v0.7.0...v1.0.0) (2015-12-30) 299 | 300 | 301 | ### Bug Fixes 302 | 303 | * fix makeDomDriver import ([1f6347c](https://github.com/motorcyclejs/dom/commit/1f6347c)) 304 | * remove unneeded test ([aef055d](https://github.com/motorcyclejs/dom/commit/aef055d)) 305 | * rename `sink.type` to `sink.event` ([34d9705](https://github.com/motorcyclejs/dom/commit/34d9705)) 306 | * **events:** use standard event.target ([5c8b231](https://github.com/motorcyclejs/dom/commit/5c8b231)) 307 | * **isolate:** update isolation semantics ([08b69f0](https://github.com/motorcyclejs/dom/commit/08b69f0)) 308 | * **select:** fix isolateSource and isolateSink ([06bb35d](https://github.com/motorcyclejs/dom/commit/06bb35d)) 309 | * **test:** fix usage errors ([4537205](https://github.com/motorcyclejs/dom/commit/4537205)) 310 | * **test:** remove unused sinon import ([7a34933](https://github.com/motorcyclejs/dom/commit/7a34933)) 311 | * **thunks:** check for data.vnode ([21e5f57](https://github.com/motorcyclejs/dom/commit/21e5f57)) 312 | * **vTreeParser:** ignore previous child observable's value ([b788e88](https://github.com/motorcyclejs/dom/commit/b788e88)), closes [#46](https://github.com/motorcyclejs/dom/issues/46) 313 | 314 | 315 | ### Code Refactoring 316 | 317 | * change `makeDomDriver` to `makeDOMDriver` ([b30c209](https://github.com/motorcyclejs/dom/commit/b30c209)), closes [#51](https://github.com/motorcyclejs/dom/issues/51) 318 | 319 | 320 | ### Features 321 | 322 | * **dom-driver:** reuse event listeners ([1a93973](https://github.com/motorcyclejs/dom/commit/1a93973)) 323 | * **events:** avoid recreating the same eventListener ([56cad78](https://github.com/motorcyclejs/dom/commit/56cad78)) 324 | * **events:** Switch to event delegation ([4c9ff0f](https://github.com/motorcyclejs/dom/commit/4c9ff0f)) 325 | * **fromEvent:** handle single DOM Nodes ([a8bd6fa](https://github.com/motorcyclejs/dom/commit/a8bd6fa)) 326 | * **isolate:** add multicast ([db6c6f4](https://github.com/motorcyclejs/dom/commit/db6c6f4)) 327 | * **makeDOMDriver:** pass a stream of the rootElem to makeElementSelector ([17cb9d9](https://github.com/motorcyclejs/dom/commit/17cb9d9)) 328 | * **makeDOMDriver:** switch to options object ([33fc153](https://github.com/motorcyclejs/dom/commit/33fc153)), closes [#57](https://github.com/motorcyclejs/dom/issues/57) 329 | * **makeDOMDriver:** throw error if modules is not an array ([11f2e35](https://github.com/motorcyclejs/dom/commit/11f2e35)) 330 | * **select:** rewrite DOM.select with snabbdom-selector ([8b231e4](https://github.com/motorcyclejs/dom/commit/8b231e4)) 331 | * **select:** use event delegation ([770541e](https://github.com/motorcyclejs/dom/commit/770541e)) 332 | * **thunk:** export thunk by default ([2e43834](https://github.com/motorcyclejs/dom/commit/2e43834)) 333 | * **vTreeParser:** Add support for a static vTree option ([89e2ba1](https://github.com/motorcyclejs/dom/commit/89e2ba1)), closes [#59](https://github.com/motorcyclejs/dom/issues/59) 334 | * **wrapVnode:** wrap top-evel vnode ([dbbca44](https://github.com/motorcyclejs/dom/commit/dbbca44)), closes [#8](https://github.com/motorcyclejs/dom/issues/8) 335 | 336 | 337 | ### BREAKING CHANGES 338 | 339 | * before: 340 | import {makeDomDriver} from '@motorcycle/dom' 341 | 342 | after: 343 | import {makeDOMDriver} from '@motorcyce/core' 344 | * wrapVnode: Before: 345 | Patching: h('h1', {}, 'Hello') 346 | to:
347 | rendered:

Hello

348 | 349 | After: 350 | Patching: h('h1', {}, 'Hello') 351 | to:
352 | renders:
367 | # [0.7.0](https://github.com/motorcyclejs/dom/compare/v0.6.1...v0.7.0) (2015-12-11) 368 | 369 | 370 | ### Bug Fixes 371 | 372 | * **isolate:** fix adding of rendundant className ([e78e90f](https://github.com/motorcyclejs/dom/commit/e78e90f)) 373 | * **node:** Fix importing on node ([a843791](https://github.com/motorcyclejs/dom/commit/a843791)), closes [#21](https://github.com/motorcyclejs/dom/issues/21) 374 | * **rootElem$:** revert rootElem$ to previous behavior ([09704ce](https://github.com/motorcyclejs/dom/commit/09704ce)) 375 | 376 | 377 | ### Features 378 | 379 | * assume NodeList ([503652d](https://github.com/motorcyclejs/dom/commit/503652d)), closes [#17](https://github.com/motorcyclejs/dom/issues/17) 380 | * use new fromEvent() semantics ([99be9d2](https://github.com/motorcyclejs/dom/commit/99be9d2)), closes [#17](https://github.com/motorcyclejs/dom/issues/17) 381 | * **fromEvent:** add check for NodeList ([0801233](https://github.com/motorcyclejs/dom/commit/0801233)) 382 | 383 | 384 | ### Performance Improvements 385 | 386 | * Remove Array.prototype.slice.call ([31ad84f](https://github.com/motorcyclejs/dom/commit/31ad84f)) 387 | * **isolate:** remove unneeded .trim() ([2f31c85](https://github.com/motorcyclejs/dom/commit/2f31c85)) 388 | 389 | 390 | 391 | 392 | ## [0.6.1](https://github.com/motorcyclejs/dom/compare/v0.6.0...v0.6.1) (2015-11-22) 393 | 394 | 395 | 396 | 397 | # [0.6.0](https://github.com/motorcyclejs/dom/compare/v0.5.2...v0.6.0) (2015-11-22) 398 | 399 | 400 | 401 | 402 | ## [0.5.2](https://github.com/motorcyclejs/dom/compare/v0.5.1...v0.5.2) (2015-11-20) 403 | 404 | 405 | 406 | 407 | ## [0.5.1](https://github.com/motorcyclejs/dom/compare/v0.5.0...v0.5.1) (2015-11-20) 408 | 409 | 410 | ### Features 411 | 412 | * **auto-scope:** Implement auto-scoping ([6d5d9cd](https://github.com/motorcyclejs/dom/commit/6d5d9cd)) 413 | 414 | 415 | 416 | 417 | # [0.5.0](https://github.com/motorcyclejs/dom/compare/v0.4.1...v0.5.0) (2015-11-16) 418 | 419 | 420 | 421 | 422 | ## [0.4.1](https://github.com/motorcyclejs/dom/compare/v0.4.0...v0.4.1) (2015-11-14) 423 | 424 | 425 | 426 | 427 | # [0.4.0](https://github.com/motorcyclejs/dom/compare/v0.3.2...v0.4.0) (2015-11-13) 428 | 429 | 430 | 431 | 432 | ## [0.3.2](https://github.com/motorcyclejs/dom/compare/v0.3.1...v0.3.2) (2015-11-11) 433 | 434 | 435 | 436 | 437 | ## [0.3.1](https://github.com/motorcyclejs/dom/compare/v0.3.0...v0.3.1) (2015-11-11) 438 | 439 | 440 | 441 | 442 | # [0.3.0](https://github.com/motorcyclejs/dom/compare/v0.2.0...v0.3.0) (2015-11-11) 443 | 444 | 445 | 446 | 447 | # [0.2.0](https://github.com/motorcyclejs/dom/compare/v0.1.5...v0.2.0) (2015-11-11) 448 | 449 | 450 | 451 | 452 | ## [0.1.5](https://github.com/motorcyclejs/dom/compare/v0.1.4...v0.1.5) (2015-11-10) 453 | 454 | 455 | 456 | 457 | ## [0.1.4](https://github.com/motorcyclejs/dom/compare/v0.1.3...v0.1.4) (2015-11-10) 458 | 459 | 460 | 461 | 462 | ## [0.1.3](https://github.com/motorcyclejs/dom/compare/v0.1.2...v0.1.3) (2015-11-09) 463 | 464 | 465 | 466 | 467 | ## [0.1.2](https://github.com/motorcyclejs/dom/compare/v0.1.1...v0.1.2) (2015-11-09) 468 | 469 | 470 | 471 | 472 | ## [0.1.1](https://github.com/motorcyclejs/dom/compare/v0.1.0...v0.1.1) (2015-11-09) 473 | 474 | 475 | 476 | 477 | # 0.1.0 (2015-11-01) 478 | 479 | 480 | 481 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, thank you so much, we need your help. 4 | 5 | ## Contributing a fix or feature 6 | 7 | 1. Fork the repository 8 | 2. Switch to a new branch `git checkout -b [branchName]` 9 | 3. Produce your fix or feature 10 | 4. Use `npm run commit` instead of `git commit` PLEASE! 11 | 5. Submit a pull request for review 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 TylorS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @motorcycle/dom 2 | 3 | > Standard DOM Driver for Motorcycle.js 4 | 5 | A Driver for Motorcycle.js built to interact with the DOM. 6 | 7 | **DEPRECATED!** Please use [the newer Motorcycle.js](https://github.com/motorcyclejs/motorcyclejs) 8 | 9 | ## Let me have it! 10 | ```sh 11 | npm install --save @motorcycle/dom 12 | ``` 13 | 14 | ## Polyfills 15 | 16 | Internally this driver makes direct use of ES2015 `Map`, if you plan to support 17 | browser that do not natively support these features a polyfill will need to be 18 | used. 19 | 20 | # API 21 | 22 | - [`makeDomDriver`](#makeDomDriver) 23 | - [`mockDomSource`](#mockDomSource) 24 | - [`h`](#h) 25 | - [`hasCssSelector`](#hasCssSelector) 26 | - [`API Wrappers`](#api-wrappers) 27 | 28 | ### `makeDomDriver(container, options)` 29 | 30 | A factory for the DOM driver function. 31 | 32 | Takes a `container` to define the target on the existing DOM which this 33 | driver will operate on, and an `options` object as the second argument. The 34 | input to this driver is a stream of virtual DOM objects, or in other words, 35 | "VNode" objects. The output of this driver is a "DomSource": a 36 | collection of streams queried with the methods `select()` and `events()`. 37 | 38 | `DomSource.select(selector)` returns a new DomSource with scope restricted to 39 | the element(s) that matches the CSS `selector` given. 40 | 41 | `DomSource.events(eventType, options)` returns a stream of events of 42 | `eventType` happening on the elements that match the current DOMSource. The 43 | event object contains the `ownerTarget` property that behaves exactly like 44 | `currentTarget`. The reason for this is that some browsers doesn't allow 45 | `currentTarget` property to be mutated, hence a new property is created. The 46 | returned stream is a most.js Stream. The `options` parameter can have the 47 | property `useCapture`, which is by default `false`, except it is `true` for 48 | event types that do not bubble. Read more here 49 | https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener 50 | about the `useCapture` and its purpose. 51 | 52 | `DomSource.elements()` returns a stream of the DOM elements matched by the 53 | selectors in the DOMSource. Also, `DomSource.select(':root').elements()` 54 | returns a stream of DOM element corresponding to the root (or container) of 55 | the app on the DOM. 56 | 57 | #### Arguments: 58 | 59 | - `container: HTMLElement` the DOM selector for the element (or the element itself) to contain the rendering of the VTrees. 60 | - `options: DomDriverOptions` an object with two optional properties: 61 | - `modules: array` overrides `@motorcycle/dom`'s default virtual-dom modules as 62 | as defined in [`src/modules`](./src/modules). 63 | 64 | #### Return: 65 | 66 | *(Function)* the DOM driver function. The function expects a stream of VNode as input, and outputs the DOMSource object. 67 | 68 | - - - 69 | 70 | ### `mockDomSource(mockConfig)` 71 | 72 | A factory function to create mocked DOMSource objects, for testing purposes. 73 | 74 | Takes a `mockConfig` object as arguments, and returns 75 | a DOMSource that can be given to any Motorcycle.js app that expects a DOMSource in 76 | the sources, for testing. 77 | 78 | The `mockConfig` parameter is an object specifying selectors, eventTypes and 79 | their streams. Example: 80 | 81 | ```js 82 | const domSource = mockDomSource({ 83 | '.foo': { 84 | 'click': most.of({target: {}}), 85 | 'mouseover': most.of({target: {}}), 86 | }, 87 | '.bar': { 88 | 'scroll': most.of({target: {}}), 89 | elements: most.of({tagName: 'div'}), 90 | } 91 | }); 92 | 93 | // Usage 94 | const click$ = domSource.select('.foo').events('click'); 95 | const element$ = domSource.select('.bar').elements(); 96 | ``` 97 | 98 | The mocked DOM Source supports isolation. It has the functions `isolateSink` 99 | and `isolateSource` attached to it, and performs simple isolation using 100 | classNames. *isolateSink* with scope `foo` will append the class `___foo` to 101 | the stream of virtual DOM nodes, and *isolateSource* with scope `foo` will 102 | perform a conventional `mockedDomSource.select('.__foo')` call. 103 | 104 | #### Arguments: 105 | 106 | - `mockConfig: Object` an object where keys are selector strings and values are objects. Those nested objects have `eventType` strings as keys 107 | and values are streams you created. 108 | 109 | #### Return: 110 | 111 | *(Object)* fake DOM source object, with an API containing `select()` and `events()` and `elements()` which can be used just like the DOM Driver's 112 | DOMSource. 113 | 114 | - - - 115 | 116 | ### `h()` 117 | 118 | The hyperscript function `h()` is a function to create virtual DOM objects, 119 | also known as VNodes. Call 120 | 121 | ```js 122 | h('div.myClass', {style: {color: 'red'}}, []) 123 | ``` 124 | 125 | to create a VNode that represents a `DIV` element with className `myClass`, 126 | styled with red color, and no children because the `[]` array was passed. The 127 | API is `h(tagOrSelector, optionalData, optionalChildrenOrText)`. 128 | 129 | However, usually you should use "hyperscript helpers", which are shortcut 130 | functions based on hyperscript. There is one hyperscript helper function for 131 | each DOM tagName, such as `h1()`, `h2()`, `div()`, `span()`, `label()`, 132 | `input()`. For instance, the previous example could have been written 133 | as: 134 | 135 | ```js 136 | div('.myClass', {style: {color: 'red'}}, []) 137 | ``` 138 | 139 | There are also SVG helper functions, which apply the appropriate SVG 140 | namespace to the resulting elements. `svg()` function creates the top-most 141 | SVG element, and `svg.g`, `svg.polygon`, `svg.circle`, `svg.path` are for 142 | SVG-specific child elements. Example: 143 | 144 | ```js 145 | svg({width: 150, height: 150}, [ 146 | svg.polygon({ 147 | attrs: { 148 | class: 'triangle', 149 | points: '20 0 20 150 150 20' 150 | } 151 | }) 152 | ]) 153 | ``` 154 | 155 | ### `hasCssSelector(cssSelector: string, vNode: VNode): boolean` 156 | 157 | Given a CSS selector **without** spaces, this function does not search children, it 158 | will return `true` if the given CSS selector matches that of the VNode and `false` 159 | if it does not. If a CSS selector **with** spaces is given it will throw an error. 160 | 161 | ```typescript 162 | import { hasCssSelector, div } from '@motorcycle/dom'; 163 | 164 | console.log(hasCssSelector('.foo', div('.foo'))) // true 165 | console.log(hasCssSelector('.bar', div('.foo'))) // false 166 | console.log(hasCssSelector('div', div('.foo'))) // true 167 | console.log(hasCssSelector('#foo', div('#foo'))) // true 168 | console.log(hasCssSelector('.foo .bar'), div('.foo.bar')) // ERROR! 169 | ``` 170 | 171 | ### `API Wrappers` 172 | 173 | **`elements(domSource: DomSource): Stream`** 174 | 175 | A functional implementation for `DomSource.elements()`. 176 | 177 | **`events(eventType: string, domSource: DomSource): Stream`** 178 | 179 | A functional implementation for `DomSource.events(eventType)`. This function is 180 | curried by default. 181 | 182 | **`query(cssSelector: string, domSource: DomSource): DomSource`** 183 | 184 | A functional implementation for `DomSource.select(cssSelector)`. This function is 185 | curried by default. 186 | 187 | The name of this function is `query` and not `select` because it is a name conflict 188 | with the hyperscript helper function for the `SELECT` HTML element. 189 | 190 | **`useCapture(domSource: DomSource): DomSource`** 191 | 192 | Combined with `events`, this allows for an equivalent of 193 | `DomSource.events(eventType, { useCapture: true })`. 194 | 195 | ```typescript 196 | import { events, useCapture } from '@motorcycle/dom' 197 | 198 | const event$ = events('click', useCapture(sources.dom)); 199 | ``` 200 | 201 | ## Types 202 | 203 | ### `DomSource` 204 | 205 | ```typescript 206 | export interface DomSource { 207 | select(selector: string): DomSource; 208 | elements(): Stream>; 209 | 210 | events(eventType: StandardEvents, options?: EventsFnOptions): Stream; 211 | events(eventType: string, options?: EventsFnOptions): Stream; 212 | 213 | namespace(): Array; 214 | isolateSource(source: DomSource, scope: string): DomSource; 215 | isolateSink(sink: Stream, scope: string): Stream; 216 | } 217 | ``` 218 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@motorcycle/dom", 3 | "description": "Standard DOM Driver for Motorcycle.js", 4 | "version": "6.7.0", 5 | "author": "Tylor Steinberger ", 6 | "main": "lib/commonjs", 7 | "module": "lib/es2015/index.js", 8 | "jsnext:main": "lib/es2015/index.js", 9 | "typings": "lib/es2015/index.d.ts", 10 | "bugs": { 11 | "url": "https://github.com/motorcyclejs/dom/issues" 12 | }, 13 | "config": { 14 | "ghooks": { 15 | "commit-msg": "node ./node_modules/.bin/validate-commit-msg" 16 | } 17 | }, 18 | "dependencies": { 19 | "@most/dom-event": "^1.3.2", 20 | "@most/prelude": "^1.4.1", 21 | "@motorcycle/core": "^1.6.0", 22 | "most": "^1.1.1", 23 | "most-subject": "^5.2.0" 24 | }, 25 | "devDependencies": { 26 | "@cycle/isolate": "^1.4.0", 27 | "@motorcycle/core": "^1.6.0", 28 | "@motorcycle/tslint": "^1.2.0", 29 | "@types/hyperscript": "0.0.1", 30 | "@types/mocha": "^2.2.33", 31 | "@types/node": "0.0.2", 32 | "commitizen": "^2.8.6", 33 | "conventional-changelog-cli": "^1.2.0", 34 | "coveralls": "^2.11.15", 35 | "cz-conventional-changelog": "^1.2.0", 36 | "ghooks": "^1.3.2", 37 | "hyperscript": "^2.0.2", 38 | "karma": "^1.3.0", 39 | "karma-chrome-launcher": "^2.0.0", 40 | "karma-coveralls": "^1.1.2", 41 | "karma-firefox-launcher": "^1.0.0", 42 | "karma-mocha": "^1.3.0", 43 | "karma-typescript": "^2.1.5", 44 | "mocha": "^3.2.0", 45 | "tslint": "^4.0.2", 46 | "typescript": "^2.1.4", 47 | "validate-commit-msg": "^2.8.2" 48 | }, 49 | "homepage": "https://github.com/motorcyclejs/dom#readme", 50 | "keywords": [ 51 | "dom", 52 | "events", 53 | "motorcycle", 54 | "reactive", 55 | "virtual", 56 | "virtual-dom" 57 | ], 58 | "license": "MIT", 59 | "repository": { 60 | "type": "git", 61 | "url": "git+https://github.com/motorcyclejs/dom.git" 62 | }, 63 | "scripts": { 64 | "build": "npm run build:es2015 && npm run build:commonjs", 65 | "build:commonjs": "tsc -P .config/tsconfig.commonjs.json", 66 | "build:es2015": "tsc -P .config/tsconfig.es2015.json", 67 | "changelog": "conventional-changelog --infile CHANGELOG.md --same-file --release-count 0 --preset angular", 68 | "commit": "git-cz", 69 | "postchangelog": "git add CHANGELOG.md && git commit -m 'docs(CHANGELOG): append to changelog'", 70 | "postversion": "npm run changelog && git push origin master --tags && npm publish", 71 | "preversion": "npm run build", 72 | "release:major": "npm version major -m 'chore(package): v%s'", 73 | "release:minor": "npm version minor -m 'chore(package): v%s'", 74 | "test": "npm run test:lint && npm run test:karma", 75 | "test:karma": "karma start --single-run", 76 | "test:lint": "tslint src/**/*.ts src/*.ts test/*.ts test/**/*.ts test/**/**/*.ts test/**/**/**/*.ts", 77 | "test:sauce": "export SAUCE=true && npm run test:karma", 78 | "test:unit": "export UNIT=true && npm run test:karma" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/dom-driver/DomSources/ElementDomSource.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'most'; 2 | import { domEvent } from '@most/dom-event'; 3 | import { EventDelegator } from './EventDelegator'; 4 | import { DomSource, EventsFnOptions, StandardEvents, VNode } from '../../types'; 5 | import { shouldUseCapture } from './shouldUseCapture'; 6 | import { MotorcycleDomSource } from './MotorcycleDomSource'; 7 | import { elementMap } from './elementMap'; 8 | import { SCOPE_PREFIX } from './common'; 9 | 10 | export class ElementDomSource implements DomSource { 11 | protected _rootElement$: Stream; 12 | protected _namespace: Array; 13 | protected _delegator: EventDelegator; 14 | protected _element: HTMLElement; 15 | 16 | constructor( 17 | rootElement$: Stream, 18 | namespace: Array, 19 | delegator: EventDelegator = new EventDelegator(), 20 | element: HTMLElement, 21 | ) { 22 | this._rootElement$ = rootElement$; 23 | this._namespace = namespace; 24 | this._delegator = delegator; 25 | this._element = element; 26 | } 27 | 28 | public namespace(): Array { 29 | return this._namespace; 30 | } 31 | 32 | public select(cssSelector: string): DomSource { 33 | const trimmedSelector = cssSelector.trim(); 34 | 35 | if (elementMap.has(trimmedSelector)) 36 | return new ElementDomSource( 37 | this._rootElement$, 38 | this._namespace, 39 | this._delegator, 40 | elementMap.get(trimmedSelector) as HTMLElement, 41 | ); 42 | 43 | const amendedNamespace = trimmedSelector === `:root` 44 | ? this._namespace 45 | : this._namespace.concat(trimmedSelector); 46 | 47 | return new MotorcycleDomSource( 48 | this._rootElement$, 49 | amendedNamespace, 50 | this._delegator, 51 | ); 52 | } 53 | 54 | public elements(): Stream { 55 | return this._rootElement$.constant([this._element]); 56 | } 57 | 58 | public events(eventType: StandardEvents, options?: EventsFnOptions): Stream; 59 | public events(eventType: string, options?: EventsFnOptions): Stream; 60 | public events(eventType: StandardEvents, options: EventsFnOptions = {}) { 61 | const useCapture: boolean = 62 | shouldUseCapture(eventType, options.useCapture || false); 63 | 64 | const event$: Stream = 65 | domEvent(eventType, this._element, useCapture); 66 | 67 | return this._rootElement$ 68 | .constant(event$) 69 | .switch() 70 | .multicast(); 71 | } 72 | 73 | public isolateSource(source: DomSource, scope: string) { 74 | return source.select(SCOPE_PREFIX + scope); 75 | } 76 | 77 | public isolateSink(sink: Stream, scope: string): Stream { 78 | return sink.tap(vNode => { 79 | if (!vNode.data) vNode.data = {}; 80 | 81 | if (!vNode.data.isolate) 82 | vNode.data.isolate = SCOPE_PREFIX + scope; 83 | 84 | if (!vNode.key) vNode.key = SCOPE_PREFIX + scope; 85 | }); 86 | } 87 | } -------------------------------------------------------------------------------- /src/dom-driver/DomSources/EventDelegator.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'most'; 2 | 3 | export type EventType = string; 4 | export type Scope = string; 5 | export type ScopeMap = Map>; 6 | export type EventMap = Map; 7 | 8 | export type EventListenerInput = 9 | { 10 | scope: Scope, 11 | scopeMap: ScopeMap, 12 | createEventStreamFromElement: (element: Element) => Stream, 13 | }; 14 | 15 | export class EventDelegator { 16 | private eventMap: EventMap = new Map(); 17 | 18 | public addEventListener(element: Element, input: EventListenerInput): Stream { 19 | const { scope, scopeMap, createEventStreamFromElement } = input; 20 | 21 | if (scopeMap.has(scope)) 22 | return scopeMap.get(scope) as Stream; 23 | 24 | const scopedEventStream = createEventStreamFromElement(element); 25 | scopeMap.set(scope, scopedEventStream); 26 | 27 | return scopedEventStream; 28 | } 29 | 30 | public findScopeMap(eventType: EventType) { 31 | const eventMap = this.eventMap; 32 | 33 | return eventMap.has(eventType) 34 | ? eventMap.get(eventType) as Map> 35 | : addScopeMap(eventMap, eventType); 36 | } 37 | } 38 | 39 | function addScopeMap(eventMap: EventMap, eventType: EventType) { 40 | const scopeMap: ScopeMap = new Map>(); 41 | 42 | eventMap.set(eventType, scopeMap); 43 | 44 | return scopeMap; 45 | } -------------------------------------------------------------------------------- /src/dom-driver/DomSources/MotorcycleDomSource.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'most'; 2 | import { copy } from '@most/prelude'; 3 | import { domEvent } from '@most/dom-event'; 4 | import { EventDelegator, EventListenerInput } from './EventDelegator'; 5 | import { DomSource, EventsFnOptions, StandardEvents, VNode, VNodeData } from '../../types'; 6 | import { shouldUseCapture } from './shouldUseCapture'; 7 | import { ElementDomSource } from './ElementDomSource'; 8 | import { elementMap } from './elementMap'; 9 | import { SCOPE_PREFIX } from './common'; 10 | import { isInScope } from './isInScope'; 11 | import { generateScope, generateSelector } from './namespaceParsers'; 12 | import { createEventStream } from './createEventStream'; 13 | 14 | const SCOPE_SEPARATOR = `~`; 15 | 16 | export class MotorcycleDomSource implements DomSource { 17 | protected _rootElement$: Stream; 18 | protected _namespace: Array; 19 | protected _delegator: EventDelegator; 20 | protected _selector: string; 21 | protected _scope: string; 22 | 23 | constructor( 24 | rootElement$: Stream, 25 | namespace: Array, 26 | delegator: EventDelegator = new EventDelegator(), 27 | ) { 28 | this._rootElement$ = rootElement$; 29 | this._namespace = namespace; 30 | this._delegator = delegator; 31 | this._scope = generateScope(namespace); 32 | this._selector = generateSelector(namespace); 33 | } 34 | 35 | public namespace(): Array { 36 | return this._namespace; 37 | } 38 | 39 | public select(cssSelector: string): DomSource { 40 | const trimmedSelector = cssSelector.trim(); 41 | 42 | if (trimmedSelector === ':root') return this; 43 | 44 | if (elementMap.has(trimmedSelector)) 45 | return new ElementDomSource( 46 | this._rootElement$, 47 | this._namespace, 48 | this._delegator, 49 | elementMap.get(trimmedSelector) as HTMLElement, 50 | ); 51 | 52 | return new MotorcycleDomSource( 53 | this._rootElement$, 54 | this._namespace.concat(trimmedSelector), 55 | this._delegator, 56 | ); 57 | } 58 | 59 | public elements(): Stream { 60 | const namespace = this._namespace; 61 | 62 | if (namespace.length === 0) 63 | return this._rootElement$.map(Array); 64 | 65 | const selector = this._selector; 66 | const scope = this._scope; 67 | 68 | if (!selector) 69 | return this._rootElement$.map(findMostSpecificElement(scope)).map(Array); 70 | 71 | const matchElement = findMatchingElements(selector, isInScope(scope)); 72 | 73 | return this._rootElement$.map(matchElement); 74 | } 75 | 76 | public events(eventType: StandardEvents, options?: EventsFnOptions): Stream; 77 | public events(eventType: string, options?: EventsFnOptions): Stream; 78 | public events(eventType: StandardEvents, options: EventsFnOptions = {}) { 79 | const namespace = this._namespace; 80 | 81 | const useCapture = shouldUseCapture(eventType, options.useCapture || false); 82 | 83 | if (namespace.length === 0) 84 | return this._rootElement$ 85 | // take(1) is added because the rootElement will never be patched, because 86 | // the comparisons inside of makDomDriver only compare tagName, className, 87 | // and id. Attributes and properties will never be altered by the virtual-dom. 88 | .take(1) 89 | .map(element => domEvent(eventType, element, useCapture)) 90 | .switch() 91 | .multicast(); 92 | 93 | const delegator = this._delegator; 94 | const scope = this._scope; 95 | const selector = this._selector; 96 | 97 | const eventListenerInput: EventListenerInput = 98 | this.createEventListenerInput(eventType, useCapture); 99 | 100 | const checkElementIsInScope = isInScope(scope); 101 | 102 | return this._rootElement$ 103 | .map(findMostSpecificElement(this._scope)) 104 | .skipRepeats() 105 | .map(function createScopedEventStream(element: Element) { 106 | const event$ = delegator.addEventListener(element, eventListenerInput); 107 | 108 | return scopeEventStream(event$, checkElementIsInScope, selector, element); 109 | }) 110 | .switch() 111 | .multicast(); 112 | } 113 | 114 | public isolateSource(source: DomSource, scope: string) { 115 | return source.select(SCOPE_PREFIX + scope); 116 | } 117 | 118 | public isolateSink(sink: Stream, scope: string): Stream { 119 | return sink.tap(vNode => { 120 | const prefixedScope = SCOPE_PREFIX + scope; 121 | 122 | if (!(vNode.data as VNodeData).isolate) 123 | (vNode.data as VNodeData).isolate = prefixedScope; 124 | 125 | if (!vNode.key) vNode.key = prefixedScope; 126 | }); 127 | } 128 | 129 | private createEventListenerInput(eventType: string, useCapture: boolean) { 130 | const scope = this._scope; 131 | const delegator = this._delegator; 132 | 133 | const scopeMap = delegator.findScopeMap(eventType); 134 | const createEventStreamFromElement = 135 | createEventStream(eventType, useCapture); 136 | 137 | const scopeWithUseCapture: string = 138 | scope + SCOPE_SEPARATOR + useCapture; 139 | 140 | return { 141 | scopeMap, 142 | createEventStreamFromElement, 143 | scope: scopeWithUseCapture, 144 | }; 145 | } 146 | } 147 | 148 | function findMostSpecificElement(scope: string) { 149 | return function queryForElement (rootElement: Element): Element { 150 | return rootElement.querySelector(`[data-isolate='${scope}']`) || rootElement; 151 | }; 152 | }; 153 | 154 | function findMatchingElements(selector: string, checkIsInScope: (element: HTMLElement) => boolean) { 155 | return function (element: HTMLElement): Array { 156 | const matchedNodes = element.querySelectorAll(selector); 157 | const matchedNodesArray = copy(matchedNodes as any as Array); 158 | 159 | if (element.matches(selector)) 160 | matchedNodesArray.push(element); 161 | 162 | return matchedNodesArray.filter(checkIsInScope); 163 | }; 164 | } 165 | 166 | function scopeEventStream( 167 | eventStream: Stream, 168 | checkElementIsInScope: (element: Element) => boolean, 169 | selector: string, 170 | element: Element, 171 | ): Stream { 172 | return eventStream 173 | .filter(ev => checkElementIsInScope(ev.target as HTMLElement)) 174 | .filter(ev => ensureMatches(selector, element, ev)) 175 | .multicast(); 176 | } 177 | 178 | function ensureMatches(selector: string, element: Element, ev: Event) { 179 | if (!selector) return true; 180 | 181 | for (let target = ev.target as Element; target !== element; target = target.parentElement as Element) 182 | if (target.matches(selector)) 183 | return true; 184 | 185 | return element.matches(selector); 186 | } 187 | -------------------------------------------------------------------------------- /src/dom-driver/DomSources/common.ts: -------------------------------------------------------------------------------- 1 | export const SCOPE_PREFIX = `$$MOTORCYCLEDOM$$-`; 2 | -------------------------------------------------------------------------------- /src/dom-driver/DomSources/createEventStream.ts: -------------------------------------------------------------------------------- 1 | import { Stream, multicast } from 'most'; 2 | import { domEvent } from '@most/dom-event'; 3 | 4 | export function createEventStream ( 5 | eventType: string, 6 | useCapture: boolean, 7 | ) { 8 | return function (element: Element): Stream { 9 | return multicast(domEvent(eventType, element, useCapture)); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/dom-driver/DomSources/elementMap.ts: -------------------------------------------------------------------------------- 1 | const noop = () => void 0; 2 | const always = (x: any) => () => x; 3 | 4 | export const elementMap = new Map([ 5 | ['document', documentElement()], 6 | ['body', bodyElement()], 7 | ['window', windowElement()], 8 | ]); 9 | 10 | const fallback: any = 11 | { 12 | matches: always(true), 13 | addEventListener: noop, 14 | removeEventListener: noop, 15 | }; 16 | 17 | function documentElement(): Document { 18 | try { 19 | return document; 20 | } catch (e) { 21 | return fallback as Document; 22 | } 23 | } 24 | 25 | function bodyElement(): HTMLBodyElement { 26 | try { 27 | return document && document.body as HTMLBodyElement; 28 | } catch (e) { 29 | return fallback as HTMLBodyElement; 30 | } 31 | } 32 | 33 | function windowElement(): Window { 34 | try { 35 | return window; 36 | } catch (e) { 37 | return fallback as Window; 38 | } 39 | } -------------------------------------------------------------------------------- /src/dom-driver/DomSources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MotorcycleDomSource'; 2 | export * from './ElementDomSource'; -------------------------------------------------------------------------------- /src/dom-driver/DomSources/isInScope.ts: -------------------------------------------------------------------------------- 1 | export function isInScope(scope: string) { 2 | return function (element: HTMLElement) { 3 | const isolate = element.getAttribute('data-isolate'); 4 | 5 | if (scope) 6 | return isolate === scope; 7 | 8 | return !isolate; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/dom-driver/DomSources/namespaceParsers.ts: -------------------------------------------------------------------------------- 1 | import { SCOPE_PREFIX } from './common'; 2 | 3 | export function generateSelector(namespace: Array): string { 4 | return namespace.filter(findSelector).join(' '); 5 | } 6 | 7 | function findSelector(selector: string) { 8 | return !findScope(selector); 9 | } 10 | 11 | export function generateScope(namespace: Array) { 12 | const scopes = namespace.filter(findScope); 13 | 14 | return scopes[scopes.length - 1]; 15 | } 16 | 17 | function findScope(selector: string): boolean { 18 | return selector.indexOf(SCOPE_PREFIX) === 0; 19 | } 20 | -------------------------------------------------------------------------------- /src/dom-driver/DomSources/shouldUseCapture.ts: -------------------------------------------------------------------------------- 1 | const eventTypesThatDontBubble = [ 2 | `blur`, 3 | `canplay`, 4 | `canplaythrough`, 5 | `change`, 6 | `durationchange`, 7 | `emptied`, 8 | `ended`, 9 | `focus`, 10 | `load`, 11 | `loadeddata`, 12 | `loadedmetadata`, 13 | `mouseenter`, 14 | `mouseleave`, 15 | `pause`, 16 | `play`, 17 | `playing`, 18 | `ratechange`, 19 | `reset`, 20 | `scroll`, 21 | `seeked`, 22 | `seeking`, 23 | `stalled`, 24 | `submit`, 25 | `suspend`, 26 | `timeupdate`, 27 | `unload`, 28 | `volumechange`, 29 | `waiting`, 30 | ]; 31 | 32 | export function shouldUseCapture(eventType: string, useCapture?: boolean): boolean { 33 | if (eventTypesThatDontBubble.indexOf(eventType) !== -1) return true; 34 | 35 | return typeof useCapture === 'boolean' 36 | ? useCapture 37 | : false; 38 | } -------------------------------------------------------------------------------- /src/dom-driver/api-wrappers/elements.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'most'; 2 | import { DomSource } from '../../types'; 3 | 4 | export function elements(domSource: DomSource): Stream> { 5 | return domSource.elements(); 6 | } 7 | -------------------------------------------------------------------------------- /src/dom-driver/api-wrappers/events.ts: -------------------------------------------------------------------------------- 1 | import { DomSource, StandardEvents } from '../../types'; 2 | import { Stream } from 'most'; 3 | import { curry2, CurriedFunction2 } from '@most/prelude'; 4 | 5 | export const events: EventsFn = curry2>( 6 | function (eventType: StandardEvents, domSource: DomSource): Stream { 7 | return domSource.events(eventType); 8 | }, 9 | ); 10 | 11 | export interface EventsFn { 12 | (): EventsFn; 13 | 14 | (eventType: StandardEvents): (domSource: DomSource) => Stream; 15 | (eventType: StandardEvents, domSource: DomSource): Stream; 16 | 17 | (eventType: string): (domSource: DomSource) => Stream; 18 | (eventType: string, domSource: DomSource): Stream; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/dom-driver/api-wrappers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './elements'; 2 | export * from './events'; 3 | export * from './query'; 4 | export * from './useCapture'; 5 | -------------------------------------------------------------------------------- /src/dom-driver/api-wrappers/query.ts: -------------------------------------------------------------------------------- 1 | import { DomSource } from '../../types'; 2 | import { curry2, CurriedFunction2 } from '@most/prelude'; 3 | 4 | export const query = curry2( 5 | function selectWrapper(cssSelector: string, domSource: DomSource) { 6 | return domSource.select(cssSelector); 7 | }, 8 | ); 9 | -------------------------------------------------------------------------------- /src/dom-driver/api-wrappers/useCapture.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'most'; 2 | import { DomSource, EventsFnOptions, StandardEvents, VNode } from '../../types'; 3 | 4 | export function useCapture(domSource: DomSource): DomSource { 5 | return { 6 | select(cssSelector: string) { 7 | return domSource.select(cssSelector); 8 | }, 9 | 10 | elements() { 11 | return domSource.elements(); 12 | }, 13 | 14 | events(eventType: StandardEvents) { 15 | return domSource.events(eventType, { useCapture: true }); 16 | }, 17 | 18 | namespace() { 19 | return domSource.namespace(); 20 | }, 21 | 22 | isolateSource(source: DomSource, scope: string) { 23 | return domSource.isolateSource(source, scope); 24 | }, 25 | 26 | isolateSink(sink: Stream, scope: string) { 27 | return domSource.isolateSink(sink, scope); 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/dom-driver/index.ts: -------------------------------------------------------------------------------- 1 | export * from './makeDomDriver'; 2 | export * from './mockDomSource'; 3 | export * from './api-wrappers'; 4 | -------------------------------------------------------------------------------- /src/dom-driver/makeDomDriver.ts: -------------------------------------------------------------------------------- 1 | import { Stream, map, scan } from 'most'; 2 | import { hold } from 'most-subject'; 3 | import { DriverFn } from '@motorcycle/core'; 4 | import { vNodeWrapper } from './vNodeWrapper'; 5 | import { MotorcycleDomSource } from './DomSources'; 6 | import { init } from '../virtual-dom'; 7 | import { 8 | IsolateModule, 9 | StyleModule, 10 | ClassModule, 11 | PropsModule, 12 | AttrsModule, 13 | DatasetModule, 14 | } from '../modules'; 15 | import { DomSource, VNode, Module } from '../types'; 16 | import { emptyNodeAt } from '../virtual-dom/util'; 17 | 18 | const defaultModules = [StyleModule, ClassModule, PropsModule, AttrsModule, DatasetModule]; 19 | 20 | export function makeDomDriver( 21 | rootElement: HTMLElement, 22 | options: DomDriverOptions = { modules: defaultModules }): DriverFn 23 | { 24 | const modules = options.modules || defaultModules; 25 | const patch = init(modules.concat(new IsolateModule())); 26 | const rootVNode = emptyNodeAt(rootElement); 27 | const wrapVNodeInRootElement = vNodeWrapper(rootElement); 28 | 29 | return function DomDriver(vNode$: Stream): DomSource { 30 | const rootVNode$: Stream = 31 | scan(patch, rootVNode, map(wrapVNodeInRootElement, vNode$)); 32 | 33 | const rootElement$: Stream = 34 | map(vNodeToElement, rootVNode$).thru(hold(1)); 35 | 36 | rootElement$.drain() 37 | .catch(err => console.error(err)) 38 | .then(() => console.log('Dom Driver has terminated')); 39 | 40 | return new MotorcycleDomSource(rootElement$, []); 41 | }; 42 | } 43 | 44 | function vNodeToElement(vNode: VNode): HTMLElement { 45 | return vNode.elm as HTMLElement; 46 | } 47 | 48 | export interface DomDriverOptions { 49 | modules: Array; 50 | } 51 | -------------------------------------------------------------------------------- /src/dom-driver/mockDomSource.ts: -------------------------------------------------------------------------------- 1 | import { DomSource, EventsFnOptions, VNode } from '../types'; 2 | import { Stream, empty } from 'most'; 3 | 4 | export interface MockConfig { 5 | [name: string]: (MockConfig | Stream); 6 | } 7 | 8 | const SCOPE_PREFIX = '___'; 9 | 10 | export class MockedDomSource implements DomSource { 11 | private _elements: any; 12 | 13 | constructor(private _mockConfig: MockConfig) { 14 | if ((_mockConfig as any).elements) { 15 | this._elements = (_mockConfig as any).elements; 16 | } else { 17 | this._elements = empty(); 18 | } 19 | } 20 | 21 | public namespace() { 22 | return []; 23 | } 24 | 25 | public elements(): any { 26 | return this._elements; 27 | } 28 | 29 | public events(eventType: string, options?: EventsFnOptions): Stream { 30 | const mockConfig = void options ? this._mockConfig : this._mockConfig; 31 | const keys = Object.keys(mockConfig); 32 | const keysLen = keys.length; 33 | for (let i = 0; i < keysLen; i++) { 34 | const key = keys[i]; 35 | if (key === eventType) { 36 | return mockConfig[key] as Stream; 37 | } 38 | } 39 | return empty() as Stream; 40 | } 41 | 42 | public select(selector: string): MockedDomSource { 43 | const mockConfig = this._mockConfig; 44 | const keys = Object.keys(mockConfig); 45 | const keysLen = keys.length; 46 | for (let i = 0; i < keysLen; i++) { 47 | const key = keys[i]; 48 | if (key === selector) { 49 | return new MockedDomSource(mockConfig[key] as MockConfig); 50 | } 51 | } 52 | return new MockedDomSource({} as MockConfig); 53 | } 54 | 55 | public isolateSource(source: MockedDomSource, scope: string): MockedDomSource { 56 | return source.select('.' + SCOPE_PREFIX + scope); 57 | } 58 | 59 | public isolateSink(sink: any, scope: string): Stream { 60 | return sink.map((vnode: VNode) => { 61 | if ((vnode.className as string).indexOf(SCOPE_PREFIX + scope) !== -1) { 62 | return vnode; 63 | } else { 64 | vnode.className += `.${SCOPE_PREFIX}${scope}`; 65 | return vnode; 66 | } 67 | }); 68 | } 69 | } 70 | 71 | export function mockDomSource(mockConfig: MockConfig): MockedDomSource { 72 | return new MockedDomSource(mockConfig); 73 | } 74 | -------------------------------------------------------------------------------- /src/dom-driver/vNodeWrapper.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from '../types'; 2 | import { MotorcycleVNode } from '../virtual-dom/MotorcycleVNode'; 3 | 4 | export function vNodeWrapper(rootElement: HTMLElement): (vNode: VNode) => VNode { 5 | const { 6 | tagName: rootElementTagName, 7 | id, 8 | className, 9 | } = rootElement; 10 | 11 | const tagName = rootElementTagName.toLowerCase(); 12 | 13 | return function execute(vNode: VNode): VNode { 14 | const { 15 | tagName: vNodeTagName = '', 16 | id: vNodeId = '', 17 | className: vNodeClassName = '', 18 | } = vNode; 19 | 20 | const isVNodeAndRootElementIdentical = 21 | vNodeId === id && 22 | vNodeTagName.toLowerCase() === tagName && 23 | vNodeClassName === className; 24 | 25 | if (isVNodeAndRootElementIdentical) return vNode; 26 | 27 | return new MotorcycleVNode( 28 | tagName, 29 | className, 30 | id, 31 | {}, 32 | [vNode], 33 | void 0, 34 | rootElement, 35 | void 0, 36 | ); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './virtual-dom'; 3 | export * from './modules'; 4 | export * from './dom-driver'; 5 | -------------------------------------------------------------------------------- /src/modules/IsolateModule.ts: -------------------------------------------------------------------------------- 1 | import { Module, VNode } from '../types'; 2 | 3 | export class IsolateModule implements Module { 4 | public create(_: VNode, vNode: VNode) { 5 | this.setAndRemoveScopes(vNode); 6 | } 7 | 8 | public update(_: VNode, vNode: VNode) { 9 | this.setAndRemoveScopes(vNode); 10 | } 11 | 12 | private setAndRemoveScopes(vNode: VNode) { 13 | const scope = scopeFromVNode(vNode); 14 | 15 | if (!scope) return; 16 | 17 | (vNode.elm as HTMLElement).setAttribute('data-isolate', scope); 18 | 19 | addScopeToChildren(vNode.elm.children, scope); 20 | } 21 | } 22 | 23 | function addScopeToChildren(children: HTMLCollection, scope: string) { 24 | if (!children) return; 25 | 26 | const count = children.length; 27 | 28 | for (let i = 0; i < count; ++i) { 29 | const child = children[i]; 30 | 31 | if (child.hasAttribute('data-isolate')) continue; 32 | 33 | child.setAttribute('data-isolate', scope); 34 | 35 | if (child.children) 36 | addScopeToChildren(child.children, scope); 37 | } 38 | } 39 | 40 | function scopeFromVNode(vNode: VNode) { 41 | return vNode.data && vNode.data.isolate || ``; 42 | } 43 | -------------------------------------------------------------------------------- /src/modules/attributes.ts: -------------------------------------------------------------------------------- 1 | import { VNode, Module } from '../types'; 2 | 3 | const booleanAttrs = [ 4 | 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', 5 | 'default', 'defaultchecked', 'defaultmuted', 'defaultselected', 'defer', 'disabled', 'draggable', 6 | 'enabled', 'formnovalidate', 'hidden', 'indeterminate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 7 | 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'readonly', 8 | 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'spellcheck', 'translate', 9 | 'truespeed', 'typemustmatch', 'visible', 10 | ]; 11 | 12 | const booleanAttrsDict: any = {}; 13 | 14 | for (let i = 0, len = booleanAttrs.length; i < len; i++) { 15 | booleanAttrsDict[booleanAttrs[i]] = true; 16 | } 17 | 18 | function updateAttrs(oldVnode: VNode, vnode: VNode) { 19 | let key: any; 20 | let cur: any; 21 | let old: any; 22 | let elm = vnode.elm as HTMLElement; 23 | let oldAttrs = oldVnode.data && oldVnode.data.attrs || {}; 24 | let attrs = vnode.data && vnode.data.attrs || {}; 25 | 26 | // update modified attributes, add new attributes 27 | for (key in attrs) { 28 | cur = attrs[key]; 29 | old = oldAttrs[key]; 30 | if (old !== cur) { 31 | // TODO: add support to namespaced attributes (setAttributeNS) 32 | if (!cur && booleanAttrsDict[key]) { 33 | ( elm).removeAttribute(key); 34 | } else { 35 | ( elm).setAttribute(key, cur); 36 | } 37 | } 38 | } 39 | //remove removed attributes 40 | for (key in oldAttrs) { 41 | if (!(key in attrs)) { 42 | ( elm).removeAttribute(key); 43 | } 44 | } 45 | } 46 | 47 | export const AttrsModule: Module = { 48 | update: updateAttrs, 49 | create: updateAttrs, 50 | }; 51 | -------------------------------------------------------------------------------- /src/modules/class.ts: -------------------------------------------------------------------------------- 1 | import { VNode, Module } from '../types'; 2 | 3 | function updateClass(oldVnode: VNode, vnode: VNode) { 4 | let cur: any; 5 | let name: string; 6 | let elm = vnode.elm as HTMLElement; 7 | let oldClass = oldVnode.data && oldVnode.data.class || {}; 8 | let klass = vnode.data && vnode.data.class || {}; 9 | 10 | for (name in oldClass) { 11 | if (!klass[name]) { 12 | (elm).classList.remove(name); 13 | } 14 | } 15 | for (name in klass) { 16 | cur = klass[name]; 17 | if (cur !== oldClass[name]) { 18 | if (cur) { 19 | elm.classList.add(name); 20 | } else { 21 | elm.classList.remove(name); 22 | } 23 | } 24 | } 25 | } 26 | 27 | export const ClassModule: Module = { 28 | create: updateClass, 29 | update: updateClass, 30 | }; 31 | -------------------------------------------------------------------------------- /src/modules/dataset.ts: -------------------------------------------------------------------------------- 1 | import { VNode, Module } from '../types'; 2 | 3 | function updateDataset(oldVnode: VNode, vnode: VNode) { 4 | let elm = vnode.elm as HTMLElement; 5 | let oldDataset = oldVnode.data && oldVnode.data.dataset || {}; 6 | let dataset = vnode.data && vnode.data.dataset || {}; 7 | let key: any; 8 | 9 | for (key in oldDataset) { 10 | if (!dataset[key]) { 11 | delete elm.dataset[key]; 12 | } 13 | } 14 | for (key in dataset) { 15 | if (oldDataset[key] !== dataset[key]) { 16 | elm.dataset[key] = dataset[key]; 17 | } 18 | } 19 | } 20 | 21 | export const DatasetModule: Module = { 22 | create: updateDataset, 23 | update: updateDataset, 24 | }; 25 | -------------------------------------------------------------------------------- /src/modules/hero.ts: -------------------------------------------------------------------------------- 1 | import { VNode, Module } from '../types'; 2 | 3 | interface HeroVNode extends VNode { 4 | isTextNode: boolean; 5 | boundingRect: ClientRect; 6 | textRect: ClientRect | null; 7 | savedStyle: any; 8 | } 9 | 10 | let raf: any; 11 | 12 | function setRequestAnimationFrame() { 13 | if (!requestAnimationFrame) 14 | raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout; 15 | } 16 | 17 | const nextFrame = function(fn: any) { raf(function() { raf(fn); }); }; 18 | 19 | function setNextFrame(obj: any, prop: string, val: any) { 20 | nextFrame(function() { obj[prop] = val; }); 21 | } 22 | 23 | function getTextNodeRect(textNode: Text) { 24 | let rect: ClientRect | null = null; 25 | if (document.createRange) { 26 | let range = document.createRange(); 27 | range.selectNodeContents(textNode); 28 | if (range.getBoundingClientRect) { 29 | rect = range.getBoundingClientRect(); 30 | } 31 | } 32 | return rect; 33 | } 34 | 35 | function calcTransformOrigin(isTextNode: boolean, textRect: ClientRect, boundingRect: ClientRect): string { 36 | if (isTextNode) { 37 | if (textRect) { 38 | //calculate pixels to center of text from left edge of bounding box 39 | let relativeCenterX = textRect.left + textRect.width / 2 - boundingRect.left; 40 | let relativeCenterY = textRect.top + textRect.height / 2 - boundingRect.top; 41 | return relativeCenterX + 'px ' + relativeCenterY + 'px'; 42 | } 43 | } 44 | return '0 0'; //top left 45 | } 46 | 47 | function getTextDx(oldTextRect: ClientRect, newTextRect: ClientRect): number { 48 | if (oldTextRect && newTextRect) { 49 | return ((oldTextRect.left + oldTextRect.width / 2) - (newTextRect.left + newTextRect.width / 2)); 50 | } 51 | return 0; 52 | } 53 | function getTextDy(oldTextRect: ClientRect, newTextRect: ClientRect): number { 54 | if (oldTextRect && newTextRect) { 55 | return ((oldTextRect.top + oldTextRect.height / 2) - (newTextRect.top + newTextRect.height / 2)); 56 | } 57 | return 0; 58 | } 59 | 60 | function isTextElement(elm: Element | Text): boolean { 61 | return elm.childNodes.length === 1 && elm.childNodes[0].nodeType === 3; 62 | } 63 | 64 | let removed: any; 65 | let created: any[]; 66 | 67 | function pre() { 68 | setRequestAnimationFrame(); 69 | removed = {}; 70 | created = []; 71 | } 72 | 73 | function create(_: VNode, vnode: VNode) { 74 | let hero = vnode.data && vnode.data.hero; 75 | if (hero && hero.id) { 76 | created.push(hero.id); 77 | created.push(vnode); 78 | } 79 | } 80 | 81 | function destroy(vnode: HeroVNode) { 82 | let hero = vnode.data && vnode.data.hero; 83 | if (hero && hero.id) { 84 | let elm = vnode.elm as Element; 85 | vnode.isTextNode = isTextElement(elm as Element); //is this a text node? 86 | vnode.boundingRect = (elm as HTMLElement).getBoundingClientRect(); //save the bounding rectangle to a new property on the vnode 87 | vnode.textRect = vnode.isTextNode ? getTextNodeRect((elm as any).childNodes[0]) : null; //save bounding rect of inner text node 88 | let computedStyle = window.getComputedStyle((elm as HTMLElement)); //get current styles (includes inherited properties) 89 | vnode.savedStyle = JSON.parse(JSON.stringify(computedStyle)); //save a copy of computed style values 90 | removed[hero.id] = vnode; 91 | } 92 | } 93 | 94 | function post() { 95 | let i: any; 96 | let id: any; 97 | let newElm: any; 98 | let oldVnode: HeroVNode; 99 | let oldElm: any; 100 | let hRatio: any; 101 | let wRatio: any; 102 | let oldRect: any; 103 | let newRect: any; 104 | let dx: any; 105 | let dy: any; 106 | let origTransform: any; 107 | let origTransition: any; 108 | let newStyle: any; 109 | let oldStyle: any; 110 | let newComputedStyle: any; 111 | let isTextNode: any; 112 | let newTextRect: any; 113 | let oldTextRect: any; 114 | for (i = 0; i < created.length; i += 2) { 115 | id = created[i]; 116 | newElm = created[i + 1].elm; 117 | oldVnode = removed[id]; 118 | if (oldVnode) { 119 | isTextNode = oldVnode.isTextNode && isTextElement(newElm); //Are old & new both text? 120 | newStyle = newElm.style; 121 | newComputedStyle = window.getComputedStyle(newElm); //get full computed style for new element 122 | oldElm = oldVnode.elm; 123 | oldStyle = oldElm.style; 124 | //Overall element bounding boxes 125 | newRect = newElm.getBoundingClientRect(); 126 | oldRect = oldVnode.boundingRect; //previously saved bounding rect 127 | //Text node bounding boxes & distances 128 | if (isTextNode) { 129 | newTextRect = getTextNodeRect(newElm.childNodes[0]); 130 | oldTextRect = oldVnode.textRect; 131 | dx = getTextDx(oldTextRect, newTextRect); 132 | dy = getTextDy(oldTextRect, newTextRect); 133 | } else { 134 | //Calculate distances between old & new positions 135 | dx = oldRect.left - newRect.left; 136 | dy = oldRect.top - newRect.top; 137 | } 138 | hRatio = newRect.height / (Math.max(oldRect.height, 1)); 139 | wRatio = isTextNode ? hRatio : newRect.width / (Math.max(oldRect.width, 1)); //text scales based on hRatio 140 | // Animate new element 141 | origTransform = newStyle.transform; 142 | origTransition = newStyle.transition; 143 | if (newComputedStyle.display === 'inline') //inline elements cannot be transformed 144 | newStyle.display = 'inline-block'; //this does not appear to have any negative side effects 145 | newStyle.transition = origTransition + 'transform 0s'; 146 | newStyle.transformOrigin = calcTransformOrigin(isTextNode, newTextRect, newRect); 147 | newStyle.opacity = '0'; 148 | newStyle.transform = origTransform + 'translate(' + dx + 'px, ' + dy + 'px) ' + 149 | 'scale(' + 1 / wRatio + ', ' + 1 / hRatio + ')'; 150 | setNextFrame(newStyle, 'transition', origTransition); 151 | setNextFrame(newStyle, 'transform', origTransform); 152 | setNextFrame(newStyle, 'opacity', '1'); 153 | // Animate old element 154 | for (let key in oldVnode.savedStyle) { //re-apply saved inherited properties 155 | if (typeof key === 'number' && parseInt(key) !== key) { 156 | let ms = (key as any).substring(0, 2) === 'ms'; 157 | let moz = (key as any).substring(0, 3) === 'moz'; 158 | let webkit = (key as any).substring(0, 6) === 'webkit'; 159 | if (!ms && !moz && !webkit) //ignore prefixed style properties 160 | oldStyle[(key as any)] = oldVnode.savedStyle[(key as any)]; 161 | } 162 | } 163 | oldStyle.position = 'absolute'; 164 | oldStyle.top = oldRect.top + 'px'; //start at existing position 165 | oldStyle.left = oldRect.left + 'px'; 166 | oldStyle.width = oldRect.width + 'px'; //Needed for elements who were sized relative to their parents 167 | oldStyle.height = oldRect.height + 'px'; //Needed for elements who were sized relative to their parents 168 | oldStyle.margin = 0; //Margin on hero element leads to incorrect positioning 169 | oldStyle.transformOrigin = calcTransformOrigin(isTextNode, oldTextRect, oldRect); 170 | oldStyle.transform = ''; 171 | oldStyle.opacity = '1'; 172 | document.body.appendChild(oldElm); 173 | // scale must be on far right for translate to be correct 174 | setNextFrame(oldStyle, 'transform', 'translate(' + -dx + 'px, ' + -dy + 'px) scale(' + wRatio + ', ' + hRatio + ')'); 175 | setNextFrame(oldStyle, 'opacity', '0'); 176 | oldElm.addEventListener('transitionend', function (ev: TransitionEvent) { 177 | if (ev.propertyName === 'transform') 178 | document.body.removeChild(ev.target as Node); 179 | }); 180 | } 181 | } 182 | removed = {}; 183 | created = []; 184 | } 185 | 186 | export const HeroModule: Module = { 187 | pre, 188 | create, 189 | destroy, 190 | post, 191 | }; 192 | -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './attributes'; 2 | export * from './class'; 3 | export * from './dataset'; 4 | export * from './hero'; 5 | export * from './props'; 6 | export * from './style'; 7 | export * from './IsolateModule'; 8 | -------------------------------------------------------------------------------- /src/modules/props.ts: -------------------------------------------------------------------------------- 1 | import { VNode, Module } from '../types'; 2 | 3 | function updateProps(oldVnode: VNode, vnode: VNode) { 4 | if (!oldVnode.data && !vnode.data) return; 5 | let key: any; 6 | let cur: any; 7 | let old: any; 8 | let elm: any = vnode.elm; 9 | let oldProps: any = oldVnode.data && oldVnode.data.props || {}; 10 | let props: any = vnode.data && vnode.data.props || {}; 11 | 12 | for (key in oldProps) { 13 | if (!props[key]) { 14 | delete elm[key]; 15 | } 16 | } 17 | for (key in props) { 18 | cur = props[key]; 19 | old = oldProps[key]; 20 | if (old !== cur && (key !== 'value' || elm[key] !== cur)) { 21 | elm[key] = cur; 22 | } 23 | } 24 | } 25 | 26 | export const PropsModule: Module = { 27 | create: updateProps, 28 | update: updateProps, 29 | }; 30 | -------------------------------------------------------------------------------- /src/modules/style.ts: -------------------------------------------------------------------------------- 1 | import { VNode, Module } from '../types'; 2 | 3 | let requestAnimationFrame: any; 4 | 5 | function setRequestAnimationFrame() { 6 | if (!requestAnimationFrame) 7 | requestAnimationFrame = 8 | (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout; 9 | } 10 | 11 | function nextFrame(fn: any) { 12 | requestAnimationFrame(function () { 13 | requestAnimationFrame(fn); 14 | }); 15 | }; 16 | 17 | function setValueOnNextFrame(obj: any, prop: string, value: any) { 18 | nextFrame(function () { 19 | obj[prop] = value; 20 | }); 21 | } 22 | 23 | function updateStyle(formerVNode: VNode, vNode: VNode): void { 24 | let styleValue: any; 25 | let key: string; 26 | let element: HTMLElement = vNode.elm; 27 | let formerStyle: any = (formerVNode.data as any).style; 28 | let style: any = (vNode.data as any).style; 29 | 30 | if (!formerStyle && !style) return; 31 | 32 | formerStyle = formerStyle || {}; 33 | style = style || {}; 34 | 35 | let formerHasDelayedProperty: boolean = 36 | !!formerStyle.delayed; 37 | 38 | for (key in formerStyle) 39 | if (!style[key]) 40 | if (key.startsWith('--')) 41 | element.style.removeProperty(key); 42 | else 43 | (element.style as any)[key] = ''; 44 | 45 | for (key in style) { 46 | styleValue = style[key]; 47 | 48 | if (key === 'delayed') { 49 | for (key in style.delayed) { 50 | styleValue = style.delayed[key]; 51 | 52 | if (!formerHasDelayedProperty || styleValue !== formerStyle.delayed[key]) 53 | setValueOnNextFrame((element as any).style, key, styleValue); 54 | } 55 | } else if (key !== 'remove' && styleValue !== formerStyle[key]) { 56 | if (key.startsWith('--')) { 57 | element.style.setProperty(key, styleValue); 58 | } 59 | else 60 | (element.style as any)[key] = styleValue; 61 | } 62 | } 63 | } 64 | 65 | function applyDestroyStyle(vNode: VNode) { 66 | let key: string; 67 | let element: any = vNode.elm; 68 | let style: any = (vNode.data as any).style; 69 | 70 | if (!style || !style.destroy) return; 71 | 72 | const destroy: any = style.destroy; 73 | 74 | for (key in destroy) 75 | element.style[key] = destroy[key]; 76 | } 77 | 78 | function applyRemoveStyle(vNode: VNode, callback: () => void) { 79 | const style = (vNode.data as any).style; 80 | 81 | if (!style || !style.remove) { 82 | callback(); 83 | return; 84 | } 85 | 86 | let key: string; 87 | let element: any = vNode.elm; 88 | let index = 0; 89 | let computedStyle: any; 90 | let listenerCount = 0; 91 | let appliedStyles: Array = []; 92 | 93 | for (key in style) { 94 | appliedStyles.push(key); 95 | element.style[key] = style[key]; 96 | } 97 | 98 | computedStyle = getComputedStyle(element); 99 | 100 | const transitionProperties: Array = 101 | computedStyle['transition-property'].split(', '); 102 | 103 | for (; index < transitionProperties.length; ++index) 104 | if (appliedStyles.indexOf(transitionProperties[index]) !== -1) 105 | listenerCount++; 106 | 107 | element.addEventListener('transitionend', function (event: TransitionEvent) { 108 | if (event.target === element) 109 | --listenerCount; 110 | 111 | if (listenerCount === 0) 112 | callback(); 113 | }); 114 | } 115 | 116 | export const StyleModule: Module = { 117 | pre: setRequestAnimationFrame, 118 | create: updateStyle, 119 | update: updateStyle, 120 | destroy: applyDestroyStyle, 121 | remove: applyRemoveStyle, 122 | }; 123 | -------------------------------------------------------------------------------- /src/types/DomSource.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from 'most'; 2 | import { VNode } from './virtual-dom'; 3 | import { StandardEvents } from './Events'; 4 | 5 | export interface EventsFnOptions { 6 | useCapture?: boolean; 7 | } 8 | 9 | export interface DomSource { 10 | 11 | select(selector: string): DomSource; 12 | elements(): Stream>; 13 | 14 | events(eventType: StandardEvents, options?: EventsFnOptions): Stream; 15 | events(eventType: string, options?: EventsFnOptions): Stream; 16 | 17 | namespace(): Array; 18 | isolateSource(source: DomSource, scope: string): DomSource; 19 | isolateSink(sink: Stream, scope: string): Stream; 20 | 21 | // TODO: implement these because strings suck 22 | 23 | // abort(options?: EventsFnOptions): Stream // UIEvent, ProgressEvent, Event 24 | // afterprint(options?: EventsFnOptions): Stream; 25 | // animationend(options?: EventsFnOptions): Stream; 26 | // animationiteration(options?: EventsFnOptions): Stream; 27 | // animationstart(options?: EventsFnOptions): Stream; 28 | // audioprocess(options?: EventsFnOptions): Stream; 29 | // audioend(options?: EventsFnOptions): Stream; 30 | // audiostart(options?: EventsFnOptions): Stream; 31 | // beforprint(options?: EventsFnOptions): Stream; 32 | // beforeunload(options?: EventsFnOptions): Stream; 33 | // beginEvent(options?: EventsFnOptions): Stream; // TimeEvent 34 | // blocked(options?: EventsFnOptions): Stream; 35 | // blur(options?: EventsFnOptions): Stream; // FocusEvent 36 | // boundary(options?: EventsFnOptions): Stream; // SpeechsynthesisEvent 37 | // cached(options?: EventsFnOptions): Stream; 38 | // canplay(options?: EventsFnOptions): Stream; 39 | // canplaythrough(options?: EventsFnOptions): Stream; 40 | // change(options?: EventsFnOptions): Stream; 41 | // chargingchange(options?: EventsFnOptions): Stream; 42 | // chargingtimechange(options?: EventsFnOptions): Stream; 43 | // checking(options?: EventsFnOptions): Stream; 44 | // click(options?: EventsFnOptions): Stream; 45 | // close(options?: EventsFnOptions): Stream; 46 | // complete(options?: EventsFnOptions): Stream; // OfflineAudioCompletionEvent 47 | // compositionend(options?: EventsFnOptions): Stream; 48 | // compositionstart(options?: EventsFnOptions): Stream; 49 | // compositionupdate(options?: EventsFnOptions): Stream; 50 | // contextmenu(options?: EventsFnOptions): Stream; 51 | // copy(options?: EventsFnOptions): Stream; 52 | // cut(options?: EventsFnOptions): Stream; 53 | // dblclick(options?: EventsFnOptions): Stream; 54 | // devicechange(options?: EventsFnOptions): Stream; 55 | // devicelight(options?: EventsFnOptions): Stream; 56 | // devicemotion(options?: EventsFnOptions): Stream; 57 | // deviceorientation(options?: EventsFnOptions): Stream; 58 | // deviceproximity(options?: EventsFnOptions): Stream; // DeviceProximityEvent 59 | // dischargingtimechange(options?: EventsFnOptions): Stream; 60 | // DOMActivate(options?: EventsFnOptions): Stream; 61 | // DOMAttributeNameChanged(options?: EventsFnOptions): Stream; // MutationNameEvent 62 | // DOMAttrModified(options?: EventsFnOptions): Stream; 63 | // DOMCharacterDataModified(options?: EventsFnOptions): Stream; 64 | // DOMContentLoaded(options?: EventsFnOptions): Stream; 65 | // DOMElementNamedChanged(options?: EventsFnOptions): Stream; // MutationNameEvent 66 | // DOMNodeInserted(options?: EventsFnOptions): Stream; 67 | // DOMNodeInsertedIntoDocument(options?: EventsFnOptions): Stream; 68 | // DOMNodeRemoved(options?: EventsFnOptions): Stream; 69 | // DOMNodeRemovedFromDocument(options?: EventsFnOptions): Stream; 70 | // DOMSubtreeModified(options?: EventsFnOptions): Stream; 71 | // downloaded(options?: EventsFnOptions): Stream; 72 | // drag(options?: EventsFnOptions): Stream; 73 | // dragend(options?: EventsFnOptions): Stream; 74 | // dragenter(options?: EventsFnOptions): Stream; 75 | // dragleave(options?: EventsFnOptions): Stream; 76 | // dragover(options?: EventsFnOptions): Stream; 77 | // dragstart(options?: EventsFnOptions): Stream; 78 | // drop(options?: EventsFnOptions): Stream; 79 | // durationchange(options?: EventsFnOptions): Stream; 80 | // emptied(options?: EventsFnOptions): Stream; 81 | // end(options?: EventsFnOptions): Stream; 82 | // ended(options?: EventsFnOptions): Stream; 83 | // endEvent(options?: EventsFnOptions): Stream; // TimeEvent 84 | // error(options?: EventsFnOptions): Stream; 85 | // focus(options?: EventsFnOptions): Stream; 86 | // fullscreenchange(options?: EventsFnOptions): Stream; 87 | // fullscreenerror(options?: EventsFnOptions): Stream; 88 | // gamepadconnected(options?: EventsFnOptions): Stream; 89 | // gamepaddisconnected(options?: EventsFnOptions): Stream; 90 | // gotpointercapture(options?: EventsFnOptions): Stream; 91 | // hashchange(options?: EventsFnOptions): Stream; 92 | // lostpointercapture(options?: EventsFnOptions): Stream; 93 | // input(options?: EventsFnOptions): Stream; 94 | // invalid(options?: EventsFnOptions): Stream; 95 | // keydown(options?: EventsFnOptions): Stream; 96 | // keypress(options?: EventsFnOptions): Stream; 97 | // keyup(options?: EventsFnOptions): Stream; 98 | // languagechange(options?: EventsFnOptions): Stream; 99 | // levelchange(options?: EventsFnOptions): Stream; 100 | // load(options?: EventsFnOptions): Stream; // UIEvent, ProgressEvent 101 | // loadeddata(options?: EventsFnOptions): Stream; 102 | // loadedmetadata(options?: EventsFnOptions): Stream; 103 | // loadend(options?: EventsFnOptions): Stream; 104 | // loadstart(options?: EventsFnOptions): Stream; 105 | // mark(options?: EventsFnOptions): Stream; // SpeechSynthesisEvent 106 | // message(options?: EventsFnOptions): Stream; // MessageEvent, ServiceWorkerMessageEvent, ExtendableMessageEvent 107 | // mousedown(options?: EventsFnOptions): Stream; 108 | // mouseenter(options?: EventsFnOptions): Stream; 109 | // mouseleave(options?: EventsFnOptions): Stream; 110 | // mousemove(options?: EventsFnOptions): Stream; 111 | // mouseout(options?: EventsFnOptions): Stream; 112 | // mouseover(options?: EventsFnOptions): Stream; 113 | // nomatch(options?: EventsFnOptions): Stream; // SpeechRecognitionEvent 114 | // notificationclick(options?: EventsFnOptions): Stream; // NotificationEvent 115 | // noupdate(options?: EventsFnOptions): Stream; 116 | // obsolete(options?: EventsFnOptions): Stream; 117 | // offline(options?: EventsFnOptions): Stream; 118 | // online(options?: EventsFnOptions): Stream; 119 | // open(options?: EventsFnOptions): Stream; 120 | // orientationchange(options?: EventsFnOptions): Stream; 121 | // pagehide(options?: EventsFnOptions): Stream; 122 | // pageshow(options?: EventsFnOptions): Stream; 123 | // paste(options?: EventsFnOptions): Stream; // ClipboardEvent 124 | // pause(options?: EventsFnOptions): Stream; // SpeechSynthesisEvent 125 | // pointercancel(options?: EventsFnOptions): Stream; 126 | // pointerdown(options?: EventsFnOptions): Stream; 127 | // pointerenter(options?: EventsFnOptions): Stream; 128 | // pointerleave(options?: EventsFnOptions): Stream; 129 | // pointerlockchange(options?: EventsFnOptions): Stream; 130 | // pointerlockerror(options?: EventsFnOptions): Stream; 131 | // pointermove(options?: EventsFnOptions): Stream; 132 | // pointerout(options?: EventsFnOptions): Stream; 133 | // pointerover(options?: EventsFnOptions): Stream; 134 | // pointerup(options?: EventsFnOptions): Stream; 135 | // play(options?: EventsFnOptions): Stream; 136 | // playing(options?: EventsFnOptions): Stream; 137 | // popstate(options?: EventsFnOptions): Stream; // PopStateEvent 138 | // progress(options?: EventsFnOptions): Stream; // ProgressEvent 139 | // push(options?: EventsFnOptions): Stream; // PushEvent 140 | // pushsubscriptionchange(options?: EventsFnOptions): Stream; // PushEvent 141 | // ratechange(options?: EventsFnOptions): Stream; 142 | // readystatechange(options?: EventsFnOptions): Stream; 143 | // repeatEvent(options?: EventsFnOptions): Stream; // TimeEvent 144 | // reset(options?: EventsFnOptions): Stream; 145 | // resize(options?: EventsFnOptions): Stream; 146 | // resourcetimingbufferfull(options?: EventsFnOptions): Stream; 147 | // result(options?: EventsFnOptions): Stream; // SpeechRecognitionEvent 148 | // resume(options?: EventsFnOptions): Stream; // SpeechSynthesisEvent 149 | // scroll(options?: EventsFnOptions): Stream; 150 | // seeked(options?: EventsFnOptions): Stream; 151 | // seeking(options?: EventsFnOptions): Stream; 152 | // selectstart(options?: EventsFnOptions): Stream; 153 | // selectionchange(options?: EventsFnOptions): Stream; 154 | // show(options?: EventsFnOptions): Stream; 155 | // soundend(options?: EventsFnOptions): Stream; 156 | // soundstart(options?: EventsFnOptions): Stream; 157 | // speechend(options?: EventsFnOptions): Stream; 158 | // speechstart(options?: EventsFnOptions): Stream; 159 | // stalled(options?: EventsFnOptions): Stream; 160 | // start(options?: EventsFnOptions): Stream; // SpeechSynthesisEvent 161 | // storage(options?: EventsFnOptions): Stream; 162 | // submit(options?: EventsFnOptions): Stream; 163 | // success(options?: EventsFnOptions): Stream; 164 | // suspend(options?: EventsFnOptions): Stream; 165 | // SVGAbort(options?: EventsFnOptions): Stream; // SvgEvent 166 | // SVGError(options?: EventsFnOptions): Stream; // SvgEvent 167 | // SVGLoad(options?: EventsFnOptions): Stream; // SvgEvent 168 | // SVGResize(options?: EventsFnOptions): Stream; // SvgEvent 169 | // SVGScroll(options?: EventsFnOptions): Stream; // SvgEvent 170 | // SVGUnload(options?: EventsFnOptions): Stream; // SvgEvent 171 | // SVGZoom(options?: EventsFnOptions): Stream; // SvgEvent 172 | // timeout(options?: EventsFnOptions): Stream; 173 | // timeupdate(options?: EventsFnOptions): Stream; 174 | // touchcancel(options?: EventsFnOptions): Stream; 175 | // touchend(options?: EventsFnOptions): Stream; 176 | // touchenter(options?: EventsFnOptions): Stream; 177 | // touchleave(options?: EventsFnOptions): Stream; 178 | // touchmove(options?: EventsFnOptions): Stream; 179 | // touchstart(options?: EventsFnOptions): Stream; 180 | // transitionend(options?: EventsFnOptions): Stream; 181 | // unload(options?: EventsFnOptions): Stream; 182 | // updateready(options?: EventsFnOptions): Stream; 183 | // upgradeneeded(options?: EventsFnOptions): Stream; 184 | // userproximity(options?: EventsFnOptions): Stream; // UserProximityEvent 185 | // voiceschanged(options?: EventsFnOptions): Stream; 186 | // versionchange(options?: EventsFnOptions): Stream; 187 | // visibilitychange(options?: EventsFnOptions): Stream; 188 | // volumechange(options?: EventsFnOptions): Stream; 189 | // vrdisplayconnected(options?: EventsFnOptions): Stream; 190 | // vrdisplaydisconnected(options?: EventsFnOptions): Stream; 191 | // vrdisplaypresentchange(options?: EventsFnOptions): Stream; 192 | // waiting(options?: EventsFnOptions): Stream; 193 | // wheel(options?: EventsFnOptions): Stream; 194 | } 195 | -------------------------------------------------------------------------------- /src/types/Events.ts: -------------------------------------------------------------------------------- 1 | // All events types described at 2 | // https://developer.mozilla.org/en-US/docs/Web/Events 3 | 4 | export type ResourceEvents = 5 | 'cached' | 'error' | 'abort' | 'load' | 'beforeunload' | 'unload'; 6 | 7 | export type NetworkEvents = 'online' | 'offline'; 8 | 9 | export type FocusEvents = 'focus' | 'blur'; 10 | 11 | export type WebsocketEvents = 'open' | 'message' | 'error' | 'close'; 12 | 13 | export type SessionHistoryEvents = 'pagehide' | 'pageshow' | 'popstate'; 14 | 15 | export type CssAnimationEvents = 16 | 'animationstart' | 'animationend' | 'animationiteration'; 17 | 18 | export type FormEvents = 'reset' | 'submit' | 'invalid'; 19 | 20 | export type PrintingEvents = 'beforeprint' | 'afterprint'; 21 | 22 | export type TextCompositionEvents = 23 | 'compositionstart' | 'compositionupdate' | 'compositionend'; 24 | 25 | export type ViewEvents = 26 | 'fullscreenchange' | 'fullscreenerror' | 'resize' | 'scroll'; 27 | 28 | export type KeyboardEvents = 'keydown' | 'keypress' | 'keyup'; 29 | 30 | export type MouseEvents = 31 | 'mouseenter' | 'mouseover' | 'mousemove' | 'mousedown' | 'mouseup' | 'click' | 32 | 'dblclick' | 'contextmenu' | 'wheel' | 'mouseleave' | 'mouseout' | 'select' | 33 | 'pointerlockchange' | 'pointerlockerror'; 34 | 35 | export type DragAndDropEvents = 36 | 'dragstart' | 'drag' | 'dragend' | 'dragend' | 'dragenter' | 'dragover' | 37 | 'dragleave' | 'drop'; 38 | 39 | export type MediaEvents = 40 | 'durationchange' | 'loadedmetadata' | 'loadeddata' | 'canplay' | 41 | 'canplaythrough' | 'ended' | 'emptied' | 'stalled' | 'suspend' | 'play' | 42 | 'playing' | 'pause' | 'waiting' | 'seeking' | 'ratechange' | 'timeupdate' | 43 | 'volumechange' | 'complete' | 'ended' | 'audioprocess'; 44 | 45 | export type ProgressEvents = 46 | 'loadstart' | 'progress' | 'error' | 'timeout' | 'abort' | 'load' | 'loaded'; 47 | 48 | export type StorageEvents = 49 | 'change' | 'storage'; 50 | 51 | export type UpdateEvents = 52 | 'checking' | 'downloading' | 'error' | 'noupdate' | 'obsolete' | 'updateready'; 53 | 54 | export type ValueChangeEvents = 55 | 'broadcast' | 'CheckboxStateChange' | 'hashchange' | 'input' | 56 | 'RadioStateChange' | 'readystatechange' | 'ValueChange'; 57 | 58 | export type LocalizationEvents = 'localized'; 59 | 60 | export type WebWorkerEvents = 'message'; 61 | 62 | export type ContextMenuEvents = 'show'; 63 | 64 | export type SvgEvents = 65 | 'SVGAbort' | 'SVGError' | 'SVGLoad' | 'SVGResize' | 'SVGScroll' | 66 | 'SVGUnload' | 'SVGZoom'; 67 | 68 | export type DatabaseEvents = 69 | 'abort' | 'blocked' | 'complete' | 'error' | 'success' | 'upgradeneeded' | 70 | 'versionchange'; 71 | 72 | export type NotificationEvents = 'AlertActive' | 'AlertClose'; 73 | 74 | export type CSSEvents = 75 | 'CssRuleViewRefreshed' | 'CssRuleViewChanged' | 'CssRuleViewCSSLinkClicked' | 76 | 'transitionend'; 77 | 78 | export type ScriptEvents = 79 | 'afterscriptexecute' | 'beforescriptexecute'; 80 | 81 | export type MenuEvents = 'DOMMenutItemActive' | 'DOMMenutItemInactive'; 82 | 83 | export type WindowEvents = 84 | 'DOMWindowCreated' | 'DOMTitleChanged' | 'DOMWindowClose' | 85 | 'SSWindowClosing' | 'SSWindowStateReady' | 'SSWindowStateBusy' | 'close'; 86 | 87 | export type DocumentEvents = 88 | 'DOMLinkAdded' | 'DOMLinkRemoved' | 'DOMMetaAdded' | 'DOMMetaRemoved' | 89 | 'DOMWillOpenModalDialog' | 'DOMModalDialogClosed'; 90 | 91 | export type PopupEvents = 92 | 'popuphidden' | 'popuphiding' | 'popupshowing' | 93 | 'popupshown' | 'DOMPopupBlocked'; 94 | 95 | export type TabEvents = 96 | 'TabOpen' | 'TabClose' | 'TabSelect' | 'TabShow' | 'TabHide' | 'TabPinned' | 97 | 'TabUnpinned' | 'SSTabClosing' | 'SSTabRestoring' | 'SSTabRestored' | 98 | 'visibilitychange'; 99 | 100 | export type BatteryEvents = 101 | 'chargingchange' | 'chargingtimechange' | 102 | 'dischargingtimechange' | 'levelchange'; 103 | 104 | export type CallEvents = 105 | 'alerting' | 'busy' | 'callschanged' | 'connected' | 'connecting' | 106 | 'dialing' | 'disconnected' | 'disconnecting' | 'error' | 'held' | 107 | 'holding' | 'incoming' | 'resuming' | 'statechange'; 108 | 109 | export type SensorEvents = 110 | 'devicelight' | 'devicemotion' | 'deviceorientation' | 'deviceproximity' | 111 | // 'MozOrientation' | 112 | 'orientationchange' | 'userproximity'; 113 | 114 | export type SmartcardEvents = 'smartcard-insert' | 'smartcard-remove'; 115 | 116 | export type SMSAndUSSDEvents = 'delivered' | 'received' | 'sent'; 117 | 118 | export type FrameEvents = 119 | // 'mozbrowserclose' | 'mozbrowsercontextmenu' | 'mozbrowsererror' | 120 | // 'mozbrowsericonchange' | 'mozbrowserlocationchange' | 'mozbrowserloadend' | 121 | // 'mozbrowserloadstart' | 'mozbrowseropenwindow' | 'mozbrowsersecuritychange' 122 | // | 'mozbrowsershowmodalprompt' | 'mozbrowsertitlechange' | 123 | 'DOMFrameContentLoaded'; 124 | 125 | export type DOMMutationEvents = 126 | 'DOMAttributeNameChanged' | 'DOMAttrModified' | 127 | 'DOMCharacterDataModified' | 'DOMContentLoaded' | 'DOMElementNamedChanged' | 128 | 'DOMNodeInserted' | 'DOMNodeInsertedIntoDocument' | 'DOMNodeRemoved' | 129 | 'DOMNodeRemovedFromDocument' | 'DOMSubtreeModified'; 130 | 131 | export type TouchEvents = 132 | // 'MozEdgeUiGestor' | 'MozMagnifyGesture' | 'MozMagnifyGestureStart' | 133 | // 'MozMagnifyGestureUpdate' | 'MozPressTapGesture' | 'MozRotateGesture' | 134 | // 'MozRotateGestureStart' | 'MozRotateGestureUpdate' | 'MozSwipeGesture' | 135 | // 'MozTapGesture' | 'MozTouchDown' | 'MozTouchMove' | 'MozTouchUp' | 136 | 'touchcancel' | 'touchend' | 'touchenter' | 'touchleave' | 'touchmove' | 137 | 'touchstart'; 138 | 139 | export type PointerEvents = 140 | 'pointerover' | 'pointerenter' | 'pointerdown' | 'pointermove' | 'pointerup' | 141 | 'pointercancel' | 'pointerout' | 'pointerleave' | 142 | 'gotpointercapture' | 'lostpointercapture'; 143 | 144 | // the events that are in the var browser specifications 145 | // all browsers should have these implemented the same 146 | export type StandardEvents = 147 | // name - Event Types 148 | 'abort' | // UIEvent, ProgressEvent, Event 149 | 'afterprint' | // Event; 150 | 'animationend' | // AnimationEvent 151 | 'animationiteration' | // AnimationEvent 152 | 'animationstart' | // AnimationEvent 153 | 'audioprocess' | // AudioProcessingEvent 154 | 'audioend' | // Event 155 | 'audiostart' | // Event 156 | 'beforprint' | // Event 157 | 'beforeunload' | // BeforeUnloadEvent 158 | 'beginEvent' | // TimeEvent 159 | 'blocked' | // Event 160 | 'blur' | // FocusEvent 161 | 'boundary' | // SpeechsynthesisEvent 162 | 'cached' | // Event 163 | 'canplay' | // Event 164 | 'canplaythrough' | // Event 165 | 'change' | // Event 166 | 'chargingchange' | // Event 167 | 'chargingtimechange' | // Event 168 | 'checking' | // Event 169 | 'click' | // MouseEvent 170 | 'close' | // Event 171 | 'complete' | // Event, OfflineAudioCompletionEvent 172 | 'compositionend' | // CompositionEvent 173 | 'compositionstart' | // CompositionEvent 174 | 'compositionupdate' | // CompositionEvent 175 | 'contextmenu' | // MoustEvent 176 | 'copy' | // ClipboardEvent 177 | 'cut' | // ClipboardEvent 178 | 'dblclick' | // MouseEvent 179 | 'devicechange' | // Event 180 | 'devicelight' | // DeviceLightEvent 181 | 'devicemotion' | // DeviceMotionEvent 182 | 'deviceorientation' | // DeviceOrientationEvent 183 | 'deviceproximity' | // DeviceProximityEvent 184 | 'dischargingtimechange' | // Event 185 | 'DOMActivate' | // UIEvent 186 | 'DOMAttributeNameChanged' | // MutationNameEvent 187 | 'DOMAttrModified' | // Mutationevent 188 | 'DOMCharacterDataModified' | // MutationEvent 189 | 'DOMContentLoaded' |// Event 190 | 'DOMElementNamedChanged' | // MutationNameEvent 191 | 'DOMNodeInserted' | // MutationEvent 192 | 'DOMNodeInsertedIntoDocument' | // MutationEvent 193 | 'DOMNodeRemoved' | // MutationEvent 194 | 'DOMNodeRemovedFromDocument' | // MutationEvent 195 | 'DOMSubtreeModified' | // MutationEvent 196 | 'downloaded' | // Event 197 | 'drag' | // DragEvent 198 | 'dragend' | // DragEvent 199 | 'dragenter' | // DragEvent 200 | 'dragleave' | // DragEvent 201 | 'dragover' | // DragEvent 202 | 'dragstart' | // DragEvent 203 | 'drop' | // DragEvent 204 | 'durationchange' | // Event 205 | 'emptied' | // Event 206 | 'end' | // Event, SpeechSynthesisEvent 207 | 'ended' | // Event 208 | 'endEvent' | // TimeEvent 209 | 'error' | // UIEvent | ProgressEvent | Event 210 | 'focus' | // FocusEvent 211 | 'fullscreenchange' | // Event 212 | 'fullscreenerror' | // Event 213 | 'gamepadconnected' | // GamepadEvent 214 | 'gamepaddisconnected' | // GamepadEvent 215 | 'gotpointercapture' | // PointerEvent 216 | 'hashchange' | // HashChangEvent 217 | 'lostpointercapture' | // PointerEvent 218 | 'input' | // event 219 | 'invalid' | // Event 220 | 'keydown' | // KeyboardEvent 221 | 'keypress' | // KeyboardEvent 222 | 'keyup' | // KeyboardEvent 223 | 'languagechange' | // Event 224 | 'levelchange' | // Event 225 | 'load' | // UIEvent, ProgressEvent 226 | 'loadeddata' | // Event 227 | 'loadedmetadata' | // Event 228 | 'loadend' | // ProgressEvent 229 | 'loadstart' | // ProgressEvent 230 | 'mark' | // SpeechSynthesisEvent 231 | 'message' | // MessageEvent, ServiceWorkerMessageEvent, ExtendableMessageEvent 232 | 'mousedown' | // MouseEvent 233 | 'mouseenter' | // MouseEvent 234 | 'mouseleave' | // MouseEvent 235 | 'mousemove' | // MouseEvent 236 | 'mouseout' | // MouseEvent 237 | 'mouseover' | // Mouseevent 238 | 'nomatch' | // SpeechRecognitionEvent 239 | 'notificationclick' | // NotificationEvent 240 | 'noupdate' | // event 241 | 'obsolete' | // Event 242 | 'offline' | // event 243 | 'online' | // Event 244 | 'open' | // event 245 | 'orientationchange' | // Event 246 | 'pagehide' | // PageTransitionEvent 247 | 'pageshow' | // PageTransitionEvent 248 | 'paste' | // ClipboardEvent 249 | 'pause' | // Event, SpeechSynthesisEvent 250 | 'pointercancel' | // PointerEvent 251 | 'pointerdown' | //PointerEvent 252 | 'pointerenter' | // PointerEvent 253 | 'pointerleave' | // PointerEvent 254 | 'pointerlockchange' | // Event 255 | 'pointerlockerror' | // Event 256 | 'pointermove' | // PointerEvent 257 | 'pointerout' | // PointerEvent 258 | 'pointerover' | // PointerEvent 259 | 'pointerup' | // PointerEvent 260 | 'play' | // Event 261 | 'playing' | // Event 262 | 'popstate' | // PopStateEvent 263 | 'progress' | // ProgressEvent 264 | 'push' | // PushEvent 265 | 'pushsubscriptionchange' | // PushEvent 266 | 'ratechange' | // Event 267 | 'readystatechange' | // Event 268 | 'repeatEvent' | // TimeEvent 269 | 'reset' | // Event 270 | 'resize' | // UIEvent 271 | 'resourcetimingbufferfull' | // Performance 272 | 'result' | // SpeechRecognitionEvent 273 | 'resume' | // SpeechSynthesisEvent 274 | 'scroll' | // UIEvent 275 | 'seeked' | // Event 276 | 'seeking' | // Event 277 | 'select' | // UIEvent 278 | 'selectstart' | // UIEvent 279 | 'selectionchange' | // Event 280 | 'show' | // MouseEvent 281 | 'soundend' | //Event 282 | 'soundstart' | // Event 283 | 'speechend' | // Event 284 | 'speechstart' | // Event 285 | 'stalled' | // Event 286 | 'start' | // SpeechSynthesisEvent 287 | 'storage' | // StorageEvent 288 | 'submit' | // Event 289 | 'success' | // Event 290 | 'suspend' | // Event 291 | 'SVGAbort' | // SvgEvent 292 | 'SVGError' | // SvgEvent 293 | 'SVGLoad' | // SvgEvent 294 | 'SVGResize' | // SvgEvent 295 | 'SVGScroll' | // SvgEvent 296 | 'SVGUnload' | // SvgEvent 297 | 'SVGZoom' | // SvgEvent 298 | 'timeout' | // ProgressEvent 299 | 'timeupdate' | // Event 300 | 'touchcancel' | // TouchEvent 301 | 'touchend' | // TouchEvent 302 | 'touchenter' | // TouchEvent 303 | 'touchleave' | // TouchEvent 304 | 'touchmove' | // TouchEvent 305 | 'touchstart' | // TouchEvent ; 306 | 'transitionend' | // Transitionevent 307 | 'unload' | // UIEvent 308 | 'updateready' | // Event 309 | 'upgradeneeded' | // Event 310 | 'userproximity' | // UserProximityEvent 311 | 'voiceschanged' | // Event 312 | 'versionchange' | // Event 313 | 'visibilitychange' | // Event 314 | 'volumechange' | // Event 315 | 'vrdisplayconnected' | // Event 316 | 'vrdisplaydisconnected' | // Event 317 | 'vrdisplaypresentchange' | // Event 318 | 'waiting' | // Event 319 | 'wheel'; // WheelEvent 320 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DomSource'; 2 | export * from './Events'; 3 | export * from './tagNames'; 4 | export * from './virtual-dom'; 5 | -------------------------------------------------------------------------------- /src/types/tagNames.ts: -------------------------------------------------------------------------------- 1 | export type HtmlTagNames = 2 | 'a' | 'abbr' | 'acronym' | 'address' | 'applet' | 'area' | 'article' | 3 | 'aside' | 'audio' | 'b' | 'base' | 'basefont' | 'bdi' | 'bdo' | 'bgsound' | 4 | 'big' | 'blink' | 'blockquote' | 'body' | 'br' | 'button' | 'canvas' | 5 | 'caption' | 'center' | 'cite' | 'code' | 'col' | 'colgroup' | 'command' | 6 | 'content' | 'data' | 'datalist' | 'dd' | 'del' | 'details' | 'dfn' | 'dialog' | 7 | 'dir' | 'div' | 'dl' | 'dt' | 'element' | 'em' | 'embed' | 'fieldset' | 8 | 'figcaption' | 'figure' | 'font' | 'footer' | 'form' | 'frame' | 'frameset' | 9 | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'head' | 'header' | 'hgroup' | 'hr' | 10 | 'html' | 'i' | 'iframe' | 'image' | 'img' | 'input' | 'ins' | 'isindex' | 11 | 'kbd' | 'keygen' | 'label' | 'legend' | 'li' | 'link' | 'listing' | 'main' | 12 | 'map' | 'mark' | 'marquee' | 'math' | 'menu' | 'menuitem' | 'meta' | 'meter' | 13 | 'multicol' | 'nav' | 'nextid' | 'nobr' | 'noembed' | 'noframes' | 14 | 'noscript' | 'object' | 'ol' | 'optgroup' | 'option' | 'output' | 'p' | 15 | 'param' | 'picture' | 'plaintext' | 'pre' | 'progress' | 'q' | 'rb' | 'rbc' | 16 | 'rp' | 'rt' | 'rtc' | 'ruby' | 's' | 'samp' | 'script' | 'section' | 'select' | 17 | 'shadow' | 'small' | 'source' | 'spacer' | 'span' | 'strike' | 'strong' | 18 | 'style' | 'sub' | 'summary' | 'sup' | 'table' | 'tbody' | 'td' | 'template' | 19 | 'textarea' | 'tfoot' | 'th' | 'thead' | 'time' | 'title' | 'tr' | 'track' | 20 | 'tt' | 'u' | 'ul' | 'video' | 'wbr' | 'xmp'; 21 | 22 | export type SvgTagNames = 'svg' | 23 | 'a' | 'altGlyph' | 'altGlyphDef' | 'altGlyphItem' | 'animate' | 'animateColor' | 24 | 'animateMotion' | 'animateTransform' | 'circle' | 'clipPath' | 'colorProfile' | 25 | 'cursor' | 'defs' | 'desc' | 'ellipse' | 'feBlend' | 'feColorMatrix' | 26 | 'feComponentTransfer' | 'feComposite' | 'feConvolveMatrix' | 'feDiffuseLighting' | 27 | 'feDisplacementMap' | 'feDistantLight' | 'feFlood' | 'feFuncA' | 'feFuncB' | 28 | 'feFuncG' | 'feFuncR' | 'feGaussianBlur' | 'feImage' | 'feMerge' | 'feMergeNode' | 29 | 'feMorphology' | 'feOffset' | 'fePointLight' | 'feSpecularLighting' | 30 | 'feSpotlight' | 'feTile' | 'feTurbulence' | 'filter' | 'font' | 'fontFace' | 31 | 'fontFaceFormat' | 'fontFaceName' | 'fontFaceSrc' | 'fontFaceUri' | 32 | 'foreignObject' | 'g' | 'glyph' | 'glyphRef' | 'hkern' | 'image' | 'line' | 33 | 'linearGradient' | 'marker' | 'mask' | 'metadata' | 'missingGlyph' | 'mpath' | 34 | 'path' | 'pattern' | 'polygon' | 'polyline' | 'radialGradient' | 'rect' | 'script' | 35 | 'set' | 'stop' | 'style' | 'switch' | 'symbol' | 'text' | 'textPath' | 'title' | 36 | 'tref' | 'tspan' | 'use' | 'view' | 'vkern'; 37 | -------------------------------------------------------------------------------- /src/types/virtual-dom.ts: -------------------------------------------------------------------------------- 1 | export type PreHook = () => any; 2 | export type InitHook = (vNode: VNode) => any; 3 | export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any; 4 | export type InsertHook = (vNode: VNode) => any; 5 | export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any; 6 | export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any; 7 | export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any; 8 | export type DestroyHook = (vNode: VNode) => any; 9 | export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any; 10 | export type PostHook = () => any; 11 | 12 | export interface Hooks { 13 | pre?: PreHook; 14 | init?: InitHook; 15 | create?: CreateHook; 16 | insert?: InsertHook; 17 | prepatch?: PrePatchHook; 18 | update?: UpdateHook; 19 | postpatch?: PostPatchHook; 20 | destroy?: DestroyHook; 21 | remove?: RemoveHook; 22 | post?: PostHook; 23 | } 24 | 25 | export interface Module { 26 | pre?: PreHook; 27 | create?: CreateHook; 28 | update?: UpdateHook; 29 | destroy?: DestroyHook; 30 | remove?: RemoveHook; 31 | post?: PostHook; 32 | } 33 | 34 | export interface SnabbdomAPI { 35 | createElement(tagName: string): A; 36 | createElementNS(namespaceURI: string, qualifiedName: string): A; 37 | createTextNode(text: string): B; 38 | insertBefore(parentNode: A | B, newNode: A | B, referenceNode: A | B): void; 39 | removeChild(node: A | B, child: A | B): void; 40 | appendChild(node: A, child: A | B): void; 41 | parentNode(node: A | B): A | B; 42 | nextSibling(node: A | B): A | C; 43 | tagName(node: A): string; 44 | setTextContent(node: A | B, text: string): void; 45 | } 46 | 47 | export interface VNodeData { 48 | // modules - use any because Object type is useless 49 | props?: any; 50 | attrs?: any; 51 | class?: any; 52 | style?: any; 53 | dataset?: any; 54 | on?: any; 55 | hero?: any; 56 | // end of modules 57 | hook?: Hooks; 58 | key?: string | number; 59 | ns?: string; // for SVGs 60 | fn?: () => VNode; // for thunks 61 | args?: Array; // for thunks 62 | attachData?: any; // for attachTo() 63 | // Cycle.js only 64 | isolate?: string; 65 | } 66 | 67 | export interface VirtualNode { 68 | tagName: string | undefined; 69 | className: string | undefined; 70 | id: string | undefined; 71 | data: VNodeData | undefined; 72 | children: Array | undefined; 73 | elm: T | undefined; 74 | text: string | undefined; 75 | key: string | number | undefined; 76 | } 77 | 78 | export type VNode = VirtualNode; 79 | export type VNodeChildren = string | number | Array; 80 | -------------------------------------------------------------------------------- /src/virtual-dom/MotorcycleVNode.ts: -------------------------------------------------------------------------------- 1 | import { VNode, VNodeData } from '../types'; 2 | 3 | export class MotorcycleVNode implements VNode { 4 | constructor( 5 | public tagName: string | undefined, 6 | public className: string | undefined, 7 | public id: string | undefined, 8 | public data: VNodeData | undefined, 9 | public children: Array | undefined, 10 | public text: string | undefined, 11 | public elm: Node | undefined, 12 | public key: string | number | undefined) { 13 | } 14 | 15 | public static createTextVNode(text: string) { 16 | return new MotorcycleVNode( 17 | undefined, 18 | undefined, 19 | undefined, 20 | undefined, 21 | undefined, 22 | text, 23 | undefined, 24 | undefined, 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/virtual-dom/helpers/h.ts: -------------------------------------------------------------------------------- 1 | import { VNode, VNodeData, VirtualNode } from '../../types'; 2 | import { MotorcycleVNode } from '../MotorcycleVNode'; 3 | import is from '../is'; 4 | 5 | function addNS(data: VNodeData, children: Array, selector: string): void { 6 | data.ns = `http://www.w3.org/2000/svg`; 7 | if (selector !== `foreignObject` && typeof children !== `undefined` && is.array(children)) { 8 | for (let i = 0; i < children.length; ++i) { 9 | addNS((children[i] as VNode).data as VNodeData, 10 | (children[i] as VNode).children as Array, 11 | (children[i] as VNode).tagName as string); 12 | } 13 | } 14 | } 15 | 16 | export interface HyperscriptFn { 17 | (sel: string): VNode; 18 | (sel: string, data: VNodeData): VNode; 19 | (sel: string, children: string | number | Array): VNode; 20 | (sel: string, data: VNodeData, children: string | number | Array): VNode; 21 | (sel: string): VirtualNode; 22 | (sel: string, data: VNodeData): VirtualNode; 23 | (sel: string, children: string | number | Array): VirtualNode; 24 | (sel: string, data: VNodeData, children: string | number | Array): VirtualNode; 25 | } 26 | 27 | export const h: HyperscriptFn = function (selector: string, b?: any, c?: any): VNode { 28 | let data: VNodeData = {}; 29 | let children: Array | undefined; 30 | let text: string | undefined; 31 | let i: number; 32 | 33 | if (arguments.length === 3) { 34 | data = b; 35 | if (is.array(c)) { 36 | children = c; 37 | } else if (is.primitive(c)) { 38 | text = String(c); 39 | } 40 | } else if (arguments.length === 2) { 41 | if (is.array(b)) { 42 | children = b; 43 | } else if (is.primitive(b)) { 44 | text = String(b); 45 | } else { 46 | data = b; 47 | } 48 | } 49 | 50 | if (is.array(children)) { 51 | children = children.filter(Boolean); 52 | 53 | for (i = 0; i < children.length; ++i) { 54 | if (is.primitive(children[i])) { 55 | children[i] = MotorcycleVNode.createTextVNode(String(children[i]) as string); 56 | } 57 | } 58 | } 59 | 60 | const { tagName, id, className } = parseSelector(selector); 61 | 62 | if (tagName === 'svg') 63 | addNS(data, children as Array, tagName); 64 | 65 | return new MotorcycleVNode( 66 | tagName, 67 | className, 68 | id, 69 | data || {}, 70 | children as Array, 71 | text, undefined, 72 | data && data.key, 73 | ); 74 | }; 75 | 76 | const classIdSplit = /([\.#]?[a-zA-Z0-9\u007F-\uFFFF_:-]+)/; 77 | 78 | export function parseSelector (selector: string) { 79 | let tagName: string | void; 80 | let id = ''; 81 | const classes: Array = []; 82 | 83 | const tagParts = selector.split(classIdSplit); 84 | 85 | let part: string | void; 86 | let type; 87 | 88 | for (let i = 0; i < tagParts.length; i++) { 89 | part = tagParts[i]; 90 | 91 | if (!part) 92 | continue; 93 | 94 | type = part.charAt(0); 95 | 96 | if (!tagName) { 97 | tagName = part; 98 | } else if (type === '.') { 99 | classes.push(part.substring(1, part.length)); 100 | } else if (type === '#') { 101 | id = part.substring(1, part.length); 102 | } 103 | } 104 | 105 | return { 106 | tagName: tagName as string, 107 | id, 108 | className: classes.join(' '), 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /src/virtual-dom/helpers/hasCssSelector.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from '../../types'; 2 | import { parseSelector } from './h'; 3 | 4 | export function hasCssSelector(cssSelector: string, vNode: VNode): boolean { 5 | if (cssSelector.indexOf(' ') > -1) 6 | throw new Error('CSS selectors can not contain spaces'); 7 | 8 | const hasTagName = cssSelector[0] !== '#' && cssSelector[0] !== '.'; 9 | 10 | const { tagName, className, id } = 11 | hasTagName ? 12 | parseSelector(cssSelector) : 13 | parseSelector(vNode.tagName + cssSelector); 14 | 15 | if (tagName !== vNode.tagName) 16 | return false; 17 | 18 | const parsedClassNames = className && className.split(' ') || []; 19 | const vNodeClassNames = vNode.className && vNode.className.split(' ') || []; 20 | 21 | for (let i = 0; i < parsedClassNames.length; ++i) { 22 | const parsedClassName = parsedClassNames[i]; 23 | 24 | if (vNodeClassNames.indexOf(parsedClassName) === -1) 25 | return false; 26 | } 27 | 28 | return id === vNode.id; 29 | } 30 | -------------------------------------------------------------------------------- /src/virtual-dom/helpers/hyperscript.ts: -------------------------------------------------------------------------------- 1 | import { VNodeData, VNodeChildren, VirtualNode, HtmlTagNames } from '../../types'; 2 | import { h } from './h'; 3 | 4 | export interface HyperscriptHelperFn { 5 | (): VirtualNode; 6 | (classNameOrId: string, data: VNodeData, children: VNodeChildren): VirtualNode; 7 | (classNameOrId: string, data: VNodeData): VirtualNode; 8 | (classNameOrId: string, children: VNodeChildren): VirtualNode; 9 | (classNameOrId: string): VirtualNode; 10 | (data: VNodeData): VirtualNode; 11 | (data: VNodeData, children: VNodeChildren): VirtualNode; 12 | (children: VNodeChildren): VirtualNode; 13 | } 14 | 15 | export function hh (tagName: HtmlTagNames): HyperscriptHelperFn { 16 | return function (): VirtualNode { 17 | const selector = arguments[0]; 18 | const data = arguments[1]; 19 | const children = arguments[2]; 20 | 21 | if (isSelector(selector)) 22 | if (Array.isArray(data)) 23 | return h(tagName + selector, {}, data); 24 | else if (typeof data === 'object') 25 | return h(tagName + selector, data, children); 26 | else 27 | return h(tagName + selector, data || {}); 28 | 29 | if (Array.isArray(selector)) 30 | return h(tagName, {}, selector); 31 | else if (typeof selector === 'object') 32 | return h(tagName, selector, data); 33 | else 34 | return h(tagName, selector || {}); 35 | }; 36 | }; 37 | 38 | function isValidString (param: any): boolean { 39 | return typeof param === 'string' && param.length > 0; 40 | } 41 | 42 | function isSelector (param: any): boolean { 43 | return isValidString(param) && (param[0] === '.' || param[0] === '#'); 44 | } 45 | 46 | export const a = hh('a'); 47 | export const abbr = hh('abbr'); 48 | export const acronym = hh('acronym'); 49 | export const address = hh('address'); 50 | export const applet = hh('applet'); 51 | export const area = hh('area'); 52 | export const article = hh('article'); 53 | export const aside = hh('aside'); 54 | export const audio = hh('audio'); 55 | export const b = hh('b'); 56 | export const base = hh('base'); 57 | export const basefont = hh('basefont'); 58 | export const bdi = hh('bdi'); 59 | export const bdo = hh('bdo'); 60 | export const bgsound = hh('bgsound'); 61 | export const big = hh('big'); 62 | export const blink = hh('blink'); 63 | export const blockquote = hh('blockquote'); 64 | export const body = hh('body'); 65 | export const br = hh('br'); 66 | export const button = hh('button'); 67 | export const canvas = hh('canvas'); 68 | export const caption = hh('caption'); 69 | export const center = hh('center'); 70 | export const cite = hh('cite'); 71 | export const code = hh('code'); 72 | export const col = hh('col'); 73 | export const colgroup = hh('colgroup'); 74 | export const command = hh('command'); 75 | export const content = hh('content'); 76 | export const data = hh('data'); 77 | export const datalist = hh('datalist'); 78 | export const dd = hh('dd'); 79 | export const del = hh('del'); 80 | export const details = hh('details'); 81 | export const dfn = hh('dfn'); 82 | export const dialog = hh('dialog'); 83 | export const dir = hh('dir'); 84 | export const div = hh('div'); 85 | export const dl = hh('dl'); 86 | export const dt = hh('dt'); 87 | export const element = hh('element'); 88 | export const em = hh('em'); 89 | export const embed = hh('embed'); 90 | export const fieldset = hh('fieldset'); 91 | export const figcaption = hh('figcaption'); 92 | export const figure = hh('figure'); 93 | export const font = hh('font'); 94 | export const footer = hh('footer'); 95 | export const form = hh('form'); 96 | export const frame = hh('frame'); 97 | export const frameset = hh('frameset'); 98 | export const h1 = hh('h1'); 99 | export const h2 = hh('h2'); 100 | export const h3 = hh('h3'); 101 | export const h4 = hh('h4'); 102 | export const h5 = hh('h5'); 103 | export const h6 = hh('h6'); 104 | export const head = hh('head'); 105 | export const header = hh('header'); 106 | export const hgroup = hh('hgroup'); 107 | export const hr = hh('hr'); 108 | export const html = hh('html'); 109 | export const img = hh('img'); 110 | export const input = hh('input'); 111 | export const ins = hh('ins'); 112 | export const isindex = hh('isindex'); 113 | export const kbd = hh('kbd'); 114 | export const keygen = hh('keygen'); 115 | export const label = hh('label'); 116 | export const legend = hh('legend'); 117 | export const li = hh('li'); 118 | export const link = hh('link'); 119 | export const listing = hh('listing'); 120 | export const main = hh('main'); 121 | export const map = hh('map'); 122 | export const mark = hh('mark'); 123 | export const marquee = hh('marquee'); 124 | export const math = hh('math'); 125 | export const menu = hh('menu'); 126 | export const menuitem = hh('menuitem'); 127 | export const meta = hh('meta'); 128 | export const meter = hh('meter'); 129 | export const multicol = hh('multicol'); 130 | export const nav = hh('nav'); 131 | export const nextid = hh('nextid'); 132 | export const nobr = hh('nobr'); 133 | export const noembed = hh('noembed'); 134 | export const noframes = hh('noframes'); 135 | export const noscript = hh('noscript'); 136 | export const object = hh('object'); 137 | export const ol = hh('ol'); 138 | export const optgroup = hh('optgroup'); 139 | export const option = hh('option'); 140 | export const output = hh('output'); 141 | export const p = hh('p'); 142 | export const param = hh('param'); 143 | export const picture = hh('picture'); 144 | export const plaintext = hh('plaintext'); 145 | export const pre = hh('pre'); 146 | export const progress = hh('progress'); 147 | export const q = hh('q'); 148 | export const rb = hh('rb'); 149 | export const rbc = hh('rbc'); 150 | export const rp = hh('rp'); 151 | export const rt = hh('rt'); 152 | export const rtc = hh('rtc'); 153 | export const ruby = hh('ruby'); 154 | export const s = hh('s'); 155 | export const samp = hh('samp'); 156 | export const script = hh('script'); 157 | export const section = hh('section'); 158 | export const select = hh('select'); 159 | export const shadow = hh('shadow'); 160 | export const small = hh('small'); 161 | export const source = hh('source'); 162 | export const spacer = hh('spacer'); 163 | export const span = hh('span'); 164 | export const strike = hh('strike'); 165 | export const strong = hh('strong'); 166 | export const style = hh('style'); 167 | export const sub = hh('sub'); 168 | export const summary = hh('summary'); 169 | export const sup = hh('sup'); 170 | export const table = hh('table'); 171 | export const tbody = hh('tbody'); 172 | export const td = hh('td'); 173 | export const template = hh('template'); 174 | export const textarea = hh('textarea'); 175 | export const tfoot = hh('tfoot'); 176 | export const th = hh('th'); 177 | export const tr = hh('tr'); 178 | export const track = hh('track'); 179 | export const tt = hh('tt'); 180 | export const u = hh('u'); 181 | export const ul = hh('ul'); 182 | export const video = hh('video'); 183 | export const wbr = hh('wbr'); 184 | export const xmp = hh('xmp'); -------------------------------------------------------------------------------- /src/virtual-dom/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './h'; 2 | export * from './hasCssSelector'; 3 | export * from './hyperscript'; 4 | export * from './svg'; 5 | -------------------------------------------------------------------------------- /src/virtual-dom/helpers/svg.ts: -------------------------------------------------------------------------------- 1 | import { VNodeData, VNodeChildren, VirtualNode, SvgTagNames } from '../../types'; 2 | import { h } from './h'; 3 | 4 | export interface SvgHyperscriptHelperFn { 5 | (): VirtualNode; 6 | (classNameOrId: string, data: VNodeData, children: VNodeChildren): VirtualNode; 7 | (classNameOrId: string, data: VNodeData): VirtualNode; 8 | (classNameOrId: string, children: VNodeChildren): VirtualNode; 9 | (classNameOrId: string): VirtualNode; 10 | (data: VNodeData): VirtualNode; 11 | (data: VNodeData, children: VNodeChildren): VirtualNode; 12 | (children: VNodeChildren): VirtualNode; 13 | } 14 | 15 | function hh (tagName: SvgTagNames): SvgHyperscriptHelperFn { 16 | return function (): VirtualNode { 17 | const selector = arguments[0]; 18 | const data = arguments[1]; 19 | const children = arguments[2]; 20 | 21 | if (isSelector(selector)) 22 | if (Array.isArray(data)) 23 | return h(tagName + selector, {}, data); 24 | else if (typeof data === 'object') 25 | return h(tagName + selector, data, children); 26 | else 27 | return h(tagName + selector, {}); 28 | 29 | if (Array.isArray(selector)) 30 | return h(tagName, {}, selector); 31 | else if (typeof selector === 'object') 32 | return h(tagName, selector, data); 33 | else 34 | return h(tagName, {}); 35 | }; 36 | }; 37 | 38 | 39 | function isValidString (param: any): boolean { 40 | return typeof param === 'string' && param.length > 0; 41 | } 42 | 43 | function isSelector (param: any): boolean { 44 | return isValidString(param) && (param[0] === '.' || param[0] === '#'); 45 | } 46 | 47 | export interface SVGHelperFn extends SvgHyperscriptHelperFn { 48 | a: SvgHyperscriptHelperFn; 49 | altGlyph: SvgHyperscriptHelperFn; 50 | altGlyphDef: SvgHyperscriptHelperFn; 51 | altGlyphItem: SvgHyperscriptHelperFn; 52 | animate: SvgHyperscriptHelperFn; 53 | animateColor: SvgHyperscriptHelperFn; 54 | animateMotion: SvgHyperscriptHelperFn; 55 | animateTransform: SvgHyperscriptHelperFn; 56 | circle: SvgHyperscriptHelperFn; 57 | clipPath: SvgHyperscriptHelperFn; 58 | colorProfile: SvgHyperscriptHelperFn; 59 | cursor: SvgHyperscriptHelperFn; 60 | defs: SvgHyperscriptHelperFn; 61 | desc: SvgHyperscriptHelperFn; 62 | ellipse: SvgHyperscriptHelperFn; 63 | feBlend: SvgHyperscriptHelperFn; 64 | feColorMatrix: SvgHyperscriptHelperFn; 65 | feComponentTransfer: SvgHyperscriptHelperFn; 66 | feComposite: SvgHyperscriptHelperFn; 67 | feConvolveMatrix: SvgHyperscriptHelperFn; 68 | feDiffuseLighting: SvgHyperscriptHelperFn; 69 | feDisplacementMap: SvgHyperscriptHelperFn; 70 | feDistantLight: SvgHyperscriptHelperFn; 71 | feFlood: SvgHyperscriptHelperFn; 72 | feFuncA: SvgHyperscriptHelperFn; 73 | feFuncB: SvgHyperscriptHelperFn; 74 | feFuncG: SvgHyperscriptHelperFn; 75 | feFuncR: SvgHyperscriptHelperFn; 76 | feGaussianBlur: SvgHyperscriptHelperFn; 77 | feImage: SvgHyperscriptHelperFn; 78 | feMerge: SvgHyperscriptHelperFn; 79 | feMergeNode: SvgHyperscriptHelperFn; 80 | feMorphology: SvgHyperscriptHelperFn; 81 | feOffset: SvgHyperscriptHelperFn; 82 | fePointLight: SvgHyperscriptHelperFn; 83 | feSpecularLighting: SvgHyperscriptHelperFn; 84 | feSpotlight: SvgHyperscriptHelperFn; 85 | feTile: SvgHyperscriptHelperFn; 86 | feTurbulence: SvgHyperscriptHelperFn; 87 | filter: SvgHyperscriptHelperFn; 88 | font: SvgHyperscriptHelperFn; 89 | fontFace: SvgHyperscriptHelperFn; 90 | fontFaceFormat: SvgHyperscriptHelperFn; 91 | fontFaceName: SvgHyperscriptHelperFn; 92 | fontFaceSrc: SvgHyperscriptHelperFn; 93 | fontFaceUri: SvgHyperscriptHelperFn; 94 | foreignObject: SvgHyperscriptHelperFn; 95 | g: SvgHyperscriptHelperFn; 96 | glyph: SvgHyperscriptHelperFn; 97 | glyphRef: SvgHyperscriptHelperFn; 98 | hkern: SvgHyperscriptHelperFn; 99 | image: SvgHyperscriptHelperFn; 100 | line: SvgHyperscriptHelperFn; 101 | linearGradient: SvgHyperscriptHelperFn; 102 | marker: SvgHyperscriptHelperFn; 103 | mask: SvgHyperscriptHelperFn; 104 | metadata: SvgHyperscriptHelperFn; 105 | missingGlyph: SvgHyperscriptHelperFn; 106 | mpath: SvgHyperscriptHelperFn; 107 | path: SvgHyperscriptHelperFn; 108 | pattern: SvgHyperscriptHelperFn; 109 | polygon: SvgHyperscriptHelperFn; 110 | polyline: SvgHyperscriptHelperFn; 111 | radialGradient: SvgHyperscriptHelperFn; 112 | rect: SvgHyperscriptHelperFn; 113 | script: SvgHyperscriptHelperFn; 114 | set: SvgHyperscriptHelperFn; 115 | stop: SvgHyperscriptHelperFn; 116 | style: SvgHyperscriptHelperFn; 117 | switch: SvgHyperscriptHelperFn; 118 | symbol: SvgHyperscriptHelperFn; 119 | text: SvgHyperscriptHelperFn; 120 | textPath: SvgHyperscriptHelperFn; 121 | title: SvgHyperscriptHelperFn; 122 | tref: SvgHyperscriptHelperFn; 123 | tspan: SvgHyperscriptHelperFn; 124 | use: SvgHyperscriptHelperFn; 125 | view: SvgHyperscriptHelperFn; 126 | vkern: SvgHyperscriptHelperFn; 127 | } 128 | 129 | function createSVGHelper (): SVGHelperFn { 130 | let svg: any = hh('svg'); 131 | 132 | svg.a = hh('a'); 133 | svg.altGlyph = hh('altGlyph'); 134 | svg.altGlyphDef = hh('altGlyphDef'); 135 | svg.altGlyphItem = hh('altGlyphItem'); 136 | svg.animate = hh('animate'); 137 | svg.animateColor = hh('animateColor'); 138 | svg.animateMotion = hh('animateMotion'); 139 | svg.animateTransform = hh('animateTransform'); 140 | svg.circle = hh('circle'); 141 | svg.clipPath = hh('clipPath'); 142 | svg.colorProfile = hh('colorProfile'); 143 | svg.cursor = hh('cursor'); 144 | svg.defs = hh('defs'); 145 | svg.desc = hh('desc'); 146 | svg.ellipse = hh('ellipse'); 147 | svg.feBlend = hh('feBlend'); 148 | svg.feColorMatrix = hh('feColorMatrix'); 149 | svg.feComponentTransfer = hh('feComponentTransfer'); 150 | svg.feComposite = hh('feComposite'); 151 | svg.feConvolveMatrix = hh('feConvolveMatrix'); 152 | svg.feDiffuseLighting = hh('feDiffuseLighting'); 153 | svg.feDisplacementMap = hh('feDisplacementMap'); 154 | svg.feDistantLight = hh('feDistantLight'); 155 | svg.feFlood = hh('feFlood'); 156 | svg.feFuncA = hh('feFuncA'); 157 | svg.feFuncB = hh('feFuncB'); 158 | svg.feFuncG = hh('feFuncG'); 159 | svg.feFuncR = hh('feFuncR'); 160 | svg.feGaussianBlur = hh('feGaussianBlur'); 161 | svg.feImage = hh('feImage'); 162 | svg.feMerge = hh('feMerge'); 163 | svg.feMergeNode = hh('feMergeNode'); 164 | svg.feMorphology = hh('feMorphology'); 165 | svg.feOffset = hh('feOffset'); 166 | svg.fePointLight = hh('fePointLight'); 167 | svg.feSpecularLighting = hh('feSpecularLighting'); 168 | svg.feSpotlight = hh('feSpotlight'); 169 | svg.feTile = hh('feTile'); 170 | svg.feTurbulence = hh('feTurbulence'); 171 | svg.filter = hh('filter'); 172 | svg.font = hh('font'); 173 | svg.fontFace = hh('fontFace'); 174 | svg.fontFaceFormat = hh('fontFaceFormat'); 175 | svg.fontFaceName = hh('fontFaceName'); 176 | svg.fontFaceSrc = hh('fontFaceSrc'); 177 | svg.fontFaceUri = hh('fontFaceUri'); 178 | svg.foreignObject = hh('foreignObject'); 179 | svg.g = hh('g'); 180 | svg.glyph = hh('glyph'); 181 | svg.glyphRef = hh('glyphRef'); 182 | svg.hkern = hh('hkern'); 183 | svg.image = hh('image'); 184 | svg.linearGradient = hh('linearGradient'); 185 | svg.marker = hh('marker'); 186 | svg.mask = hh('mask'); 187 | svg.metadata = hh('metadata'); 188 | svg.missingGlyph = hh('missingGlyph'); 189 | svg.mpath = hh('mpath'); 190 | svg.path = hh('path'); 191 | svg.pattern = hh('pattern'); 192 | svg.polygon = hh('polygon'); 193 | svg.polyline = hh('polyline'); 194 | svg.radialGradient = hh('radialGradient'); 195 | svg.rect = hh('rect'); 196 | svg.script = hh('script'); 197 | svg.set = hh('set'); 198 | svg.stop = hh('stop'); 199 | svg.style = hh('style'); 200 | svg.switch = hh('switch'); 201 | svg.symbol = hh('symbol'); 202 | svg.text = hh('text'); 203 | svg.textPath = hh('textPath'); 204 | svg.title = hh('title'); 205 | svg.tref = hh('tref'); 206 | svg.tspan = hh('tspan'); 207 | svg.use = hh('use'); 208 | svg.view = hh('view'); 209 | svg.vkern = hh('vkern'); 210 | 211 | return svg as SVGHelperFn; 212 | } 213 | 214 | export const svg: SVGHelperFn = createSVGHelper(); -------------------------------------------------------------------------------- /src/virtual-dom/htmldomapi.ts: -------------------------------------------------------------------------------- 1 | import { SnabbdomAPI } from '../types'; 2 | 3 | export function createElement(tagName: string): Element { 4 | return document.createElement(tagName); 5 | } 6 | 7 | export function createElementNS(namespaceURI: string, qualifiedName: string): Element { 8 | return document.createElementNS(namespaceURI, qualifiedName); 9 | } 10 | 11 | export function createTextNode(text: string): Text { 12 | return document.createTextNode(text); 13 | } 14 | 15 | export function insertBefore( 16 | parentNode: Element | Text, 17 | newNode: Element | Text, 18 | referenceNode: Element | Text | null): void 19 | { 20 | parentNode.insertBefore(newNode, referenceNode); 21 | } 22 | 23 | export function removeChild(node: Element | Text, child: Element | Text): void { 24 | if (node === void 0) { return; } 25 | node.removeChild(child); 26 | } 27 | 28 | export function appendChild(node: Element, child: Element | Text): void { 29 | node.appendChild(child); 30 | } 31 | 32 | export function parentNode(node: Element | Text): Element | Text { 33 | return node.parentElement as Element | Text; 34 | } 35 | 36 | export function nextSibling(node: Element | Text): Node | Element { 37 | return node.nextSibling as Node | Element; 38 | } 39 | 40 | export function tagName(node: Element | Text): string { 41 | return (node as Element).tagName || ''; 42 | } 43 | 44 | export function setTextContent(node: Element | Text, text: string): void { 45 | node.textContent = text; 46 | } 47 | 48 | const HTMLDOMAPI: SnabbdomAPI = { 49 | createElement, 50 | createElementNS, 51 | createTextNode, 52 | insertBefore, 53 | removeChild, 54 | appendChild, 55 | parentNode, 56 | nextSibling, 57 | tagName, 58 | setTextContent, 59 | }; 60 | 61 | export default HTMLDOMAPI; 62 | -------------------------------------------------------------------------------- /src/virtual-dom/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers'; 2 | export * from './init'; 3 | -------------------------------------------------------------------------------- /src/virtual-dom/init.ts: -------------------------------------------------------------------------------- 1 | import { Module, SnabbdomAPI, VNode, VNodeData } from '../types'; 2 | import { MotorcycleVNode } from './MotorcycleVNode'; 3 | import is from './is'; 4 | import domApi from './htmldomapi'; 5 | import { isDef, isUndef, sameVNode, createKeyToOldIdx, emptyNodeAt } from './util'; 6 | 7 | const hooks: string[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; 8 | 9 | const emptyVnode = new MotorcycleVNode(void 0, void 0, void 0, {}, [], void 0, void 0, void 0); 10 | 11 | export function init( 12 | modules: Module[], 13 | api?: SnabbdomAPI): (previous: VNode | HTMLElement, current: VNode) => VNode 14 | { 15 | let i: number; 16 | let j: number; 17 | let cbs: any = {}; 18 | 19 | if (isUndef(api)) api = domApi; 20 | 21 | for (i = 0; i < hooks.length; ++i) { 22 | cbs[hooks[i]] = []; 23 | for (j = 0; j < modules.length; ++j) { 24 | const hook: any = (modules[j] as any)[hooks[i]]; 25 | if (isDef(hook)) cbs[hooks[i]].push(hook.bind(modules[j])); 26 | } 27 | } 28 | 29 | function createRmCb(childElm: Element, listeners: number) { 30 | return function () { 31 | if (--listeners === 0) { 32 | const parent = (api as SnabbdomAPI).parentNode(childElm) as Element; 33 | (api as SnabbdomAPI).removeChild(parent, childElm); 34 | } 35 | }; 36 | } 37 | 38 | function createElm(vnode: VNode, insertedVnodeQueue: VNode[]) { 39 | let i: any; 40 | let data = vnode.data; 41 | if (isDef(data)) { 42 | if (isDef(i = (data as VNodeData).hook) && isDef(i = i.init)) { 43 | i(vnode); 44 | data = vnode.data; 45 | } 46 | } 47 | let elm: Element | Text; 48 | let children = vnode.children; 49 | let tagName = vnode.tagName; 50 | 51 | if (isDef(tagName)) { 52 | elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) 53 | ? (api as SnabbdomAPI).createElementNS(i, tagName as string) as HTMLElement 54 | : (api as SnabbdomAPI).createElement(tagName as string) as HTMLElement; 55 | 56 | if (vnode.id) elm.id = vnode.id; 57 | if (vnode.className) elm.className = vnode.className; 58 | 59 | if (is.array(children)) { 60 | for (i = 0; i < children.length; ++i) { 61 | (api as SnabbdomAPI).appendChild(elm, createElm(children[i] as VNode, insertedVnodeQueue) as Element | Text); 62 | } 63 | } else if (is.primitive(vnode.text)) { 64 | (api as SnabbdomAPI).appendChild(elm, (api as SnabbdomAPI).createTextNode(vnode.text as string)); 65 | } 66 | for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyVnode, vnode); 67 | i = vnode.data && vnode.data.hook; // Reuse letiable 68 | if (isDef(i)) { 69 | if (i.create) i.create(emptyVnode, vnode); 70 | if (i.insert) insertedVnodeQueue.push(vnode); 71 | } 72 | } else { 73 | elm = vnode.elm = (api as SnabbdomAPI).createTextNode(vnode.text as string); 74 | } 75 | return vnode.elm; 76 | } 77 | 78 | function addVnodes( 79 | parentElm: Element, 80 | before: Element | Text | null, 81 | vnodes: VNode[], 82 | startIdx: number, 83 | endIdx: number, 84 | insertedVnodeQueue: VNode[], 85 | ) { 86 | for (; startIdx <= endIdx; ++startIdx) { 87 | (api as SnabbdomAPI).insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue) as Element, before as Element); 88 | } 89 | } 90 | 91 | function invokeDestroyHook(vnode: VNode) { 92 | let i: any; 93 | let j: number; 94 | let data = vnode.data; 95 | if (isDef(data)) { 96 | if (isDef(i = (data as VNodeData).hook) && isDef(i = i.destroy)) i(vnode); 97 | for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); 98 | if (isDef(i = vnode.children)) { 99 | for (j = 0; j < (vnode.children as VNode[]).length; ++j) { 100 | invokeDestroyHook((vnode.children as VNode[])[j] as VNode); 101 | } 102 | } 103 | } 104 | } 105 | 106 | function removeVnodes(parentElm: Element, vnodes: VNode[], startIdx: number, endIdx: number) { 107 | for (; startIdx <= endIdx; ++startIdx) { 108 | let i: any; 109 | let listeners: number; 110 | let rm: () => void; 111 | let ch = vnodes[startIdx]; 112 | if (isDef(ch)) { 113 | if (isDef(ch.tagName)) { 114 | invokeDestroyHook(ch); 115 | listeners = cbs.remove.length + 1; 116 | rm = createRmCb(ch.elm as Element, listeners); 117 | for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); 118 | if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) { 119 | i(ch, rm); 120 | } else { 121 | rm(); 122 | } 123 | } else { // Text node 124 | (api as SnabbdomAPI).removeChild(parentElm, ch.elm as HTMLElement); 125 | } 126 | } 127 | } 128 | } 129 | 130 | function updateChildren(parentElm: Element, oldCh: Array, newCh: VNode[], insertedVnodeQueue: VNode[]) { 131 | let oldStartIdx = 0, newStartIdx = 0; 132 | let oldEndIdx = oldCh.length - 1; 133 | let oldStartVnode = oldCh[0]; 134 | let oldEndVnode = oldCh[oldEndIdx]; 135 | let newEndIdx = newCh.length - 1; 136 | let newStartVnode = newCh[0]; 137 | let newEndVnode = newCh[newEndIdx]; 138 | let oldKeyToIdx: string | number | undefined = undefined; 139 | let idxInOld: number; 140 | let elmToMove: VNode | undefined; 141 | let before: Element | null; 142 | 143 | while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { 144 | if (isUndef(oldStartVnode)) { 145 | oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left 146 | } else if (isUndef(oldEndVnode)) { 147 | oldEndVnode = oldCh[--oldEndIdx]; 148 | } else if (sameVNode(oldStartVnode as VNode, newStartVnode)) { 149 | patchVnode(oldStartVnode as VNode, newStartVnode, insertedVnodeQueue); 150 | oldStartVnode = oldCh[++oldStartIdx]; 151 | newStartVnode = newCh[++newStartIdx]; 152 | } else if (sameVNode(oldEndVnode as VNode, newEndVnode)) { 153 | patchVnode(oldEndVnode as VNode, newEndVnode, insertedVnodeQueue); 154 | oldEndVnode = oldCh[--oldEndIdx]; 155 | newEndVnode = newCh[--newEndIdx]; 156 | } else if (sameVNode(oldStartVnode as VNode, newEndVnode)) { // Vnode moved right 157 | patchVnode(oldStartVnode as VNode, newEndVnode, insertedVnodeQueue); 158 | (api as SnabbdomAPI).insertBefore( 159 | parentElm, 160 | (oldStartVnode as VNode).elm as Element, 161 | (api as SnabbdomAPI).nextSibling((oldEndVnode as VNode).elm as Element | Text) as Element, 162 | ); 163 | oldStartVnode = oldCh[++oldStartIdx]; 164 | newEndVnode = newCh[--newEndIdx]; 165 | } else if (sameVNode(oldEndVnode as VNode, newStartVnode)) { // Vnode moved left 166 | patchVnode(oldEndVnode as VNode, newStartVnode, insertedVnodeQueue); 167 | (api as SnabbdomAPI).insertBefore( 168 | parentElm, 169 | (oldEndVnode as VNode).elm as Element, 170 | (oldStartVnode as VNode).elm as Element, 171 | ); 172 | oldEndVnode = oldCh[--oldEndIdx]; 173 | newStartVnode = newCh[++newStartIdx]; 174 | } else { 175 | if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh as VNode[], oldStartIdx, oldEndIdx); 176 | idxInOld = (oldKeyToIdx as any)[(newStartVnode.key as number)] as number; 177 | if (isUndef(idxInOld)) { // New element 178 | (api as SnabbdomAPI).insertBefore( 179 | parentElm, 180 | createElm(newStartVnode, insertedVnodeQueue), 181 | (oldStartVnode as VNode).elm as Element, 182 | ); 183 | 184 | newStartVnode = newCh[++newStartIdx]; 185 | } else { 186 | elmToMove = oldCh[idxInOld]; 187 | patchVnode(elmToMove as VNode, newStartVnode, insertedVnodeQueue); 188 | oldCh[idxInOld] = void 0; 189 | const newNode = (elmToMove as any).elm; 190 | const referenceNode = (oldStartVnode as any).elm; 191 | if (newNode !== referenceNode) 192 | (api as SnabbdomAPI).insertBefore( 193 | parentElm, 194 | (elmToMove as VNode).elm as Element, 195 | (oldStartVnode as VNode).elm as Element, 196 | ); 197 | newStartVnode = newCh[++newStartIdx]; 198 | } 199 | } 200 | } 201 | if (oldStartIdx > oldEndIdx) { 202 | before = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm as Element; 203 | addVnodes(parentElm, before as Element, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); 204 | } else if (newStartIdx > newEndIdx) { 205 | removeVnodes(parentElm, oldCh as VNode[], oldStartIdx, oldEndIdx); 206 | } 207 | } 208 | 209 | function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNode[]) { 210 | let i: any; 211 | let hook: any; 212 | if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { 213 | i(oldVnode, vnode); 214 | } 215 | let elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children; 216 | 217 | if (vnode.id) 218 | elm.id = vnode.id; 219 | 220 | if (vnode.className) 221 | elm.className = vnode.className; 222 | 223 | if (oldVnode === vnode) return; 224 | if (!sameVNode(oldVnode, vnode)) { 225 | let parentElm = (api as SnabbdomAPI).parentNode(oldVnode.elm as Element); 226 | elm = createElm(vnode, insertedVnodeQueue) as HTMLElement; 227 | (api as SnabbdomAPI).insertBefore(parentElm, elm, oldVnode.elm as Element); 228 | removeVnodes(parentElm as Element, [oldVnode], 0, 0); 229 | return; 230 | } 231 | if (isDef(vnode.data)) { 232 | for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); 233 | i = (vnode.data as VNodeData).hook; 234 | if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); 235 | } 236 | if (isUndef(vnode.text)) { 237 | if (isDef(oldCh) && isDef(ch)) { 238 | if (oldCh !== ch) updateChildren(elm as Element, oldCh as VNode[], ch as VNode[], insertedVnodeQueue); 239 | } else if (isDef(ch)) { 240 | if (isDef(oldVnode.text)) (api as SnabbdomAPI).setTextContent(elm as Element, ''); 241 | addVnodes(elm as Element, null, ch as VNode[], 0, (ch as VNode[]).length - 1, insertedVnodeQueue); 242 | } else if (isDef(oldCh)) { 243 | removeVnodes(elm as Element, oldCh as VNode[], 0, (oldCh as VNode[]).length - 1); 244 | } else if (isDef(oldVnode.text)) { 245 | (api as SnabbdomAPI).setTextContent(elm as Element, ''); 246 | } 247 | } else if (oldVnode.text !== vnode.text) { 248 | (api as SnabbdomAPI).setTextContent(elm as Element, vnode.text as string); 249 | } 250 | if (isDef(hook) && isDef(i = hook.postpatch)) { 251 | i(oldVnode, vnode); 252 | } 253 | } 254 | 255 | return function patch(oldVNode: VNode | HTMLElement, vNode: VNode): VNode { 256 | let elm: Element; 257 | let parent: Element | Text; 258 | let insertedVnodeQueue: VNode[] = []; 259 | for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); 260 | 261 | if (isUndef((oldVNode as VNode).elm)) { 262 | oldVNode = emptyNodeAt(oldVNode as HTMLElement); 263 | } 264 | 265 | if (sameVNode(oldVNode as VNode, vNode)) { 266 | patchVnode(oldVNode as VNode, vNode, insertedVnodeQueue); 267 | } else { 268 | elm = (oldVNode as VNode).elm as Element; 269 | parent = (api as SnabbdomAPI).parentNode(elm); 270 | 271 | createElm(vNode, insertedVnodeQueue); 272 | 273 | if (parent !== null) { 274 | (api as SnabbdomAPI).insertBefore( 275 | parent, 276 | vNode.elm as Element, 277 | (api as SnabbdomAPI).nextSibling(elm) as Element, 278 | ); 279 | removeVnodes(parent as Element, [oldVNode as VNode], 0, 0); 280 | } 281 | } 282 | 283 | for (let i = 0; i < insertedVnodeQueue.length; ++i) { 284 | (insertedVnodeQueue[i] as any).data.hook.insert(insertedVnodeQueue[i]); 285 | } 286 | for (let i = 0; i < cbs.post.length; ++i) cbs.post[i](); 287 | return vNode; 288 | }; 289 | } 290 | -------------------------------------------------------------------------------- /src/virtual-dom/is.ts: -------------------------------------------------------------------------------- 1 | const is = { 2 | array: Array.isArray, 3 | primitive(x: any) { 4 | return typeof x === 'string' || typeof x === 'number'; 5 | }, 6 | }; 7 | 8 | export default is; 9 | -------------------------------------------------------------------------------- /src/virtual-dom/util.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from '../types'; 2 | import { tagName } from './htmldomapi'; 3 | import { MotorcycleVNode } from './MotorcycleVNode'; 4 | 5 | export function isDef (x: any): boolean { 6 | return typeof x !== 'undefined'; 7 | } 8 | 9 | export function isUndef(x: any): boolean { 10 | return typeof x === 'undefined'; 11 | } 12 | 13 | export function sameVNode(vNode1: VNode, vNode2: VNode): boolean { 14 | return vNode1.key === vNode2.key && vNode1.tagName === vNode2.tagName; 15 | } 16 | 17 | export function createKeyToOldIdx(children: VNode[], beginIdx: number, endIdx: number): any { 18 | let map: any = {}; 19 | let key: string | number; 20 | for (let i = beginIdx; i <= endIdx; ++i) { 21 | key = children[i].key as string | number; 22 | if (isDef(key)) map[key] = i; 23 | } 24 | return map; 25 | } 26 | 27 | export function emptyNodeAt(elm: HTMLElement): VNode { 28 | return new MotorcycleVNode( 29 | tagName(elm).toLowerCase(), 30 | elm.className, 31 | elm.id, 32 | {}, 33 | elm.children ? Array.prototype.slice.call(elm.childNodes).map(emptyNodeAt) : [], 34 | elm.textContent || void 0, 35 | elm, 36 | undefined, 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /test/driver/MotorcycleDomSource.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { empty, just } from 'most'; 3 | import { DomSource, div, button } from '../../src'; 4 | import * as h from 'hyperscript'; 5 | import { MotorcycleDomSource } from '../../src/dom-driver/DomSources'; 6 | import { IsolateModule } from '../../src/modules/IsolateModule'; 7 | import { SCOPE_PREFIX } from '../../src/dom-driver/DomSources/common'; 8 | 9 | describe('MotorcycleDomSource', () => { 10 | it('implements DomSource interface', () => { 11 | const domSource: DomSource = new MotorcycleDomSource(empty(), []); 12 | assert.strictEqual(typeof domSource.select, 'function'); 13 | assert.strictEqual(typeof domSource.elements, 'function'); 14 | assert.strictEqual(typeof domSource.events, 'function'); 15 | assert.strictEqual(typeof domSource.isolateSink, 'function'); 16 | assert.strictEqual(typeof domSource.isolateSource, 'function'); 17 | }); 18 | 19 | describe('select', () => { 20 | it('appends to namespace', () => { 21 | const domSource: DomSource = new MotorcycleDomSource(empty(), []); 22 | const namespace = domSource.select('hello').namespace(); 23 | 24 | assert.deepEqual(namespace, ['hello']); 25 | }); 26 | 27 | it('does not append to namespace when given `:root`', () => { 28 | const domSource: DomSource = new MotorcycleDomSource(empty(), []); 29 | const namespace = domSource.select(':root').namespace(); 30 | 31 | assert.deepEqual(namespace, []); 32 | }); 33 | }); 34 | 35 | describe('elements', () => { 36 | describe('with an empty namespace', () => { 37 | it('returns an array containing given root element', () => { 38 | const element = document.createElement('div'); 39 | const domSource = new MotorcycleDomSource(just(element), []); 40 | 41 | return domSource.elements().observe(([root]) => { 42 | assert.strictEqual(root, element); 43 | }); 44 | }); 45 | }); 46 | 47 | describe('with non-empty namespace', () => { 48 | describe('with no previous select', () => { 49 | it('returns an array of most specific element', () => { 50 | const element = document.createElement('div'); 51 | const scope = SCOPE_PREFIX + '1'; 52 | 53 | const wrongElement = document.createElement('div'); 54 | 55 | const isolatedElement = document.createElement('div'); 56 | isolatedElement.setAttribute('data-isolate', scope); 57 | 58 | element.appendChild(wrongElement); 59 | element.appendChild(isolatedElement); 60 | 61 | const domSource = new MotorcycleDomSource(just(element), [scope]); 62 | 63 | return domSource.elements().observe(elements => { 64 | assert.strictEqual(elements.length, 1); 65 | assert.strictEqual(elements[0].tagName, 'DIV'); 66 | assert.notStrictEqual(elements[0], element, 'Should not match rootElement'); 67 | assert.notStrictEqual(elements[0], wrongElement, 'Should not match non-isolated element'); 68 | assert.strictEqual(elements[0], isolatedElement); 69 | }); 70 | }); 71 | }); 72 | 73 | it('returns an array of elements matching a given selector', () => { 74 | const element = h('div', h('i.hello'), h('a.hello'), h('b.hello'), h('a.hello2')); 75 | const domSource = new MotorcycleDomSource(just(element), []); 76 | 77 | return domSource.select('.hello').elements().observe(elements => { 78 | assert.strictEqual(elements.length, 3); 79 | const [i, a, b] = elements; 80 | assert.strictEqual(i.tagName, 'I'); 81 | assert.strictEqual(a.tagName, 'A'); 82 | assert.strictEqual(b.tagName, 'B'); 83 | }); 84 | }); 85 | 86 | it('does not return elements outside of a given scope', () => { 87 | const element = h('div', 88 | h('div.foo', h('h2.baz')), 89 | h('div.bar', h('h2.baz')), 90 | ); 91 | 92 | const domSource = new MotorcycleDomSource(just(element), ['.foo']); 93 | 94 | return domSource.select('.baz').elements().observe(elements => { 95 | assert.strictEqual(elements.length, 1); 96 | }); 97 | }); 98 | 99 | it('returns svg elements', (done) => { 100 | const svgElement = document.createElementNS(`http://www.w3.org/2000/svg`, 'svg'); 101 | svgElement.setAttribute('className', 'triangle'); 102 | 103 | try { 104 | svgElement.classList.add('triangle'); 105 | } catch (e) { 106 | // done(); 107 | } 108 | 109 | const element = h('div', svgElement); 110 | const domSource = new MotorcycleDomSource(just(element), ['.triangle']); 111 | 112 | domSource.elements() 113 | .observe(elements => { 114 | assert.strictEqual(elements.length, 1); 115 | assert.strictEqual(elements[0], svgElement); 116 | done(); 117 | }) 118 | .catch(done); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('events', () => { 124 | it('should capture events from elements', (done) => { 125 | const element = h('div', [ 126 | h('button', { className: 'btn' }), 127 | ]); 128 | 129 | const domSource = new MotorcycleDomSource(just(element), ['.btn']); 130 | 131 | domSource.events('click') 132 | .observe(ev => { 133 | assert.ok(ev instanceof Event); 134 | assert.strictEqual(ev.type, 'click'); 135 | done(); 136 | }) 137 | .catch(done); 138 | 139 | domSource.elements().observe((elements) => { 140 | assert.strictEqual(elements.length, 1); 141 | 142 | (elements[0] as any).click(); 143 | }); 144 | }); 145 | 146 | it('should only create 1 event listener per event type', (done) => { 147 | const element = h('div', [ 148 | h('button', { className: 'btn' }), 149 | ]); 150 | 151 | let called = 0; 152 | 153 | element.addEventListener = function () { 154 | ++called; 155 | }; 156 | 157 | const domSource = new MotorcycleDomSource(just(element), ['.btn']); 158 | 159 | domSource.events('click').drain(); 160 | domSource.events('click').drain(); 161 | 162 | domSource.elements() 163 | .observe(() => { 164 | assert.strictEqual(called, 1); 165 | done(); 166 | }) 167 | .catch(done); 168 | }); 169 | 170 | it('removes listener when event streams end', (done) => { 171 | const element = h('div', [ 172 | h('button', { className: 'btn' }), 173 | ]); 174 | 175 | let called = 0; 176 | 177 | element.removeEventListener = function () { 178 | ++called; 179 | }; 180 | 181 | const domSource = new MotorcycleDomSource(just(element), ['.btn']); 182 | 183 | domSource.events('click').take(1).drain(); 184 | domSource.events('click').take(2).drain(); 185 | 186 | domSource.elements().observe(elements => { 187 | const btn: any = elements[0]; 188 | 189 | btn.click(); 190 | btn.click(); 191 | 192 | setTimeout(() => { 193 | assert.strictEqual(called, 1); 194 | done(); 195 | }); 196 | }); 197 | }); 198 | 199 | it('captures events using id', (done) => { 200 | const element = h('div', { id: 'myElement' }, [ 201 | h('button', { id: 'btn' }), 202 | ]); 203 | 204 | const domSource = new MotorcycleDomSource(just(element), ['#btn']); 205 | 206 | domSource.events('click').observe(ev => { 207 | assert.strictEqual(ev.type, 'click'); 208 | assert.strictEqual((ev.target as HTMLElement).id, 'btn'); 209 | done(); 210 | }); 211 | 212 | setTimeout(() => { 213 | (element.querySelector('#btn') as any).click(); 214 | }); 215 | }); 216 | 217 | it('captures rootElement events using id', (done) => { 218 | const element = h('div', { id: 'myElement' }, [ 219 | h('button', { id: 'btn' }), 220 | ]); 221 | 222 | const domSource = new MotorcycleDomSource(just(element), ['#myElement']); 223 | 224 | domSource.events('click').observe(ev => { 225 | assert.strictEqual(ev.type, 'click'); 226 | assert.strictEqual((ev.target as HTMLElement).id, 'myElement'); 227 | done(); 228 | }); 229 | 230 | setTimeout(() => { 231 | element.click(); 232 | }); 233 | }); 234 | 235 | it('captures events from multiple elements', (done) => { 236 | const element = h('div', {}, [ 237 | h('button.clickable.first', {}, 'first'), 238 | h('button.clickable.second', {}, 'second'), 239 | ]); 240 | 241 | const domSource = new MotorcycleDomSource(just(element), ['.clickable']); 242 | 243 | domSource.events('click').take(1).observe(ev => { 244 | assert.strictEqual((ev.target as HTMLElement).textContent, 'first'); 245 | }); 246 | 247 | domSource.events('click').skip(1).take(1).observe(ev => { 248 | assert.strictEqual((ev.target as HTMLElement).textContent, 'second'); 249 | done(); 250 | }); 251 | 252 | setTimeout(() => { 253 | (element.querySelector('.first') as any).click(); 254 | (element.querySelector('.second') as any).click(); 255 | }); 256 | }); 257 | 258 | it('captures non-bubbling events', () => { 259 | const form = h('form.form', [ 260 | h('input', { type: 'text' }), 261 | ]); 262 | 263 | const element = h('div', {}, [form]); 264 | 265 | const domSource = new MotorcycleDomSource(just(element), ['.form']); 266 | 267 | setTimeout(() => { 268 | form.dispatchEvent(new Event('reset', { bubbles: false })); 269 | }); 270 | 271 | return domSource.events('reset').take(1).observe(ev => { 272 | assert.strictEqual(ev.type, 'reset'); 273 | }); 274 | }); 275 | }); 276 | 277 | describe('isolateSink', () => { 278 | it('adds isolation information to vNode', () => { 279 | const buttonElement = h('button', { className: 'btn' }) as HTMLButtonElement; 280 | const divElement = h('div', [buttonElement]) as HTMLDivElement; 281 | 282 | const btn = button('.btn'); 283 | const vNode = div({}, [btn]); 284 | 285 | btn.elm = buttonElement; 286 | vNode.elm = divElement; 287 | 288 | const vNode$ = just(vNode); 289 | 290 | const domSource = new MotorcycleDomSource(just(divElement), []); 291 | 292 | return domSource.isolateSink(vNode$, `hello`).observe(vNode => { 293 | assert.strictEqual(vNode.data && vNode.data.isolate, `$$MOTORCYCLEDOM$$-hello`); 294 | }); 295 | }); 296 | }); 297 | 298 | describe('isolateSource', () => { 299 | it('returns a new DomSource with amended namespace', () => { 300 | const domSource = new MotorcycleDomSource(just(h('div')), []); 301 | 302 | assert.deepEqual(domSource.isolateSource(domSource, 'hello').namespace(), ['$$MOTORCYCLEDOM$$-hello']); 303 | }); 304 | }); 305 | 306 | describe('isolation', () => { 307 | it('prevents parent from DOM.selecting() inside the isolation', function () { 308 | const isolatedButton = h('button.btn', {}, []) as HTMLButtonElement; 309 | const isolatedButtonVNode = button('.btn'); 310 | isolatedButtonVNode.elm = isolatedButton; 311 | 312 | const isolatedDiv = h('div', {}, [isolatedButton]) as HTMLDivElement; 313 | const isolatedDivVNode = div({ isolate: '$$MOTORCYCLEDOM$$-foo' }, [isolatedButtonVNode]); 314 | isolatedDivVNode.elm = isolatedDiv; 315 | 316 | const buttonElement = h('button.btn', {}, []) as HTMLButtonElement; 317 | const buttonVNode = button('.btn'); 318 | buttonVNode.elm = buttonElement; 319 | 320 | const divElement = h('div', {}, [buttonElement]) as HTMLDivElement; 321 | const divVNode = div({}, [buttonVNode]); 322 | divVNode.elm = divElement; 323 | 324 | const parentDiv = h('div', {}, [divElement, isolatedDiv]) as HTMLDivElement; 325 | const parentDivVNode = div({}, [divVNode, isolatedDivVNode]); 326 | parentDivVNode.elm = parentDiv; 327 | 328 | const isolateModule = new IsolateModule(); 329 | 330 | isolateModule.create(isolatedDivVNode, isolatedDivVNode); 331 | isolateModule.create(isolatedButtonVNode, isolatedButtonVNode); 332 | 333 | assert.strictEqual(isolatedButton.getAttribute('data-isolate'), '$$MOTORCYCLEDOM$$-foo'); 334 | 335 | const domSource = new MotorcycleDomSource(just(parentDiv), []); 336 | const isolatedDomSource = domSource.isolateSource(domSource, 'foo'); 337 | 338 | domSource.select('.btn').events('click').observe(() => { 339 | throw new Error('Parent event listener should not receive isolated event'); 340 | }); 341 | 342 | setTimeout(() => { 343 | isolatedButton.click(); 344 | }); 345 | 346 | return isolatedDomSource.select('.btn').events('click').take(1).observe((ev) => { 347 | assert.strictEqual(ev.target, isolatedButton); 348 | }); 349 | }); 350 | }); 351 | }); 352 | -------------------------------------------------------------------------------- /test/driver/integration/events.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as Motorcycle from '@motorcycle/core'; 3 | import { div, h4, h3, h2, input, makeDomDriver } from '../../../src'; 4 | import * as most from 'most'; 5 | import { createRenderTarget } from '../../helpers/createRenderTarget'; 6 | 7 | describe('DOMSource.events()', function () { 8 | it('should catch a basic click interaction Observable', function (done) { 9 | function app() { 10 | return { 11 | DOM: most.of(h3('.myelementclass', 'Foobar')), 12 | }; 13 | } 14 | 15 | const { sources, dispose } = Motorcycle.run(app, { 16 | DOM: makeDomDriver(createRenderTarget()), 17 | }); 18 | 19 | sources.DOM.select('.myelementclass').events('click').observe((ev: Event) => { 20 | assert.strictEqual(ev.type, 'click'); 21 | assert.strictEqual((ev.target as HTMLHeadElement).textContent, 'Foobar'); 22 | dispose(); 23 | done(); 24 | }); 25 | // Make assertions 26 | sources.DOM.select(':root').elements().skip(1).take(1).observe(function ([root]: HTMLElement[]) { 27 | const myElement: any = root.querySelector('.myelementclass'); 28 | assert.notStrictEqual(myElement, null); 29 | assert.notStrictEqual(typeof myElement, 'undefined'); 30 | assert.strictEqual(myElement.tagName, 'H3'); 31 | assert.doesNotThrow(function () { 32 | setTimeout(() => myElement.click()); 33 | }); 34 | }); 35 | 36 | }); 37 | 38 | it('should setup click detection with events() after run() occurs', function (done) { 39 | function app() { 40 | return { 41 | DOM: most.of(h3('.test2.myelementclass', 'Foobar')), 42 | }; 43 | } 44 | 45 | const { sources, dispose } = Motorcycle.run(app, { 46 | DOM: makeDomDriver(createRenderTarget()), 47 | }); 48 | 49 | sources.DOM.select('.myelementclass').events('click').observe((ev: Event) => { 50 | assert.strictEqual(ev.type, 'click'); 51 | assert.strictEqual((ev.target as HTMLHeadElement).textContent, 'Foobar'); 52 | dispose(); 53 | done(); 54 | }); 55 | // Make assertions 56 | setTimeout(() => { 57 | const myElement = document.querySelector('.test2.myelementclass') as HTMLElement; 58 | assert.notStrictEqual(myElement, null); 59 | assert.notStrictEqual(typeof myElement, 'undefined'); 60 | assert.strictEqual(myElement.tagName, 'H3'); 61 | assert.doesNotThrow(function () { 62 | setTimeout(() => (myElement as any).click()); 63 | }); 64 | }, 200); 65 | }); 66 | 67 | it('should setup click detection on a ready DOM element (e.g. from server)', function (done) { 68 | function app() { 69 | return { 70 | DOM: most.never(), 71 | }; 72 | } 73 | 74 | const containerElement = createRenderTarget(); 75 | let headerElement = document.createElement('H3'); 76 | headerElement.className = 'myelementclass'; 77 | headerElement.textContent = 'Foobar'; 78 | containerElement.appendChild(headerElement); 79 | 80 | const { sources, dispose } = Motorcycle.run(app, { 81 | DOM: makeDomDriver(containerElement), 82 | }); 83 | 84 | sources.DOM.select('.myelementclass').events('click').observe((ev: Event) => { 85 | assert.strictEqual(ev.type, 'click'); 86 | assert.strictEqual((ev.target as HTMLHeadElement).textContent, 'Foobar'); 87 | dispose(); 88 | done(); 89 | }); 90 | // Make assertions 91 | setTimeout(() => { 92 | const myElement = containerElement.querySelector('.myelementclass') as HTMLElement; 93 | assert.notStrictEqual(myElement, null); 94 | assert.notStrictEqual(typeof myElement, 'undefined'); 95 | assert.strictEqual(myElement.tagName, 'H3'); 96 | assert.doesNotThrow(function () { 97 | setTimeout(() => (myElement as any).click()); 98 | }); 99 | }, 200); 100 | }); 101 | 102 | it('should catch events using id of root element in DOM.select', function (done) { 103 | function app() { 104 | return { 105 | DOM: most.of(h3('.myelementclass', 'Foobar')), 106 | }; 107 | } 108 | 109 | const { sources, dispose } = Motorcycle.run(app, { 110 | DOM: makeDomDriver(createRenderTarget('parent-001')), 111 | }); 112 | 113 | // Make assertions 114 | sources.DOM.select('#parent-001').events('click').observe((ev: Event) => { 115 | assert.strictEqual(ev.type, 'click'); 116 | assert.strictEqual((ev.target as HTMLHeadElement).textContent, 'Foobar'); 117 | dispose(); 118 | done(); 119 | }); 120 | 121 | sources.DOM.select(':root').elements().skip(1).take(1).observe(function ([root]: HTMLElement[]) { 122 | const myElement: any = root.querySelector('.myelementclass'); 123 | assert.notStrictEqual(myElement, null); 124 | assert.notStrictEqual(typeof myElement, 'undefined'); 125 | assert.strictEqual(myElement.tagName, 'H3'); 126 | assert.doesNotThrow(function () { 127 | setTimeout(() => myElement.click()); 128 | }); 129 | }); 130 | 131 | }); 132 | 133 | it('should catch events using id of top element in DOM.select', function (done) { 134 | function app() { 135 | return { 136 | DOM: most.of(h3('#myElementId', 'Foobar')), 137 | }; 138 | } 139 | 140 | const { sources, dispose } = Motorcycle.run(app, { 141 | DOM: makeDomDriver(createRenderTarget('parent-002')), 142 | }); 143 | 144 | // Make assertions 145 | sources.DOM.select('#myElementId').events('click').observe((ev: Event) => { 146 | assert.strictEqual(ev.type, 'click'); 147 | assert.strictEqual((ev.target as HTMLHeadElement).textContent, 'Foobar'); 148 | dispose(); 149 | done(); 150 | }); 151 | 152 | sources.DOM.select(':root').elements().skip(1).take(1) 153 | .observe(function ([root]: HTMLElement[]) { 154 | const myElement: any = root.querySelector('#myElementId'); 155 | assert.notStrictEqual(myElement, null); 156 | assert.notStrictEqual(typeof myElement, 'undefined'); 157 | assert.strictEqual(myElement.tagName, 'H3'); 158 | assert.doesNotThrow(function () { 159 | setTimeout(() => myElement.click()); 160 | }); 161 | }); 162 | 163 | }); 164 | 165 | it('should catch interaction events without prior select()', function (done) { 166 | function app() { 167 | return { 168 | DOM: most.of(div('.parent', [ 169 | h3('.myelementclass', 'Foobar'), 170 | ])), 171 | }; 172 | } 173 | 174 | const { sources, dispose } = Motorcycle.run(app, { 175 | DOM: makeDomDriver(createRenderTarget()), 176 | }); 177 | 178 | // Make assertions 179 | sources.DOM.events('click').observe((ev: Event) => { 180 | assert.strictEqual(ev.type, 'click'); 181 | assert.strictEqual((ev.target as HTMLHeadElement).textContent, 'Foobar'); 182 | dispose(); 183 | done(); 184 | }); 185 | 186 | sources.DOM.select(':root').elements().skip(1).take(1).observe(function ([root]: HTMLElement[]) { 187 | const myElement: any = root.querySelector('.myelementclass'); 188 | assert.notStrictEqual(myElement, null); 189 | assert.notStrictEqual(typeof myElement, 'undefined'); 190 | assert.strictEqual(myElement.tagName, 'H3'); 191 | assert.doesNotThrow(function () { 192 | setTimeout(() => myElement.click()); 193 | }); 194 | }); 195 | 196 | }); 197 | 198 | it('should catch user events using DOM.select().select().events()', function (done) { 199 | function app() { 200 | return { 201 | DOM: most.of( 202 | h3('.top-most', [ 203 | h2('.bar', 'Wrong'), 204 | div('.foo', [ 205 | h4('.bar', 'Correct'), 206 | ]), 207 | ]), 208 | ), 209 | }; 210 | } 211 | 212 | const { sources, dispose } = Motorcycle.run(app, { 213 | DOM: makeDomDriver(createRenderTarget()), 214 | }); 215 | 216 | // Make assertions 217 | sources.DOM.select('.foo').select('.bar').events('click').observe((ev: Event) => { 218 | assert.strictEqual(ev.type, 'click'); 219 | assert.strictEqual((ev.target as HTMLHeadElement).textContent, 'Correct'); 220 | dispose(); 221 | done(); 222 | }); 223 | 224 | sources.DOM.select(':root').elements().skip(1).take(1) 225 | .observe(function ([root]: HTMLElement[]) { 226 | const wrongElement: any = root.querySelector('.bar'); 227 | const correctElement: any = root.querySelector('.foo .bar'); 228 | assert.notStrictEqual(wrongElement, null); 229 | assert.notStrictEqual(correctElement, null); 230 | assert.notStrictEqual(typeof wrongElement, 'undefined'); 231 | assert.notStrictEqual(typeof correctElement, 'undefined'); 232 | assert.strictEqual(wrongElement.tagName, 'H2'); 233 | assert.strictEqual(correctElement.tagName, 'H4'); 234 | assert.doesNotThrow(function () { 235 | setTimeout(() => wrongElement.click()); 236 | setTimeout(() => correctElement.click(), 15); 237 | }); 238 | }); 239 | 240 | }); 241 | 242 | it('should catch events from many elements using DOM.select().events()', function (done) { 243 | function app() { 244 | return { 245 | DOM: most.of(div('.parent', [ 246 | h4('.clickable.first', 'First'), 247 | h4('.clickable.second', 'Second'), 248 | ])), 249 | }; 250 | } 251 | 252 | const { sources, dispose } = Motorcycle.run(app, { 253 | DOM: makeDomDriver(createRenderTarget()), 254 | }); 255 | 256 | // Make assertions 257 | sources.DOM.select('.clickable').events('click').take(1) 258 | .observe((ev: Event) => { 259 | assert.strictEqual(ev.type, 'click'); 260 | assert.strictEqual((ev.target as HTMLHeadElement).textContent, 'First'); 261 | }); 262 | 263 | sources.DOM.select('.clickable').events('click').skip(1).take(1) 264 | .observe((ev: Event) => { 265 | assert.strictEqual(ev.type, 'click'); 266 | assert.strictEqual((ev.target as HTMLHeadElement).textContent, 'Second'); 267 | dispose(); 268 | done(); 269 | }); 270 | 271 | sources.DOM.select(':root').elements().skip(1).take(1) 272 | .observe(function ([root]: HTMLElement[]) { 273 | const firstElem: any = root.querySelector('.first'); 274 | const secondElem: any = root.querySelector('.second'); 275 | assert.notStrictEqual(firstElem, null); 276 | assert.notStrictEqual(typeof firstElem, 'undefined'); 277 | assert.notStrictEqual(secondElem, null); 278 | assert.notStrictEqual(typeof secondElem, 'undefined'); 279 | assert.doesNotThrow(function () { 280 | setTimeout(() => firstElem.click()); 281 | setTimeout(() => secondElem.click(), 5); 282 | }); 283 | }); 284 | 285 | }); 286 | 287 | it('should catch interaction events from future elements', function (done) { 288 | function app() { 289 | return { 290 | DOM: most.concat( 291 | most.of(h2('.blesh', 'Blesh')), 292 | most.of(h3('.blish', 'Blish')).delay(100), 293 | ).concat(most.of(h4('.blosh', 'Blosh')).delay(100)), 294 | }; 295 | } 296 | 297 | const { sources, dispose } = Motorcycle.run(app, { 298 | DOM: makeDomDriver(createRenderTarget('parent-002')), 299 | }); 300 | 301 | // Make assertions 302 | sources.DOM.select('.blosh').events('click').observe((ev: Event) => { 303 | assert.strictEqual(ev.type, 'click'); 304 | assert.strictEqual((ev.target as HTMLHeadElement).textContent, 'Blosh'); 305 | dispose(); 306 | done(); 307 | }); 308 | 309 | sources.DOM.select(':root').elements().skip(3).take(1) 310 | .observe(function ([root]: HTMLElement[]) { 311 | const myElement: any = root.querySelector('.blosh'); 312 | assert.notStrictEqual(myElement, null); 313 | assert.notStrictEqual(typeof myElement, 'undefined'); 314 | assert.strictEqual(myElement.tagName, 'H4'); 315 | assert.strictEqual(myElement.textContent, 'Blosh'); 316 | assert.doesNotThrow(function () { 317 | setTimeout(() => myElement.click()); 318 | }); 319 | }); 320 | 321 | }); 322 | 323 | it('should catch a non-bubbling click event with useCapture', function (done) { 324 | function app() { 325 | return { 326 | DOM: most.of(div('.parent', [ 327 | div('.clickable', 'Hello'), 328 | ])), 329 | }; 330 | } 331 | 332 | function click (el: HTMLElement) { 333 | const ev: any = document.createEvent(`MouseEvent`); 334 | ev.initMouseEvent( 335 | `click`, 336 | false /* bubble */, true /* cancelable */, 337 | window, null, 338 | 0, 0, 0, 0, /* coordinates */ 339 | false, false, false, false, /* modifier keys */ 340 | 0 /*left*/, null, 341 | ); 342 | el.dispatchEvent(ev); 343 | } 344 | 345 | const { sources } = Motorcycle.run(app, { 346 | DOM: makeDomDriver(createRenderTarget()), 347 | }); 348 | 349 | sources.DOM.select('.clickable').events('click', {useCapture: true}) 350 | .observe((ev: Event) => { 351 | assert.strictEqual(ev.type, 'click'); 352 | assert.strictEqual((ev.target as HTMLElement).tagName, 'DIV'); 353 | assert.strictEqual((ev.target as HTMLElement).className, 'clickable'); 354 | assert.strictEqual((ev.target as HTMLHeadElement).textContent, 'Hello'); 355 | done(); 356 | }); 357 | 358 | sources.DOM.select('.clickable').events('click', {useCapture: false}) 359 | .observe(assert.fail); 360 | 361 | sources.DOM.select(':root').elements().skip(1).take(1).observe(([root]: HTMLElement[]) => { 362 | const clickable: any = root.querySelector('.clickable'); 363 | setTimeout(() => click(clickable)); 364 | }); 365 | ; 366 | }); 367 | 368 | it('should catch a blur event with useCapture', function (done) { 369 | if (!document.hasFocus()) return done(); 370 | 371 | function app() { 372 | return { 373 | DOM: most.of(div('.parent', [ 374 | input('.correct', { props: { type: 'text' } }, []), 375 | input('.wrong', { props: { type: 'text' } }, []), 376 | input('.dummy', { props: { type: 'text' } }), 377 | ])), 378 | }; 379 | } 380 | 381 | const { sources } = Motorcycle.run(app, { 382 | DOM: makeDomDriver(createRenderTarget()), 383 | }); 384 | 385 | sources.DOM.select('.correct').events('blur', {useCapture: true}) 386 | .observe((ev: Event) => { 387 | assert.strictEqual(ev.type, 'blur'); 388 | assert.strictEqual((ev.target as HTMLElement).className, 'correct'); 389 | done(); 390 | }); 391 | 392 | sources.DOM.select(':root').elements().skip(1).take(1).observe(([root]: HTMLElement[]) => { 393 | const correct: any = root.querySelector('.correct'); 394 | const wrong: any = root.querySelector('.wrong'); 395 | const dummy: any = root.querySelector('.dummy'); 396 | setTimeout(() => wrong.focus(), 50); 397 | setTimeout(() => dummy.focus(), 100); 398 | setTimeout(() => correct.focus(), 150); 399 | setTimeout(() => dummy.focus(), 200); 400 | }); 401 | ; 402 | }); 403 | 404 | it('should catch a blur event by default (no options)', function (done) { 405 | if (!document.hasFocus()) return done(); 406 | 407 | function app() { 408 | return { 409 | DOM: most.of(div('.parent', [ 410 | input('.correct', { props: { type: 'text' } }, []), 411 | input('.wrong', { props: { type: 'text' } }, []), 412 | input('.dummy', { props: { type: 'text' } }), 413 | ])), 414 | }; 415 | } 416 | 417 | const { sources } = Motorcycle.run(app, { 418 | DOM: makeDomDriver(createRenderTarget()), 419 | }); 420 | 421 | sources.DOM.select('.correct').events('blur') 422 | .observe((ev: Event) => { 423 | assert.strictEqual(ev.type, 'blur'); 424 | assert.strictEqual((ev.target as HTMLElement).className, 'correct'); 425 | done(); 426 | }); 427 | 428 | sources.DOM.select(':root').elements().skip(1).take(1).observe(([root]: HTMLElement[]) => { 429 | const correct: any = root.querySelector('.correct'); 430 | const wrong: any = root.querySelector('.wrong'); 431 | const dummy: any = root.querySelector('.dummy'); 432 | setTimeout(() => wrong.focus(), 50); 433 | setTimeout(() => dummy.focus(), 100); 434 | setTimeout(() => correct.focus(), 150); 435 | setTimeout(() => dummy.focus(), 200); 436 | }); 437 | }); 438 | }); 439 | -------------------------------------------------------------------------------- /test/driver/integration/rendering.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { Stream, just } from 'most'; 3 | import { makeDomDriver, div, h2, a, p, DomSource, VNode } from '../../../src'; 4 | import { run } from '@motorcycle/core'; 5 | 6 | interface DomSources { 7 | dom: DomSource; 8 | } 9 | 10 | interface DomSinks { 11 | dom: Stream; 12 | } 13 | 14 | describe('rendering', () => { 15 | it('patches text nodes initially present on rootElement', (done) => { 16 | function main() { 17 | const dom = just( 18 | div('#page', {}, [ 19 | h2('Home'), 20 | div({}, [ 21 | a('.connect', { props: { href: '/connect' } }, 'Connect'), 22 | p(' | '), 23 | a('.signin', { props: { href: '/signin' } }, 'Sign In'), 24 | ]), 25 | div(`Hello world`), 26 | ]), 27 | ); 28 | 29 | return { dom }; 30 | } 31 | 32 | const { sources, dispose } = run(main, { 33 | dom: makeDomDriver(createInitialRenderTarget()), 34 | }); 35 | 36 | sources.dom.elements().skip(1).take(1) 37 | .observe(elements => { 38 | const rootElement = elements[0] as HTMLDivElement; 39 | 40 | assert.strictEqual(rootElement.id, 'test'); 41 | assert.strictEqual(rootElement.childNodes.length, 1); 42 | 43 | const page = rootElement.childNodes[0] as HTMLDivElement; 44 | 45 | assert.strictEqual(page.childNodes.length, 3); 46 | 47 | assert.strictEqual((page.childNodes[0] as HTMLHeadingElement).tagName, 'H2'); 48 | assert.strictEqual((page.childNodes[1] as HTMLDivElement).tagName, 'DIV'); 49 | 50 | dispose(); 51 | done(); 52 | }) 53 | .catch(done); 54 | }); 55 | }); 56 | 57 | function createInitialRenderTarget(): HTMLDivElement { 58 | const rootElement = document.createElement('div'); 59 | rootElement.id = 'test'; 60 | rootElement.className = 'unresolved'; 61 | 62 | const textContainer = document.createElement('div'); 63 | 64 | const firstTextNode = document.createTextNode('Motorcycle'); 65 | 66 | textContainer.appendChild(firstTextNode); 67 | 68 | const spanElement = document.createElement('span'); 69 | 70 | const secondTextNode = document.createTextNode('.js'); 71 | 72 | spanElement.appendChild(secondTextNode); 73 | 74 | textContainer.appendChild(spanElement); 75 | textContainer.appendChild(spanElement); 76 | 77 | rootElement.appendChild(textContainer); 78 | 79 | document.body.appendChild(rootElement); 80 | 81 | return rootElement; 82 | } 83 | -------------------------------------------------------------------------------- /test/driver/issue-105.ts: -------------------------------------------------------------------------------- 1 | import { run } from '@motorcycle/core'; 2 | import { makeDomDriver, button, div } from '../../src'; 3 | import { just } from 'most'; 4 | import isolate from '@cycle/isolate'; 5 | import * as assert from 'assert'; 6 | 7 | // TESTS 8 | describe('issue 105', () => { 9 | it('should only emit a single event with useCapture false', (done) => { 10 | const { sources, sinks }: any = run(withUseCaptureFalse, { 11 | dom: makeDomDriver(document.createElement('div')), 12 | }); 13 | 14 | sources.dom.elements().skip(1).take(1).observe(([root]: Element[]) => { 15 | const button = root.querySelector('button'); 16 | if (!button) 17 | done(new Error('Can not find button')); 18 | 19 | setTimeout(() => (button as any).click()); 20 | }); 21 | 22 | let called = 0; 23 | sinks.toggle$.skip(1).tap(() => ++called).observe(() => { 24 | assert.strictEqual(called, 1); 25 | setTimeout(done, 100); 26 | }).catch(done); 27 | }); 28 | 29 | it('should only emit a single event with useCapture true', (done) => { 30 | const { sources, sinks }: any = run(withUseCaptureTrue, { 31 | dom: makeDomDriver(document.createElement('div')), 32 | }); 33 | 34 | sources.dom.elements().skip(1).take(1).observe(([root]: Element[]) => { 35 | const button = root.querySelector('button'); 36 | if (!button) 37 | done(new Error('Can not find button')); 38 | 39 | setTimeout(() => (button as any).click()); 40 | }); 41 | 42 | let called = 0; 43 | sinks.toggle$.skip(1).tap(() => ++called).observe(() => { 44 | assert.strictEqual(called, 1); 45 | done(); 46 | }).catch(done); 47 | }); 48 | }); 49 | 50 | function withUseCaptureFalse (sources: any) { 51 | // ORIGINAL PROBLEM: 52 | // ------- 53 | // Setting `useCapture: true` causes 54 | // Uncaught SyntaxError: Failed to execute 'matches' on 'Element': '' is not a valid selector. 55 | // 56 | // Setting `useCapture: false` causes event to fire twice. 57 | const events = { click: { useCapture: false } }; 58 | 59 | const aButton = isolate( 60 | augmentComponentWithEvents(Button, events), 61 | )(sources); 62 | 63 | const aButtonToggle$ = aButton.click$.scan((previous: boolean) => !previous, false).multicast(); 64 | 65 | const childViews$ = aButton.dom.map((view: any) => [view]); 66 | 67 | const aHeaderSources = { childViews$ }; 68 | 69 | const aHeader = augmentComponent(Header, { aButtonToggle$ })(aHeaderSources); 70 | 71 | return { 72 | dom: aHeader.dom, 73 | toggle$: aButtonToggle$, 74 | }; 75 | } 76 | 77 | function withUseCaptureTrue (sources: any) { 78 | // ORIGINAL PROBLEM: 79 | // ------- 80 | // Setting `useCapture: true` causes 81 | // Uncaught SyntaxError: Failed to execute 'matches' on 'Element': '' is not a valid selector. 82 | // 83 | // Setting `useCapture: false` causes event to fire twice. 84 | const events = { click: { useCapture: true } }; 85 | 86 | const aButton = isolate( 87 | augmentComponentWithEvents(Button, events), 88 | )(sources); 89 | 90 | const aButtonToggle$ = aButton.click$.scan((previous: boolean) => !previous, false).multicast(); 91 | 92 | const childViews$ = aButton.dom.map((view: any) => [view]); 93 | 94 | const aHeaderSources = { childViews$ }; 95 | 96 | const aHeader = augmentComponent(Header, { aButtonToggle$ })(aHeaderSources); 97 | 98 | return { 99 | dom: aHeader.dom, 100 | toggle$: aButtonToggle$, 101 | }; 102 | } 103 | 104 | function Header(sources: any) { 105 | const { childViews$ } = sources; 106 | return { 107 | dom: childViews$.map((childViews: any) => { 108 | return div(`#header`, childViews); 109 | }), 110 | }; 111 | } 112 | 113 | function Button() { 114 | return { 115 | dom: just( 116 | div(`#container`, [ 117 | button([`click me`]), 118 | ]), 119 | ), 120 | }; 121 | } 122 | 123 | function augmentComponentWithEvents( 124 | Component: Function, 125 | events: { [name: string]: { useCapture?: boolean } }, 126 | ) { 127 | return function AugmentationComponent(sources: any) { 128 | const sinks = Component(sources); 129 | 130 | Object.keys(events) 131 | .forEach(function (key) { 132 | sinks[`${key}$`] = sources.dom.events(key, events[key]); 133 | }); 134 | 135 | return sinks; 136 | }; 137 | } 138 | 139 | function augmentComponent(Component: any, augmentationSinks: any) { 140 | return function AugmentationComponent(sources: any) { 141 | const sinks = Component(sources); 142 | 143 | return Object.assign(sinks, augmentationSinks); 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /test/driver/mockDomSource.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { run } from '@motorcycle/core'; 3 | import { h4, h3, h2, div, h, mockDomSource, DomSource } from '../../src/index'; 4 | import * as most from 'most'; 5 | 6 | describe('mockDOMSource', function () { 7 | it('should be in accessible in the API', function () { 8 | assert.strictEqual(typeof mockDomSource, 'function'); 9 | }); 10 | 11 | it('should make an Observable for clicks on `.foo`', function (done) { 12 | const userEvents = mockDomSource({ 13 | '.foo': { 14 | 'click': most.of(135), 15 | }, 16 | }); 17 | userEvents.select('.foo').events('click').subscribe({ 18 | next: ev => { 19 | assert.strictEqual(ev, 135); 20 | done(); 21 | }, 22 | error: err => done(err), 23 | complete: () => {}, 24 | }); 25 | }); 26 | 27 | it('should make multiple user event Observables', function (done) { 28 | const userEvents = mockDomSource({ 29 | '.foo': { 30 | 'click': most.of(135), 31 | }, 32 | '.bar': { 33 | 'scroll': most.of(2), 34 | }, 35 | }); 36 | most.combine( 37 | (a: number, b: number) => a * b, 38 | userEvents.select('.foo').events('click'), 39 | userEvents.select('.bar').events('scroll'), 40 | ).subscribe({ 41 | next: ev => { 42 | assert.strictEqual(ev, 270); 43 | done(); 44 | }, 45 | error: err => done(err), 46 | complete: () => void 0, 47 | }); 48 | }); 49 | 50 | it('should make multiple user event Observables on the same selector', function (done) { 51 | const userEvents = mockDomSource({ 52 | '.foo': { 53 | 'click': most.of(135), 54 | 'scroll': most.of(3), 55 | }, 56 | }); 57 | most.combine( 58 | (a: number, b: number) => a * b, 59 | userEvents.select('.foo').events('click'), 60 | userEvents.select('.foo').events('scroll'), 61 | ).subscribe({ 62 | next: ev => { 63 | assert.strictEqual(ev, 405); 64 | done(); 65 | }, 66 | error: err => done(err), 67 | complete: () => void 0, 68 | }); 69 | }); 70 | 71 | it('should return an empty Observable if query does not match', function (done) { 72 | const userEvents = mockDomSource({ 73 | '.foo': { 74 | 'click': most.of(135), 75 | }, 76 | }); 77 | userEvents.select('.impossible').events('scroll') 78 | .subscribe({next: done, error: done, complete: done}); 79 | }); 80 | 81 | it('should return empty Observable for select().elements and none is defined', function (done) { 82 | const userEvents = mockDomSource({ 83 | '.foo': { 84 | 'click': most.of(135), 85 | }, 86 | }); 87 | userEvents.select('.foo').elements() 88 | .subscribe({next: assert.fail, error: assert.fail, complete: done}); 89 | }); 90 | 91 | it('should return defined Observable for select().elements', function (done) { 92 | const mockedDOMSource = mockDomSource({ 93 | '.foo': { 94 | elements: most.of(135), 95 | }, 96 | }); 97 | mockedDOMSource.select('.foo').elements() 98 | .subscribe({ 99 | next: (e: number) => { 100 | assert.strictEqual(e, 135); 101 | done(); 102 | }, 103 | error: (err: Error) => done(err), 104 | complete: () => void 0, 105 | }); 106 | }); 107 | 108 | it('should return defined Observable when chaining .select()', function (done) { 109 | const mockedDOMSource = mockDomSource({ 110 | '.bar': { 111 | '.foo': { 112 | '.baz': { 113 | elements: most.of(135), 114 | }, 115 | }, 116 | }, 117 | }); 118 | mockedDOMSource.select('.bar').select('.foo').select('.baz').elements() 119 | .subscribe({ 120 | next: (e: number) => { 121 | assert.strictEqual(e, 135); 122 | done(); 123 | }, 124 | error: (err: Error) => done(err), 125 | complete: () => void 0, 126 | }); 127 | }); 128 | 129 | it('multiple .select()s should not throw when given empty mockedSelectors', () => { 130 | assert.doesNotThrow(() => { 131 | const DOM = mockDomSource({}); 132 | DOM.select('.something').select('.other').events('click'); 133 | }); 134 | }); 135 | 136 | it('multiple .select()s should return some observable if not defined', () => { 137 | const DOM = mockDomSource({}); 138 | const domSource = DOM.select('.something').select('.other'); 139 | assert(domSource.events('click') instanceof most.Stream, 'domSource.events(click) should be an Observable instance'); 140 | assert.strictEqual(domSource.elements() instanceof most.Stream, true, 'domSource.elements() should be an Observable instance'); 141 | }); 142 | }); 143 | 144 | describe('isolation on MockedDOMSource', function () { 145 | it('should have the same effect as DOM.select()', function (done) { 146 | function app() { 147 | return { 148 | DOM: most.of( 149 | h3('.top-most', [ 150 | h2('.bar', 'Wrong'), 151 | div('.child.___foo', [ 152 | h4('.bar', 'Correct'), 153 | ]), 154 | ]), 155 | ), 156 | }; 157 | } 158 | 159 | const {sources, dispose} = run(app, { 160 | DOM: () => mockDomSource({ 161 | '.___foo': { 162 | '.bar': { 163 | elements: most.from(['skipped', 135]), 164 | }, 165 | }, 166 | }), 167 | }); 168 | 169 | const isolatedDOMSource = sources.DOM.isolateSource(sources.DOM, 'foo'); 170 | 171 | // Make assertions 172 | isolatedDOMSource.select('.bar').elements().skip(1).take(1).observe((elements: number) => { 173 | assert.strictEqual(elements, 135); 174 | setTimeout(() => { 175 | dispose(); 176 | done(); 177 | }); 178 | }); 179 | }); 180 | 181 | it('should have isolateSource and isolateSink', function (done) { 182 | function app() { 183 | return { 184 | DOM: most.of(h('h3.top-most.___foo')), 185 | }; 186 | } 187 | 188 | const {sources, dispose} = run(app, { 189 | DOM: () => mockDomSource({}), 190 | }); 191 | 192 | const isolatedDOMSource = sources.DOM.isolateSource(sources.DOM, 'foo'); 193 | // Make assertions 194 | assert.strictEqual(typeof isolatedDOMSource.isolateSource, 'function'); 195 | assert.strictEqual(typeof isolatedDOMSource.isolateSink, 'function'); 196 | dispose(); 197 | done(); 198 | }); 199 | 200 | it('should prevent parent from DOM.selecting() inside the isolation', function (done) { 201 | type AppSources = { 202 | DOM: DomSource, 203 | }; 204 | function app(sources: AppSources) { 205 | return { 206 | DOM: most.of( 207 | h3('.top-most', [ 208 | sources.DOM.isolateSink(most.of( 209 | div('.foo', [ 210 | h4('.bar', 'Wrong'), 211 | ]), 212 | ), 'ISOLATION'), 213 | h2('.bar', 'Correct'), 214 | ]), 215 | ), 216 | }; 217 | } 218 | 219 | const {sources} = run(app, { 220 | DOM: () => mockDomSource({ 221 | '.___ISOLATION': { 222 | '.bar': { 223 | elements: most.from(['skipped', 'Wrong']), 224 | }, 225 | }, 226 | '.bar': { 227 | elements: most.from(['skipped', 'Correct']), 228 | }, 229 | }), 230 | }); 231 | 232 | sources.DOM.select('.bar').elements().skip(1).take(1).observe(function (x: any) { 233 | assert.strictEqual(x, 'Correct'); 234 | done(); 235 | }); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /test/driver/vNodeWrapper.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { div, h } from '../../src'; 3 | import { vNodeWrapper } from '../../src/dom-driver/vNodeWrapper'; 4 | 5 | describe('vNodeWrapper', () => { 6 | it('wraps a vNode in a vNode representation of an element', () => { 7 | const divElement = document.createElement('div'); 8 | const vNode = h('h1', {}, 'Hello'); 9 | const { elm, children } = vNodeWrapper(divElement)(vNode); 10 | 11 | assert.strictEqual(divElement, elm); 12 | assert.strictEqual(children && children[0], vNode); 13 | }); 14 | 15 | it('returns a vNode if identical to rootElement', () => { 16 | const element = document.createElement('div'); 17 | const vNode = div(); 18 | 19 | assert.strictEqual(vNodeWrapper(element)(vNode), vNode); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/helpers/createRenderTarget.ts: -------------------------------------------------------------------------------- 1 | export function createRenderTarget (id: string | null = null) { 2 | let element = document.createElement('div'); 3 | element.className = 'cycletest'; 4 | 5 | if (id) 6 | element.id = id; 7 | 8 | document.body.appendChild(element); 9 | return element; 10 | } 11 | -------------------------------------------------------------------------------- /test/helpers/fake-raf.ts: -------------------------------------------------------------------------------- 1 | let original: (fn: FrameRequestCallback) => number; 2 | 3 | let requesters: any[] = []; 4 | 5 | function fakeRaf(fn: FrameRequestCallback): number { 6 | requesters.push(fn); 7 | return requesters.length; 8 | } 9 | 10 | function use() { 11 | original = window.requestAnimationFrame; 12 | window.requestAnimationFrame = fakeRaf; 13 | } 14 | 15 | function restore() { 16 | setTimeout(() => { 17 | window.requestAnimationFrame = original; 18 | }, 2000); 19 | } 20 | 21 | function step() { 22 | let cur = requesters; 23 | requesters = []; 24 | cur.forEach(function(f) { return f(16); }); 25 | } 26 | 27 | export default { 28 | use, 29 | restore, 30 | step, 31 | }; 32 | -------------------------------------------------------------------------------- /test/helpers/interval.ts: -------------------------------------------------------------------------------- 1 | import { Stream, skip, scan, periodic } from 'most'; 2 | 3 | export const interval = (period: number) => skip(1, scan((x, y) => x + y, -1, periodic(period, 1))) as Stream; 4 | -------------------------------------------------------------------------------- /test/helpers/shuffle.ts: -------------------------------------------------------------------------------- 1 | export function shuffle(array: Array): Array { 2 | let currentIndex = array.length; 3 | let temporaryValue: any; 4 | let randomIndex: any; 5 | 6 | // While there remain elements to shuffle... 7 | while (0 !== currentIndex) { 8 | 9 | // Pick a remaining element... 10 | randomIndex = Math.floor(Math.random() * currentIndex); 11 | currentIndex -= 1; 12 | 13 | // And swap it with the current element. 14 | temporaryValue = array[currentIndex]; 15 | array[currentIndex] = array[randomIndex]; 16 | array[randomIndex] = temporaryValue; 17 | } 18 | 19 | return array; 20 | } 21 | -------------------------------------------------------------------------------- /test/modules/IsolateModule.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { IsolateModule } from '../../src/modules/IsolateModule'; 3 | import { h } from '../../src'; 4 | 5 | describe('IsolateModule', () => { 6 | it('implements create and update hooks', () => { 7 | const isolateModule = new IsolateModule(); 8 | 9 | assert.strictEqual(typeof isolateModule.create, 'function'); 10 | assert.strictEqual(typeof isolateModule.update, 'function'); 11 | }); 12 | 13 | describe('create', () => { 14 | it('adds a new element to isolation', () => { 15 | const formerVNode = h('div', {}, []); 16 | formerVNode.elm = document.createElement('div'); 17 | 18 | const vNode = h('div', { isolate: 'hello' }, []); 19 | const element = document.createElement('div'); 20 | vNode.elm = element; 21 | 22 | const isolateModule = new IsolateModule(); 23 | 24 | isolateModule.create(formerVNode, vNode); 25 | 26 | assert.strictEqual(element.getAttribute('data-isolate'), 'hello'); 27 | }); 28 | }); 29 | 30 | describe('update', () => { 31 | it('adds a new element to isolation', () => { 32 | const formerVNode = h('div', {}, []); 33 | formerVNode.elm = document.createElement('div'); 34 | 35 | const vNode = h('div', { isolate: 'hello' }, []); 36 | const element = document.createElement('div'); 37 | vNode.elm = element; 38 | 39 | const isolateModule = new IsolateModule(); 40 | 41 | isolateModule.update(formerVNode, vNode); 42 | 43 | assert.strictEqual(element.getAttribute('data-isolate'), 'hello'); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/modules/dataset.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import fakeRaf from '../helpers/fake-raf'; 3 | import { init, h, DatasetModule } from '../../src'; 4 | 5 | fakeRaf.use(); 6 | const patch = init([ DatasetModule ]); 7 | 8 | // doesnt work because jsdom does not support dataset 9 | describe.skip('dataset', function() { 10 | let element: HTMLElement, vnode0: HTMLElement; 11 | beforeEach(function() { 12 | element = document.createElement('div'); 13 | vnode0 = element; 14 | }); 15 | it('is set on initial element creation', function() { 16 | element = patch(vnode0, h('div', {dataset: {foo: 'foo'}})).elm as HTMLElement; 17 | assert.equal(element.dataset['foo'], 'foo'); 18 | }); 19 | it('updates dataset', function() { 20 | const vnode1 = h('i', {dataset: {foo: 'foo', bar: 'bar'}}); 21 | const vnode2 = h('i', {dataset: {baz: 'baz'}}); 22 | element = patch(vnode0, vnode1).elm as HTMLElement; 23 | assert.equal(element.dataset['foo'], 'foo'); 24 | assert.equal(element.dataset['bar'], 'bar'); 25 | element = patch(vnode1, vnode2).elm as HTMLElement; 26 | assert.equal(element.dataset['baz'], 'baz'); 27 | assert.equal(element.dataset['foo'], undefined); 28 | }); 29 | it('handles string conversions', function() { 30 | const vnode1 = h('i', { 31 | dataset: { empty: '', dash: '-', dashed: 'foo-bar', camel: 'fooBar', integer: 0, float: 0.1 }, 32 | }); 33 | element = patch(vnode0, vnode1).elm as HTMLElement; 34 | 35 | assert.equal(element.dataset['empty'], ''); 36 | assert.equal(element.dataset['dash'], '-'); 37 | assert.equal(element.dataset['dashed'], 'foo-bar'); 38 | assert.equal(element.dataset['camel'], 'fooBar'); 39 | assert.equal(element.dataset['integer'], '0'); 40 | assert.equal(element.dataset['float'], '0.1'); 41 | }); 42 | 43 | }); 44 | 45 | fakeRaf.restore(); 46 | -------------------------------------------------------------------------------- /test/modules/style.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import fakeRaf from '../helpers/fake-raf'; 3 | import { init, h, StyleModule } from '../../src/index'; 4 | 5 | fakeRaf.use(); 6 | 7 | let patch = init([ 8 | StyleModule, 9 | ]); 10 | 11 | describe('style', function() { 12 | let element: HTMLElement, vnode0: HTMLElement; 13 | 14 | beforeEach(function() { 15 | element = document.createElement('div'); 16 | 17 | vnode0 = element; 18 | }); 19 | 20 | it('is being styled', function() { 21 | element = patch(vnode0, h('div', {style: {fontSize: '12px'}})).elm as HTMLElement; 22 | 23 | assert.equal(element.style.fontSize, '12px'); 24 | }); 25 | 26 | it('updates styles', function() { 27 | let vnode1 = h('i', {style: {fontSize: '14px', display: 'inline'}}); 28 | let vnode2 = h('i', {style: {fontSize: '12px', display: 'block'}}); 29 | let vnode3 = h('i', {style: {fontSize: '10px', display: 'block'}}); 30 | 31 | element = patch(vnode0, vnode1).elm as HTMLElement; 32 | 33 | assert.equal(element.style.fontSize, '14px'); 34 | assert.equal(element.style.display, 'inline'); 35 | 36 | element = patch(vnode1, vnode2).elm as HTMLElement; 37 | 38 | assert.equal(element.style.fontSize, '12px'); 39 | assert.equal(element.style.display, 'block'); 40 | 41 | element = patch(vnode2, vnode3).elm as HTMLElement; 42 | 43 | assert.equal(element.style.fontSize, '10px'); 44 | assert.equal(element.style.display, 'block'); 45 | }); 46 | 47 | it('explicialy removes styles', function() { 48 | let vnode1 = h('i', {style: {fontSize: '14px'}}); 49 | let vnode2 = h('i', {style: {fontSize: ''}}); 50 | let vnode3 = h('i', {style: {fontSize: '10px'}}); 51 | 52 | element = patch(vnode0, vnode1).elm as HTMLElement; 53 | 54 | assert.equal(element.style.fontSize, '14px'); 55 | 56 | patch(vnode1, vnode2); 57 | 58 | assert.equal(element.style.fontSize, ''); 59 | 60 | patch(vnode2, vnode3); 61 | 62 | assert.equal(element.style.fontSize, '10px'); 63 | }); 64 | 65 | it('implicially removes styles from element', function() { 66 | let vnode1 = h('div', [h('i', {style: {fontSize: '14px'}})]); 67 | let vnode2 = h('div', [h('i')]); 68 | let vnode3 = h('div', [h('i', {style: {fontSize: '10px'}})]); 69 | 70 | patch(vnode0, vnode1); 71 | 72 | assert.equal((element.firstChild as HTMLElement).style.fontSize, '14px'); 73 | 74 | patch(vnode1, vnode2); 75 | 76 | assert.equal((element.firstChild as HTMLElement).style.fontSize, ''); 77 | 78 | patch(vnode2, vnode3); 79 | 80 | assert.equal((element.firstChild as HTMLElement).style.fontSize, '10px'); 81 | }); 82 | 83 | it('updates delayed styles in next frame', function() { 84 | let patch = init([ 85 | StyleModule, 86 | ]); 87 | 88 | let vnode1 = h('i', {style: {fontSize: '14px', delayed: {fontSize: '16px'}}}); 89 | let vnode2 = h('i', {style: {fontSize: '18px', delayed: {fontSize: '20px'}}}); 90 | 91 | element = patch(vnode0, vnode1).elm as HTMLElement; 92 | 93 | assert.equal(element.style.fontSize, '14px'); 94 | 95 | fakeRaf.step(); 96 | fakeRaf.step(); 97 | 98 | assert.equal(element.style.fontSize, '16px'); 99 | element = patch(vnode1, vnode2).elm as HTMLElement; 100 | 101 | assert.equal(element.style.fontSize, '18px'); 102 | 103 | fakeRaf.step(); 104 | fakeRaf.step(); 105 | 106 | assert.equal(element.style.fontSize, '20px'); 107 | }); 108 | 109 | it('updates css variables', function(done) { 110 | // only run in real browsers 111 | if (typeof process !== undefined) done(); 112 | 113 | let vnode1 = h('div', {style: {'--mylet': 1}}); 114 | let vnode2 = h('div', {style: {'--mylet': 2}}); 115 | let vnode3 = h('div', { style: { '--mylet': 3 } }); 116 | 117 | element = patch(vnode0, vnode1).elm; 118 | 119 | assert.equal(element.style.getPropertyValue('--mylet'), 1); 120 | 121 | element = patch(vnode1, vnode2).elm; 122 | 123 | assert.equal(element.style.getPropertyValue('--mylet'), 2); 124 | 125 | element = patch(vnode2, vnode3).elm; 126 | 127 | assert.equal(element.style.getPropertyValue('--mylet'), 3); 128 | 129 | done(); 130 | }); 131 | 132 | it('explicitly removes css variables', function(done) { 133 | // only run in real browsers 134 | if (typeof process !== undefined) done(); 135 | 136 | let vnode1 = h('i', {style: {'--mylet': 1}}); 137 | let vnode2 = h('i', {style: {'--mylet': ''}}); 138 | let vnode3 = h('i', {style: {'--mylet': 2}}); 139 | 140 | element = patch(vnode0, vnode1).elm; 141 | 142 | assert.equal(element.style.getPropertyValue('--mylet'), 1); 143 | 144 | patch(vnode1, vnode2); 145 | 146 | assert.equal(element.style.getPropertyValue('--mylet'), ''); 147 | 148 | patch(vnode2, vnode3); 149 | 150 | assert.equal(element.style.getPropertyValue('--mylet'), 2); 151 | 152 | done(); 153 | }); 154 | 155 | it('implicitly removes css varaibles from element', function(done) { 156 | // only run in real browsers 157 | if (typeof process !== undefined) done(); 158 | 159 | let vnode1 = h('div', [h('i', {style: {'--mylet': 1}})]); 160 | let vnode2 = h('div', [h('i')]); 161 | let vnode3 = h('div', [h('i', {style: {'--mylet': 2}})]); 162 | 163 | patch(vnode0, vnode1); 164 | 165 | assert.equal((element.firstChild as HTMLElement).style.getPropertyValue('--mylet'), 1); 166 | 167 | patch(vnode1, vnode2); 168 | 169 | assert.equal((element.firstChild as HTMLElement).style.getPropertyValue('--mylet'), ''); 170 | 171 | patch(vnode2, vnode3); 172 | 173 | assert.equal((element.firstChild as HTMLElement).style.getPropertyValue('--mylet'), 2); 174 | 175 | done(); 176 | }); 177 | }); 178 | 179 | fakeRaf.restore(); 180 | -------------------------------------------------------------------------------- /test/virtual-dom/hasCssSelector.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { div, hasCssSelector } from '../../src'; 3 | 4 | describe('hasCssSelector', () => { 5 | describe('given a class selector .foo and VNode with class foo', () => { 6 | it('returns true', () => { 7 | assert.ok(hasCssSelector('.foo', div('.foo'))); 8 | }); 9 | }); 10 | 11 | describe('given a class selector .bar and VNode with class foo', () => { 12 | it('returns false', () => { 13 | assert.ok(!hasCssSelector('.bar', div('.foo'))); 14 | }); 15 | }); 16 | 17 | describe('given a class selector xbar and VNode with class bar', () => { 18 | it('returns false', () => { 19 | assert.ok(!hasCssSelector('xbar', div('.bar'))); 20 | }); 21 | }); 22 | 23 | describe('given a class selector .foo.bar and VNode with classes foo and bar', () => { 24 | it('returns true', () => { 25 | assert.ok(hasCssSelector('.foo.bar', div('.foo.bar'))); 26 | }); 27 | }); 28 | 29 | describe('given a class selector .foo.bar and VNode with classes bar and foo', () => { 30 | it('returns true', () => { 31 | assert.ok(hasCssSelector('.foo.bar', div('.bar.foo'))); 32 | }); 33 | }); 34 | 35 | describe('given a class selector .foo.bar.baz and VNode with classes bar foo and baz', () => { 36 | it('returns true', () => { 37 | assert.ok(hasCssSelector('.foo.bar.baz', div('.baz.bar.foo'))); 38 | }); 39 | }); 40 | 41 | describe('given a class selector .foo and VNode with no classes', () => { 42 | it('returns false', () => { 43 | assert.ok(!hasCssSelector('.foo', div())); 44 | }); 45 | }); 46 | 47 | describe('given an id selector #foo and VNode with id foo', () => { 48 | it('returns true', () => { 49 | assert.ok(hasCssSelector('#foo', div('#foo'))); 50 | }); 51 | }); 52 | 53 | describe('given an id selector #bar and VNode with id foo', () => { 54 | it('returns false', () => { 55 | assert.ok(!hasCssSelector('#bar', div('#foo'))); 56 | }); 57 | }); 58 | 59 | describe('given an id selector #foo#bar and VNode with id foo', () => { 60 | it('returns false', () => { 61 | assert.ok(!hasCssSelector('#foo#bar', div('#foo'))); 62 | }); 63 | }); 64 | 65 | describe('given a cssSelector .foo#bar and VNode with class foo and id bar', () => { 66 | it('returns true', () => { 67 | assert.ok(hasCssSelector('.foo#bar', div('.foo#bar'))); 68 | }); 69 | }); 70 | 71 | describe('given a cssSelector .foo#bar and VNode with class foo', () => { 72 | it('returns false', () => { 73 | assert.ok(!hasCssSelector('.foo#bar', div('.foo'))); 74 | }); 75 | }); 76 | 77 | describe('given a cssSelector #bar.foo and VNode with class foo', () => { 78 | it('returns false', () => { 79 | assert.ok(!hasCssSelector('#bar.foo', div('.foo'))); 80 | }); 81 | }); 82 | 83 | describe('given a cssSelector .foo .bar and VNode with classes foo and bar', () => { 84 | it('throws error', () => { 85 | assert.throws(() => { 86 | hasCssSelector('.foo .bar', div('.foo.bar')); 87 | }, /CSS selectors can not contain spaces/); 88 | }); 89 | }); 90 | 91 | describe('given a cssSelector .foo#bar.baz and VNode with classes foo and baz and id bar', () => { 92 | it('returns true', () => { 93 | assert.ok(hasCssSelector('.foo#bar.baz', div('.foo#bar.baz'))); 94 | }); 95 | }); 96 | 97 | describe('given cssSelector .foo#bar.baz and VNode with class foo and id bar', () => { 98 | it('returns false', () => { 99 | assert.ok(!hasCssSelector('.foo#bar.baz', div('.foo#bar'))); 100 | }); 101 | }); 102 | 103 | describe('given cssSelector .foo#bar.baz and VNode with class baz and id bar', () => { 104 | it('returns false', () => { 105 | assert.ok(!hasCssSelector('.foo#bar.baz', div('.baz#bar'))); 106 | }); 107 | }); 108 | 109 | describe('given a cssSelector div and VNode with tagName div', () => { 110 | it('returns true', () => { 111 | assert.ok(hasCssSelector('div', div())); 112 | }); 113 | }); 114 | 115 | describe('given a cssSelector div.foo and VNode with tagName div and class foo', () => { 116 | it('returns true', () => { 117 | assert.ok(hasCssSelector('div.foo', div('.foo'))); 118 | }); 119 | }); 120 | 121 | describe('given a cssSelector h2.foo and VNode with tagName div and class foo', () => { 122 | it('returns false', () => { 123 | assert.ok(!hasCssSelector('h2.foo', div('.foo'))); 124 | }); 125 | }); 126 | 127 | describe('given a cssSelector div.foo and VNode with tagName div', () => { 128 | it('returns false', () => { 129 | assert.ok(!hasCssSelector('div.foo', div())); 130 | }); 131 | }); 132 | 133 | describe('given a cssSelector div.foo and VNode with tagName div and id foo', () => { 134 | it('returns false', () => { 135 | assert.ok(!hasCssSelector('div.foo', div('#foo'))); 136 | }); 137 | }); 138 | 139 | describe('given a cssSelector div#foo.bar and VNode with tagName div and id foo and class bar', () => { 140 | it('returns true', () => { 141 | assert.ok(hasCssSelector('div#foo.bar', div('#foo.bar'))); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /test/virtual-dom/parseSelector.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { parseSelector } from '../../src/virtual-dom/helpers/h'; 3 | 4 | describe('parseSelector', () => { 5 | it('parses selectors', () => { 6 | let result = parseSelector('p'); 7 | assert.deepEqual(result, { tagName: 'p', id: '', className: '' }); 8 | 9 | result = parseSelector('p#foo'); 10 | assert.deepEqual(result, { tagName: 'p', id: 'foo', className: '' }); 11 | 12 | result = parseSelector('p.bar'); 13 | assert.deepEqual(result, { tagName: 'p', id: '', className: 'bar' }); 14 | 15 | result = parseSelector('p.bar.baz'); 16 | assert.deepEqual(result, { tagName: 'p', id: '', className: 'bar baz' }); 17 | 18 | result = parseSelector('p#foo.bar.baz'); 19 | assert.deepEqual(result, { tagName: 'p', id: 'foo', className: 'bar baz' }); 20 | 21 | result = parseSelector('div#foo'); 22 | assert.deepEqual(result, { tagName: 'div', id: 'foo', className: '' }); 23 | 24 | result = parseSelector('div#foo.bar.baz'); 25 | assert.deepEqual(result, { tagName: 'div', id: 'foo', className: 'bar baz' }); 26 | 27 | result = parseSelector('div.bar.baz'); 28 | assert.deepEqual(result, { tagName: 'div', id: '', className: 'bar baz' }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "lib": [ 8 | "dom", 9 | "es5", 10 | "es2015" 11 | ], 12 | "noImplicitAny": false, 13 | "sourceMap": true, 14 | "noUnusedParameters": false, 15 | "noUnusedLocals": false, 16 | "strictNullChecks": true, 17 | "outDir": ".tmp", 18 | "types": [ 19 | "hyperscript", 20 | "node", 21 | "mocha" 22 | ] 23 | }, 24 | "include": [ 25 | "src/**/*.ts", 26 | "test/**/*.ts" 27 | ], 28 | "exclude": [ 29 | "lib/" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@motorcycle/tslint" 4 | ], 5 | "rules": { 6 | "eofline": true, 7 | "max-line-length": [ 8 | 150 9 | ], 10 | "max-file-line-count": [ 11 | 350 12 | ], 13 | "no-angle-bracket-type-assertion": false, 14 | "no-shadowed-variable": false 15 | } 16 | } 17 | --------------------------------------------------------------------------------