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