jails-js 5.9.0 → 6.0.1-beta.1

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.
package/src/component.ts CHANGED
@@ -1,110 +1,136 @@
1
- import { type Component } from '..'
1
+ import { safe, rAF, g } from './utils'
2
2
  import { Idiomorph } from 'idiomorph/dist/idiomorph.esm'
3
- import { rAF, dup, safe } from './utils'
4
- import { buildtemplates } from './template-system'
5
- import { on, off, trigger } from './utils/events'
6
3
  import { publish, subscribe } from './utils/pubsub'
7
4
 
8
- export default function Component( elm, { module, dependencies, templates, components }) {
9
-
10
- const options = getOptions( module )
11
- const initialState = (new Function( `return ${elm.getAttribute('html-model') || '{}'}`))()
12
- const selector = Object.keys(components).toString()
13
-
14
- buildtemplates( elm, selector, templates, components )
15
-
16
- const tplid = elm.getAttribute('tplid')
17
- const template = tplid ? templates[tplid] : null
18
- const state = { data: module.model ? dup(module.model) : {} }
19
- state.data = Object.assign( state.data, initialState)
20
-
21
- const base: Component = {
22
- template,
23
- elm,
5
+ export const Component = ({ name, module, dependencies, node, templates, signal }) => {
6
+ const _model = module.model || {}
7
+ const initialState = (new Function( `return ${node.getAttribute('html-model') || '{}'}`))()
8
+ const tplid = node.getAttribute('tplid')
9
+ const scopeid = node.getAttribute('html-scope-id')
10
+ const tpl = templates[ tplid ]
11
+ const data = g.scope[ scopeid ]
12
+ const model = module?.model?.apply ? _model({ elm:node, initialState }) : _model
13
+ const state = Object.assign({}, data, model, initialState)
14
+ const view = module.view? module.view : (data) => data
15
+
16
+ let updates = []
17
+
18
+ const base = {
19
+ name,
20
+ model,
21
+ elm: node,
22
+ template: tpl.template,
24
23
  dependencies,
25
24
  publish,
26
25
  subscribe,
27
26
 
28
27
  main(fn) {
29
- options.main = fn
30
- },
31
-
32
- unmount(fn) {
33
- options.unmount = fn
28
+ node.addEventListener(':mount', fn)
34
29
  },
35
30
 
36
- onupdate(fn) {
37
- options.onupdate = fn
38
- },
39
-
40
- on(eventName, selectorOrCallback, callback) {
41
- on(elm, eventName, selectorOrCallback, callback)
42
- },
31
+ /**
32
+ * @State
33
+ */
34
+ state : {
43
35
 
44
- off(eventName, callback) {
45
- off(elm, eventName, callback)
46
- },
36
+ save(data) {
37
+ if( data.constructor === Function ) {
38
+ data( state )
39
+ } else {
40
+ Object.assign(state, data)
41
+ }
42
+ },
47
43
 
48
- trigger(eventName, target, args) {
49
- if (target.constructor === String) {
50
- Array
51
- .from(elm.querySelectorAll(target))
52
- .forEach( children => trigger(children, eventName, { args: args }) )
53
- }
54
- else trigger(elm, eventName, { args: target })
55
- },
44
+ set( data ) {
56
45
 
57
- emit: ( ...args ) => {
58
- trigger(elm, args.shift(), { args: args })
59
- },
46
+ if (!document.body.contains(node)) {
47
+ return
48
+ }
60
49
 
61
- state: {
62
- set( data ) {
63
- if (data.constructor === Function) {
64
- const newstate = dup(state.data)
65
- data(newstate)
66
- base.render(newstate)
67
- } else {
68
- base.render(data)
50
+ if( data.constructor === Function ) {
51
+ data( state )
69
52
  }
70
- return new Promise((resolve) => rAF(_ => rAF(() => resolve(state.data))))
71
- },
72
- get() {
73
- return dup(state.data)
53
+ const newstate = Object.assign({}, state)
54
+ render( newstate )
55
+
56
+ updates.push(data)
57
+
58
+ return new Promise((resolve) => {
59
+ rAF(() => rAF(() => {
60
+ Object.assign.apply(null, [state, ...updates ])
61
+ if( updates.length ){
62
+ const newstate = Object.assign({}, state)
63
+ render(newstate)
64
+ resolve(newstate)
65
+ updates = []
66
+ }
67
+ }))
68
+ })
74
69
  },
75
70
 
76
- getRaw(){
77
- return state.data
71
+ get() {
72
+ return Object.assign({}, state)
78
73
  }
79
74
  },
80
-
81
- render(data = state.data) {
82
-
83
- if (!document.body.contains(elm)) {
84
- return
75
+ /**
76
+ * @Events
77
+ */
78
+ on( ev, selectorOrCallback, callback ) {
79
+
80
+ if( callback ) {
81
+ callback.handler = (e) => {
82
+ const detail = e.detail || {}
83
+ let parent = e.target
84
+ while (parent) {
85
+ if (parent.matches(selectorOrCallback)) {
86
+ e.delegateTarget = parent
87
+ callback.apply(node, [e].concat(detail.args))
88
+ }
89
+ if (parent === node) break
90
+ parent = parent.parentNode
91
+ }
92
+ }
93
+ node.addEventListener(ev, callback.handler, {
94
+ signal,
95
+ capture: (ev == 'focus' || ev == 'blur' || ev == 'mouseenter' || ev == 'mouseleave')
96
+ })
97
+
98
+ } else {
99
+ selectorOrCallback.handler = (e) => {
100
+ e.delegateTarget = node
101
+ selectorOrCallback.apply(node, [e].concat(e.detail.args))
102
+ }
103
+ node.addEventListener(ev, selectorOrCallback.handler, { signal })
85
104
  }
105
+ },
86
106
 
87
- state.data = Object.assign(state.data, data)
88
-
89
- const newdata = dup(state.data)
90
- const newhtml = templates[tplid].call(Object.assign(options.view(newdata), elm.___scope___), elm, safe)
91
-
92
- Idiomorph.morph(elm, newhtml, IdiomorphOptions(elm))
93
- updateScope( elm )
107
+ off( ev, callback ) {
108
+ if( callback.handler ) {
109
+ node.removeEventListener(ev, callback.handler)
110
+ }
111
+ },
94
112
 
95
- rAF(_ => {
113
+ trigger(ev, selectorOrCallback, data) {
114
+ if( selectorOrCallback.constructor === String ) {
96
115
  Array
97
- .from(elm.querySelectorAll('[tplid]'))
98
- .forEach((child: any) => {
99
- const props = Object.assign( child.base.state.getRaw(), data )
100
- child.options.onupdate(props)
101
- child.base.render(props)
116
+ .from(node.querySelectorAll(selectorOrCallback))
117
+ .forEach( children => {
118
+ children.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail: { args: data } }) )
102
119
  })
103
- })
120
+ } else {
121
+ node.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail:{ args: data } }))
122
+ }
104
123
  },
105
124
 
106
- innerHTML( target, html_ ) {
125
+ emit(ev, data) {
126
+ node.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail: { args: data } }))
127
+ },
128
+
129
+ unmount( fn ) {
130
+ node.addEventListener(':unmount', fn)
131
+ },
107
132
 
133
+ innerHTML ( target, html_ ) {
108
134
  const element = html_? target : elm
109
135
  const clone = element.cloneNode()
110
136
  const html = html_? html_ : target
@@ -114,32 +140,34 @@ export default function Component( elm, { module, dependencies, templates, compo
114
140
  }
115
141
  }
116
142
 
117
- return { base, options }
118
- }
143
+ const render = ( data ) => {
119
144
 
120
- const getOptions = (module) => ({
121
- main: (a) => a,
122
- unmount: (a) => a,
123
- onupdate: (a) => a,
124
- view: module.view ? module.view : (a) => a
125
- })
145
+ const html = tpl.render.call( view(data), node, safe, g )
146
+ Idiomorph.morph( node, html, IdiomorphOptions(node) )
126
147
 
127
- const updateScope = (node) => {
128
- node.querySelectorAll('[scope]').forEach( scopeElement => {
129
- scopeElement.querySelectorAll('[tplid]').forEach( cp => {
130
- if( !cp.___scope___ ) {
131
- const script = scopeElement.lastElementChild
132
- cp.___scope___ = 'scope' in script.dataset? (new Function(`return ${script.text}`))() : {}
148
+ node.querySelectorAll('[tplid]').forEach((element) => {
149
+ if(!element.base) return
150
+ const base = element.base
151
+ const props = Object.keys(base.model).reduce((acc, key) => {
152
+ if( key in data ) {
153
+ if( !acc ) acc = {}
154
+ acc[key] = data[key]
155
+ }
156
+ return acc
157
+ }, null)
158
+ if( props ) {
159
+ base.state.set( props )
133
160
  }
134
161
  })
135
- })
136
- }
137
-
162
+ rAF(() => g.scope = {})
163
+ }
138
164
 
139
- const IdiomorphOptions = (parent) => ({
165
+ node.base = base
166
+ module.default( base )
167
+ }
140
168
 
169
+ const IdiomorphOptions = ( parent ) => ({
141
170
  callbacks: {
142
-
143
171
  beforeNodeMorphed( node ) {
144
172
  if( node.nodeType === 1 ) {
145
173
  if( 'html-static' in node.attributes ) {
package/src/element.ts CHANGED
@@ -1,14 +1,11 @@
1
- import Component from './component'
2
- import { purge, rAF } from './utils'
1
+ import { Component } from './component'
3
2
 
4
- export default function Element(module, dependencies, templates, components) {
3
+ export const Element = ({ component, templates, start }) => {
5
4
 
6
- return class extends HTMLElement {
5
+ const { name, module, dependencies } = component
6
+ const abortController = new AbortController()
7
7
 
8
- base: any
9
- options: any
10
- returns : any
11
- __events: any
8
+ return class extends HTMLElement {
12
9
 
13
10
  constructor() {
14
11
  super()
@@ -16,59 +13,28 @@ export default function Element(module, dependencies, templates, components) {
16
13
 
17
14
  connectedCallback() {
18
15
 
19
- const { base, options } = Component(this, { module, dependencies, templates, components })
20
-
21
- this.base = base
22
- this.options = options
23
- this.base.render()
24
- this.returns = module.default(base)
25
-
26
- if( this.__template && this.__template.constructor === Promise ) {
27
- this.__template.then( _ => {
28
- if( this.base && this.options.main) {
29
- const array = this.options.main(this.base)
30
- if( array && array.length ){
31
- array.forEach(f => f(this.base))
32
- }
33
- }
34
- })
35
- return
16
+ if( !this.getAttribute('tplid') ) {
17
+ start( this.parentNode )
36
18
  }
37
19
 
38
- if( this.returns && this.returns.constructor === Promise ) {
39
- this.returns.then( _ => {
40
- if( this.base && this.options.main) {
41
- const array = this.options.main(this.base)
42
- if( array && array.length ){
43
- array.forEach(f => f(this.base))
44
- }
45
- }
46
- })
20
+ Component({
21
+ node:this,
22
+ name,
23
+ module,
24
+ dependencies,
25
+ templates,
26
+ signal: abortController.signal
27
+ })
28
+
29
+ this.dispatchEvent( new CustomEvent(':mount') )
30
+ this.base.state.set({})
47
31
 
48
- } else {
49
- if( this.base && this.options.main ){
50
- const array = this.options.main(this.base)
51
- if( array && array.length ) {
52
- array.forEach(f => f(this.base))
53
- }
54
- }
55
- }
56
32
  }
57
33
 
58
34
  disconnectedCallback() {
59
- this.options.unmount(this.base)
60
- rAF(() => {
61
- if(!document.body.contains(this) ) {
62
- this.__events? this.__events = null : null
63
- this.base? this.base.elm = null : null
64
- this.base? this.base = null : null
65
- purge(this)
66
- }
67
- })
68
- }
69
-
70
- attributeChangedCallback() {
71
- //TODO
35
+ this.dispatchEvent( new CustomEvent(':unmount') )
36
+ abortController.abort()
37
+ delete this.base
72
38
  }
73
39
  }
74
40
  }
package/src/index.ts CHANGED
@@ -1,42 +1,27 @@
1
- import { templateConfig, buildtemplates } from './template-system'
2
- import { publish, subscribe } from './utils/pubsub'
3
- import { html, attributes } from '../html'
4
- import Element from './element'
1
+ import { Element } from './element'
2
+ import { template, templateConfig as config } from './template-system'
5
3
 
6
- const templates = {}
7
4
  const components = {}
8
5
 
9
- export { html, attributes }
6
+ export { publish, subscribe } from './utils/pubsub'
10
7
 
11
- export default {
12
-
13
- templateConfig,
8
+ export const templateConfig = (options) => {
9
+ config( options )
10
+ }
14
11
 
15
- publish,
16
- subscribe,
12
+ export const register = ( name, module, dependencies ) => {
13
+ components[ name ] = { name, module, dependencies }
14
+ }
17
15
 
18
- register( name, module, dependencies = {} ) {
19
- components[name] = { name, module, dependencies }
20
- },
16
+ export const start = ( target = document.body ) => {
21
17
 
22
- start( target = document.body ) {
23
- const keys = Object.keys(components)
24
- const selector = keys.toString()
25
- if( keys.length ) {
26
- buildtemplates( target, selector, templates, components )
27
- registerComponents()
28
- }
29
- }
30
- }
18
+ const templates = template( target, { components } )
31
19
 
32
- const registerComponents = () => {
33
20
  Object
34
21
  .values( components )
35
- .forEach( (component) => {
36
- const { name, module, dependencies } = component as any
37
- if( !customElements.get(name) ){
38
- const Base = Element(module, dependencies, templates, components)
39
- customElements.define(name, Base)
22
+ .forEach(({ name, module, dependencies }) => {
23
+ if( !customElements.get(name) ) {
24
+ customElements.define( name, Element({ component: { name, module, dependencies }, templates, start }) )
40
25
  }
41
- })
26
+ })
42
27
  }
@@ -1,76 +1,194 @@
1
- import Transpile from './transpile'
2
1
  import { uuid, decodeHTML } from './utils'
3
2
 
3
+ const templates = {}
4
+
4
5
  const config = {
5
6
  tags: ['{{', '}}']
6
7
  }
7
8
 
8
9
  export const templateConfig = (newconfig) => {
9
- Object.assign(config, newconfig)
10
+ Object.assign( config, newconfig )
10
11
  }
11
12
 
12
- export default function Template(element) {
13
+ export const template = ( target, { components }) => {
14
+
15
+ tagElements( target, [...Object.keys( components ), 'template'] )
16
+ const clone = target.cloneNode( true )
13
17
 
14
- fixDIffIf(element)
15
- const html = Transpile(element.outerHTML, config)
16
- const decodedHTML = JSON.stringify(html)
18
+ transformTemplate( clone )
19
+ removeTemplateTagsRecursively( clone )
20
+ setTemplates( clone, components )
21
+
22
+ return templates
23
+ }
17
24
 
18
- return new Function('$element', 'safe',`
25
+ export const compile = ( outerHTML ) => {
26
+
27
+ const html = transformAttributes( outerHTML )
28
+ const parsedHtml = JSON.stringify( html )
29
+
30
+ return new Function('$element', 'safe', '$g',`
19
31
  var $data = this;
20
32
  with( $data ){
21
- var output=${decodedHTML
33
+ var output=${parsedHtml
22
34
  .replace(/%%_=(.+?)_%%/g, function(_, variable){
23
- return '"+safe(function(){return '+decodeHTML(variable)+';})+"'
35
+ return '"+safe(function(){return '+ variable +';})+"'
24
36
  })
25
37
  .replace(/%%_(.+?)_%%/g, function(_, variable){
26
- return '";' + decodeHTML(variable) +'\noutput+="'
38
+ return '";' + variable +'\noutput+="'
27
39
  })};return output;
28
40
  }
29
41
  `)
30
42
  }
31
43
 
32
- export const buildtemplates = ( target, selector, templates, components ) => {
33
- []
34
- .concat( target.matches? (target.matches(selector)? target : []) : [] )
35
- .concat( Array.from(target.querySelectorAll( selector )) )
44
+ const tagElements = ( target, keys ) => {
45
+ target
46
+ .querySelectorAll( keys.toString() )
47
+ .forEach((node) => {
48
+ if( node.localName === 'template' ) {
49
+ return tagElements( node.content, keys )
50
+ }
51
+ node.setAttribute('tplid', uuid())
52
+ })
53
+ }
54
+
55
+ const transformAttributes = ( html ) => {
56
+
57
+ const regexTags = new RegExp(`\\${config.tags[0]}(.+?)\\${config.tags[1]}`, 'g')
58
+
59
+ return html
60
+ .replace(/jails___scope-id/g, '%%_=$scopeid_%%')
61
+ .replace(regexTags, '%%_=$1_%%')
62
+ // Booleans
63
+ // https://meiert.com/en/blog/boolean-attributes-of-html/
64
+ .replace(/html-(allowfullscreen|async|autofocus|autoplay|checked|controls|default|defer|disabled|formnovalidate|inert|ismap|itemscope|loop|multiple|muted|nomodule|novalidate|open|playsinline|readonly|required|reversed|selected)=\"(.*?)\"/g, `%%_if(safe(function(){ return $2 })){_%%$1%%_}_%%`)
65
+ // The rest
66
+ .replace(/html-(.*?)=\"(.*?)\"/g, (all, key, value) => {
67
+ if (key === 'key' || key === 'model' || key === 'scope-id' ) {
68
+ return all
69
+ }
70
+ if (value) {
71
+ value = value.replace(/^{|}$/g, '')
72
+ return `${key}="%%_=safe(function(){ return ${value} })_%%"`
73
+ } else {
74
+ return all
75
+ }
76
+ })
77
+ }
78
+
79
+ const transformTemplate = ( clone ) => {
80
+
81
+ clone.querySelectorAll('template, [html-for], [html-if], [html-inner], [html-class]')
82
+ .forEach(( element ) => {
83
+
84
+ const htmlFor = element.getAttribute('html-for')
85
+ const htmlIf = element.getAttribute('html-if')
86
+ const htmlInner = element.getAttribute('html-inner')
87
+ const htmlClass = element.getAttribute('html-class')
88
+
89
+ if ( htmlFor ) {
90
+
91
+ element.removeAttribute('html-for')
92
+
93
+ const split = htmlFor.match(/(.*)\sin\s(.*)/) || ''
94
+ const varname = split[1]
95
+ const object = split[2]
96
+ const open = document.createTextNode(`%%_ ;(function(){ var $index = 0; for(var $key in safe(function(){ return ${object} }) ){ var $scopeid = Math.random().toString(36).substring(2, 9); var ${varname} = ${object}[$key]; $g.scope[$scopeid] = { ${varname} :${varname}, ${object}: ${object}, $index: $index, $key: $key }; _%%`)
97
+ const close = document.createTextNode(`%%_ $index++; } })() _%%`)
98
+
99
+ wrap(open, element, close)
100
+ }
101
+
102
+ if (htmlIf) {
103
+ element.removeAttribute('html-if')
104
+ const open = document.createTextNode(`%%_ if ( safe(function(){ return ${htmlIf} }) ){ _%%`)
105
+ const close = document.createTextNode(`%%_ } _%%`)
106
+ wrap(open, element, close)
107
+ }
108
+
109
+ if (htmlInner) {
110
+ element.removeAttribute('html-inner')
111
+ element.innerHTML = `%%_=${htmlInner}_%%`
112
+ }
113
+
114
+ if (htmlClass) {
115
+ element.removeAttribute('html-class')
116
+ element.className = (element.className + ` %%_=${htmlClass}_%%`).trim()
117
+ }
118
+
119
+ if( element.localName === 'template' ) {
120
+ transformTemplate(element.content)
121
+ }
122
+ })
123
+ }
124
+
125
+ const setTemplates = ( clone, components ) => {
126
+
127
+ Array.from(clone.querySelectorAll('[tplid]'))
36
128
  .reverse()
37
- .forEach( (node:HTMLElement) => {
38
- node.querySelectorAll('template').forEach( template => buildtemplates(template.content, selector, templates, components ))
39
- createTemplateId(node, templates, components)
129
+ .forEach((node) => {
130
+
131
+ const tplid = node.getAttribute('tplid')
132
+ const name = node.localName
133
+ node.setAttribute('html-scope-id', 'jails___scope-id')
134
+
135
+ if( name in components && components[name].module.template ) {
136
+ const children = node.innerHTML
137
+ const html = components[name].module.template({ elm:node, children })
138
+
139
+ if( html.constructor === Promise ) {
140
+ html.then( htmlstring => {
141
+ node.innerHTML = htmlstring
142
+ const html = node.outerHTML
143
+ templates[tplid] = {
144
+ template: html,
145
+ render: compile(html)
146
+ }
147
+ })
148
+ } else {
149
+ node.innerHTML = html
150
+ }
151
+ }
152
+
153
+ const html = node.outerHTML
154
+
155
+ templates[ tplid ] = {
156
+ template: html,
157
+ render : compile(html)
158
+ }
40
159
  })
41
160
  }
42
161
 
43
- const createTemplateId = (element, templates, components ) => {
162
+ const removeTemplateTagsRecursively = (node) => {
44
163
 
45
- const tplid = element.getAttribute('tplid')
164
+ // Get all <template> elements within the node
165
+ const templates = node.querySelectorAll('template')
46
166
 
47
- if (!tplid) {
48
- const id = uuid()
49
- element.setAttribute('tplid', id)
50
- const name = element.localName
167
+ templates.forEach((template) => {
51
168
 
52
- if( name in components && components[name].module.template ) {
53
- const children = element.innerHTML
54
- const html = components[name].module.template({ children })
55
- if( html.constructor === Promise ) {
56
- element.__template = html
57
- html.then( htmlstring => {
58
- element.innerHTML = htmlstring
59
- templates[id] = Template(element)
60
- })
61
- }else {
62
- element.innerHTML = html
169
+ if( template.getAttribute('html-if') || template.getAttribute('html-inner') ) {
170
+ return
171
+ }
172
+
173
+ // Process any nested <template> tags within this <template> first
174
+ removeTemplateTagsRecursively(template.content)
175
+
176
+ // Get the parent of the <template> tag
177
+ const parent = template.parentNode
178
+
179
+ if (parent) {
180
+ // Move all child nodes from the <template>'s content to its parent
181
+ const content = template.content
182
+ while (content.firstChild) {
183
+ parent.insertBefore(content.firstChild, template)
63
184
  }
185
+ // Remove the <template> tag itself
186
+ parent.removeChild(template)
64
187
  }
65
- templates[id] = Template(element)
66
- }
188
+ })
67
189
  }
68
190
 
69
- // Sometimes we don't know why something works, especially when using third party code like Idiomorph.
70
- // So just letting anyone knows that this part has to be improved some time.
71
- const fixDIffIf = (node) => {
72
- const _if = node.querySelectorAll('[html-if]')
73
- _if.forEach( el => {
74
- el.parentNode.insertBefore(document.createComment(''), el.nextSibling)
75
- })
191
+ const wrap = (open, node, close) => {
192
+ node.parentNode?.insertBefore(open, node)
193
+ node.parentNode?.insertBefore(close, node.nextSibling)
76
194
  }