jails-js 5.9.0 → 6.0.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,151 +1,176 @@
1
- import { type Component } from '..'
1
+ import { safe, g, dup } 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, register }) => {
6
+
7
+ let tick
8
+ let preserve = []
9
+
10
+ const _model = module.model || {}
11
+ const initialState = (new Function( `return ${node.getAttribute('html-model') || '{}'}`))()
12
+ const tplid = node.getAttribute('tplid')
13
+ const scopeid = node.getAttribute('html-scopeid')
14
+ const tpl = templates[ tplid ]
15
+ const scope = g.scope[ scopeid ]
16
+ const model = dup(module?.model?.apply ? _model({ elm:node, initialState }) : _model)
17
+ const state = Object.assign({}, scope, model, initialState)
18
+ const view = module.view? module.view : (data) => data
19
+
20
+ const base = {
21
+ name,
22
+ model,
23
+ elm: node,
24
+ template: tpl.template,
24
25
  dependencies,
25
26
  publish,
26
27
  subscribe,
27
28
 
28
29
  main(fn) {
29
- options.main = fn
30
- },
31
-
32
- unmount(fn) {
33
- options.unmount = fn
34
- },
35
-
36
- onupdate(fn) {
37
- options.onupdate = fn
38
- },
39
-
40
- on(eventName, selectorOrCallback, callback) {
41
- on(elm, eventName, selectorOrCallback, callback)
30
+ node.addEventListener(':mount', fn)
42
31
  },
43
32
 
44
- off(eventName, callback) {
45
- off(elm, eventName, callback)
46
- },
33
+ /**
34
+ * @State
35
+ */
36
+ state : {
47
37
 
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
- },
38
+ protected( list ) {
39
+ if( list ) {
40
+ preserve = list
41
+ } else {
42
+ return preserve
43
+ }
44
+ },
56
45
 
57
- emit: ( ...args ) => {
58
- trigger(elm, args.shift(), { args: args })
59
- },
46
+ save(data) {
47
+ if( data.constructor === Function ) {
48
+ data( state )
49
+ } else {
50
+ Object.assign(state, data)
51
+ }
52
+ },
60
53
 
61
- state: {
62
54
  set( data ) {
63
- if (data.constructor === Function) {
64
- const newstate = dup(state.data)
65
- data(newstate)
66
- base.render(newstate)
55
+
56
+ if (!document.body.contains(node)) {
57
+ return
58
+ }
59
+ if( data.constructor === Function ) {
60
+ data(state)
67
61
  } else {
68
- base.render(data)
62
+ Object.assign(state, data)
69
63
  }
70
- return new Promise((resolve) => rAF(_ => rAF(() => resolve(state.data))))
71
- },
72
- get() {
73
- return dup(state.data)
64
+
65
+ const newstate = Object.assign({}, state, scope)
66
+ render(newstate)
67
+
68
+ return Promise.resolve(newstate)
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
  }
86
105
 
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)
106
+ },
91
107
 
92
- Idiomorph.morph(elm, newhtml, IdiomorphOptions(elm))
93
- updateScope( elm )
108
+ off( ev, callback ) {
109
+ if( callback.handler ) {
110
+ node.removeEventListener(ev, callback.handler)
111
+ }
112
+ },
94
113
 
95
- rAF(_ => {
114
+ trigger(ev, selectorOrCallback, data) {
115
+ if( selectorOrCallback.constructor === String ) {
96
116
  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)
117
+ .from(node.querySelectorAll(selectorOrCallback))
118
+ .forEach( children => {
119
+ children.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail: { args: data } }) )
102
120
  })
103
- })
121
+ } else {
122
+ node.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail:{ args: data } }))
123
+ }
104
124
  },
105
125
 
106
- innerHTML( target, html_ ) {
126
+ emit(ev, data) {
127
+ node.dispatchEvent(new CustomEvent(ev, { bubbles: true, detail: { args: data } }))
128
+ },
129
+
130
+ unmount( fn ) {
131
+ node.addEventListener(':unmount', fn)
132
+ },
107
133
 
108
- const element = html_? target : elm
134
+ innerHTML ( target, html_ ) {
135
+ const element = html_? target : node
109
136
  const clone = element.cloneNode()
110
137
  const html = html_? html_ : target
111
138
  clone.innerHTML = html
112
-
113
- rAF( _ => Idiomorph.morph(element, clone, IdiomorphOptions) )
139
+ Idiomorph.morph(element, clone)
114
140
  }
115
141
  }
116
142
 
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
- }
143
+ const render = ( data ) => {
144
+ clearTimeout( tick )
145
+ tick = setTimeout(() => {
146
+ const html = tpl.render.call( view(data), node, safe, g )
147
+ Idiomorph.morph( node, html, IdiomorphOptions(node, register) )
148
+ Promise.resolve().then(() => {
149
+ node.querySelectorAll('[tplid]')
150
+ .forEach((element) => {
151
+ const child = register.get(element)
152
+ if(!child) return
153
+ child.state.protected().forEach( key => delete data[key] )
154
+ child.state.set(data)
155
+ })
156
+ Promise.resolve().then(() => g.scope = {})
157
+ })
134
158
  })
135
- })
136
- }
137
-
159
+ }
138
160
 
139
- const IdiomorphOptions = (parent) => ({
161
+ render( state )
162
+ register.set( node, base )
163
+ return module.default( base )
164
+ }
140
165
 
166
+ const IdiomorphOptions = ( parent, register ) => ({
141
167
  callbacks: {
142
-
143
168
  beforeNodeMorphed( node ) {
144
169
  if( node.nodeType === 1 ) {
145
170
  if( 'html-static' in node.attributes ) {
146
171
  return false
147
172
  }
148
- if( node.base && node !== parent ) {
173
+ if( register.get(node) && node !== parent ) {
149
174
  return false
150
175
  }
151
176
  }
package/src/element.ts CHANGED
@@ -1,14 +1,12 @@
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
+ const register = new WeakMap()
5
4
 
6
- return class extends HTMLElement {
5
+ export const Element = ({ component, templates, start }) => {
6
+
7
+ const { name, module, dependencies } = component
7
8
 
8
- base: any
9
- options: any
10
- returns : any
11
- __events: any
9
+ return class extends HTMLElement {
12
10
 
13
11
  constructor() {
14
12
  super()
@@ -16,59 +14,34 @@ export default function Element(module, dependencies, templates, components) {
16
14
 
17
15
  connectedCallback() {
18
16
 
19
- const { base, options } = Component(this, { module, dependencies, templates, components })
17
+ this.abortController = new AbortController()
20
18
 
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
19
+ if( !this.getAttribute('tplid') ) {
20
+ start( this.parentNode )
36
21
  }
37
22
 
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
- })
23
+ const rtrn = Component({
24
+ node:this,
25
+ name,
26
+ module,
27
+ dependencies,
28
+ templates,
29
+ signal: this.abortController.signal,
30
+ register
31
+ })
47
32
 
33
+ if ( rtrn && rtrn.constructor === Promise ) {
34
+ rtrn.then(() => {
35
+ this.dispatchEvent( new CustomEvent(':mount') )
36
+ })
48
37
  } 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
- }
38
+ this.dispatchEvent( new CustomEvent(':mount') )
55
39
  }
56
40
  }
57
41
 
58
42
  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
43
+ this.dispatchEvent( new CustomEvent(':unmount') )
44
+ this.abortController.abort()
72
45
  }
73
46
  }
74
47
  }
package/src/index.ts CHANGED
@@ -1,42 +1,28 @@
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'
5
1
 
6
- const templates = {}
7
- const components = {}
2
+ import { Element } from './element'
3
+ import { template, templateConfig as config } from './template-system'
8
4
 
9
- export { html, attributes }
5
+ const components = {}
10
6
 
11
- export default {
7
+ export { publish, subscribe } from './utils/pubsub'
12
8
 
13
- templateConfig,
9
+ export const templateConfig = (options) => {
10
+ config( options )
11
+ }
14
12
 
15
- publish,
16
- subscribe,
13
+ export const register = ( name, module, dependencies ) => {
14
+ components[ name ] = { name, module, dependencies }
15
+ }
17
16
 
18
- register( name, module, dependencies = {} ) {
19
- components[name] = { name, module, dependencies }
20
- },
17
+ export const start = ( target = document.body ) => {
21
18
 
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
- }
19
+ const templates = template( target, { components } )
31
20
 
32
- const registerComponents = () => {
33
21
  Object
34
22
  .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)
23
+ .forEach(({ name, module, dependencies }) => {
24
+ if( !customElements.get(name) ) {
25
+ customElements.define( name, Element({ component: { name, module, dependencies }, templates, start }))
40
26
  }
41
- })
27
+ })
42
28
  }
@@ -1,26 +1,37 @@
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 ), '[html-if]', '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 = ( html ) => {
26
+
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 '+ decodeHTML(variable) +';})+"'
24
35
  })
25
36
  .replace(/%%_(.+?)_%%/g, function(_, variable){
26
37
  return '";' + decodeHTML(variable) +'\noutput+="'
@@ -29,48 +40,146 @@ export default function Template(element) {
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
+ if( node.getAttribute('html-if') && !node.id ) {
52
+ node.id = uuid()
53
+ }
54
+ })
55
+ }
56
+
57
+ const transformAttributes = ( html ) => {
58
+
59
+ const regexTags = new RegExp(`\\${config.tags[0]}(.+?)\\${config.tags[1]}`, 'g')
60
+
61
+ return html
62
+ .replace(/jails___scope-id/g, '%%_=$scopeid_%%')
63
+ .replace(regexTags, '%%_=$1_%%')
64
+ // Booleans
65
+ // https://meiert.com/en/blog/boolean-attributes-of-html/
66
+ .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%%_}_%%`)
67
+ // The rest
68
+ .replace(/html-(.*?)=\"(.*?)\"/g, (all, key, value) => {
69
+ if (key === 'key' || key === 'model' || key === 'scopeid' ) {
70
+ return all
71
+ }
72
+ if (value) {
73
+ value = value.replace(/^{|}$/g, '')
74
+ return `${key}="%%_=safe(function(){ return ${value} })_%%"`
75
+ } else {
76
+ return all
77
+ }
78
+ })
79
+ }
80
+
81
+ const transformTemplate = ( clone ) => {
82
+
83
+ clone.querySelectorAll('template, [html-for], [html-if], [html-inner], [html-class]')
84
+ .forEach(( element ) => {
85
+
86
+ const htmlFor = element.getAttribute('html-for')
87
+ const htmlIf = element.getAttribute('html-if')
88
+ const htmlInner = element.getAttribute('html-inner')
89
+ const htmlClass = element.getAttribute('html-class')
90
+
91
+ if ( htmlFor ) {
92
+
93
+ element.removeAttribute('html-for')
94
+
95
+ const split = htmlFor.match(/(.*)\sin\s(.*)/) || ''
96
+ const varname = split[1]
97
+ const object = split[2]
98
+ const objectname = object.split(/\./).shift()
99
+ 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] = Object.assign({}, { ${objectname}: ${objectname} }, { ${varname} :${varname}, $index: $index, $key: $key }); _%%`)
100
+ const close = document.createTextNode(`%%_ $index++; } })() _%%`)
101
+
102
+ wrap(open, element, close)
103
+ }
104
+
105
+ if (htmlIf) {
106
+ element.removeAttribute('html-if')
107
+ const open = document.createTextNode(`%%_ if ( safe(function(){ return ${htmlIf} }) ){ _%%`)
108
+ const close = document.createTextNode(`%%_ } _%%`)
109
+ wrap(open, element, close)
110
+ }
111
+
112
+ if (htmlInner) {
113
+ element.removeAttribute('html-inner')
114
+ element.innerHTML = `%%_=${htmlInner}_%%`
115
+ }
116
+
117
+ if (htmlClass) {
118
+ element.removeAttribute('html-class')
119
+ element.className = (element.className + ` %%_=${htmlClass}_%%`).trim()
120
+ }
121
+
122
+ if( element.localName === 'template' ) {
123
+ transformTemplate(element.content)
124
+ }
125
+ })
126
+ }
127
+
128
+ const setTemplates = ( clone, components ) => {
129
+
130
+ Array.from(clone.querySelectorAll('[tplid]'))
36
131
  .reverse()
37
- .forEach( (node:HTMLElement) => {
38
- node.querySelectorAll('template').forEach( template => buildtemplates(template.content, selector, templates, components ))
39
- createTemplateId(node, templates, components)
132
+ .forEach((node) => {
133
+
134
+ const tplid = node.getAttribute('tplid')
135
+ const name = node.localName
136
+ node.setAttribute('html-scopeid', 'jails___scope-id')
137
+
138
+ if( name in components && components[name].module.template ) {
139
+ const children = node.innerHTML
140
+ const html = components[name].module.template({ elm:node, children })
141
+ node.innerHTML = html
142
+ }
143
+
144
+ const html = transformAttributes(node.outerHTML)
145
+
146
+ templates[ tplid ] = {
147
+ template: html,
148
+ render : compile(html)
149
+ }
40
150
  })
41
151
  }
42
152
 
43
- const createTemplateId = (element, templates, components ) => {
153
+ const removeTemplateTagsRecursively = (node) => {
44
154
 
45
- const tplid = element.getAttribute('tplid')
155
+ // Get all <template> elements within the node
156
+ const templates = node.querySelectorAll('template')
46
157
 
47
- if (!tplid) {
48
- const id = uuid()
49
- element.setAttribute('tplid', id)
50
- const name = element.localName
158
+ templates.forEach((template) => {
51
159
 
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
160
+ if( template.getAttribute('html-if') || template.getAttribute('html-inner') ) {
161
+ return
162
+ }
163
+
164
+ // Process any nested <template> tags within this <template> first
165
+ removeTemplateTagsRecursively(template.content)
166
+
167
+ // Get the parent of the <template> tag
168
+ const parent = template.parentNode
169
+
170
+ if (parent) {
171
+ // Move all child nodes from the <template>'s content to its parent
172
+ const content = template.content
173
+ while (content.firstChild) {
174
+ parent.insertBefore(content.firstChild, template)
63
175
  }
176
+ // Remove the <template> tag itself
177
+ parent.removeChild(template)
64
178
  }
65
- templates[id] = Template(element)
66
- }
179
+ })
67
180
  }
68
181
 
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
- })
182
+ const wrap = (open, node, close) => {
183
+ node.parentNode?.insertBefore(open, node)
184
+ node.parentNode?.insertBefore(close, node.nextSibling)
76
185
  }