UNPKG

26.9 kBJavaScriptView Raw
1const fs = require('fs');
2const coffee = require('coffeescript');
3const K = require('kcore');
4
5require('./KBSCNateNamespace.js');
6require('./KBSCNateHtml.js');
7require('./KNateAbstractStatic.js');
8require('./KNateXmlStatic.js');
9require('./KNateSvgStatic.js');
10
11const COMPACT = true;
12// hard LB - always present
13const LBH = '\n';
14// soft LB - present only when COMPACT = false
15const LB = COMPACT ? '' : '\n';
16
17// turn off the cache when you are rapidly developing js/css etc.
18let RESOURCE_CACHE_ON = true;
19
20const RESOURCE_CACHE_STATE_PENDING = Symbol('pending');
21const RESOURCE_CACHE = {};
22const RESOURCE_COMPILED_CACHE = {};
23
24const EMIT_RESOURCE_LOADING_TO_CONSOLE_LOG = false;
25const EMIT_STATS_TO_CONSOLE_LOG = false;
26
27// This is rate limiter implemented locally, to prevent too many opened file handles (which crashes the server).
28// This happens mostly in dev environments, where cache-ing files is disabled and so processing takes too long.
29// Possible improvement:
30// - use some already exisiting rate limiter implementation
31// - use rate limiter instance taken for whole server/workspace, not just locally for Nate
32let RESOURCES_LOAD_PENDING_CNT = 0;
33const RESOURCES_LOAD_PENDING_LIMIT = 128;
34const RESOURCES_WAITING_OBJECTS = [];
35
36// -----------------------------------------------------------------------------
37// HTML Prototypes
38// -----------------------------------------------------------------------------
39
40K.JsCollectorProto =
41{
42 init()
43 {
44 this.js = '';
45 },
46
47 _generateJsTagFromText(jsTxt)
48 {
49 return '<script>' + LBH + jsTxt + LBH + '</script>' + LB;
50 },
51
52 getAllAsTag()
53 {
54 let rv = '';
55
56 if (this.js != '')
57 {
58 rv = this._generateJsTagFromText(this.js);
59 }
60
61 return rv;
62 },
63
64 getAllAsOnLoadCode()
65 {
66 if (this.js != '')
67 {
68 K.Error.reportNotImplemented('K.JsCollector::getAllAsOnLoadCode()');
69 }
70
71 return '';
72
73 // OLD IMPLEMENTATION
74 // Removed due to jQuery dependency.
75 // New implementation without jQuery needed.
76 //
77 // rv = ''
78 // if @js != ''
79 // onLoadJs = '$(function() {' + LB + @js + LB + '});'
80 // rv = @_generateJsTagFromText(onLoadJs)
81 // return rv
82 },
83
84 pushFromTxt(txt)
85 {
86 this.js += txt.trim();
87 }
88};
89
90K.NateHtmlProto = Object.assign({}, K.NateXmlAbstractProto,
91{
92 setResourceCacheEnabled(onOff)
93 {
94 RESOURCE_CACHE_ON = onOff;
95 },
96
97 nl2br(text) {
98 return text.replace(/(?:\r\n|\r|\n)/g, '<br>');
99 },
100
101 escapeHtmlContent(unsafe)
102 {
103 // Possible improvement: Add HTML specific escapes if needed.
104 return K.NateXmlAbstractProto.escapeXmlTags(unsafe)
105 },
106
107 escapeHtmlAttribute(unsafe)
108 {
109 // We escapes quotas only inside attributes string.
110 // https://webmasters.stackexchange.com/questions/12335/should-i-escape-the-apostrophe-character-with-its-html-entity-39
111 return K.NateHtmlProto.escapeHtmlContent(unsafe)
112 .replace(/"/g, '&quot;')
113 .replace(/'/g, '&apos;');
114 },
115
116 _getDefaultNamespace() {
117 // Default namespace for all NateHtmlStatic elements used when there
118 // is no parent item.
119 return K.nateHtmlStaticNamespace
120 }
121});
122
123K.NateHtmlElemProto = Object.assign({}, K.NateHtmlProto,
124{
125 ON_CLICK_EVENT_BLOCKER: K.LinksUtil.getOnClickEventBlockerFunctionCodeAsText(),
126
127 _concatParamStrings(obligatoryX, optionalY)
128 {
129 let rv = obligatoryX.trim()
130
131 if (optionalY)
132 {
133 optionalY = optionalY.trim()
134
135 if (optionalY != '')
136 {
137 rv = obligatoryX + ' ' + optionalY
138 }
139 }
140
141 return rv
142 },
143
144 init(parent, namespace)
145 {
146 // Init super class.
147 K.NateHtmlProto.init.call(this, parent, namespace);
148
149 if (K.Object.isNotNull(parent))
150 {
151 parent.push(this);
152 }
153 else
154 {
155 this.tagOpenCloseMode = K.NateXmlAbstractProto.JUST_CHILDREN;
156 }
157
158 this.tagParamsList = {style: ''};
159 },
160
161 newTxt(theTxt)
162 {
163 const rv = this.getNamespace().createNate(this);
164
165 rv.setTag(null, null, theTxt, K.NateXmlAbstractProto.JUST_THE_INNER);
166
167 return rv;
168 },
169
170 br()
171 {
172 return this.newBr();
173 },
174
175 newScriptUrl(params)
176 {
177 return this.tagOpenClose('script', params);
178 },
179
180 newScriptJsonLd(dataObj = null)
181 {
182 const newTag = this.tagOpenClose('script', {
183 type: 'application/ld+json',
184 innerJSON: dataObj
185 })
186
187 return newTag;
188 },
189
190 newSvgNate(params)
191 {
192 const rv = K.NateSvgDocument(this);
193 rv.setFromMap(params)
194
195 return rv;
196 },
197
198 setOnClickEventBlocker()
199 {
200 this.set('onclick', K.NateHtmlElemProto.ON_CLICK_EVENT_BLOCKER);
201
202 return this;
203 },
204
205 updateStyle(param, value)
206 {
207 value = value.trim();
208
209 if (K.Object.isNotNull(value) && (value != ''))
210 {
211 const paramsMap = {
212 'overflowX' : 'overflow-x',
213 'overflowY' : 'overflow-y',
214 'zIndex' : 'z-index',
215 'textAlign' : 'text-align',
216 'verticalAlign' : 'vertical-align',
217 'backgroundColor': 'background-color',
218 'borderCollapse' : 'border-collapse',
219 'fontFamily' : 'font-family',
220 'fontSize' : 'font-size',
221 'fontStyle' : 'font-style',
222 'fontWeight' : 'font-weight',
223 };
224
225 let styleKey = paramsMap[param];
226
227 if (K.Object.isUndefinedOrNull(styleKey))
228 {
229 styleKey = param;
230 }
231
232 this.tagParamsList.style += styleKey + ': ' + value + ';';
233 }
234 },
235
236 _renderParams()
237 {
238 let rv = '';
239
240 for (let paramKey in this.tagParamsList)
241 {
242 const value = this.tagParamsList[paramKey]
243 if ((paramKey === 'style') && (value === ''))
244 {
245 // Do nothing for empty style
246 }
247 else
248 {
249 // If this is "boolean attribute", according to "2.4.2. Boolean attributes" of "HTML 5.3" spec
250 // (http://w3c.github.io/html/infrastructure.html#sec-boolean-attributes)
251 // then we just add the key without value.
252 // For example in <script async> - the "async" is boolean
253 // another example <input type="checkbox" checked> - the "checked" is boolean
254 if (typeof value === 'boolean') {
255 if (value) {
256 rv += ' ' + paramKey
257 }
258 } else if (paramKey === 'innerHTML') {
259 this.tagInnerContent = value
260 } else {
261 rv += ' ' + paramKey + '="' + value + '"'
262 }
263 }
264 }
265
266 return rv;
267 },
268
269 setData(key, value)
270 {
271 this._validateDataKey(key)
272 this.tagParamsList['data-' + key.toLowerCase()] = value;
273
274 return this;
275 }
276});
277
278K.NateHtmlHeadProto = Object.assign({}, K.NateHtmlElemProto,
279{
280 init(parent, namespace)
281 {
282 K.NateHtmlElemProto.init.call(this, parent, namespace)
283
284 this.htmlDocument = parent;
285 this.css = '';
286 this.meta = {};
287 this.alternateLinksForLanguages = null;
288 this.linkCanonical = null;
289 this.iconTxt = null;
290 },
291
292 setTitle(title)
293 {
294 this.meta.title = title;
295 },
296
297 setMetaAuthor(author)
298 {
299 this.meta.author = author;
300 },
301
302 setMetaDescription(description)
303 {
304 this.meta.description = description;
305 },
306
307 setMetaKeywords(keywords)
308 {
309 this.meta.keywords = keywords;
310 },
311
312 setLinksForAlternateLanguages(links)
313 {
314 this.alternateLinksForLanguages = links;
315 },
316
317 setLinkCanonical(link)
318 {
319 this.linkCanonical = link;
320 },
321
322 setIcon(href, type = "image/png")
323 {
324 this.iconTxt = '<link rel="icon" type="' + type + '" href="' + href + '" />';
325 },
326
327 render()
328 {
329 let txt = '<head>' + LB;
330
331 // title and metas
332 txt += '<meta charset="UTF-8">' + LB;
333
334 if (K.Object.isNotNull(this.meta.title))
335 {
336 txt += '<title>' + this.meta.title + '</title>' + LB;
337 }
338
339 if (K.Object.isNotNull(this.meta.description))
340 {
341 txt += '<meta name="description" content="' + K.NateHtmlProto.escapeHtmlAttribute(this.meta.description) + '">' + LB;
342 }
343
344 if (K.Object.isNotNull(this.meta.keywords))
345 {
346 txt += '<meta name="keywords" content="' + K.NateHtmlProto.escapeHtmlAttribute(this.meta.keywords) + '">' + LB;
347 }
348
349 if (K.Object.isNotNull(this.meta.author))
350 {
351 txt += '<meta name="author" content="' + K.NateHtmlProto.escapeHtmlAttribute(this.meta.author) + '">' + LB;
352 }
353
354 txt += '<meta http-equiv="X-UA-Compatible" content="IE=Edge"/>' + LB;
355 txt += '<meta name="viewport" content="width=device-width, initial-scale=1">' + LB;
356
357 if (K.Object.isNotNull(this.iconTxt))
358 {
359 txt += this.iconTxt;
360 }
361
362 // alternate links for different langauges
363 if (K.Object.isNotNull(this.alternateLinksForLanguages))
364 {
365 for (let langId in this.alternateLinksForLanguages)
366 {
367 const url = this.alternateLinksForLanguages[langId];
368
369 txt += '<link rel="alternate" hreflang="' + langId + '" href="' + url + '" />' + LB;
370 }
371 }
372
373 // canonical link
374 if (K.Object.isNotNull(this.linkCanonical))
375 {
376 txt += '<link rel="canonical" href="' + this.linkCanonical + '" />';
377 }
378
379 // css code
380 if (K.Object.isNotNull(this.css != ''))
381 {
382 if (EMIT_STATS_TO_CONSOLE_LOG)
383 {
384 console.log('head/css :', (this.css.length / 1024).toFixed(2), 'KB');
385 }
386
387 txt += '<style>' + LB + this.css + LB + '</style>' + LB;
388 }
389
390 // js code
391 const headJsCode = this.htmlDocument.jsGetCollector('head').getAllAsTag();
392
393 if (EMIT_STATS_TO_CONSOLE_LOG)
394 {
395 console.log('head/js :', (headJsCode.length / 1024).toFixed(2), 'KB');
396 }
397
398 txt += headJsCode;
399
400 // finish
401 txt += '</head>' + LB;
402
403 return txt;
404 },
405
406 pushCss(css)
407 {
408 this.css += css.trim() + LB;
409 }
410});
411
412K.NateHtmlDocumentProto = Object.assign({}, K.NateHtmlProto,
413{
414 init(parent, namespace)
415 {
416 K.NateHtmlProto.init.call(this, parent, namespace);
417
418 this.theHead = K.NateHtmlHead(this);
419 this.theBody = K.NateHtmlBody(this);
420
421 this.jsPipes = {
422 head: K.JsCollector(),
423 bodyEnd: K.JsCollector(),
424 onLoad: K.JsCollector(),
425 };
426
427 this.resourcesToLoad = {
428 css: [],
429 js_head: [],
430 js_bodyEnd: [],
431 js_onLoad: [],
432 };
433
434 this.resourcesToLoadCnt = 0;
435 this.resourcesLoadDoneCb = null;
436 this.waitingToEmit = false;
437 this.langId = null;
438
439 // debugging helpers
440 if (EMIT_RESOURCE_LOADING_TO_CONSOLE_LOG) {
441 this.LOG_resourceLoading = console.log;
442 } else {
443 this.LOG_resourceLoading = () => {};
444 }
445 },
446
447 head()
448 {
449 return this.theHead;
450 },
451
452 body()
453 {
454 return this.theBody;
455 },
456
457 render()
458 {
459 let langMeta = '';
460
461 if (K.Object.isNotNull(this.langId))
462 {
463 langMeta = ' lang="' + this.langId + '"';
464 }
465
466 if (EMIT_STATS_TO_CONSOLE_LOG)
467 {
468 console.log('------------------------------------------------------');
469 }
470
471 const bodyHtml = this.theBody.render();
472 const headHtml = this.theHead.render();
473
474 if (EMIT_STATS_TO_CONSOLE_LOG)
475 {
476 console.log('body :', (bodyHtml.length / 1024).toFixed(2), 'KB');
477 console.log('head :', (headHtml.length / 1024).toFixed(2), 'KB');
478 }
479
480 let txt = '';
481
482 txt += '<!DOCTYPE html>' + LB;
483 txt += '<html' + langMeta + '>' + LB;
484 txt += headHtml;
485 txt += bodyHtml;
486 txt += '</html>' + LB;
487
488 return txt;
489 },
490
491 setLang(langId)
492 {
493 this.langId = langId;
494 },
495
496 loadResource(resourceData, done) {
497 const cacheEntry = RESOURCE_CACHE[resourceData.path];
498
499 if (RESOURCE_CACHE_ON && cacheEntry) {
500 if (cacheEntry === RESOURCE_CACHE_STATE_PENDING) {
501 // This resource is beeing loaded by another Nate instance.
502 // Delay the work to use cached result built by another instance, so
503 // we don't call done callbck yet.
504 this.LOG_resourceLoading('[Nate] RESOURCE_CACHE already pending !');
505
506 resourceData.loadResourceCalled = false;
507
508 if (this.resourcesToLoadCnt === 1) {
509 // It's the last resource in current instance queue, so we can't
510 // retry load by itself.
511 // Delay the job until one of other Nate instance finished and then
512 // it will respawn us.
513 if (RESOURCES_WAITING_OBJECTS.indexOf(this) === -1) {
514 RESOURCES_WAITING_OBJECTS.push(this);
515 }
516 }
517
518 } else {
519 // Resource already loaded and cached. Just use it.
520 this.LOG_resourceLoading('[Nate] RESOURCE_CACHE hit !');
521 done(cacheEntry)
522 }
523
524 } else {
525 // First resource hit or cache disabled.
526 // Load resource the first time and cache for further use if needed.
527 this.LOG_resourceLoading('[Nate] RESOURCE_CACHE miss !');
528
529 // Avoid loading the same resource many time on first hit.
530 RESOURCE_CACHE[resourceData.path] = RESOURCE_CACHE_STATE_PENDING;
531
532 // Load resource from disk.
533 fs.readFile(resourceData.path, 'utf8', (readError, data) => {
534 if (readError) {
535 // Error while loading resource from disk.
536 console.log(readError);
537 throw new Error('File loading failed from path:' + resourceData.path);
538
539 } else {
540 // Success - resource loaded from disk.
541 // Cache result for further calls.
542 RESOURCE_CACHE[resourceData.path] = data;
543 done(data);
544 }
545 });
546 }
547 },
548
549 // important:
550 // done() callback is not called at the moment the resource is loaded as this may cause change of order
551 // (for example, loading: A-B-C, then calling done(): A-C-B, may cause a lot of problems)
552 // done() is called in right order after ALL resources are loaded
553 _onResourceLoaded(resourceData, data)
554 {
555 resourceData.loaded = true;
556 this.resourcesToLoadCnt--;
557 this.LOG_resourceLoading('[Nate] loaded resource:', resourceData.path, '(', 'total pending:', RESOURCES_LOAD_PENDING_CNT, ')');
558 resourceData.loadedContent = data;
559 this._resourcesCheckForPendingResources()
560 this._resourcesCheckForResourcesReady();
561 },
562
563 _triggerResourceLoad(resourceData)
564 {
565 if (RESOURCES_LOAD_PENDING_CNT < RESOURCES_LOAD_PENDING_LIMIT)
566 {
567 RESOURCES_LOAD_PENDING_CNT++;
568
569 // get from cache or trigger loading resource
570 this.LOG_resourceLoading('[Nate] trigger resource load:', resourceData.path, '(', 'total pending:', RESOURCES_LOAD_PENDING_CNT, ')');
571
572 resourceData.loadResourceCalled = true;
573
574 this.loadResource(resourceData, (data) => {
575 RESOURCES_LOAD_PENDING_CNT--;
576 this._onResourceLoaded(resourceData, data);
577 });
578
579 } else {
580 console.log('[Nate] WARNING! Too many open resource files. Pending resources limit reached (' + RESOURCES_LOAD_PENDING_CNT + '). Performance may be decreased.');
581
582 if (this.resourcesToLoadCnt === 1) {
583 // There are no free load resource slots even for the first resource.
584 // So, we can't start work for this object.
585 // Delay the job related to this object until other one finished its work.
586 if (RESOURCES_WAITING_OBJECTS.indexOf(this) === -1) {
587 RESOURCES_WAITING_OBJECTS.push(this);
588 }
589 }
590 }
591 },
592
593 addResourceToLoad(pipe, resourceData, done)
594 {
595 // add load request to loading-pipe
596 this.resourcesToLoadCnt++;
597 resourceData.loaded = false;
598 resourceData.done = done;
599 this.resourcesToLoad[pipe].push(resourceData);
600 this.LOG_resourceLoading('[Nate] add resource:', resourceData.path);
601 this._triggerResourceLoad(resourceData);
602 },
603
604 _resourcesCheckForPendingResources()
605 {
606 if (!this._resourcesCheckForPendingResourcesCalled)
607 {
608 this._resourcesCheckForPendingResourcesCalled = true;
609
610 this.LOG_resourceLoading('[Nate] check for pending resources');
611
612 // Check are there any pending resources to trigger.
613 // Possible improvement: optimize it (avoid loop after each load).
614 for (let pipe in this.resourcesToLoad)
615 {
616 this.resourcesToLoad[pipe].forEach((resourceData) => {
617 if (!resourceData.loadResourceCalled)
618 {
619 this._triggerResourceLoad(resourceData);
620 }
621 })
622 }
623
624 this._resourcesCheckForPendingResourcesCalled = false;
625 }
626 },
627
628 _resourcesCheckForResourcesReady()
629 {
630 if ((this.waitingToEmit) && (this.resourcesToLoadCnt == 0))
631 {
632 if (K.Object.isNotNull(this.resourcesLoadDoneCb))
633 {
634 this._resourcesLoadDoneCb();
635 this.resourcesLoadDoneCb = null;
636 }
637 }
638 },
639
640 _resourcesLoadDoneCb()
641 {
642 this.LOG_resourceLoading('[Nate] All resources loaded...');
643
644 for (let resourceType in this.resourcesToLoad)
645 {
646 this.resourcesToLoadItem = this.resourcesToLoad[resourceType];
647
648 for (let resourceItemIdx in this.resourcesToLoadItem)
649 {
650 const resourceItem = this.resourcesToLoadItem[resourceItemIdx];
651
652 resourceItem.done(resourceItem.loadedContent)
653 }
654 }
655
656 if (K.Object.isNotNull(this.resourcesLoadDoneCb))
657 {
658 this.resourcesLoadDoneCb();
659 }
660
661 // We finished all work related to resources for this object.
662 // Maybe some other object is waiting for free resources slot?
663 if (RESOURCES_WAITING_OBJECTS.length > 0) {
664 RESOURCES_WAITING_OBJECTS.shift()._resourcesCheckForPendingResources();
665 }
666 },
667
668 cssFromStaticFile(path, done)
669 {
670 this.addResourceToLoad('css', {path: path}, (data) =>
671 {
672 this.theHead.pushCss(data);
673
674 if (K.Object.isNotNull(done))
675 {
676 done();
677 }
678 });
679 },
680
681 // supported pipes are:
682 // head - goes to <head></head>
683 // bodyEnd - goes to bottom of <body></body>
684 // onLoad - goes to jQuery onLoad handler at the end of <body></body> section
685 jsGetCollector(pipeName)
686 {
687 return this.jsPipes[pipeName];
688 },
689
690 jsFromStaticFile(path, pipeName, done)
691 {
692 K.Error.reportIfParameterNotSet(path, 'javascript path');
693
694 this.addResourceToLoad('js_' + pipeName, {path: path}, (data) =>
695 {
696 this.jsGetCollector(pipeName).pushFromTxt(data);
697
698 if (EMIT_STATS_TO_CONSOLE_LOG)
699 {
700 console.log('[', path, ']', (data.length / 1024).toFixed(2), 'KB');
701 }
702
703 if (K.Object.isNotNull(done))
704 {
705 done();
706 }
707 });
708 },
709
710 coffeeFromStaticFile(path, pipeName, done)
711 {
712 K.Error.reportIfParameterNotSet(path, 'coffeeScript path');
713
714 this.addResourceToLoad('js_' + pipeName, {path: path}, (data) =>
715 {
716 let compiled = null;
717
718 // compile or get from cache
719 if (RESOURCE_CACHE_ON && K.Object.isNotNull(RESOURCE_COMPILED_CACHE[path]))
720 {
721 compiled = RESOURCE_COMPILED_CACHE[path];
722 }
723 else
724 {
725 compiled = coffee.compile(data);
726
727 RESOURCE_COMPILED_CACHE[path] = compiled;
728 }
729
730 // emit and done()
731 this.jsGetCollector(pipeName).pushFromTxt(compiled);
732
733 if (EMIT_STATS_TO_CONSOLE_LOG)
734 {
735 console.log('[', path, ']', (compiled.length / 1024).toFixed(2), 'KB');
736 }
737
738 if (K.Object.isNotNull(done))
739 {
740 done();
741 }
742 });
743 },
744
745 _onReadyToEmit(done)
746 {
747 this.waitingToEmit = true;
748 this.resourcesLoadDoneCb = done;
749 this._resourcesCheckForResourcesReady();
750 },
751
752 emitAsResponse(httpResponse, cb)
753 {
754 this._onReadyToEmit(() =>
755 {
756 const responseBody = this.render();
757
758 httpResponse.send(responseBody);
759
760 if (K.Object.isNotNull(cb))
761 {
762 const responseStats = {htmlSize: responseBody.length};
763
764 cb(responseStats);
765 }
766 });
767 },
768
769 emitToString(cb)
770 {
771 this._onReadyToEmit(() =>
772 {
773 if (K.Object.isNotNull(cb))
774 {
775 cb(this.render());
776 }
777 });
778 }
779});
780
781K.NateHtmlBodyProto = Object.assign({}, K.NateHtmlElemProto,
782{
783 init(parent, namespace)
784 {
785 K.NateHtmlElemProto.init.call(this, parent, namespace);
786
787 this.htmlDocument = parent;
788 this.tagOpenCloseMode = K.NateXmlAbstractProto.JUST_CHILDREN;
789 },
790
791 render()
792 {
793 const jsBodyEnd = this.htmlDocument.jsGetCollector('bodyEnd').getAllAsTag();
794 const jsOnLoad = this.htmlDocument.jsGetCollector('onLoad').getAllAsOnLoadCode();
795
796 if (EMIT_STATS_TO_CONSOLE_LOG)
797 {
798 console.log('js/bodyEnd :', (jsBodyEnd.length / 1024).toFixed(2), 'KB');
799 console.log('js/onLoad :', (jsOnLoad.length / 1024).toFixed(2), 'KB');
800 }
801
802 let txt = '<body';
803
804 txt += '>';
805 txt += K.NateXmlAbstractProto.render.call(this); // TODO: super.render() ?
806 txt += jsBodyEnd;
807 txt += jsOnLoad;
808 txt += '</body>';
809
810 return txt;
811 }
812});
813
814// -----------------------------------------------------------------------------
815// HTML namespace
816// -----------------------------------------------------------------------------
817
818K.nateHtmlStaticNamespace =
819 K.nateXmlStaticNamespace.createExtension('HtmlStatic', {nateProto: K.NateHtmlElemProto})
820
821// -----------------------------------------------------------------------------
822// HTML setters
823// -----------------------------------------------------------------------------
824
825const commonStyleAttributeSetter = (nNode, key, value) => {
826 nNode.updateStyle(key, value);
827}
828
829const commonJsHandlerAttributeSetter = (nNode, key, value) => {
830 switch (typeof(value)) {
831 case 'function': {
832 // We got JS function - get source code.
833 value = value.toString()
834
835 // Remove function() {...} wrapper if existent.
836 const idxFunction = value.indexOf('function')
837
838 if (idxFunction !== -1) {
839 const idxBegin = value.indexOf('{', idxFunction) + 1
840 const idxEnd = value.lastIndexOf('}')
841
842 value = value.substr(idxBegin, idxEnd - idxBegin)
843 }
844
845 nNode.tagParamsList[key] = value
846
847 break
848 }
849
850 case 'string': {
851 // We got JS source as pure string - just pass it as is.
852 nNode.tagParamsList[key] = value
853
854 break
855 }
856
857 default: {
858 // Fatal - unexpected handler format.
859 // Should never happen on production.
860 console.log('PANIC! Unexpected ', key, 'handler', typeof(value), value);
861
862 break
863 }
864 }
865}
866
867K.BSC.nateCommonHtmlAttributes.forEach((key) => {
868 K.nateHtmlStaticNamespace.addSetter(key, K.NateXmlAbstractProto.commonAttributeSetter)
869})
870
871K.BSC.nateCommonHtmlProperties.forEach((key) => {
872 K.nateHtmlStaticNamespace.addSetter(key, K.NateXmlAbstractProto.commonAttributeSetter)
873})
874
875K.BSC.nateCommonStyleAttributes.forEach((key) => {
876 K.nateHtmlStaticNamespace.addSetter(key, commonStyleAttributeSetter)
877})
878
879K.nateHtmlStaticNamespace.addSetter('style', K.NateXmlAbstractProto.commonAttributeSetter)
880
881K.nateHtmlStaticNamespace.addSetter('cssText', (nNode, key, value) => {
882 nNode.set('style', value)
883})
884
885K.nateHtmlStaticNamespace.addSetter('on', (nNode, key, value) => {
886 // TODO: handling of other things than 'block'
887 if (value) {
888 nNode.set('display', 'block')
889 } else {
890 nNode.set('display', 'none')
891 }
892})
893
894K.nateHtmlStaticNamespace.addSetter('innerHTML',
895 K.NateXmlAbstractProto.commonInnerContentSetter)
896
897// Possible imprevement: stringify json at render time.
898K.nateHtmlStaticNamespace.addSetter('innerJSON',
899 K.NateXmlAbstractProto.commonInnerContentSetter)
900
901K.nateHtmlStaticNamespace.addSetter('rect', (nNode, key, value) => {
902 // TODO: Not implemented.
903})
904
905K.nateHtmlStaticNamespace.addSetter('textContent', (nNode, key, value) => {
906 const escapedValue = K.NateHtmlProto.escapeHtmlContent(value)
907 K.NateXmlAbstractProto.commonInnerContentSetter(nNode, 'textContent', escapedValue)
908})
909
910K.nateHtmlStaticNamespace.addSetter('innerText', (nNode, key, value) => {
911 // Convert \n to <br>
912 const escapedValue = K.NateHtmlProto.nl2br(K.NateHtmlProto.escapeHtmlContent(value))
913 K.NateXmlAbstractProto.commonInnerContentSetter(nNode, 'innerText', escapedValue)
914})
915
916K.nateHtmlStaticNamespace.addSetter('readOnly', (nNode, key, value) => {
917 if (value) {
918 // Adjust to DOM beheavior.
919 nNode.tagParamsList['readonly'] = ''
920 } else if (nNode.tagParamsList['readonly']) {
921 delete nNode.tagParamsList['readonly']
922 }
923})
924
925K.nateHtmlStaticNamespace.addSetter('checked' , K.NateXmlAbstractProto.commonOnOffAttributeSetter)
926K.nateHtmlStaticNamespace.addSetter('selected', K.NateXmlAbstractProto.commonOnOffAttributeSetter)
927
928K.nateHtmlStaticNamespace.addSetter('onscroll', commonJsHandlerAttributeSetter)
929K.nateHtmlStaticNamespace.addSetter('onclick' , commonJsHandlerAttributeSetter)
930
931// -----------------------------------------------------------------------------
932// HTML tags
933// -----------------------------------------------------------------------------
934
935K.BSC.nateCommonHtmlOpenCloseTags.forEach((key) => {
936 K.nateHtmlStaticNamespace.addCreateFunction(key, K.NateXmlAbstractProto.tagOpenClose);
937})
938
939K.BSC.nateCommonHtmlOpenCloseAtOnceTags.forEach((key) => {
940 K.nateHtmlStaticNamespace.addCreateFunction(key, K.NateXmlAbstractProto.tagOpenCloseAtOnce);
941})
942
943K.BSC.nateCommonHtmlInputTypes.forEach((inputType) => {
944 const key = 'input' + inputType.charAt(0).toUpperCase() + inputType.substr(1);
945
946 K.nateHtmlStaticNamespace.addCreateFunction(key, function(tagName, params = {}) {
947 params = K.Object.merge({type: inputType}, params);
948
949 return this.tagOpenCloseAtOnce('input', params);
950 });
951})
952
953K.nateHtmlStaticNamespace.addCreateFunction('svg', function() {
954 return K.NateSvgDocument(this, K.BSC.NateAbstractProto.USE_DEFAULT_NAMESPACE);
955});
956
957// -----------------------------------------------------------------------------
958// Factory functions
959// -----------------------------------------------------------------------------
960
961K.NateHtmlElem = K.nateHtmlStaticNamespace.createFactoryFunction(K.NateHtmlElemProto);
962K.NateHtmlBody = K.nateHtmlStaticNamespace.createFactoryFunction(K.NateHtmlBodyProto);
963K.NateHtmlHead = K.nateHtmlStaticNamespace.createFactoryFunction(K.NateHtmlHeadProto);
964K.NateHtmlDocument = K.nateHtmlStaticNamespace.createFactoryFunction(K.NateHtmlDocumentProto);
965
966K.JsCollector = function() {
967 const thiz = Object.create(K.JsCollectorProto);
968
969 thiz.init();
970
971 return thiz;
972}