1 | const EMPTY = {};
|
2 | const NO_RENDER = { render: false };
|
3 | const SYNC_RENDER = { renderSync: true };
|
4 | const DOM_RENDER = { build: true };
|
5 | const NON_DIMENSION_PROPS = `
|
6 | boxFlex boxFlexGroup columnCount fillOpacity flex flexGrow
|
7 | flexPositive flexShrink flexNegative fontWeight lineClamp
|
8 | lineHeight opacity order orphans strokeOpacity widows zIndex zoom
|
9 | `.trim().split(/\s+/g).reduce( (acc, prop) => (acc[prop] = true, acc), {});
|
10 |
|
11 | let slice = Array.prototype.slice,
|
12 | options = {
|
13 | syncComponentUpdates: true
|
14 | },
|
15 | hooks = {};
|
16 |
|
17 | export { options, hooks };
|
18 |
|
19 |
|
20 | export function render(component, parent) {
|
21 | let built = build(null, component),
|
22 | c = built._component;
|
23 | if (c) hook(c, 'componentWillMount');
|
24 | parent.appendChild(built);
|
25 | if (c) hook(c, 'componentDidMount');
|
26 | return build;
|
27 | }
|
28 |
|
29 |
|
30 | hooks.vnode = ({ attributes }) => {
|
31 | if (!attributes) return;
|
32 |
|
33 | let s = attributes.style;
|
34 | if (s && !s.substring) {
|
35 | attributes.style = styleObjToCss(s);
|
36 | }
|
37 |
|
38 | let c = attributes['class'];
|
39 | if (attributes.hasOwnProperty('className')) {
|
40 | c = attributes['class'] = attributes.className;
|
41 | delete attributes.className;
|
42 | }
|
43 | if (c && !c.substring) {
|
44 | attributes['class'] = hashToClassName(c);
|
45 | }
|
46 | };
|
47 |
|
48 | function styleObjToCss(s) {
|
49 | let str = '',
|
50 | sep = ': ',
|
51 | term = '; ';
|
52 | for (let prop in s) {
|
53 | if (s.hasOwnProperty(prop)) {
|
54 | let val = s[prop];
|
55 | str += jsToCss(prop);
|
56 | str += sep;
|
57 | str += val;
|
58 | if (typeof val==='number' && !NON_DIMENSION_PROPS.hasOwnProperty(prop)) {
|
59 | str += 'px';
|
60 | }
|
61 | str += term;
|
62 | }
|
63 | }
|
64 | return str;
|
65 | }
|
66 |
|
67 | function hashToClassName(c) {
|
68 | let str = '';
|
69 | for (let prop in c) {
|
70 | if (c[prop]) {
|
71 | if (str) str += ' ';
|
72 | str += prop;
|
73 | }
|
74 | }
|
75 | return str;
|
76 | }
|
77 |
|
78 | let jsToCss = s => s.replace(/([A-Z])/,'-$1').toLowerCase();
|
79 |
|
80 |
|
81 |
|
82 |
|
83 | export class Component {
|
84 | constructor() {
|
85 | this._dirty = false;
|
86 | this.props = hook(this, 'getDefaultProps') || {};
|
87 | this.state = hook(this, 'getInitialState') || {};
|
88 | hook(this, 'initialize');
|
89 | }
|
90 |
|
91 | shouldComponentUpdate(props, state) {
|
92 | return true;
|
93 | }
|
94 |
|
95 | setState(state) {
|
96 | extend(this.state, state);
|
97 | this.triggerRender();
|
98 | }
|
99 |
|
100 | setProps(props, opts=EMPTY) {
|
101 | let d = this._disableRendering===true;
|
102 | this._disableRendering = true;
|
103 | hook(this, 'componentWillReceiveProps', props, this.props);
|
104 |
|
105 | this.nextProps = props;
|
106 | this._disableRendering = d;
|
107 | if (opts.renderSync===true && options.syncComponentUpdates===true) {
|
108 | this._render();
|
109 | }
|
110 | else if (opts.render!==false) {
|
111 | this.triggerRender();
|
112 | }
|
113 | }
|
114 |
|
115 | triggerRender() {
|
116 | if (this._dirty!==true) {
|
117 | this._dirty = true;
|
118 | renderQueue.add(this);
|
119 | }
|
120 | }
|
121 |
|
122 | render(props, state) {
|
123 | return h('div', { component:this.constructor.name }, props.children);
|
124 | }
|
125 |
|
126 | _render(opts=EMPTY) {
|
127 | if (this._disableRendering===true) return;
|
128 |
|
129 | this._dirty = false;
|
130 |
|
131 | if (this.base && hook(this, 'shouldComponentUpdate', this.props, this.state)===false) {
|
132 | this.props = this.nextProps;
|
133 | return;
|
134 | }
|
135 |
|
136 | this.props = this.nextProps;
|
137 |
|
138 | hook(this, 'componentWillUpdate');
|
139 |
|
140 | let rendered = hook(this, 'render', this.props, this.state);
|
141 |
|
142 | if (this.base || opts.build===true) {
|
143 | let base = build(this.base, rendered);
|
144 | if (this.base && base!==this.base) {
|
145 | this.base.parentNode.insertBefore(base, this.base);
|
146 | this.base.parentNode.removeChild(this.base);
|
147 | }
|
148 | this.base = base;
|
149 | }
|
150 |
|
151 | hook(this, 'componentDidUpdate');
|
152 | }
|
153 | }
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 | export function h(nodeName, attributes, ...args) {
|
164 | let children = null,
|
165 | sharedArr = [],
|
166 | arr, lastSimple;
|
167 | if (args.length) {
|
168 | children = [];
|
169 | for (let i=0; i<args.length; i++) {
|
170 | if (Array.isArray(args[i])) {
|
171 | arr = args[i];
|
172 | }
|
173 | else {
|
174 | arr = sharedArr;
|
175 | arr[0] = args[i];
|
176 | }
|
177 | for (let j=0; j<arr.length; j++) {
|
178 | let child = arr[j];
|
179 | let simple = notEmpty(child) && !isVNode(child);
|
180 | if (simple) child = String(child);
|
181 | if (simple && lastSimple) {
|
182 | children[children.length-1] += child;
|
183 | }
|
184 | else if (child!==null && child!==undefined) {
|
185 | children.push(child);
|
186 | }
|
187 | lastSimple = simple;
|
188 | }
|
189 | }
|
190 | }
|
191 |
|
192 | let p = new VNode(nodeName, attributes, children);
|
193 | hook(hooks, 'vnode', p);
|
194 | return p;
|
195 | }
|
196 |
|
197 | class VNode {
|
198 | constructor(nodeName, attributes, children) {
|
199 | this.nodeName = nodeName;
|
200 | this.attributes = attributes;
|
201 | this.children = children;
|
202 | }
|
203 | }
|
204 | VNode.prototype.__isVNode = true;
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 | function hook(obj, name, ...args) {
|
211 | let fn = obj[name];
|
212 | if (fn && typeof fn==='function') return fn.apply(obj, args);
|
213 | }
|
214 |
|
215 | function isVNode(obj) {
|
216 | return obj && obj.__isVNode===true;
|
217 | }
|
218 |
|
219 | function notEmpty(x) {
|
220 | return x!==null && x!==undefined;
|
221 | }
|
222 |
|
223 | function isSameNodeType(node, vnode) {
|
224 | if (node.nodeType===3) {
|
225 | return typeof vnode==='string';
|
226 | }
|
227 | let nodeName = vnode.nodeName;
|
228 | if (typeof nodeName==='function') return node._componentConstructor===nodeName;
|
229 | return node.nodeName.toLowerCase()===nodeName;
|
230 | }
|
231 |
|
232 |
|
233 | function buildComponentFromVNode(dom, vnode) {
|
234 | let c = dom && dom._component;
|
235 |
|
236 | if (c && dom._componentConstructor===vnode.nodeName) {
|
237 | let props = getNodeProps(vnode);
|
238 | c.setProps(props, SYNC_RENDER);
|
239 | return dom;
|
240 | }
|
241 | else {
|
242 | if (c) unmountComponent(dom, c);
|
243 | return createComponentFromVNode(vnode)
|
244 | }
|
245 | }
|
246 |
|
247 | function createComponentFromVNode(vnode) {
|
248 | let component = componentRecycler.create(vnode.nodeName);
|
249 |
|
250 | let props = getNodeProps(vnode);
|
251 | component.setProps(props, NO_RENDER);
|
252 | component._render(DOM_RENDER);
|
253 |
|
254 | let node = component.base;
|
255 | node._component = component;
|
256 | node._componentConstructor = vnode.nodeName;
|
257 | return node;
|
258 | }
|
259 |
|
260 | function unmountComponent(dom, component) {
|
261 | console.warn('unmounting mismatched component', component);
|
262 |
|
263 | delete dom._component;
|
264 | hook(component, 'componentWillUnmount');
|
265 | let base = component.base;
|
266 | if (base && base.parentNode) {
|
267 | base.parentNode.removeChild(base);
|
268 | }
|
269 | hook(component, 'componentDidUnmount');
|
270 | componentRecycler.collect(component);
|
271 | }
|
272 |
|
273 |
|
274 |
|
275 | function build(dom, vnode) {
|
276 | let out = dom,
|
277 | nodeName = vnode.nodeName;
|
278 |
|
279 | if (typeof nodeName==='function') {
|
280 | return buildComponentFromVNode(dom, vnode);
|
281 | }
|
282 |
|
283 | if (typeof vnode==='string') {
|
284 | if (dom) {
|
285 | if (dom.nodeType===3) {
|
286 | dom.textContent = vnode;
|
287 | return dom;
|
288 | }
|
289 | else {
|
290 | if (dom.nodeType===1) recycler.collect(dom);
|
291 | }
|
292 | }
|
293 | return document.createTextNode(vnode);
|
294 | }
|
295 |
|
296 | if (!dom) {
|
297 | out = recycler.create(nodeName);
|
298 | }
|
299 | else if (dom.nodeName.toLowerCase()!==nodeName) {
|
300 | out = recycler.create(nodeName);
|
301 | appendChildren(out, slice.call(dom.childNodes));
|
302 |
|
303 | if (dom.nodeType===1) recycler.collect(dom);
|
304 | }
|
305 |
|
306 |
|
307 | let old = getNodeAttributes(out) || EMPTY,
|
308 | attrs = vnode.attributes || EMPTY;
|
309 |
|
310 |
|
311 | if (old!==EMPTY) {
|
312 | for (let name in old) {
|
313 | if (old.hasOwnProperty(name)) {
|
314 | let o = attrs[name];
|
315 | if (o===undefined || o===null || o===false) {
|
316 | setAccessor(out, name, null, old[name]);
|
317 | }
|
318 | }
|
319 | }
|
320 | }
|
321 |
|
322 |
|
323 | if (attrs!==EMPTY) {
|
324 | for (let name in attrs) {
|
325 | if (attrs.hasOwnProperty(name)) {
|
326 | let value = attrs[name];
|
327 | if (value!==undefined && value!==null && value!==false) {
|
328 | let prev = getAccessor(out, name, old[name]);
|
329 | if (value!==prev) {
|
330 | setAccessor(out, name, value, prev);
|
331 | }
|
332 | }
|
333 | }
|
334 | }
|
335 | }
|
336 |
|
337 |
|
338 | let children = slice.call(out.childNodes);
|
339 | let keyed = {};
|
340 | for (let i=children.length; i--; ) {
|
341 | let t = children[i].nodeType;
|
342 | let key;
|
343 | if (t===3) {
|
344 | key = t.key;
|
345 | }
|
346 | else if (t===1) {
|
347 | key = children[i].getAttribute('key');
|
348 | }
|
349 | else {
|
350 | continue;
|
351 | }
|
352 | if (key) keyed[key] = children.splice(i, 1)[0];
|
353 | }
|
354 | let newChildren = [];
|
355 |
|
356 | if (vnode.children) {
|
357 | for (let i=0, vlen=vnode.children.length; i<vlen; i++) {
|
358 | let vchild = vnode.children[i];
|
359 | let attrs = vchild.attributes;
|
360 | let key, child;
|
361 | if (attrs) {
|
362 | key = attrs.key;
|
363 | child = key && keyed[key];
|
364 | }
|
365 |
|
366 |
|
367 | if (!child) {
|
368 | let len = children.length;
|
369 | if (children.length) {
|
370 | for (let j=0; j<len; j++) {
|
371 | if (isSameNodeType(children[j], vchild)) {
|
372 | child = children.splice(j, 1)[0];
|
373 | break;
|
374 | }
|
375 | }
|
376 | }
|
377 | }
|
378 |
|
379 |
|
380 | newChildren.push(build(child, vchild));
|
381 | }
|
382 | }
|
383 |
|
384 |
|
385 | for (let i=0, len=newChildren.length; i<len; i++) {
|
386 |
|
387 | if (out.childNodes[i]!==newChildren[i]) {
|
388 | let child = newChildren[i],
|
389 | c = child._component,
|
390 | next = out.childNodes[i+1];
|
391 | if (c) hook(c, 'componentWillMount');
|
392 | if (next) {
|
393 | out.insertBefore(child, next);
|
394 | }
|
395 | else {
|
396 | out.appendChild(child);
|
397 | }
|
398 | if (c) hook(c, 'componentDidMount');
|
399 | }
|
400 | }
|
401 |
|
402 |
|
403 | for (let i=0, len=children.length; i<len; i++) {
|
404 | let child = children[i],
|
405 | c = child._component;
|
406 | if (c) hook(c, 'componentWillUnmount');
|
407 | child.parentNode.removeChild(child);
|
408 | if (c) {
|
409 | hook(c, 'componentDidUnmount');
|
410 | componentRecycler.collect(c);
|
411 | }
|
412 | else if (child.nodeType===1) {
|
413 | recycler.collect(child);
|
414 | }
|
415 | }
|
416 |
|
417 | return out;
|
418 | }
|
419 |
|
420 |
|
421 | let renderQueue = {
|
422 | items: [],
|
423 | itemsOffline: [],
|
424 | pending: false,
|
425 | add(component) {
|
426 | if (renderQueue.items.push(component)!==1) return;
|
427 |
|
428 | let d = hooks.debounceRendering;
|
429 | if (d) d(renderQueue.process);
|
430 | else setTimeout(renderQueue.process, 0);
|
431 | },
|
432 | process() {
|
433 | let items = renderQueue.items,
|
434 | len = items.length;
|
435 | if (!len) return;
|
436 | renderQueue.items = renderQueue.itemsOffline;
|
437 | renderQueue.items.length = 0;
|
438 | renderQueue.itemsOffline = items;
|
439 | while (len--) {
|
440 | if (items[len]._dirty) {
|
441 | items[len]._render();
|
442 | }
|
443 | }
|
444 | }
|
445 | };
|
446 |
|
447 | let rerender = renderQueue.process;
|
448 | export { rerender };
|
449 |
|
450 |
|
451 |
|
452 | let recycler = {
|
453 | nodes: {},
|
454 | collect(node) {
|
455 | let name = node.nodeName;
|
456 | recycler.clean(node);
|
457 | let list = recycler.nodes[name] || (recycler.nodes[name] = []);
|
458 | list.push(node);
|
459 | },
|
460 | create(nodeName) {
|
461 | let list = recycler.nodes[name];
|
462 | if (list && list.length) {
|
463 | return list.splice(0, 1)[0];
|
464 | }
|
465 | return document.createElement(nodeName);
|
466 | },
|
467 | clean(node) {
|
468 | node.remove();
|
469 |
|
470 | if (node.attributes) {
|
471 | let attrs = getNodeAttributes(node);
|
472 | for (let attr in attrs) if (attrs.hasOwnProperty(attr)) {
|
473 | node.removeAttribute(attr);
|
474 | }
|
475 | }
|
476 |
|
477 |
|
478 |
|
479 |
|
480 |
|
481 | }
|
482 | };
|
483 |
|
484 |
|
485 | let componentRecycler = {
|
486 | components: {},
|
487 | collect(component) {
|
488 | let name = component.constructor.name;
|
489 | let list = componentRecycler.components[name] || (componentRecycler.components[name] = []);
|
490 | list.push(component);
|
491 | },
|
492 | create(ctor) {
|
493 | let name = ctor.name,
|
494 | list = componentRecycler.components[name];
|
495 | if (list && list.length) {
|
496 | return list.splice(0, 1)[0];
|
497 | }
|
498 | return new ctor();
|
499 | }
|
500 | };
|
501 |
|
502 |
|
503 | function appendChildren(parent, children) {
|
504 | let len = children.length;
|
505 | if (len<=2) {
|
506 | parent.appendChild(children[0]);
|
507 | if (len===2) parent.appendChild(children[1]);
|
508 | return;
|
509 | }
|
510 |
|
511 | let frag = document.createDocumentFragment();
|
512 | for (let i=0; i<len; i++) frag.appendChild(children[i]);
|
513 | parent.appendChild(frag);
|
514 | }
|
515 |
|
516 |
|
517 | function getAccessor(node, name, value) {
|
518 | if (name==='class') return node.className;
|
519 | if (name==='style') return node.style.cssText;
|
520 | return value;
|
521 |
|
522 | }
|
523 |
|
524 |
|
525 |
|
526 |
|
527 |
|
528 |
|
529 |
|
530 |
|
531 |
|
532 |
|
533 |
|
534 |
|
535 |
|
536 |
|
537 | function setAccessor(node, name, value, old) {
|
538 | if (name==='class') {
|
539 | node.className = value;
|
540 | }
|
541 | else if (name==='style') {
|
542 | node.style.cssText = value;
|
543 | }
|
544 | else {
|
545 | setComplexAccessor(node, name, value, old);
|
546 | }
|
547 | }
|
548 |
|
549 | function eventProxy(e) {
|
550 | let l = this._listeners,
|
551 | fn = l[e.type.toLowerCase()];
|
552 | if (fn) return fn.call(l, hook(hooks, 'event', e) || e);
|
553 | }
|
554 |
|
555 | function setComplexAccessor(node, name, value, old) {
|
556 | if (name.substring(0,2)==='on') {
|
557 | let type = name.substring(2).toLowerCase(),
|
558 | l = node._listeners || (node._listeners = {});
|
559 | if (!l[type]) node.addEventListener(type, eventProxy);
|
560 | l[type] = value;
|
561 | return;
|
562 | }
|
563 |
|
564 | let uc = nameToAccessor(name);
|
565 | if (node[uc] && typeof node[uc]==='function') {
|
566 | node[uc](value);
|
567 | }
|
568 | else if (value!==null) {
|
569 | node.setAttribute(name, value);
|
570 | }
|
571 | else {
|
572 | node.removeAttribute(name);
|
573 | }
|
574 | }
|
575 |
|
576 | function nameToAccessor(name) {
|
577 | let c = nameToAccessorCache[name];
|
578 | if (!c) {
|
579 | c = 'set' + name.charAt(0).toUpperCase() + name.substring(1);
|
580 | nameToAccessorCache[name] = c;
|
581 | }
|
582 | return c;
|
583 | }
|
584 | let nameToAccessorCache = {};
|
585 |
|
586 |
|
587 | function getNodeAttributes(node) {
|
588 | let list = node.attributes;
|
589 | if (!list.getNamedItem) return list;
|
590 | if (list.length) return getAttributesAsObject(list);
|
591 | }
|
592 |
|
593 | function getAttributesAsObject(list) {
|
594 | let attrs = {};
|
595 | for (let i=list.length; i--; ) {
|
596 | let item = list[i];
|
597 | attrs[item.name] = item.value;
|
598 | }
|
599 | return attrs;
|
600 | }
|
601 |
|
602 |
|
603 | function getNodeProps(vnode) {
|
604 | let props = extend({}, vnode.attributes);
|
605 | if (vnode.children) {
|
606 | props.children = vnode.children;
|
607 | }
|
608 | if (vnode.text) {
|
609 | props._content = vnode.text;
|
610 | }
|
611 | return props;
|
612 | }
|
613 |
|
614 |
|
615 | function extend(obj, props) {
|
616 | for (let i in props) if (props.hasOwnProperty(i)) {
|
617 | obj[i] = props[i];
|
618 | }
|
619 | return obj;
|
620 | }
|