jails-js 5.9.0 → 6.0.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/component.ts CHANGED
@@ -1,110 +1,134 @@
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
28
+ node.addEventListener(':mount', fn)
30
29
  },
31
30
 
32
- unmount(fn) {
33
- options.unmount = fn
34
- },
35
-
36
- onupdate(fn) {
37
- options.onupdate = fn
38
- },
31
+ /**
32
+ * @State
33
+ */
34
+ state : {
39
35
 
40
- on(eventName, selectorOrCallback, callback) {
41
- on(elm, eventName, selectorOrCallback, callback)
42
- },
43
-
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
+
54
+ updates.push(data)
55
+
56
+ return new Promise((resolve) => {
57
+ rAF(() => rAF(() => {
58
+ Object.assign.apply(null, [state, ...updates ])
59
+ if( updates.length ){
60
+ const newstate = Object.assign({}, state)
61
+ render(newstate)
62
+ resolve(newstate)
63
+ updates = []
64
+ }
65
+ }))
66
+ })
74
67
  },
75
68
 
76
- getRaw(){
77
- return state.data
69
+ get() {
70
+ return Object.assign({}, state)
78
71
  }
79
72
  },
80
-
81
- render(data = state.data) {
82
-
83
- if (!document.body.contains(elm)) {
84
- return
73
+ /**
74
+ * @Events
75
+ */
76
+ on( ev, selectorOrCallback, callback ) {
77
+
78
+ if( callback ) {
79
+ callback.handler = (e) => {
80
+ const detail = e.detail || {}
81
+ let parent = e.target
82
+ while (parent) {
83
+ if (parent.matches(selectorOrCallback)) {
84
+ e.delegateTarget = parent
85
+ callback.apply(node, [e].concat(detail.args))
86
+ }
87
+ if (parent === node) break
88
+ parent = parent.parentNode
89
+ }
90
+ }
91
+ node.addEventListener(ev, callback.handler, {
92
+ signal,
93
+ capture: (ev == 'focus' || ev == 'blur' || ev == 'mouseenter' || ev == 'mouseleave')
94
+ })
95
+
96
+ } else {
97
+ selectorOrCallback.handler = (e) => {
98
+ e.delegateTarget = node
99
+ selectorOrCallback.apply(node, [e].concat(e.detail.args))
100
+ }
101
+ node.addEventListener(ev, selectorOrCallback.handler, { signal })
85
102
  }
103
+ },
86
104
 
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 )
105
+ off( ev, callback ) {
106
+ if( callback.handler ) {
107
+ node.removeEventListener(ev, callback.handler)
108
+ }
109
+ },
94
110
 
95
- rAF(_ => {
111
+ trigger(ev, selectorOrCallback, data) {
112
+ if( selectorOrCallback.constructor === String ) {
96
113
  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)
114
+ .from(node.querySelectorAll(selectorOrCallback))
115
+ .forEach( children => {
116
+ children.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail: { args: data } }) )
102
117
  })
103
- })
118
+ } else {
119
+ node.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail:{ args: data } }))
120
+ }
104
121
  },
105
122
 
106
- innerHTML( target, html_ ) {
123
+ emit(ev, data) {
124
+ node.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail: { args: data } }))
125
+ },
126
+
127
+ unmount( fn ) {
128
+ node.addEventListener(':unmount', fn)
129
+ },
107
130
 
131
+ innerHTML ( target, html_ ) {
108
132
  const element = html_? target : elm
109
133
  const clone = element.cloneNode()
110
134
  const html = html_? html_ : target
@@ -114,32 +138,36 @@ export default function Component( elm, { module, dependencies, templates, compo
114
138
  }
115
139
  }
116
140
 
117
- return { base, options }
118
- }
119
-
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
- })
126
-
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}`))() : {}
133
- }
141
+ const render = ( data ) => {
142
+
143
+ const html = tpl.render.call( view(data), node, safe, g )
144
+ Idiomorph.morph( node, html, IdiomorphOptions(node) )
145
+
146
+ rAF(() => {
147
+ node.querySelectorAll('[tplid]').forEach((element) => {
148
+ if(!element.base) return
149
+ const base = element.base
150
+ const props = Object.keys(base.model).reduce((acc, key) => {
151
+ if( key in data ) {
152
+ if( !acc ) acc = {}
153
+ acc[key] = data[key]
154
+ }
155
+ return acc
156
+ }, null)
157
+ if( props ) {
158
+ base.state.set( props )
159
+ }
160
+ })
161
+ rAF(() => g.scope = {})
134
162
  })
135
- })
136
- }
137
-
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,193 @@
1
- import Transpile from './transpile'
2
- import { uuid, decodeHTML } from './utils'
1
+ import { uuid } from './utils'
2
+
3
+ const templates = {}
3
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
+ transformAttributes( clone )
21
+ setTemplates( clone, components )
22
+
23
+ return templates
24
+ }
17
25
 
18
- return new Function('$element', 'safe',`
26
+ export const compile = ( html ) => {
27
+ const parsedHtml = JSON.stringify(html)
28
+
29
+ return new Function('$element', 'safe', '$g',`
19
30
  var $data = this;
20
31
  with( $data ){
21
- var output=${decodedHTML
32
+ var output=${parsedHtml
22
33
  .replace(/%%_=(.+?)_%%/g, function(_, variable){
23
- return '"+safe(function(){return '+decodeHTML(variable)+';})+"'
34
+ return '"+safe(function(){return '+variable+';})+"'
24
35
  })
25
36
  .replace(/%%_(.+?)_%%/g, function(_, variable){
26
- return '";' + decodeHTML(variable) +'\noutput+="'
37
+ return '";' + variable +'\noutput+="'
27
38
  })};return output;
28
39
  }
29
40
  `)
30
41
  }
31
42
 
32
- export const buildtemplates = ( target, selector, templates, components ) => {
33
- []
34
- .concat( target.matches? (target.matches(selector)? target : []) : [] )
35
- .concat( Array.from(target.querySelectorAll( selector )) )
43
+ const tagElements = ( target, keys ) => {
44
+ target
45
+ .querySelectorAll( keys.toString() )
46
+ .forEach((node) => {
47
+ if( node.localName === 'template' ) {
48
+ return tagElements( node.content, keys )
49
+ }
50
+ node.setAttribute('tplid', uuid())
51
+ })
52
+ }
53
+
54
+ const transformAttributes = ( clone ) => {
55
+
56
+ const regexTags = new RegExp(`\\${config.tags[0]}(.+?)\\${config.tags[1]}`, 'g')
57
+
58
+ clone.innerHTML = clone.innerHTML
59
+ .replace(regexTags, '%%_=$1_%%')
60
+ // Booleans
61
+ // https://meiert.com/en/blog/boolean-attributes-of-html/
62
+ .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%%_}_%%`)
63
+ // The rest
64
+ .replace(/html-(.*?)=\"(.*?)\"/g, (all, key, value) => {
65
+ if (key === 'key' || key === 'model') {
66
+ return all
67
+ }
68
+ if (value) {
69
+ value = value.replace(/^{|}$/g, '')
70
+ return `${key}="%%_=safe(function(){ return ${value} })_%%"`
71
+ } else {
72
+ return all
73
+ }
74
+ })
75
+ }
76
+
77
+ const transformTemplate = ( clone ) => {
78
+
79
+ clone.querySelectorAll('template, [html-for], [html-if], [html-inner], [html-class]')
80
+ .forEach(( element ) => {
81
+
82
+ const htmlFor = element.getAttribute('html-for')
83
+ const htmlIf = element.getAttribute('html-if')
84
+ const htmlInner = element.getAttribute('html-inner')
85
+ const htmlClass = element.getAttribute('html-class')
86
+
87
+ if ( htmlFor ) {
88
+
89
+ element.removeAttribute('html-for')
90
+
91
+ const split = htmlFor.match(/(.*)\sin\s(.*)/) || ''
92
+ const varname = split[1]
93
+ const object = split[2]
94
+ 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 }; _%%`)
95
+ const close = document.createTextNode(`%%_ $index++; } })() _%%`)
96
+
97
+ wrap(open, element, close)
98
+ }
99
+
100
+ if (htmlIf) {
101
+ element.removeAttribute('html-if')
102
+ const open = document.createTextNode(`%%_ if ( safe(function(){ return ${htmlIf} }) ){ _%%`)
103
+ const close = document.createTextNode(`%%_ } _%%`)
104
+ wrap(open, element, close)
105
+ }
106
+
107
+ if (htmlInner) {
108
+ element.removeAttribute('html-inner')
109
+ element.innerHTML = `%%_=${htmlInner}_%%`
110
+ }
111
+
112
+ if (htmlClass) {
113
+ element.removeAttribute('html-class')
114
+ element.className = (element.className + ` %%_=${htmlClass}_%%`).trim()
115
+ }
116
+
117
+ if( element.localName === 'template' ) {
118
+ transformTemplate(element.content)
119
+ }
120
+
121
+ })
122
+ }
123
+
124
+ const setTemplates = ( clone, components ) => {
125
+
126
+ Array.from(clone.querySelectorAll('[tplid]'))
36
127
  .reverse()
37
- .forEach( (node:HTMLElement) => {
38
- node.querySelectorAll('template').forEach( template => buildtemplates(template.content, selector, templates, components ))
39
- createTemplateId(node, templates, components)
128
+ .forEach((node) => {
129
+
130
+ const tplid = node.getAttribute('tplid')
131
+ const name = node.localName
132
+ node.setAttribute('html-scope-id', '%%_=$scopeid_%%')
133
+
134
+ if( name in components && components[name].module.template ) {
135
+ const children = node.innerHTML
136
+ const html = components[name].module.template({ elm:node, children })
137
+
138
+ if( html.constructor === Promise ) {
139
+ html.then( htmlstring => {
140
+ node.innerHTML = htmlstring
141
+ const html = node.outerHTML
142
+ templates[tplid] = {
143
+ template: html,
144
+ render: compile(html)
145
+ }
146
+ })
147
+ } else {
148
+ node.innerHTML = html
149
+ }
150
+ }
151
+
152
+ const html = node.outerHTML
153
+
154
+ templates[ tplid ] = {
155
+ template: html,
156
+ render : compile(html)
157
+ }
40
158
  })
41
159
  }
42
160
 
43
- const createTemplateId = (element, templates, components ) => {
161
+ const removeTemplateTagsRecursively = (node) => {
44
162
 
45
- const tplid = element.getAttribute('tplid')
163
+ // Get all <template> elements within the node
164
+ const templates = node.querySelectorAll('template')
46
165
 
47
- if (!tplid) {
48
- const id = uuid()
49
- element.setAttribute('tplid', id)
50
- const name = element.localName
166
+ templates.forEach((template) => {
51
167
 
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
168
+ if( template.getAttribute('html-if') || template.getAttribute('html-inner') ) {
169
+ return
170
+ }
171
+
172
+ // Process any nested <template> tags within this <template> first
173
+ removeTemplateTagsRecursively(template.content)
174
+
175
+ // Get the parent of the <template> tag
176
+ const parent = template.parentNode
177
+
178
+ if (parent) {
179
+ // Move all child nodes from the <template>'s content to its parent
180
+ const content = template.content
181
+ while (content.firstChild) {
182
+ parent.insertBefore(content.firstChild, template)
63
183
  }
184
+ // Remove the <template> tag itself
185
+ parent.removeChild(template)
64
186
  }
65
- templates[id] = Template(element)
66
- }
187
+ })
67
188
  }
68
189
 
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
- })
190
+ const wrap = (open, node, close) => {
191
+ node.parentNode?.insertBefore(open, node)
192
+ node.parentNode?.insertBefore(close, node.nextSibling)
76
193
  }