1 | import {Driver, FantasyObservable} from '@cycle/run';
|
2 | import {init, Module, Options as SnabbdomOptions, VNode, toVNode} from 'snabbdom';
|
3 | import xs, {Stream, Listener} from 'xstream';
|
4 | import concat from 'xstream/extra/concat';
|
5 | import sampleCombine from 'xstream/extra/sampleCombine';
|
6 | import {DOMSource} from './DOMSource';
|
7 | import {MainDOMSource} from './MainDOMSource';
|
8 | import {VNodeWrapper} from './VNodeWrapper';
|
9 | import {getValidNode, checkValidContainer} from './utils';
|
10 | import defaultModules from './modules';
|
11 | import {IsolateModule} from './IsolateModule';
|
12 | import {EventDelegator} from './EventDelegator';
|
13 |
|
14 | function makeDOMDriverInputGuard(modules: any) {
|
15 | if (!Array.isArray(modules)) {
|
16 | throw new Error(
|
17 | `Optional modules option must be an array for snabbdom modules`
|
18 | );
|
19 | }
|
20 | }
|
21 |
|
22 | function domDriverInputGuard(view$: Stream<VNode>): void {
|
23 | if (
|
24 | !view$ ||
|
25 | typeof view$.addListener !== `function` ||
|
26 | typeof view$.fold !== `function`
|
27 | ) {
|
28 | throw new Error(
|
29 | `The DOM driver function expects as input a Stream of ` +
|
30 | `virtual DOM elements`
|
31 | );
|
32 | }
|
33 | }
|
34 |
|
35 | export interface DOMDriverOptions {
|
36 | modules?: Array<Partial<Module>>;
|
37 | reportSnabbdomError?(err: any): void;
|
38 | snabbdomOptions?: SnabbdomOptions;
|
39 | }
|
40 |
|
41 | function dropCompletion<T>(input: Stream<T>): Stream<T> {
|
42 | return xs.merge(input, xs.never());
|
43 | }
|
44 |
|
45 | function unwrapElementFromVNode(vnode: VNode): Element {
|
46 | return vnode.elm as Element;
|
47 | }
|
48 |
|
49 | function defaultReportSnabbdomError(err: any): void {
|
50 | (console.error || console.log)(err);
|
51 | }
|
52 |
|
53 | function makeDOMReady$(): Stream<null> {
|
54 | return xs.create<null>({
|
55 | start(lis: Listener<null>) {
|
56 | if (document.readyState === 'loading') {
|
57 | document.addEventListener('readystatechange', () => {
|
58 | const state = document.readyState;
|
59 | if (state === 'interactive' || state === 'complete') {
|
60 | lis.next(null);
|
61 | lis.complete();
|
62 | }
|
63 | });
|
64 | } else {
|
65 | lis.next(null);
|
66 | lis.complete();
|
67 | }
|
68 | },
|
69 | stop() {},
|
70 | });
|
71 | }
|
72 |
|
73 | function addRootScope(vnode: VNode): VNode {
|
74 | vnode.data = vnode.data || {};
|
75 | vnode.data.isolate = [];
|
76 | return vnode;
|
77 | }
|
78 |
|
79 | function makeDOMDriver(
|
80 | container: string | Element | DocumentFragment,
|
81 | options: DOMDriverOptions = {}
|
82 | ): Driver<Stream<VNode>, MainDOMSource> {
|
83 | checkValidContainer(container);
|
84 | const modules = options.modules || defaultModules;
|
85 | makeDOMDriverInputGuard(modules);
|
86 | const isolateModule = new IsolateModule();
|
87 | const snabbdomOptions = options && options.snabbdomOptions || undefined;
|
88 | const patch = init([isolateModule.createModule() as Partial<Module>].concat(modules), undefined, snabbdomOptions);
|
89 | const domReady$ = makeDOMReady$();
|
90 | let vnodeWrapper: VNodeWrapper;
|
91 | let mutationObserver: MutationObserver;
|
92 | const mutationConfirmed$ = xs.create<null>({
|
93 | start(listener) {
|
94 | mutationObserver = new MutationObserver(() => listener.next(null));
|
95 | },
|
96 | stop() {
|
97 | mutationObserver.disconnect();
|
98 | },
|
99 | });
|
100 |
|
101 | function DOMDriver(vnode$: Stream<VNode>, name = 'DOM'): MainDOMSource {
|
102 | domDriverInputGuard(vnode$);
|
103 | const sanitation$ = xs.create<null>();
|
104 |
|
105 | const firstRoot$ = domReady$.map(() => {
|
106 | const firstRoot = getValidNode(container) || document.body;
|
107 | vnodeWrapper = new VNodeWrapper(firstRoot);
|
108 | return firstRoot;
|
109 | });
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 | const rememberedVNode$ = vnode$.remember();
|
117 | rememberedVNode$.addListener({});
|
118 |
|
119 |
|
120 |
|
121 | mutationConfirmed$.addListener({});
|
122 |
|
123 | const elementAfterPatch$ = firstRoot$
|
124 | .map(
|
125 | firstRoot =>
|
126 | xs
|
127 | .merge(rememberedVNode$.endWhen(sanitation$), sanitation$)
|
128 | .map(vnode => vnodeWrapper.call(vnode))
|
129 | .startWith(addRootScope(toVNode(firstRoot)))
|
130 | .fold(patch, toVNode(firstRoot))
|
131 | .drop(1)
|
132 | .map(unwrapElementFromVNode)
|
133 | .startWith(firstRoot as any)
|
134 | .map(el => {
|
135 | mutationObserver.observe(el, {
|
136 | childList: true,
|
137 | attributes: true,
|
138 | characterData: true,
|
139 | subtree: true,
|
140 | attributeOldValue: true,
|
141 | characterDataOldValue: true,
|
142 | });
|
143 | return el;
|
144 | })
|
145 | .compose(dropCompletion)
|
146 | )
|
147 | .flatten();
|
148 |
|
149 | const rootElement$ = concat(domReady$, mutationConfirmed$)
|
150 | .endWhen(sanitation$)
|
151 | .compose(sampleCombine(elementAfterPatch$))
|
152 | .map(arr => arr[1])
|
153 | .remember();
|
154 |
|
155 |
|
156 | rootElement$.addListener({
|
157 | error: options.reportSnabbdomError || defaultReportSnabbdomError,
|
158 | });
|
159 |
|
160 | const delegator = new EventDelegator(rootElement$, isolateModule);
|
161 |
|
162 | return new MainDOMSource(
|
163 | rootElement$,
|
164 | sanitation$,
|
165 | [],
|
166 | isolateModule,
|
167 | delegator,
|
168 | name
|
169 | );
|
170 | }
|
171 |
|
172 | return DOMDriver as any;
|
173 | }
|
174 |
|
175 | export {makeDOMDriver};
|