p-elements-core 1.2.0-rc7 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1328 +0,0 @@
1
- // Comment that is displayed in the API documentation for the maquette module:
2
- /**
3
- * Welcome to the API documentation of the **maquette** library.
4
- *
5
- * [[http://maquettejs.org/|To the maquette homepage]]
6
- */
7
-
8
- /**
9
- * A virtual representation of a DOM Node. Maquette assumes that [[VNode]] objects are never modified externally.
10
- * Instances of [[VNode]] can be created using [[h]].
11
- */
12
- export interface VNode {
13
- /**
14
- * The CSS selector containing tagname, css classnames and id. An empty string is used to denote a text node.
15
- */
16
- readonly vnodeSelector: string;
17
- /**
18
- * Object containing attributes, properties, event handlers and more, see [[h]].
19
- */
20
- readonly properties: VNodeProperties | undefined;
21
- /**
22
- * Array of [[VNode]]s to be used as children. This array is already flattened.
23
- */
24
- readonly children: Array<VNode> | undefined;
25
- /**
26
- * Used in a special case when a [[VNode]] only has one child node which is a text node. Only used in combination with children === undefined.
27
- */
28
- readonly text: string | undefined;
29
- /**
30
- * Used by maquette to store the domNode that was produced from this [[VNode]].
31
- */
32
- domNode: Node | null;
33
- }
34
-
35
- /**
36
- * A projector is used to create the real DOM from the the virtual DOM and to keep it up-to-date afterwards.
37
- *
38
- * You can call [[append]], [[merge]], [[insertBefore]] and [[replace]] to add the virtual DOM to the real DOM.
39
- * The `renderMaquetteFunction` callbacks will be called to create the real DOM immediately.
40
- * Afterwards, the `renderMaquetteFunction` callbacks will be called again to update the DOM on the next animation-frame after:
41
- *
42
- * - The Projector's [[scheduleRender]] function was called
43
- * - An event handler (like `onclick`) on a rendered [[VNode]] was called.
44
- *
45
- * The projector stops when [[stop]] is called or when an error is thrown during rendering.
46
- * It is possible to use `window.onerror` to handle these errors.
47
- * Instances of [[Projector]] can be created using [[createProjector]].
48
- */
49
- export interface Projector {
50
- /**
51
- * Appends a new child node to the DOM using the result from the provided `renderMaquetteFunction`.
52
- * The `renderMaquetteFunction` will be invoked again to update the DOM when needed.
53
- * @param parentNode - The parent node for the new child node.
54
- * @param renderMaquetteFunction - Function with zero arguments that returns a [[VNode]] tree.
55
- */
56
- append(parentNode: Element, renderMaquetteFunction: () => VNode): void;
57
- /**
58
- * Inserts a new DOM node using the result from the provided `renderMaquetteFunction`.
59
- * The `renderMaquetteFunction` will be invoked again to update the DOM when needed.
60
- * @param beforeNode - The node that the DOM Node is inserted before.
61
- * @param renderMaquetteFunction - Function with zero arguments that returns a [[VNode]] tree.
62
- */
63
- insertBefore(beforeNode: Element, renderMaquetteFunction: () => VNode): void;
64
- /**
65
- * Merges a new DOM node using the result from the provided `renderMaquetteFunction` with an existing DOM Node.
66
- * This means that the virtual DOM and real DOM have one overlapping element.
67
- * Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided
68
- * The `renderMaquetteFunction` will be invoked again to update the DOM when needed.
69
- * @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and child nodes are preserved.
70
- * @param renderMaquetteFunction - Function with zero arguments that returns a [[VNode]] tree.
71
- */
72
- merge(domNode: Element, renderMaquetteFunction: () => VNode): void;
73
- /**
74
- * Replaces an existing DOM node with the result from the provided `renderMaquetteFunction`.
75
- * The `renderMaquetteFunction` will be invoked again to update the DOM when needed.
76
- * @param domNode - The DOM node to replace.
77
- * @param renderMaquetteFunction - Function with zero arguments that returns a [[VNode]] tree.
78
- */
79
- replace(domNode: Element, renderMaquetteFunction: () => VNode): void;
80
- /**
81
- * Resumes the projector. Use this method to resume rendering after [[stop]] was called or an error occurred during rendering.
82
- */
83
- resume(): void;
84
- /**
85
- * Instructs the projector to re-render to the DOM at the next animation-frame using the registered `renderMaquette` functions.
86
- * This method is automatically called for you when event-handlers that are registered in the [[VNode]]s are invoked.
87
- *
88
- * You need to call this method when timeouts expire, when AJAX responses arrive or other asynchronous actions happen.
89
- */
90
- scheduleRender(): void;
91
- /**
92
- * Synchronously re-renders to the DOM. You should normally call the `scheduleRender()` function to keep the
93
- * user interface more performant. There is however one good reason to call renderNow(),
94
- * when you want to put the focus into a newly created element in iOS.
95
- * This is only allowed when triggered by a user-event, not during requestAnimationFrame.
96
- */
97
- renderNow(): void;
98
- /**
99
- * Stops running the `renderMaquetteFunction` to update the DOM. The `renderMaquetteFunction` must have been
100
- * registered using [[append]], [[merge]], [[insertBefore]] or [[replace]].
101
- *
102
- * @returns The [[Projection]] which was created using this `renderMaquetteFunction`.
103
- * The [[Projection]] contains a reference to the DOM Node that was rendered.
104
- */
105
- detach(renderMaquetteFunction: () => VNode): Projection;
106
- /**
107
- * Stops the projector. This means that the registered `renderMaquette` functions will not be called anymore.
108
- *
109
- * Note that calling [[stop]] is not mandatory. A projector is a passive object that will get garbage collected
110
- * as usual if it is no longer in scope.
111
- */
112
- stop(): void;
113
- }
114
-
115
- /**
116
- * These functions are called when [[VNodeProperties.enterAnimation]] and [[VNodeProperties.exitAnimation]] are provided as strings.
117
- * See [[ProjectionOptions.transitions]].
118
- */
119
- export interface TransitionStrategy {
120
- /**
121
- * Function that is called when a [[VNode]] with an `enterAnimation` string is added to an already existing parent [[VNode]].
122
- *
123
- * @param element Element that was just added to the DOM.
124
- * @param properties The properties object that was supplied to the [[h]] method
125
- * @param enterAnimation The string that was passed to [[VNodeProperties.enterAnimation]].
126
- */
127
- enter(element: Element, properties: VNodeProperties, enterAnimation: string): void;
128
- /**
129
- * Function that is called when a [[VNode]] with an `exitAnimation` string is removed from a existing parent [[VNode]] that remains.
130
- *
131
- * @param element Element that ought to be removed from the DOM.
132
- * @param properties The properties object that was supplied to the [[h]] method that rendered this [[VNode]] the previous time.
133
- * @param exitAnimation The string that was passed to [[VNodeProperties.exitAnimation]].
134
- * @param removeElement Function that removes the element from the DOM.
135
- * This argument is provided purely for convenience.
136
- * You may use this function to remove the element when the animation is done.
137
- */
138
- exit(element: Element, properties: VNodeProperties, exitAnimation: string, removeElement: () => void): void;
139
- }
140
-
141
- /**
142
- * Options that may be passed when creating the [[Projector]]
143
- */
144
- export interface ProjectorOptions {
145
- /**
146
- * A transition strategy to invoke when enterAnimation and exitAnimation properties are provided as strings.
147
- * The module `cssTransitions` in the provided `css-transitions.js` file provides such a strategy.
148
- * A transition strategy is not needed when enterAnimation and exitAnimation properties are provided as functions.
149
- */
150
- readonly transitions?: TransitionStrategy;
151
- /**
152
- * May be used to add vendor prefixes when applying inline styles when needed.
153
- * This function is called when [[styles]] is used.
154
- * This function should execute `domNode.style[styleName] = value` or do something smarter.
155
- *
156
- * @param domNode The DOM Node that needs to receive the style
157
- * @param styleName The name of the style that should be applied, for example `transform`.
158
- * @param value The value of this style, for example `rotate(45deg)`.
159
- */
160
- styleApplyer?(domNode: HTMLElement, styleName: string, value: string): void;
161
- }
162
-
163
- /**
164
- * Options that influence how the DOM is rendered and updated.
165
- */
166
- export interface ProjectionOptions extends ProjectorOptions {
167
- /**
168
- * Only for internal use. Used for rendering SVG Nodes.
169
- */
170
- readonly namespace?: string;
171
- /**
172
- * May be used to intercept registration of event-handlers.
173
- *
174
- * Used by the [[Projector]] to wrap eventHandler-calls to call [[scheduleRender]] as well.
175
- *
176
- * @param propertyName The name of the property to be assigned, for example onclick
177
- * @param eventHandler The function that was registered on the [[VNode]]
178
- * @param domNode The real DOM element
179
- * @param properties The whole set of properties that was put on the VNode
180
- * @returns The function that is to be placed on the DOM node as the event handler, instead of `eventHandler`.
181
- */
182
- eventHandlerInterceptor?: (propertyName: string, eventHandler: Function, domNode: Node, properties: VNodeProperties) => Function;
183
- }
184
-
185
- /**
186
- * Object containing attributes, properties, event handlers and more that can be put on DOM nodes.
187
- *
188
- * For your convenience, all common attributes, properties and event handlers are listed here and are
189
- * type-checked when using Typescript.
190
- */
191
- export interface VNodeProperties {
192
- /**
193
- * The animation to perform when this node is added to an already existing parent.
194
- * When this value is a string, you must pass a `projectionOptions.transitions` object when creating the
195
- * projector using [[createProjector]].
196
- * {@link http://maquettejs.org/docs/animations.html|More about animations}.
197
- * @param element - Element that was just added to the DOM.
198
- * @param properties - The properties object that was supplied to the [[h]] method
199
- */
200
- enterAnimation?: ((element: Element, properties?: VNodeProperties) => void) | string;
201
- /**
202
- * The animation to perform when this node is removed while its parent remains.
203
- * When this value is a string, you must pass a `projectionOptions.transitions` object when creating the projector using [[createProjector]].
204
- * {@link http://maquettejs.org/docs/animations.html|More about animations}.
205
- * @param element - Element that ought to be removed from to the DOM.
206
- * @param removeElement - Function that removes the element from the DOM.
207
- * This argument is provided purely for convenience.
208
- * You may use this function to remove the element when the animation is done.
209
- * @param properties - The properties object that was supplied to the [[h]] method that rendered this [[VNode]] the previous time.
210
- */
211
- exitAnimation?: ((element: Element, removeElement: () => void, properties?: VNodeProperties) => void) | string;
212
- /**
213
- * The animation to perform when the properties of this node change.
214
- * This also includes attributes, styles, css classes. This callback is also invoked when node contains only text and that text changes.
215
- * {@link http://maquettejs.org/docs/animations.html|More about animations}.
216
- * @param element - Element that was modified in the DOM.
217
- * @param properties - The last properties object that was supplied to the [[h]] method
218
- * @param previousProperties - The previous properties object that was supplied to the [[h]] method
219
- */
220
- updateAnimation?: (element: Element, properties?: VNodeProperties, previousProperties?: VNodeProperties) => void;
221
- /**
222
- * Callback that is executed after this node is added to the DOM. Child nodes and properties have
223
- * already been applied.
224
- * @param element - The element that was added to the DOM.
225
- * @param projectionOptions - The projection options that were used, see [[createProjector]].
226
- * @param vnodeSelector - The selector passed to the [[h]] function.
227
- * @param properties - The properties passed to the [[h]] function.
228
- * @param children - The children that were created.
229
- */
230
- afterCreate?(element: Element, projectionOptions: ProjectionOptions, vnodeSelector: string, properties: VNodeProperties,
231
- children: VNode[]): void;
232
- /**
233
- * Callback that is executed every time this node may have been updated. Child nodes and properties
234
- * have already been updated.
235
- * @param element - The element that may have been updated in the DOM.
236
- * @param projectionOptions - The projection options that were used, see [[createProjector]].
237
- * @param vnodeSelector - The selector passed to the [[h]] function.
238
- * @param properties - The properties passed to the [[h]] function.
239
- * @param children - The children for this node.
240
- */
241
- afterUpdate?(element: Element, projectionOptions: ProjectionOptions, vnodeSelector: string, properties: VNodeProperties,
242
- children: VNode[]): void;
243
- /**
244
- * Callback that is called when a node has been removed from the tree.
245
- * The callback is called during idle state or after a timeout (fallback).
246
- * {@link https://maquettejs.org/docs/dom-node-removal.html|More info}
247
- * @param element - The element that has been removed from the DOM.
248
- */
249
- afterRemoved?(element: Element): void;
250
- /**
251
- * When specified, the event handlers will be invoked with 'this' pointing to the value.
252
- * This is useful when using the prototype/class based implementation of Components.
253
- *
254
- * When no [[key]] is present, this object is also used to uniquely identify a DOM node.
255
- */
256
- readonly bind?: Object;
257
- /**
258
- * Used to uniquely identify a DOM node among siblings.
259
- * A key is required when there are more children with the same selector and these children are added or removed dynamically.
260
- * NOTE: this does not have to be a string or number, a [[Component]] Object for instance is also possible.
261
- */
262
- readonly key?: Object;
263
- /**
264
- * An object literal like `{important:true}` which allows css classes, like `important` to be added and removed
265
- * dynamically.
266
- */
267
- readonly classes?: { [index: string]: boolean | null | undefined };
268
- /**
269
- * An object literal like `{height:'100px'}` which allows styles to be changed dynamically. All values must be strings.
270
- */
271
- readonly styles?: { [index: string]: string | null | undefined };
272
-
273
- // From Element
274
- ontouchcancel?(ev?: TouchEvent): boolean | void;
275
- ontouchend?(ev?: TouchEvent): boolean | void;
276
- ontouchmove?(ev?: TouchEvent): boolean | void;
277
- ontouchstart?(ev?: TouchEvent): boolean | void;
278
- // From HTMLFormElement
279
- readonly action?: string;
280
- readonly encoding?: string;
281
- readonly enctype?: string;
282
- readonly method?: string;
283
- readonly name?: string;
284
- readonly target?: string;
285
- // From HTMLAnchorElement
286
- readonly href?: string;
287
- readonly rel?: string;
288
- // From HTMLElement
289
- onblur?(ev?: FocusEvent): boolean | void;
290
- onchange?(ev?: Event): boolean | void;
291
- onclick?(ev?: MouseEvent): boolean | void;
292
- ondblclick?(ev?: MouseEvent): boolean | void;
293
- onfocus?(ev?: FocusEvent): boolean | void;
294
- oninput?(ev?: Event): boolean | void;
295
- onkeydown?(ev?: KeyboardEvent): boolean | void;
296
- onkeypress?(ev?: KeyboardEvent): boolean | void;
297
- onkeyup?(ev?: KeyboardEvent): boolean | void;
298
- onload?(ev?: Event): boolean | void;
299
- onmousedown?(ev?: MouseEvent): boolean | void;
300
- onmouseenter?(ev?: MouseEvent): boolean | void;
301
- onmouseleave?(ev?: MouseEvent): boolean | void;
302
- onmousemove?(ev?: MouseEvent): boolean | void;
303
- onmouseout?(ev?: MouseEvent): boolean | void;
304
- onmouseover?(ev?: MouseEvent): boolean | void;
305
- onmouseup?(ev?: MouseEvent): boolean | void;
306
- onmousewheel?(ev?: WheelEvent | MouseWheelEvent): boolean | void;
307
- onscroll?(ev?: UIEvent): boolean | void;
308
- onsubmit?(ev?: Event): boolean | void;
309
- readonly spellcheck?: boolean;
310
- readonly tabIndex?: number;
311
- readonly disabled?: boolean;
312
- readonly title?: string;
313
- readonly accessKey?: string;
314
- readonly id?: string;
315
- // From HTMLInputElement
316
- readonly type?: string;
317
- readonly autocomplete?: string;
318
- readonly checked?: boolean;
319
- readonly placeholder?: string;
320
- readonly readOnly?: boolean;
321
- readonly src?: string;
322
- readonly value?: string;
323
- // From HTMLImageElement
324
- readonly alt?: string;
325
- readonly srcset?: string;
326
- /**
327
- * Puts a non-interactive string of html inside the DOM node.
328
- *
329
- * Note: if you use innerHTML, maquette cannot protect you from XSS vulnerabilities and you must make sure that the innerHTML value is safe.
330
- */
331
- readonly innerHTML?: string;
332
-
333
- /**
334
- * Everything that is not explicitly listed (properties and attributes that are either uncommon or custom).
335
- */
336
- readonly [index: string]: any;
337
- }
338
-
339
- /**
340
- * Represents a [[VNode]] tree that has been rendered to a real DOM tree.
341
- */
342
- export interface Projection {
343
- /**
344
- * The DOM node that is used as the root of this [[Projection]].
345
- */
346
- readonly domNode: Element;
347
- /**
348
- * Updates the real DOM to match the new virtual DOM tree.
349
- * @param updatedVnode The updated virtual DOM tree. Note: The selector for the root of the [[VNode]] tree may not change.
350
- */
351
- update(updatedVnode: VNode): void;
352
- }
353
-
354
- const NAMESPACE_W3 = 'http://www.w3.org/';
355
- const NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg';
356
- const NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink';
357
-
358
- // Utilities
359
-
360
- let emptyArray = <VNode[]>[];
361
-
362
- let extend = <T>(base: T, overrides: any): T => {
363
- let result = {} as any;
364
- Object.keys(base).forEach(function(key) {
365
- result[key] = (base as any)[key];
366
- });
367
- if (overrides) {
368
- Object.keys(overrides).forEach((key) => {
369
- result[key] = overrides[key];
370
- });
371
- }
372
- return result;
373
- };
374
-
375
- // Hyperscript helper functions
376
-
377
- let same = (vnode1: VNode, vnode2: VNode) => {
378
- if (vnode1.vnodeSelector !== vnode2.vnodeSelector) {
379
- return false;
380
- }
381
- if (vnode1.properties && vnode2.properties) {
382
- if (vnode1.properties.key !== vnode2.properties.key) {
383
- return false;
384
- }
385
- return vnode1.properties.bind === vnode2.properties.bind;
386
- }
387
- return !vnode1.properties && !vnode2.properties;
388
- };
389
-
390
- let toTextVNode = (data: any): VNode => {
391
- return {
392
- vnodeSelector: '',
393
- properties: undefined,
394
- children: undefined,
395
- text: data.toString(),
396
- domNode: null
397
- };
398
- };
399
-
400
- let appendChildren = function(parentSelector: string, insertions: any[], main: VNode[]) {
401
- for (let i = 0, length = insertions.length; i < length; i++) {
402
- let item = insertions[i];
403
- if (Array.isArray(item)) {
404
- appendChildren(parentSelector, item, main);
405
- } else {
406
- if (item !== null && item !== undefined) {
407
- if (!item.hasOwnProperty('vnodeSelector')) {
408
- item = toTextVNode(item);
409
- }
410
- main.push(item);
411
- }
412
- }
413
- }
414
- };
415
-
416
- // Render helper functions
417
-
418
- let missingTransition = function() {
419
- throw new Error('Provide a transitions object to the projectionOptions to do animations');
420
- };
421
-
422
- const DEFAULT_PROJECTION_OPTIONS: ProjectionOptions = {
423
- namespace: undefined,
424
- eventHandlerInterceptor: undefined,
425
- styleApplyer: function(domNode: HTMLElement, styleName: string, value: string) {
426
- // Provides a hook to add vendor prefixes for browsers that still need it.
427
- (domNode.style as any)[styleName] = value;
428
- },
429
- transitions: {
430
- enter: missingTransition,
431
- exit: missingTransition
432
- }
433
- };
434
-
435
- let applyDefaultProjectionOptions = (projectorOptions?: ProjectionOptions) => {
436
- return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions);
437
- };
438
-
439
- let checkStyleValue = (styleValue: Object) => {
440
- if (typeof styleValue !== 'string') {
441
- throw new Error('Style values must be strings');
442
- }
443
- };
444
-
445
- let setProperties = function(domNode: Node, properties: VNodeProperties | undefined, projectionOptions: ProjectionOptions) {
446
- if (!properties) {
447
- return;
448
- }
449
- let eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor;
450
- let propNames = Object.keys(properties);
451
- let propCount = propNames.length;
452
- for (let i = 0; i < propCount; i++) {
453
- let propName = propNames[i];
454
- /* tslint:disable:no-var-keyword: edge case */
455
- let propValue = properties[propName];
456
- /* tslint:enable:no-var-keyword */
457
- if (propName === 'className') {
458
- throw new Error('Property "className" is not supported, use "class".');
459
- } else if (propName === 'class') {
460
- (propValue as string).split(/\s+/).forEach(token => (domNode as Element).classList.add(token));
461
- } else if (propName === 'classes') {
462
- // object with string keys and boolean values
463
- let classNames = Object.keys(propValue);
464
- let classNameCount = classNames.length;
465
- for (let j = 0; j < classNameCount; j++) {
466
- let className = classNames[j];
467
- if (propValue[className]) {
468
- (domNode as Element).classList.add(className);
469
- }
470
- }
471
- } else if (propName === 'styles') {
472
- // object with string keys and string (!) values
473
- let styleNames = Object.keys(propValue);
474
- let styleCount = styleNames.length;
475
- for (let j = 0; j < styleCount; j++) {
476
- let styleName = styleNames[j];
477
- let styleValue = propValue[styleName];
478
- if (styleValue) {
479
- checkStyleValue(styleValue);
480
- projectionOptions.styleApplyer!(<HTMLElement>domNode, styleName, styleValue);
481
- }
482
- }
483
- } else if (propName !== 'key' && propValue !== null && propValue !== undefined) {
484
- let type = typeof propValue;
485
- if (type === 'function') {
486
- if (propName.lastIndexOf('on', 0) === 0) { // lastIndexOf(,0)===0 -> startsWith
487
- if (eventHandlerInterceptor) {
488
- propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers
489
- }
490
- if (propName === 'oninput') {
491
- (function() {
492
- // record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput
493
- let oldPropValue = propValue;
494
- propValue = function(this: HTMLElement, evt: Event) {
495
- oldPropValue.apply(this, [evt]);
496
- (evt.target as any)['oninput-value'] = (evt.target as HTMLInputElement).value; // may be HTMLTextAreaElement as well
497
- };
498
- } ());
499
- }
500
- (domNode as any)[propName] = propValue;
501
- }
502
- } else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') {
503
- if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
504
- (domNode as Element).setAttributeNS(NAMESPACE_XLINK, propName, propValue);
505
- } else {
506
- (domNode as Element).setAttribute(propName, propValue);
507
- }
508
- } else {
509
- (domNode as any)[propName] = propValue;
510
- }
511
- }
512
- }
513
- };
514
-
515
- let updateProperties = function(domNode: Node, previousProperties: VNodeProperties | undefined, properties: VNodeProperties | undefined, projectionOptions: ProjectionOptions) {
516
- if (!properties) {
517
- return;
518
- }
519
- let propertiesUpdated = false;
520
- let propNames = Object.keys(properties);
521
- let propCount = propNames.length;
522
- for (let i = 0; i < propCount; i++) {
523
- let propName = propNames[i];
524
- // assuming that properties will be nullified instead of missing is by design
525
- let propValue = properties[propName];
526
- let previousValue = previousProperties![propName];
527
- if (propName === 'class') {
528
- if (previousValue !== propValue) {
529
- throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.');
530
- }
531
- } else if (propName === 'classes') {
532
- let classList = (domNode as Element).classList;
533
- let classNames = Object.keys(propValue);
534
- let classNameCount = classNames.length;
535
- for (let j = 0; j < classNameCount; j++) {
536
- let className = classNames[j];
537
- let on = !!propValue[className];
538
- let previousOn = !!previousValue[className];
539
- if (on === previousOn) {
540
- continue;
541
- }
542
- propertiesUpdated = true;
543
- if (on) {
544
- classList.add(className);
545
- } else {
546
- classList.remove(className);
547
- }
548
- }
549
- } else if (propName === 'styles') {
550
- let styleNames = Object.keys(propValue);
551
- let styleCount = styleNames.length;
552
- for (let j = 0; j < styleCount; j++) {
553
- let styleName = styleNames[j];
554
- let newStyleValue = propValue[styleName];
555
- let oldStyleValue = previousValue[styleName];
556
- if (newStyleValue === oldStyleValue) {
557
- continue;
558
- }
559
- propertiesUpdated = true;
560
- if (newStyleValue) {
561
- checkStyleValue(newStyleValue);
562
- projectionOptions.styleApplyer!(domNode as HTMLElement, styleName, newStyleValue);
563
- } else {
564
- projectionOptions.styleApplyer!(domNode as HTMLElement, styleName, '');
565
- }
566
- }
567
- } else {
568
- if (!propValue && typeof previousValue === 'string') {
569
- propValue = '';
570
- }
571
- if (propName === 'value') { // value can be manipulated by the user directly and using event.preventDefault() is not an option
572
- let domValue = (domNode as any)[propName];
573
- if ( // The edge cases are described in the tests
574
- domValue !== propValue // The 'value' in the DOM tree !== newValue
575
- && ((domNode as any)['oninput-value']
576
- ? domValue === (domNode as any)['oninput-value'] // If the last reported value to 'oninput' does not match domValue, do nothing and wait for oninput
577
- : propValue !== previousValue // Only update the value if the vdom changed
578
- )
579
- ) {
580
- (domNode as any)[propName] = propValue; // Reset the value, even if the virtual DOM did not change
581
- (domNode as any)['oninput-value'] = undefined;
582
- } // else do not update the domNode, otherwise the cursor position would be changed
583
- if (propValue !== previousValue) {
584
- propertiesUpdated = true;
585
- }
586
- } else if (propValue !== previousValue) {
587
- let type = typeof propValue;
588
- if (type === 'function') {
589
- throw new Error('Functions may not be updated on subsequent renders (property: ' + propName +
590
- '). Hint: declare event handler functions outside the render() function.');
591
- }
592
- if (type === 'string' && propName !== 'innerHTML') {
593
- if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
594
- (domNode as Element).setAttributeNS(NAMESPACE_XLINK, propName, propValue);
595
- } else if (propName === 'role' && propValue === '') {
596
- (domNode as any).removeAttribute(propName);
597
- } else {
598
- (domNode as Element).setAttribute(propName, propValue);
599
- }
600
- } else {
601
- if ((domNode as any)[propName] !== propValue) { // Comparison is here for side-effects in Edge with scrollLeft and scrollTop
602
- (domNode as any)[propName] = propValue;
603
- }
604
- }
605
- propertiesUpdated = true;
606
- }
607
- }
608
- }
609
- return propertiesUpdated;
610
- };
611
-
612
- let findIndexOfChild = function(children: VNode[], sameAs: VNode, start: number) {
613
- if (sameAs.vnodeSelector !== '') {
614
- // Never scan for text-nodes
615
- for (let i = start; i < children.length; i++) {
616
- if (same(children[i], sameAs)) {
617
- return i;
618
- }
619
- }
620
- }
621
- return -1;
622
- };
623
-
624
- let nodeAdded = function(vNode: VNode, transitions: TransitionStrategy) {
625
- if (vNode.properties) {
626
- let enterAnimation = vNode.properties.enterAnimation;
627
- if (enterAnimation) {
628
- if (typeof enterAnimation === 'function') {
629
- enterAnimation(vNode.domNode as Element, vNode.properties);
630
- } else {
631
- transitions.enter(vNode.domNode as Element, vNode.properties, enterAnimation as string);
632
- }
633
- }
634
- }
635
- };
636
-
637
- let removedNodes: VNode[] = [];
638
- let requestedIdleCallback = false;
639
-
640
- let visitRemovedNode = (node: VNode) => {
641
- (node.children || []).forEach(visitRemovedNode);
642
-
643
- if (node.properties && node.properties.afterRemoved) {
644
- node.properties.afterRemoved.apply(
645
- node.properties.bind || node.properties,
646
- [<Element>node.domNode]
647
- );
648
- }
649
- };
650
-
651
- let processPendingNodeRemovals = (): void => {
652
- requestedIdleCallback = false;
653
-
654
- removedNodes.forEach(visitRemovedNode);
655
- removedNodes.length = 0;
656
- };
657
-
658
- let scheduleNodeRemoval = (vNode: VNode): void => {
659
- removedNodes.push(vNode);
660
-
661
- if (!requestedIdleCallback) {
662
- requestedIdleCallback = true;
663
- if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
664
- (window as any).requestIdleCallback(processPendingNodeRemovals, { timeout: 16 });
665
- } else {
666
- setTimeout(processPendingNodeRemovals, 16);
667
- }
668
- }
669
- };
670
-
671
- let nodeToRemove = function(vNode: VNode, transitions: TransitionStrategy) {
672
- let domNode: Node = vNode.domNode!;
673
- if (vNode.properties) {
674
- let exitAnimation = vNode.properties.exitAnimation;
675
- if (exitAnimation) {
676
- (domNode as HTMLElement).style.pointerEvents = 'none';
677
- let removeDomNode = function() {
678
- if (domNode.parentNode) {
679
- domNode.parentNode.removeChild(domNode);
680
- scheduleNodeRemoval(vNode);
681
- }
682
- };
683
- if (typeof exitAnimation === 'function') {
684
- exitAnimation(domNode as Element, removeDomNode, vNode.properties);
685
- return;
686
- } else {
687
- transitions.exit(vNode.domNode as Element, vNode.properties, exitAnimation as string, removeDomNode);
688
- return;
689
- }
690
- }
691
- }
692
- if (domNode.parentNode) {
693
- domNode.parentNode.removeChild(domNode);
694
- scheduleNodeRemoval(vNode);
695
- }
696
- };
697
-
698
- let checkDistinguishable = function(childNodes: VNode[], indexToCheck: number, parentVNode: VNode, operation: string) {
699
- let childNode = childNodes[indexToCheck];
700
- if (childNode.vnodeSelector === '') {
701
- return; // Text nodes need not be distinguishable
702
- }
703
- let properties = childNode.properties;
704
- let key = properties ? (properties.key === undefined ? properties.bind : properties.key) : undefined;
705
- if (!key) { // A key is just assumed to be unique
706
- for (let i = 0; i < childNodes.length; i++) {
707
- if (i !== indexToCheck) {
708
- let node = childNodes[i];
709
- if (same(node, childNode)) {
710
- if (operation === 'added') {
711
- throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' +
712
- 'added, but there is now more than one. You must add unique key properties to make them distinguishable.');
713
- } else {
714
- throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' +
715
- 'removed, but there were more than one. You must add unique key properties to make them distinguishable.');
716
- }
717
- }
718
- }
719
- }
720
- }
721
- };
722
-
723
- let createDom: (vnode: VNode, parentNode: Node, insertBefore: Node | null | undefined, projectionOptions: ProjectionOptions) => void;
724
- let updateDom: (previous: VNode, vnode: VNode, projectionOptions: ProjectionOptions) => boolean;
725
-
726
- let updateChildren = function(vnode: VNode, domNode: Node, oldChildren: VNode[] | undefined, newChildren: VNode[] | undefined, projectionOptions: ProjectionOptions) {
727
- if (oldChildren === newChildren) {
728
- return false;
729
- }
730
- oldChildren = oldChildren || emptyArray;
731
- newChildren = newChildren || emptyArray;
732
- let oldChildrenLength = oldChildren.length;
733
- let newChildrenLength = newChildren.length;
734
- let transitions = projectionOptions.transitions!;
735
-
736
- let oldIndex = 0;
737
- let newIndex = 0;
738
- let i: number;
739
- let textUpdated = false;
740
- while (newIndex < newChildrenLength) {
741
- let oldChild = (oldIndex < oldChildrenLength) ? oldChildren[oldIndex] : undefined;
742
- let newChild = newChildren[newIndex];
743
- if (oldChild !== undefined && same(oldChild, newChild)) {
744
- textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated;
745
- oldIndex++;
746
- } else {
747
- let findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1);
748
- if (findOldIndex >= 0) {
749
- // Remove preceding missing children
750
- for (i = oldIndex; i < findOldIndex; i++) {
751
- nodeToRemove(oldChildren[i], transitions);
752
- checkDistinguishable(oldChildren, i, vnode, 'removed');
753
- }
754
- textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated;
755
- oldIndex = findOldIndex + 1;
756
- } else {
757
- // New child
758
- createDom(newChild, domNode, (oldIndex < oldChildrenLength) ? oldChildren[oldIndex].domNode : undefined, projectionOptions);
759
- nodeAdded(newChild, transitions);
760
- checkDistinguishable(newChildren, newIndex, vnode, 'added');
761
- }
762
- }
763
- newIndex++;
764
- }
765
- if (oldChildrenLength > oldIndex) {
766
- // Remove child fragments
767
- for (i = oldIndex; i < oldChildrenLength; i++) {
768
- nodeToRemove(oldChildren[i], transitions);
769
- checkDistinguishable(oldChildren, i, vnode, 'removed');
770
- }
771
- }
772
- return textUpdated;
773
- };
774
-
775
- let addChildren = function(domNode: Node, children: VNode[] | undefined, projectionOptions: ProjectionOptions) {
776
- if (!children) {
777
- return;
778
- }
779
- for (let i = 0; i < children.length; i++) {
780
- createDom(children[i], domNode, undefined, projectionOptions);
781
- }
782
- };
783
-
784
- let initPropertiesAndChildren = function(domNode: Node, vnode: VNode, projectionOptions: ProjectionOptions) {
785
- addChildren(domNode, vnode.children, projectionOptions); // children before properties, needed for value property of <select>.
786
- if (vnode.text) {
787
- domNode.textContent = vnode.text;
788
- }
789
- setProperties(domNode, vnode.properties, projectionOptions);
790
- if (vnode.properties && vnode.properties.afterCreate) {
791
- vnode.properties.afterCreate.apply(vnode.properties.bind || vnode.properties, [domNode as Element, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children]);
792
- }
793
- };
794
-
795
- createDom = function(vnode, parentNode, insertBefore, projectionOptions) {
796
- let domNode: Node | undefined, i: number, c: string, start = 0, type: string, found: string;
797
- let vnodeSelector = vnode.vnodeSelector;
798
- let doc = parentNode.ownerDocument;
799
- if (vnodeSelector === '') {
800
- domNode = vnode.domNode = doc.createTextNode(vnode.text!);
801
- if (insertBefore !== undefined) {
802
- parentNode.insertBefore(domNode, insertBefore);
803
- } else {
804
- parentNode.appendChild(domNode);
805
- }
806
- } else {
807
- for (i = 0; i <= vnodeSelector.length; ++i) {
808
- c = vnodeSelector.charAt(i);
809
- if (i === vnodeSelector.length || c === '.' || c === '#') {
810
- type = vnodeSelector.charAt(start - 1);
811
- found = vnodeSelector.slice(start, i);
812
- if (type === '.') {
813
- (domNode as HTMLElement).classList.add(found);
814
- } else if (type === '#') {
815
- (domNode as Element).id = found;
816
- } else {
817
- if (found === 'svg') {
818
- projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
819
- }
820
- if (projectionOptions.namespace !== undefined) {
821
- domNode = vnode.domNode = doc.createElementNS(projectionOptions.namespace, found);
822
- } else {
823
- let n;
824
-
825
- if(vnode.properties && (vnode as any).properties.is) {
826
- // for ios 9
827
- let tempElement = document.createElement("div");
828
- tempElement.style.display = "none";
829
- document.body.appendChild(tempElement);
830
- tempElement.innerHTML = `<${found} is="${(vnode as any).properties.is}"></${found}>`;
831
- setTimeout(()=>{
832
- document.body.removeChild(tempElement);
833
- }, 10)
834
-
835
- n = (doc as any).createElement(found, {is: (vnode as any).properties.is});
836
- } else {
837
- n = doc.createElement(found);
838
- }
839
- domNode = vnode.domNode = (vnode.domNode || n);
840
- if (found === 'input' && vnode.properties && vnode.properties.type !== undefined) {
841
- // IE8 and older don't support setting input type after the DOM Node has been added to the document
842
- (domNode as Element).setAttribute("type", vnode.properties.type);
843
- }
844
- }
845
- if(domNode){
846
- if (insertBefore !== undefined) {
847
- parentNode.insertBefore(domNode, insertBefore);
848
- } else if (domNode.parentNode !== parentNode) {
849
- parentNode.appendChild(domNode);
850
- }
851
- }
852
- }
853
- start = i + 1;
854
- }
855
- }
856
- initPropertiesAndChildren(domNode!, vnode, projectionOptions);
857
- }
858
- };
859
-
860
- updateDom = function(previous, vnode, projectionOptions) {
861
- let domNode = previous.domNode!;
862
- let textUpdated = false;
863
- if (previous === vnode) {
864
- return false; // By contract, VNode objects may not be modified anymore after passing them to maquette
865
- }
866
- let updated = false;
867
- if (vnode.vnodeSelector === '') {
868
- if (vnode.text !== previous.text) {
869
- let newVNode = domNode.ownerDocument.createTextNode(vnode.text!);
870
- domNode.parentNode!.replaceChild(newVNode, domNode);
871
- vnode.domNode = newVNode;
872
- textUpdated = true;
873
- return textUpdated;
874
- }
875
- } else {
876
- if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) { // lastIndexOf(needle,0)===0 means StartsWith
877
- projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
878
- }
879
- if (previous.text !== vnode.text) {
880
- updated = true;
881
- if (vnode.text === undefined) {
882
- domNode.removeChild(domNode.firstChild!); // the only textnode presumably
883
- } else {
884
- domNode.textContent = vnode.text;
885
- }
886
- }
887
- updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated;
888
- updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated;
889
- if (vnode.properties && vnode.properties.afterUpdate) {
890
- vnode.properties.afterUpdate.apply(vnode.properties.bind || vnode.properties, [<Element>domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children]);
891
- }
892
- }
893
- if (updated && vnode.properties && vnode.properties.updateAnimation) {
894
- vnode.properties.updateAnimation(<Element>domNode, vnode.properties, previous.properties);
895
- }
896
- vnode.domNode = previous.domNode;
897
- return textUpdated;
898
- };
899
-
900
- let createProjection = function(vnode: VNode, projectionOptions: ProjectionOptions): Projection {
901
- return {
902
- update: function(updatedVnode: VNode) {
903
- if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) {
904
- throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)');
905
- }
906
- updateDom(vnode, updatedVnode, projectionOptions);
907
- vnode = updatedVnode;
908
- },
909
- domNode: <Element>vnode.domNode
910
- };
911
- };
912
-
913
- // The following line is not possible in Typescript, hence the workaround in the two lines below
914
- // export type VNodeChild = string|VNode|Array<VNodeChild>
915
- /**
916
- * Only needed for the definition of [[VNodeChild]].
917
- */
918
- export interface VNodeChildren extends Array<VNodeChild> { }
919
- /**
920
- * These are valid values for the children parameter of the [[h]] function.
921
- */
922
- export type VNodeChild = string | VNode | VNodeChildren | null | undefined;
923
-
924
- /**
925
- * Contains all valid method signatures for the [[h]] function.
926
- */
927
- export interface H {
928
- /**
929
- * @param selector Contains the tagName, id and fixed css classnames in CSS selector format.
930
- * It is formatted as follows: `tagname.cssclass1.cssclass2#id`.
931
- * @param properties An object literal containing properties that will be placed on the DOM node.
932
- * @param children Virtual DOM nodes and strings to add as child nodes.
933
- * `children` may contain [[VNode]]s, `string`s, nested arrays, `null` and `undefined`.
934
- * Nested arrays are flattened, `null` and `undefined` are removed.
935
- *
936
- * @returns A VNode object, used to render a real DOM later.
937
- */
938
- (selector: string, properties?: VNodeProperties, ...children: VNodeChild[]): VNode;
939
- (selector: string, ...children: VNodeChild[]): VNode;
940
- }
941
-
942
- /**
943
- * The `h` function is used to create a virtual DOM node.
944
- * This function is largely inspired by the mercuryjs and mithril frameworks.
945
- * The `h` stands for (virtual) hyperscript.
946
- *
947
- * All possible method signatures of this function can be found in the [[H]] 'interface'.
948
- *
949
- * NOTE: There are {@link http://maquettejs.org/docs/rules.html|three basic rules} you should be aware of when updating the virtual DOM.
950
- */
951
- export let h: H;
952
-
953
- // The other two parameters are not added here, because the Typescript compiler creates surrogate code for destructuring 'children'.
954
- h = function(selector: string): VNode {
955
- let properties = arguments[1];
956
- if (typeof selector !== 'string') {
957
- throw new Error();
958
- }
959
- let childIndex = 1;
960
- if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') {
961
- childIndex = 2;
962
- } else {
963
- // Optional properties argument was omitted
964
- properties = undefined;
965
- }
966
- let text: string | undefined;
967
- let children: VNode[] | undefined;
968
- let argsLength = arguments.length;
969
- // Recognize a common special case where there is only a single text node
970
- if (argsLength === childIndex + 1) {
971
- let onlyChild = arguments[childIndex];
972
- if (typeof onlyChild === 'string') {
973
- text = onlyChild;
974
- } else if (onlyChild !== undefined && onlyChild !== null && onlyChild.length === 1 && typeof onlyChild[0] === 'string') {
975
- text = onlyChild[0];
976
- }
977
- }
978
- if (text === undefined) {
979
- children = [];
980
- for (; childIndex < argsLength; childIndex++) {
981
- let child = arguments[childIndex];
982
- if (child === null || child === undefined) {
983
- } else if (Array.isArray(child)) {
984
- appendChildren(selector, child, children);
985
- } else if (child.hasOwnProperty('vnodeSelector')) {
986
- children.push(child);
987
- } else {
988
- children.push(toTextVNode(child));
989
- }
990
- }
991
- }
992
- return {
993
- vnodeSelector: selector,
994
- properties: properties,
995
- children: children,
996
- text: (text === '') ? undefined : text,
997
- domNode: null
998
- };
999
- };
1000
-
1001
- /**
1002
- * Contains simple low-level utility functions to manipulate the real DOM.
1003
- */
1004
- export let dom = {
1005
-
1006
- /**
1007
- * Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in
1008
- * its [[Projection.domNode|domNode]] property.
1009
- * This is a low-level method. Users will typically use a [[Projector]] instead.
1010
- * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
1011
- * objects may only be rendered once.
1012
- * @param projectionOptions - Options to be used to create and update the projection.
1013
- * @returns The [[Projection]] which also contains the DOM Node that was created.
1014
- */
1015
- create: function(vnode: VNode, projectionOptions?: ProjectionOptions): Projection {
1016
- projectionOptions = applyDefaultProjectionOptions(projectionOptions);
1017
- createDom(vnode, document.createElement('div'), undefined, projectionOptions);
1018
- return createProjection(vnode, projectionOptions);
1019
- },
1020
-
1021
- /**
1022
- * Appends a new child node to the DOM which is generated from a [[VNode]].
1023
- * This is a low-level method. Users will typically use a [[Projector]] instead.
1024
- * @param parentNode - The parent node for the new child node.
1025
- * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
1026
- * objects may only be rendered once.
1027
- * @param projectionOptions - Options to be used to create and update the [[Projection]].
1028
- * @returns The [[Projection]] that was created.
1029
- */
1030
- append: function(parentNode: Element, vnode: VNode, projectionOptions?: ProjectionOptions): Projection {
1031
- projectionOptions = applyDefaultProjectionOptions(projectionOptions);
1032
- createDom(vnode, parentNode, undefined, projectionOptions);
1033
- return createProjection(vnode, projectionOptions);
1034
- },
1035
-
1036
- /**
1037
- * Inserts a new DOM node which is generated from a [[VNode]].
1038
- * This is a low-level method. Users wil typically use a [[Projector]] instead.
1039
- * @param beforeNode - The node that the DOM Node is inserted before.
1040
- * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function.
1041
- * NOTE: [[VNode]] objects may only be rendered once.
1042
- * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
1043
- * @returns The [[Projection]] that was created.
1044
- */
1045
- insertBefore: function(beforeNode: Element, vnode: VNode, projectionOptions?: ProjectionOptions): Projection {
1046
- projectionOptions = applyDefaultProjectionOptions(projectionOptions);
1047
- createDom(vnode, beforeNode.parentNode!, beforeNode, projectionOptions);
1048
- return createProjection(vnode, projectionOptions);
1049
- },
1050
-
1051
- /**
1052
- * Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node.
1053
- * This means that the virtual DOM and the real DOM will have one overlapping element.
1054
- * Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided.
1055
- * This is a low-level method. Users wil typically use a [[Projector]] instead.
1056
- * @param element - The existing element to adopt as the root of the new virtual DOM. Existing attributes and child nodes are preserved.
1057
- * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects
1058
- * may only be rendered once.
1059
- * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
1060
- * @returns The [[Projection]] that was created.
1061
- */
1062
- merge: function(element: Element, vnode: VNode, projectionOptions?: ProjectionOptions): Projection {
1063
- projectionOptions = applyDefaultProjectionOptions(projectionOptions);
1064
- vnode.domNode = element;
1065
- initPropertiesAndChildren(element, vnode, projectionOptions);
1066
- return createProjection(vnode, projectionOptions);
1067
- },
1068
-
1069
- /**
1070
- * Replaces an existing DOM node with a node generated from a [[VNode]].
1071
- * This is a low-level method. Users will typically use a [[Projector]] instead.
1072
- * @param element - The node for the [[VNode]] to replace.
1073
- * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
1074
- * objects may only be rendered once.
1075
- * @param projectionOptions - Options to be used to create and update the [[Projection]].
1076
- * @returns The [[Projection]] that was created.
1077
- */
1078
- replace: function(element: Element, vnode: VNode, projectionOptions?: ProjectionOptions): Projection {
1079
- projectionOptions = applyDefaultProjectionOptions(projectionOptions);
1080
- createDom(vnode, element.parentNode!, element, projectionOptions);
1081
- element.parentNode!.removeChild(element);
1082
- return createProjection(vnode, projectionOptions);
1083
- }
1084
- };
1085
-
1086
- /**
1087
- * A CalculationCache object remembers the previous outcome of a calculation along with the inputs.
1088
- * On subsequent calls the previous outcome is returned if the inputs are identical.
1089
- * This object can be used to bypass both rendering and diffing of a virtual DOM subtree.
1090
- * Instances of CalculationCache can be created using [[createCache]].
1091
- *
1092
- * @param <Result> The type of the value that is cached.
1093
- */
1094
- export interface CalculationCache<Result> {
1095
- /**
1096
- * Manually invalidates the cached outcome.
1097
- */
1098
- invalidate(): void;
1099
- /**
1100
- * If the inputs array matches the inputs array from the previous invocation, this method returns the result of the previous invocation.
1101
- * Otherwise, the calculation function is invoked and its result is cached and returned.
1102
- * Objects in the inputs array are compared using ===.
1103
- * @param inputs - Array of objects that are to be compared using === with the inputs from the previous invocation.
1104
- * These objects are assumed to be immutable primitive values.
1105
- * @param calculation - Function that takes zero arguments and returns an object (A [[VNode]] presumably) that can be cached.
1106
- */
1107
- result(inputs: Object[], calculation: () => Result): Result;
1108
- }
1109
-
1110
- /**
1111
- * Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees.
1112
- * In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem.
1113
- * For more information, see [[CalculationCache]].
1114
- *
1115
- * @param <Result> The type of the value that is cached.
1116
- */
1117
- export let createCache = <Result>(): CalculationCache<Result> => {
1118
- let cachedInputs: Object[] | undefined;
1119
- let cachedOutcome: Result | undefined;
1120
- return {
1121
-
1122
- invalidate: function() {
1123
- cachedOutcome = undefined;
1124
- cachedInputs = undefined;
1125
- },
1126
-
1127
- result: function(inputs: Object[], calculation: () => Result) {
1128
- if (cachedInputs) {
1129
- for (let i = 0; i < inputs.length; i++) {
1130
- if (cachedInputs[i] !== inputs[i]) {
1131
- cachedOutcome = undefined;
1132
- }
1133
- }
1134
- }
1135
- if (!cachedOutcome) {
1136
- cachedOutcome = calculation();
1137
- cachedInputs = inputs;
1138
- }
1139
- return cachedOutcome;
1140
- }
1141
- };
1142
- };
1143
-
1144
- /**
1145
- * Keeps an array of result objects synchronized with an array of source objects.
1146
- * See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}.
1147
- *
1148
- * Mapping provides a [[map]] function that updates its [[results]].
1149
- * The [[map]] function can be called multiple times and the results will get created, removed and updated accordingly.
1150
- * A Mapping can be used to keep an array of components (objects with a `renderMaquette` method) synchronized with an array of data.
1151
- * Instances of Mapping can be created using [[createMapping]].
1152
- *
1153
- * @param <Source> The type of source elements. Usually the data type.
1154
- * @param <Target> The type of target elements. Usually the component type.
1155
- */
1156
- export interface Mapping<Source, Target> {
1157
- /**
1158
- * The array of results. These results will be synchronized with the latest array of sources that were provided using [[map]].
1159
- */
1160
- results: Array<Target>;
1161
- /**
1162
- * Maps a new array of sources and updates [[results]].
1163
- *
1164
- * @param newSources The new array of sources.
1165
- */
1166
- map(newSources: Array<Source>): void;
1167
- }
1168
-
1169
- /**
1170
- * Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects.
1171
- * See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}.
1172
- *
1173
- * @param <Source> The type of source items. A database-record for instance.
1174
- * @param <Target> The type of target items. A [[Component]] for instance.
1175
- * @param getSourceKey `function(source)` that must return a key to identify each source object. The result must either be a string or a number.
1176
- * @param createResult `function(source, index)` that must create a new result object from a given source. This function is identical
1177
- * to the `callback` argument in `Array.map(callback)`.
1178
- * @param updateResult `function(source, target, index)` that updates a result to an updated source.
1179
- */
1180
- export let createMapping = <Source, Target>(
1181
- getSourceKey: (source: Source) => (string | number),
1182
- createResult: (source: Source, index: number) => Target,
1183
- updateResult: (source: Source, target: Target, index: number) => void): Mapping<Source, Target> => {
1184
- let keys = [] as Object[];
1185
- let results = [] as Target[];
1186
-
1187
- return {
1188
- results: results,
1189
- map: function(newSources: Source[]) {
1190
- let newKeys = newSources.map(getSourceKey);
1191
- let oldTargets = results.slice();
1192
- let oldIndex = 0;
1193
- for (let i = 0; i < newSources.length; i++) {
1194
- let source = newSources[i];
1195
- let sourceKey = newKeys[i];
1196
- if (sourceKey === keys[oldIndex]) {
1197
- results[i] = oldTargets[oldIndex];
1198
- updateResult(source, oldTargets[oldIndex], i);
1199
- oldIndex++;
1200
- } else {
1201
- let found = false;
1202
- for (let j = 1; j < keys.length + 1; j++) {
1203
- let searchIndex = (oldIndex + j) % keys.length;
1204
- if (keys[searchIndex] === sourceKey) {
1205
- results[i] = oldTargets[searchIndex];
1206
- updateResult(newSources[i], oldTargets[searchIndex], i);
1207
- oldIndex = searchIndex + 1;
1208
- found = true;
1209
- break;
1210
- }
1211
- }
1212
- if (!found) {
1213
- results[i] = createResult(source, i);
1214
- }
1215
- }
1216
- }
1217
- results.length = newSources.length;
1218
- keys = newKeys;
1219
- }
1220
- };
1221
- };
1222
-
1223
- /**
1224
- * Creates a [[Projector]] instance using the provided projectionOptions.
1225
- *
1226
- * For more information, see [[Projector]].
1227
- *
1228
- * @param projectorOptions Options that influence how the DOM is rendered and updated.
1229
- */
1230
- export let createProjector = function(projectorOptions?: ProjectorOptions): Projector {
1231
- let projector: Projector;
1232
- let projectionOptions = applyDefaultProjectionOptions(projectorOptions);
1233
- projectionOptions.eventHandlerInterceptor = function(propertyName: string, eventHandler: Function, domNode: Node, properties: VNodeProperties) {
1234
- return function(this: Node) {
1235
- // intercept function calls (event handlers) to do a render afterwards.
1236
- projector.scheduleRender();
1237
- return eventHandler.apply(properties.bind || this, arguments);
1238
- };
1239
- };
1240
- let renderCompleted = true;
1241
- let scheduled: number | undefined;
1242
- let stopped = false;
1243
- let projections = [] as Projection[];
1244
- let renderFunctions = [] as (() => VNode)[]; // matches the projections array
1245
-
1246
- let doRender = function() {
1247
- scheduled = undefined;
1248
- if (!renderCompleted) {
1249
- return; // The last render threw an error, it should be logged in the browser console.
1250
- }
1251
- renderCompleted = false;
1252
- for (let i = 0; i < projections.length; i++) {
1253
- let updatedVnode = renderFunctions[i]();
1254
- projections[i].update(updatedVnode);
1255
- }
1256
- renderCompleted = true;
1257
- };
1258
-
1259
- projector = {
1260
- renderNow: doRender,
1261
- scheduleRender: function() {
1262
- if (!scheduled && !stopped) {
1263
- scheduled = requestAnimationFrame(doRender);
1264
- }
1265
- },
1266
- stop: function() {
1267
- if (scheduled) {
1268
- cancelAnimationFrame(scheduled);
1269
- scheduled = undefined;
1270
- }
1271
- stopped = true;
1272
- },
1273
-
1274
- resume: function() {
1275
- stopped = false;
1276
- renderCompleted = true;
1277
- projector.scheduleRender();
1278
- },
1279
-
1280
- append: function(parentNode, renderMaquetteFunction) {
1281
- projections.push(dom.append(parentNode, renderMaquetteFunction(), projectionOptions));
1282
- renderFunctions.push(renderMaquetteFunction);
1283
- },
1284
-
1285
- insertBefore: function(beforeNode, renderMaquetteFunction) {
1286
- projections.push(dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions));
1287
- renderFunctions.push(renderMaquetteFunction);
1288
- },
1289
-
1290
- merge: function(domNode, renderMaquetteFunction) {
1291
- projections.push(dom.merge(domNode, renderMaquetteFunction(), projectionOptions));
1292
- renderFunctions.push(renderMaquetteFunction);
1293
- },
1294
-
1295
- replace: function(domNode, renderMaquetteFunction) {
1296
- projections.push(dom.replace(domNode, renderMaquetteFunction(), projectionOptions));
1297
- renderFunctions.push(renderMaquetteFunction);
1298
- },
1299
-
1300
- detach: function(renderMaquetteFunction) {
1301
- for (let i = 0; i < renderFunctions.length; i++) {
1302
- if (renderFunctions[i] === renderMaquetteFunction) {
1303
- renderFunctions.splice(i, 1);
1304
- return projections.splice(i, 1)[0];
1305
- }
1306
- }
1307
- throw new Error('renderMaquetteFunction was not found');
1308
- }
1309
-
1310
- };
1311
- return projector;
1312
- };
1313
-
1314
- /**
1315
- * A component is a pattern with which you can split up your web application into self-contained parts.
1316
- *
1317
- * A component may contain other components.
1318
- * This can be achieved by calling the subcomponents `renderMaquette` functions during the [[renderMaquette]] function and by using the
1319
- * resulting [[VNode]]s in the return value.
1320
- *
1321
- * This interface is not used anywhere in the maquette sourcecode, but this is a widely used pattern.
1322
- */
1323
- export interface Component {
1324
- /**
1325
- * A function that returns the DOM representation of the component.
1326
- */
1327
- renderMaquette(): VNode | null | undefined;
1328
- }