~repos /atoms-element

#js

git clone https://pyrossh.dev/repos/atoms-element.git

A simple web component library for defining your custom elements. It works on both client and server.


be4ce514 Peter John

4 years ago
fix tw css
Files changed (5) hide show
  1. example/app-counter.js +1 -1
  2. example/page.js +2 -8
  3. example/server.js +1 -2
  4. example/styles.js +0 -121
  5. index.js +534 -394
example/app-counter.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createElement, html, object, number, string, unsafeHTML } from '../index.js';
1
+ import { createElement, html, object, number, string } from '../index.js';
2
2
 
3
3
  const name = () => 'app-counter';
4
4
 
example/page.js CHANGED
@@ -1,14 +1,8 @@
1
- import { createPage, html, css } from '../index.js';
1
+ import { createPage, html } from '../index.js';
2
- import { pageStyles } from './styles.js';
3
2
  import './app-counter.js';
4
3
 
5
4
  const head = ({ config }) => {
6
- return html`
7
- <title>${config.title}</title>
5
+ return html` <title>${config.title}</title> `;
8
- <style>
9
- ${css(pageStyles)}
10
- </style>
11
- `;
12
6
  };
13
7
 
14
8
  const body = () => {
example/server.js CHANGED
@@ -11,12 +11,11 @@ const port = process.argv[2] || 3000;
11
11
  const srcMap = {
12
12
  '/index.js': `${__dirname}/../index.js`,
13
13
  '/lit-html.js': `${__dirname}/../lit-html.js`,
14
- '/styles.js': `${__dirname}/styles.js`,
15
14
  '/app-counter.js': `${__dirname}/app-counter.js`,
16
15
  };
17
16
 
18
17
  http
19
- .createServer(function (req, res) {
18
+ .createServer((req, res) => {
20
19
  if (req.url === '/') {
21
20
  res.statusCode = 200;
22
21
  res.setHeader('Content-type', 'text/html');
example/styles.js DELETED
@@ -1,121 +0,0 @@
1
- export const pageStyles = {
2
- '*, ::before, ::after': {
3
- boxSizing: 'border-box',
4
- borderWidth: '0',
5
- borderStyle: 'solid',
6
- borderColor: '#e5e7eb',
7
- },
8
- hr: { height: '0', color: 'inherit', borderTopWidth: '1px' },
9
- 'abbr[title]': {
10
- WebkitTextDecoration: 'underline dotted',
11
- textDecoration: 'underline dotted',
12
- },
13
- 'b, strong': { fontWeight: 'bolder' },
14
- 'code, kbd, samp, pre': {
15
- fontFamily: "ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace",
16
- fontSize: '1em',
17
- },
18
- small: { fontSize: '80%' },
19
- 'sub, sup': {
20
- fontSize: '75%',
21
- lineHeight: 0,
22
- position: 'relative',
23
- verticalAlign: 'baseline',
24
- },
25
- sub: { bottom: '-0.25em' },
26
- sup: { top: '-0.5em' },
27
- table: {
28
- textIndent: '0',
29
- borderColor: 'inherit',
30
- borderCollapse: 'collapse',
31
- },
32
- 'button, input, optgroup, select, textarea': {
33
- fontSize: '100%',
34
- margin: '0',
35
- padding: '0',
36
- lineHeight: 'inherit',
37
- color: 'inherit',
38
- },
39
- 'button, select': {},
40
- "button, [type='button'], [type='reset'], [type='submit']": {},
41
- '::-moz-focus-inner': { borderStyle: 'none', padding: '0' },
42
- ':-moz-focusring': { outline: '1px dotted ButtonText' },
43
- ':-moz-ui-invalid': { boxShadow: 'none' },
44
- legend: { padding: '0' },
45
- progress: { verticalAlign: 'baseline' },
46
- '::-webkit-inner-spin-button, ::-webkit-outer-spin-button': {
47
- height: 'auto',
48
- },
49
- "[type='search']": { WebkitAppearance: 'textfield', outlineOffset: '-2px' },
50
- '::-webkit-search-decoration': { WebkitAppearance: 'none' },
51
- '::-webkit-file-upload-button': {
52
- WebkitAppearance: 'button',
53
- font: 'inherit',
54
- },
55
- summary: { display: 'list-item' },
56
- 'blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre': {
57
- margin: '0',
58
- },
59
- button: {
60
- backgroundImage: 'none',
61
- ':focus': {
62
- outline: '1px dotted, 5px auto -webkit-focus-ring-color',
63
- },
64
- },
65
- fieldset: { margin: '0', padding: '0' },
66
- 'ol, ul': { listStyle: 'none', margin: '0', padding: '0' },
67
- img: { borderStyle: 'solid' },
68
- textarea: { resize: 'vertical' },
69
- 'input::-moz-placeholder, textarea::-moz-placeholder': {
70
- opacity: 1,
71
- color: '#9ca3af',
72
- },
73
- 'input:-ms-input-placeholder, textarea:-ms-input-placeholder': {
74
- opacity: 1,
75
- color: '#9ca3af',
76
- },
77
- 'input::placeholder, textarea::placeholder': {
78
- opacity: 1,
79
- color: '#9ca3af',
80
- },
81
- "button, [role='button']": { cursor: 'pointer' },
82
- 'h1, h2, h3, h4, h5, h6': { fontSize: 'inherit', fontWeight: 'inherit' },
83
- a: { color: 'inherit', textDecoration: 'inherit' },
84
- 'pre, code, kbd, samp': {
85
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
86
- },
87
- 'img, svg, video, canvas, audio, iframe, embed, object': {
88
- display: 'block',
89
- verticalAlign: 'middle',
90
- },
91
- 'img, video': { maxWidth: '100%', height: 'auto' },
92
- html: {
93
- MozTabSize: '4',
94
- OTabSize: '4',
95
- tabSize: 4,
96
- lineHeight: 1.5,
97
- WebkitTextSizeAdjust: '100%',
98
- fontFamily:
99
- "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
100
- width: '100%',
101
- height: '100%',
102
- },
103
- body: {
104
- margin: '0px',
105
- fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
106
- lineHeight: 1.4,
107
- backgroundColor: 'white',
108
- width: '100%',
109
- height: '100%',
110
- display: 'flex',
111
- flexDirection: 'column',
112
- flex: '1 1 0%',
113
- minWidth: '320px',
114
- minHeight: '100vh',
115
- fontWeight: 400,
116
- color: 'rgba(44, 62, 80, 1)',
117
- direction: 'ltr',
118
- fontSynthesis: 'none',
119
- textRendering: 'optimizeLegibility',
120
- },
121
- };
index.js CHANGED
@@ -46,19 +46,20 @@ export const css = (obj, isChild = false, indent = '') => {
46
46
  return cssText;
47
47
  };
48
48
 
49
+ const extractClasses = (html) => {
50
+ let str = '';
51
+ const matches = html.match(/class=(?:["']\W+\s*(?:\w+)\()?["']([^'"]+)['"]/gim);
52
+ if (matches) {
53
+ matches.forEach((matched, i) => {
54
+ str += matched.replace('class="', '').replace('"', '') + (i === matches.length - 1 ? '' : ' ');
55
+ });
56
+ }
57
+ return str;
58
+ };
59
+
49
60
  export const getClassList = (template) => {
61
+ // const classes = template.strings.reduce((acc, item) => acc + extractClasses(item), '').split(' ');
50
- const classes = template.strings
62
+ const classes = [];
51
- .reduce((acc, item) => {
52
- const matches = item.match(/class=(?:["']\W+\s*(?:\w+)\()?["']([^'"]+)['"]/gim);
53
- if (matches) {
54
- matches.forEach((matched) => {
55
- acc += matched.replace('class="', '').replace('"', '') + ' ';
56
- });
57
- }
58
- return acc;
59
- }, '')
60
- .split(' ')
61
- .filter((it) => it !== '');
62
63
  template.values.forEach((item) => {
63
64
  if (typeof item === 'string') {
64
65
  const list = item.split(' ');
@@ -69,7 +70,20 @@ export const getClassList = (template) => {
69
70
  return classes;
70
71
  };
71
72
 
73
+ export const apply = (classes) => {
74
+ const classStyles = {};
75
+ classes.split(' ').forEach((cls) => {
76
+ const styles = classLookup[cls];
77
+ if (styles) {
78
+ Object.keys(styles).forEach((key) => {
79
+ classStyles[key] = styles[key];
80
+ });
81
+ }
82
+ });
83
+ return classStyles;
84
+ };
85
+
72
- export const getStyleSheet = (classList) => {
86
+ export const generateTWStyleSheet = (classList) => {
73
87
  let styleSheet = ``;
74
88
  classList.forEach((cls) => {
75
89
  const item = classLookup[cls];
@@ -504,114 +518,528 @@ const classLookup = {
504
518
  ring: createStyle('box-shadow', ' 0 0 0 calc(3px + 0px) rgba(59, 130, 246, 0.5'),
505
519
  };
506
520
 
521
+ const pageStyles = {
522
+ '*, ::before, ::after': {
523
+ boxSizing: 'border-box',
524
+ borderWidth: '0',
525
+ borderStyle: 'solid',
526
+ borderColor: '#e5e7eb',
527
+ },
528
+ hr: { height: '0', color: 'inherit', borderTopWidth: '1px' },
529
+ 'abbr[title]': {
530
+ WebkitTextDecoration: 'underline dotted',
531
+ textDecoration: 'underline dotted',
532
+ },
533
+ 'b, strong': { fontWeight: 'bolder' },
534
+ 'code, kbd, samp, pre': {
535
+ fontFamily: "ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace",
536
+ fontSize: '1em',
537
+ },
538
+ small: { fontSize: '80%' },
539
+ 'sub, sup': {
540
+ fontSize: '75%',
541
+ lineHeight: 0,
542
+ position: 'relative',
543
+ verticalAlign: 'baseline',
544
+ },
545
+ sub: { bottom: '-0.25em' },
546
+ sup: { top: '-0.5em' },
547
+ table: {
548
+ textIndent: '0',
549
+ borderColor: 'inherit',
550
+ borderCollapse: 'collapse',
551
+ },
552
+ 'button, input, optgroup, select, textarea': {
553
+ fontSize: '100%',
554
+ margin: '0',
555
+ padding: '0',
556
+ lineHeight: 'inherit',
557
+ color: 'inherit',
558
+ },
559
+ 'button, select': {},
560
+ "button, [type='button'], [type='reset'], [type='submit']": {},
561
+ '::-moz-focus-inner': { borderStyle: 'none', padding: '0' },
562
+ ':-moz-focusring': { outline: '1px dotted ButtonText' },
563
+ ':-moz-ui-invalid': { boxShadow: 'none' },
564
+ legend: { padding: '0' },
565
+ progress: { verticalAlign: 'baseline' },
566
+ '::-webkit-inner-spin-button, ::-webkit-outer-spin-button': {
567
+ height: 'auto',
568
+ },
569
+ "[type='search']": { WebkitAppearance: 'textfield', outlineOffset: '-2px' },
570
+ '::-webkit-search-decoration': { WebkitAppearance: 'none' },
571
+ '::-webkit-file-upload-button': {
572
+ WebkitAppearance: 'button',
573
+ font: 'inherit',
574
+ },
575
+ summary: { display: 'list-item' },
576
+ 'blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre': {
577
+ margin: '0',
578
+ },
579
+ button: {
580
+ backgroundImage: 'none',
581
+ ':focus': {
582
+ outline: '1px dotted, 5px auto -webkit-focus-ring-color',
583
+ },
584
+ },
585
+ fieldset: { margin: '0', padding: '0' },
586
+ 'ol, ul': { listStyle: 'none', margin: '0', padding: '0' },
587
+ img: { borderStyle: 'solid' },
588
+ textarea: { resize: 'vertical' },
589
+ 'input::-moz-placeholder, textarea::-moz-placeholder': {
590
+ opacity: 1,
591
+ color: '#9ca3af',
592
+ },
593
+ 'input:-ms-input-placeholder, textarea:-ms-input-placeholder': {
594
+ opacity: 1,
595
+ color: '#9ca3af',
596
+ },
597
+ 'input::placeholder, textarea::placeholder': {
598
+ opacity: 1,
599
+ color: '#9ca3af',
600
+ },
601
+ "button, [role='button']": { cursor: 'pointer' },
602
+ 'h1, h2, h3, h4, h5, h6': { fontSize: 'inherit', fontWeight: 'inherit' },
603
+ a: { color: 'inherit', textDecoration: 'inherit' },
604
+ 'pre, code, kbd, samp': {
605
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
606
+ },
607
+ 'img, svg, video, canvas, audio, iframe, embed, object': {
608
+ display: 'block',
609
+ verticalAlign: 'middle',
610
+ },
611
+ 'img, video': { maxWidth: '100%', height: 'auto' },
612
+ html: {
613
+ MozTabSize: '4',
614
+ OTabSize: '4',
615
+ tabSize: 4,
616
+ lineHeight: 1.5,
617
+ WebkitTextSizeAdjust: '100%',
618
+ fontFamily:
619
+ "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'",
620
+ width: '100%',
621
+ height: '100%',
622
+ },
623
+ body: {
624
+ margin: '0px',
625
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
626
+ lineHeight: 1.4,
627
+ backgroundColor: 'white',
628
+ width: '100%',
629
+ height: '100%',
630
+ display: 'flex',
631
+ flexDirection: 'column',
632
+ flex: '1 1 0%',
633
+ minWidth: '320px',
634
+ minHeight: '100vh',
635
+ fontWeight: 400,
636
+ color: 'rgba(44, 62, 80, 1)',
637
+ direction: 'ltr',
638
+ fontSynthesis: 'none',
639
+ textRendering: 'optimizeLegibility',
640
+ },
641
+ };
642
+
507
643
  // hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-indigo-500
508
644
 
509
- const lastAttributeNameRegex =
510
- /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
511
- const tagRE = /<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
512
- const whitespaceRE = /^\s*$/;
645
+ const previousValues = new WeakMap();
646
+ export const unsafeHTML = isBrowser
647
+ ? directive((value) => (part) => {
648
+ if (!(part instanceof NodePart)) {
649
+ throw new Error('unsafeHTML can only be used in text bindings');
650
+ }
651
+ const previousValue = previousValues.get(part);
513
- const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;
652
+ if (previousValue !== undefined && isPrimitive(value) && value === previousValue.value && part.value === previousValue.fragment) {
653
+ return;
654
+ }
655
+ const template = document.createElement('template');
514
- const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
656
+ template.innerHTML = value; // innerHTML casts to string internally
657
+ const fragment = document.importNode(template.content, true);
658
+ part.setValue(fragment);
659
+ previousValues.set(part, { value, fragment });
660
+ })
661
+ : (value) => value;
515
662
 
516
- const parseTag = (tag) => {
663
+ const logError = (msg) => {
664
+ if (isBrowser ? window.__DEV__ : global.__DEV) {
517
- const res = {
665
+ console.warn(msg);
518
- type: 'tag',
519
- name: '',
520
- voidElement: false,
521
- attrs: {},
666
+ }
522
- children: [],
523
- };
667
+ };
524
668
 
669
+ const validator = (type, validate) => (innerType) => {
525
- const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/);
670
+ const isPrimitiveType = ['number', 'string', 'boolean'].includes(type);
671
+ const common = {
672
+ type: type,
673
+ parse: isPrimitiveType ? (attr) => attr : (attr) => (attr ? JSON.parse(attr.replace(/'/g, `"`)) : null),
674
+ validate: (context, data) => {
675
+ if (data === null || typeof data === 'undefined') {
676
+ if (common.__required) {
677
+ logError(`'${context}' Field is required`);
678
+ }
679
+ return;
680
+ }
681
+ if (!isPrimitiveType) {
682
+ validate(innerType, context, data);
683
+ } else {
684
+ const dataType = typeof data;
526
- if (tagMatch) {
685
+ if (dataType !== type) {
527
- res.name = tagMatch[1];
528
- if (voidElements.includes(tagMatch[1]) || tag.charAt(tag.length - 2) === '/') {
686
+ logError(`'${context}' Expected type '${type}' got type '${dataType}'`);
687
+ }
688
+ }
689
+ },
690
+ };
691
+ common.required = () => {
529
- res.voidElement = true;
692
+ common.__required = true;
693
+ return common;
694
+ };
695
+ common.default = (fnOrValue) => {
696
+ common.__default = fnOrValue;
697
+ return common;
698
+ };
699
+ common.compute = (...args) => {
700
+ const fn = args[args.length - 1];
701
+ const deps = args.slice(0, args.length - 1);
702
+ common.__compute = {
703
+ fn,
704
+ deps,
705
+ };
706
+ return common;
707
+ };
708
+ common.action = (name, fn) => {
709
+ if (!common.__handlers) {
710
+ common.__handlers = {};
530
711
  }
712
+ common.__handlers[name] = fn;
713
+ return common;
714
+ };
715
+ return common;
716
+ };
531
717
 
532
- // handle comment tag
533
- if (res.name.startsWith('!--')) {
534
- const endIndex = tag.indexOf('-->');
718
+ export const number = validator('number');
535
- return {
536
- type: 'comment',
537
- comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',
719
+ export const string = validator('string');
720
+ export const boolean = validator('boolean');
721
+ export const object = validator('object', (innerType, context, data) => {
538
- };
722
+ if (data.constructor !== Object) {
723
+ logError(`'${context}' Expected object literal '{}' got '${typeof data}'`);
539
- }
724
+ }
725
+ for (const key of Object.keys(innerType)) {
726
+ const fieldValidator = innerType[key];
727
+ const item = data[key];
728
+ fieldValidator.validate(`${context}.${key}`, item);
729
+ }
730
+ });
731
+ export const array = validator('array', (innerType, context, data) => {
732
+ if (!Array.isArray(data)) {
733
+ logError(`Expected Array got ${data}`);
540
734
  }
735
+ for (let i = 0; i < data.length; i++) {
736
+ const item = data[i];
737
+ innerType.validate(`${context}[${i}]`, item);
738
+ }
739
+ });
541
740
 
542
- const reg = new RegExp(attrRE);
741
+ const fifo = (q) => q.shift();
543
- let result = null;
544
- for (;;) {
545
- result = reg.exec(tag);
742
+ const microtask = (flush) => () => queueMicrotask(flush);
546
743
 
547
- if (result === null) {
744
+ const registry = {};
548
- break;
745
+ const BaseElement = isBrowser ? window.HTMLElement : class {};
549
- }
550
746
 
747
+ export class AtomsElement extends BaseElement {
748
+ static register() {
749
+ registry[this.name] = this;
551
- if (!result[0].trim()) {
750
+ if (isBrowser) {
751
+ if (window.customElements.get(this.name)) {
552
- continue;
752
+ return;
753
+ } else {
754
+ window.customElements.define(this.name, registry[this.name]);
755
+ }
553
756
  }
757
+ }
554
758
 
555
- if (result[1]) {
556
- const attr = result[1].trim();
759
+ static getElement(name) {
557
- let arr = [attr, ''];
760
+ return registry[name];
558
-
559
- if (attr.indexOf('=') > -1) {
560
- arr = attr.split('=');
561
- }
761
+ }
562
762
 
563
- res.attrs[arr[0]] = arr[1];
763
+ static get observedAttributes() {
564
- reg.lastIndex--;
565
- } else if (result[2]) {
764
+ if (!this.attrTypes) {
566
- res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1);
765
+ return [];
567
766
  }
767
+ return Object.keys(this.attrTypes).map((k) => k.toLowerCase());
568
768
  }
569
769
 
570
- return res;
571
- };
572
- const parseHtml = (html) => {
770
+ constructor(attrs) {
771
+ super();
772
+ this._dirty = false;
773
+ this._connected = false;
774
+ this.attrs = attrs || {};
775
+ this.state = {};
776
+ this.config = isBrowser ? window.config : global.config;
777
+ this.location = isBrowser ? window.location : global.location;
573
- const result = [];
778
+ this.prevClassList = [];
779
+ if (!isBrowser) {
574
- const arr = [];
780
+ this.initState();
575
- let current;
576
- let level = -1;
781
+ } else {
782
+ // this.shadow = this.attachShadow({ mode: 'open' });
783
+ }
784
+ }
577
785
 
578
- // handle text at top level
579
- if (html.indexOf('<') !== 0) {
580
- var end = html.indexOf('<');
581
- result.push({
786
+ initAttrs() {
582
- type: 'text',
787
+ Object.keys(this.constructor.attrTypes).forEach((key) => {
583
- content: end === -1 ? html : html.substring(0, end),
788
+ const attrType = this.constructor.attrTypes[key];
789
+ const newValue = this.getAttribute(key.toLowerCase());
790
+ const data = attrType.parse(newValue);
791
+ attrType.validate(`<${this.constructor.name}> ${key}`, data);
792
+ this.attrs[key] = data;
584
793
  });
585
794
  }
586
795
 
587
- html.replace(tagRE, function (tag, index) {
588
- const isOpen = tag.charAt(1) !== '/';
589
- const isComment = tag.startsWith('<!--');
590
- const start = index + tag.length;
591
- const nextChar = html.charAt(start);
592
- let parent;
593
-
594
- if (isComment) {
796
+ initState() {
797
+ Object.keys(this.constructor.stateTypes).forEach((key) => {
595
- const comment = parseTag(tag);
798
+ const stateType = this.constructor.stateTypes[key];
596
-
597
- // if we're at root, push new base node
799
+ if (!this.state[key] && typeof stateType.__default !== 'undefined') {
598
- if (level < 0) {
599
- result.push(comment);
800
+ this.state[key] = typeof stateType.__default === 'function' ? stateType.__default(this.attrs, this.state) : stateType.__default;
600
- return result;
601
801
  }
602
- parent = arr[level];
603
- parent.children.push(comment);
604
- return result;
605
- }
606
-
607
- if (isOpen) {
608
- level++;
609
-
610
- current = parseTag(tag);
611
-
612
- if (!current.voidElement && nextChar && nextChar !== '<') {
613
- current.children.push({
614
- type: 'text',
802
+ const setKey = `set${key[0].toUpperCase()}${key.slice(1)}`;
803
+ this.state[setKey] = (v) => {
804
+ // TODO: check type on set
805
+ this.state[key] = typeof v === 'function' ? v(this.state[key]) : v;
806
+ this.update();
807
+ };
808
+ if (stateType.__handlers) {
809
+ Object.keys(stateType.__handlers).map((hkey) => {
810
+ this.state[hkey] = () => stateType.__handlers[hkey]({ attrs: this.attrs, state: this.state });
811
+ });
812
+ }
813
+ });
814
+ }
815
+
816
+ connectedCallback() {
817
+ this._connected = true;
818
+ this.initAttrs();
819
+ this.initState();
820
+ this.update();
821
+ }
822
+ disconnectedCallback() {
823
+ this._connected = false;
824
+ }
825
+
826
+ attributeChangedCallback(key, oldValue, newValue) {
827
+ if (this._connected) {
828
+ this.initAttrs();
829
+ this.update();
830
+ }
831
+ }
832
+
833
+ update() {
834
+ if (this._dirty) {
835
+ return;
836
+ }
837
+ this._dirty = true;
838
+ this.enqueueUpdate();
839
+ }
840
+
841
+ _performUpdate() {
842
+ if (!this._connected) {
843
+ return;
844
+ }
845
+ this.renderTemplate();
846
+ this._dirty = false;
847
+ }
848
+
849
+ batch(runner, pick, callback) {
850
+ const q = [];
851
+ const flush = () => {
852
+ let p;
853
+ while ((p = pick(q))) callback(p);
854
+ };
855
+ const run = runner(flush);
856
+ q.push(this) === 1 && run();
857
+ }
858
+
859
+ enqueueUpdate() {
860
+ this.batch(microtask, fifo, () => this._performUpdate());
861
+ }
862
+
863
+ get computed() {
864
+ return Object.keys(this.constructor.computedTypes).reduceRight((acc, key) => {
865
+ const type = this.constructor.computedTypes[key];
866
+ const state = this.state;
867
+ const values = type.__compute.deps.reduce((dacc, key) => {
868
+ if (typeof state[key] !== undefined) {
869
+ dacc.push(state[key]);
870
+ }
871
+ return dacc;
872
+ }, []);
873
+ acc[key] = type.__compute.fn(...values);
874
+ return acc;
875
+ }, {});
876
+ }
877
+
878
+ renderTemplate() {
879
+ const template = this.render();
880
+ if (isBrowser) {
881
+ // TODO: this can be optimized when we know whether the value belongs in a class (AttributePart)
882
+ // maybe do this in lit-html itselfs
883
+ const newClassList = getClassList(template).filter((cls) => {
884
+ const globalStyles = document.getElementById('global').textContent;
885
+ return !globalStyles.includes('.' + cls);
886
+ });
887
+ if (newClassList.length > 0) {
888
+ document.getElementById('global').textContent += generateTWStyleSheet(newClassList);
889
+ }
890
+ render(template, this);
891
+ // For shadows only
892
+ // if (!this.styleElement) {
893
+ // render(template, this.shadow);
894
+ // const styleSheet = generateTWStyleSheet(classList);
895
+ // this.prevClassList = classList;
896
+ // this.styleElement = document.createElement('style');
897
+ // this.shadow.appendChild(this.styleElement).textContent = css(pageStyles) + styleSheet;
898
+ // } else {
899
+ // const missingClassList = classList.filter((cls) => !this.prevClassList.includes(cls));
900
+ // if (missingClassList.length > 0) {
901
+ // const styleSheet = generateTWStyleSheet(missingClassList);
902
+ // this.styleElement.textContent += '\n' + styleSheet;
903
+ // this.prevClassList.push(...missingClassList);
904
+ // }
905
+ // render(template, this.shadow);
906
+ // }
907
+ } else {
908
+ return render(template);
909
+ }
910
+ }
911
+ }
912
+ export const getConfig = () => (isBrowser ? window.props.config : global.props.config);
913
+ export const getLocation = () => (isBrowser ? window.location : global.location);
914
+
915
+ export const createElement = ({ name, attrTypes, stateTypes, computedTypes, render }) => {
916
+ const Element = class extends AtomsElement {
917
+ static name = name();
918
+
919
+ static attrTypes = attrTypes ? attrTypes() : {};
920
+
921
+ static stateTypes = stateTypes ? stateTypes() : {};
922
+
923
+ static computedTypes = computedTypes ? computedTypes() : {};
924
+
925
+ render() {
926
+ return render({
927
+ attrs: this.attrs,
928
+ state: this.state,
929
+ computed: this.computed,
930
+ });
931
+ }
932
+ };
933
+ Element.register();
934
+ return { name, attrTypes, stateTypes, computedTypes, render };
935
+ };
936
+
937
+ const lastAttributeNameRegex =
938
+ /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
939
+ const tagRE = /<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
940
+ const whitespaceRE = /^\s*$/;
941
+ const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;
942
+ const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
943
+
944
+ const parseTag = (tag) => {
945
+ const res = {
946
+ type: 'tag',
947
+ name: '',
948
+ voidElement: false,
949
+ attrs: {},
950
+ children: [],
951
+ };
952
+
953
+ const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/);
954
+ if (tagMatch) {
955
+ res.name = tagMatch[1];
956
+ if (voidElements.includes(tagMatch[1]) || tag.charAt(tag.length - 2) === '/') {
957
+ res.voidElement = true;
958
+ }
959
+
960
+ // handle comment tag
961
+ if (res.name.startsWith('!--')) {
962
+ const endIndex = tag.indexOf('-->');
963
+ return {
964
+ type: 'comment',
965
+ comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',
966
+ };
967
+ }
968
+ }
969
+
970
+ const reg = new RegExp(attrRE);
971
+ let result = null;
972
+ for (;;) {
973
+ result = reg.exec(tag);
974
+
975
+ if (result === null) {
976
+ break;
977
+ }
978
+
979
+ if (!result[0].trim()) {
980
+ continue;
981
+ }
982
+
983
+ if (result[1]) {
984
+ const attr = result[1].trim();
985
+ let arr = [attr, ''];
986
+
987
+ if (attr.indexOf('=') > -1) {
988
+ arr = attr.split('=');
989
+ }
990
+
991
+ res.attrs[arr[0]] = arr[1];
992
+ reg.lastIndex--;
993
+ } else if (result[2]) {
994
+ res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1);
995
+ }
996
+ }
997
+
998
+ return res;
999
+ };
1000
+ const parseHtml = (html) => {
1001
+ const result = [];
1002
+ const arr = [];
1003
+ let current;
1004
+ let level = -1;
1005
+
1006
+ // handle text at top level
1007
+ if (html.indexOf('<') !== 0) {
1008
+ var end = html.indexOf('<');
1009
+ result.push({
1010
+ type: 'text',
1011
+ content: end === -1 ? html : html.substring(0, end),
1012
+ });
1013
+ }
1014
+
1015
+ html.replace(tagRE, function (tag, index) {
1016
+ const isOpen = tag.charAt(1) !== '/';
1017
+ const isComment = tag.startsWith('<!--');
1018
+ const start = index + tag.length;
1019
+ const nextChar = html.charAt(start);
1020
+ let parent;
1021
+
1022
+ if (isComment) {
1023
+ const comment = parseTag(tag);
1024
+
1025
+ // if we're at root, push new base node
1026
+ if (level < 0) {
1027
+ result.push(comment);
1028
+ return result;
1029
+ }
1030
+ parent = arr[level];
1031
+ parent.children.push(comment);
1032
+ return result;
1033
+ }
1034
+
1035
+ if (isOpen) {
1036
+ level++;
1037
+
1038
+ current = parseTag(tag);
1039
+
1040
+ if (!current.voidElement && nextChar && nextChar !== '<') {
1041
+ current.children.push({
1042
+ type: 'text',
615
1043
  content: html.slice(start, html.indexOf('<', start)),
616
1044
  });
617
1045
  }
@@ -784,301 +1212,12 @@ export const render = isBrowser
784
1212
  return html;
785
1213
  };
786
1214
 
787
- const previousValues = new WeakMap();
788
- export const unsafeHTML = isBrowser
789
- ? directive((value) => (part) => {
790
- if (!(part instanceof NodePart)) {
791
- throw new Error('unsafeHTML can only be used in text bindings');
792
- }
793
- const previousValue = previousValues.get(part);
794
- if (previousValue !== undefined && isPrimitive(value) && value === previousValue.value && part.value === previousValue.fragment) {
795
- return;
796
- }
797
- const template = document.createElement('template');
798
- template.innerHTML = value; // innerHTML casts to string internally
799
- const fragment = document.importNode(template.content, true);
800
- part.setValue(fragment);
801
- previousValues.set(part, { value, fragment });
802
- })
803
- : (value) => value;
804
-
805
- const logError = (msg) => {
806
- if (isBrowser ? window.__DEV__ : global.__DEV) {
807
- console.warn(msg);
808
- }
809
- };
810
-
811
- const validator = (type, validate) => (innerType) => {
812
- const isPrimitiveType = ['number', 'string', 'boolean'].includes(type);
813
- const common = {
814
- type: type,
815
- parse: isPrimitiveType ? (attr) => attr : (attr) => (attr ? JSON.parse(attr.replace(/'/g, `"`)) : null),
816
- validate: (context, data) => {
817
- if (data === null || typeof data === 'undefined') {
818
- if (common.__required) {
819
- logError(`'${context}' Field is required`);
820
- }
821
- return;
822
- }
823
- if (!isPrimitiveType) {
824
- validate(innerType, context, data);
825
- } else {
826
- const dataType = typeof data;
827
- if (dataType !== type) {
828
- logError(`'${context}' Expected type '${type}' got type '${dataType}'`);
829
- }
830
- }
831
- },
832
- };
833
- common.required = () => {
834
- common.__required = true;
835
- return common;
836
- };
837
- common.default = (fnOrValue) => {
838
- common.__default = fnOrValue;
839
- return common;
840
- };
841
- common.compute = (...args) => {
842
- const fn = args[args.length - 1];
843
- const deps = args.slice(0, args.length - 1);
844
- common.__compute = {
845
- fn,
846
- deps,
847
- };
848
- return common;
849
- };
850
- common.action = (name, fn) => {
851
- if (!common.__handlers) {
852
- common.__handlers = {};
853
- }
854
- common.__handlers[name] = fn;
855
- return common;
856
- };
857
- return common;
858
- };
859
-
860
- export const number = validator('number');
861
- export const string = validator('string');
862
- export const boolean = validator('boolean');
863
- export const object = validator('object', (innerType, context, data) => {
864
- if (data.constructor !== Object) {
865
- logError(`'${context}' Expected object literal '{}' got '${typeof data}'`);
866
- }
867
- for (const key of Object.keys(innerType)) {
868
- const fieldValidator = innerType[key];
869
- const item = data[key];
870
- fieldValidator.validate(`${context}.${key}`, item);
871
- }
872
- });
873
- export const array = validator('array', (innerType, context, data) => {
874
- if (!Array.isArray(data)) {
875
- logError(`Expected Array got ${data}`);
876
- }
877
- for (let i = 0; i < data.length; i++) {
878
- const item = data[i];
879
- innerType.validate(`${context}[${i}]`, item);
880
- }
881
- });
882
-
883
- const fifo = (q) => q.shift();
884
- const microtask = (flush) => () => queueMicrotask(flush);
885
-
886
- const registry = {};
887
- const BaseElement = isBrowser ? window.HTMLElement : class {};
888
-
889
- export class AtomsElement extends BaseElement {
890
- static register() {
891
- registry[this.name] = this;
892
- if (isBrowser) {
893
- if (window.customElements.get(this.name)) {
894
- return;
895
- } else {
896
- window.customElements.define(this.name, registry[this.name]);
897
- }
898
- }
899
- }
900
-
901
- static getElement(name) {
902
- return registry[name];
903
- }
904
-
905
- static get observedAttributes() {
906
- if (!this.attrTypes) {
907
- return [];
908
- }
909
- return Object.keys(this.attrTypes).map((k) => k.toLowerCase());
910
- }
911
-
912
- constructor(attrs) {
913
- super();
914
- this._dirty = false;
915
- this._connected = false;
916
- this.attrs = attrs || {};
917
- this.state = {};
918
- this.config = isBrowser ? window.config : global.config;
919
- this.location = isBrowser ? window.location : global.location;
920
- this.prevClassList = [];
921
- if (!isBrowser) {
922
- this.initState();
923
- }
924
- }
925
-
926
- initAttrs() {
927
- Object.keys(this.constructor.attrTypes).forEach((key) => {
928
- const attrType = this.constructor.attrTypes[key];
929
- const newValue = this.getAttribute(key.toLowerCase());
930
- const data = attrType.parse(newValue);
931
- attrType.validate(`<${this.constructor.name}> ${key}`, data);
932
- this.attrs[key] = data;
933
- });
934
- }
935
-
936
- initState() {
937
- Object.keys(this.constructor.stateTypes).forEach((key) => {
938
- const stateType = this.constructor.stateTypes[key];
939
- if (!this.state[key] && typeof stateType.__default !== 'undefined') {
940
- this.state[key] = typeof stateType.__default === 'function' ? stateType.__default(this.attrs, this.state) : stateType.__default;
941
- }
942
- const setKey = `set${key[0].toUpperCase()}${key.slice(1)}`;
943
- this.state[setKey] = (v) => {
944
- // TODO: check type on set
945
- this.state[key] = typeof v === 'function' ? v(this.state[key]) : v;
946
- this.update();
947
- };
948
- if (stateType.__handlers) {
949
- Object.keys(stateType.__handlers).map((hkey) => {
950
- this.state[hkey] = () => stateType.__handlers[hkey]({ attrs: this.attrs, state: this.state });
951
- });
952
- }
953
- });
954
- }
955
-
956
- connectedCallback() {
957
- this._connected = true;
958
- this.initAttrs();
959
- this.initState();
960
- this.update();
961
- }
962
- disconnectedCallback() {
963
- this._connected = false;
964
- }
965
-
966
- attributeChangedCallback(key, oldValue, newValue) {
967
- if (this._connected) {
968
- this.initAttrs();
969
- this.update();
970
- }
971
- }
972
-
973
- update() {
974
- if (this._dirty) {
975
- return;
976
- }
977
- this._dirty = true;
978
- this.enqueueUpdate();
979
- }
980
-
981
- _performUpdate() {
982
- if (!this._connected) {
983
- return;
984
- }
985
- this.renderTemplate();
986
- this._dirty = false;
987
- }
988
-
989
- batch(runner, pick, callback) {
990
- const q = [];
991
- const flush = () => {
992
- let p;
993
- while ((p = pick(q))) callback(p);
994
- };
995
- const run = runner(flush);
996
- q.push(this) === 1 && run();
997
- }
998
-
999
- enqueueUpdate() {
1000
- this.batch(microtask, fifo, () => this._performUpdate());
1001
- }
1002
-
1003
- get computed() {
1004
- return Object.keys(this.constructor.computedTypes).reduceRight((acc, key) => {
1005
- const type = this.constructor.computedTypes[key];
1006
- const state = this.state;
1007
- const values = type.__compute.deps.reduce((dacc, key) => {
1008
- if (typeof state[key] !== undefined) {
1009
- dacc.push(state[key]);
1010
- }
1011
- return dacc;
1012
- }, []);
1013
- acc[key] = type.__compute.fn(...values);
1014
- return acc;
1015
- }, {});
1016
- }
1017
-
1018
- renderTemplate() {
1019
- const template = this.render();
1020
- if (isBrowser) {
1021
- if (!this.styleElement) {
1022
- render(template, this);
1023
- const classList = getClassList(template);
1024
- const styleSheet = getStyleSheet(classList);
1025
- this.prevClassList = classList;
1026
- this.styleElement = document.createElement('style');
1027
- this.appendChild(this.styleElement).textContent = styleSheet;
1028
- } else {
1029
- const classList = getClassList(template);
1030
- const missingClassList = classList.filter((cls) => !this.prevClassList.includes(cls));
1031
- if (missingClassList.length > 0) {
1032
- const styleSheet = getStyleSheet(missingClassList);
1033
- this.styleElement.textContent += '\n' + styleSheet;
1034
- this.prevClassList.push(...missingClassList);
1035
- }
1036
- render(template, this);
1037
- }
1038
- } else {
1039
- const result = render(template, this);
1040
- const classList = getClassList(template);
1041
- const styleSheet = getStyleSheet(classList);
1042
- return `
1043
- ${result}
1044
- <style>
1045
- ${styleSheet}
1046
- </style>
1047
- `;
1048
- }
1049
- }
1050
- }
1051
- export const getConfig = () => (isBrowser ? window.props.config : global.props.config);
1052
- export const getLocation = () => (isBrowser ? window.location : global.location);
1053
-
1054
- export const createElement = ({ name, attrTypes, stateTypes, computedTypes, render }) => {
1055
- const Element = class extends AtomsElement {
1056
- static name = name();
1057
-
1058
- static attrTypes = attrTypes ? attrTypes() : {};
1059
-
1060
- static stateTypes = stateTypes ? stateTypes() : {};
1061
-
1062
- static computedTypes = computedTypes ? computedTypes() : {};
1063
-
1064
- render() {
1065
- return render({
1066
- attrs: this.attrs,
1067
- state: this.state,
1068
- computed: this.computed,
1069
- });
1070
- }
1071
- };
1072
- Element.register();
1073
- return { name, attrTypes, stateTypes, computedTypes, render };
1074
- };
1075
-
1076
- export const createPage = ({ route, datapaths, head, body }) => {
1215
+ export const createPage = ({ head, body }) => {
1077
1216
  return ({ headScript, bodyScript, lang, props }) => {
1078
1217
  const isProd = process.env.NODE_ENV === 'production';
1079
1218
  const headHtml = render(head(props));
1080
- const bodyTemplate = body(props);
1081
- const bodyHtml = render(bodyTemplate);
1219
+ const bodyHtml = render(body(props));
1220
+ const classes = extractClasses(bodyHtml);
1082
1221
  return `
1083
1222
  <!DOCTYPE html>
1084
1223
  <html lang="${lang}">
@@ -1090,8 +1229,9 @@ export const createPage = ({ route, datapaths, head, body }) => {
1090
1229
  <link rel="sitemap" type="application/xml" href="/sitemap.xml" />
1091
1230
  <link rel="icon" type="image/png" href="/assets/icon.png" />
1092
1231
  ${headHtml}
1232
+ <style id="global">
1093
- <style>
1233
+ ${css(pageStyles)}
1094
- ${getStyleSheet(getClassList(bodyTemplate))}
1234
+ ${generateTWStyleSheet(new Set(classes.split(' ')))}
1095
1235
  </style>
1096
1236
  ${headScript}
1097
1237
  </head>