-
Notifications
You must be signed in to change notification settings - Fork 1
/
celo.mjs
137 lines (118 loc) · 4.61 KB
/
celo.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
export default (function (){
// Defining variables
let config = window.celoConfig ? window.celoConfig : {}
const componentsPath = config.componentsPath ? config.componentsPath : "/components"
const containerId = config.containerId ? config.containerId : "_celo"
// A list to keep track of loaded components, so we don't fetch them twice.
const loadedComponents = []
// Sets up an observer that reacts to elements being added to the DOM.
function setUpObserver () {
const observer = new MutationObserver(parseElements)
observer.observe(document, { childList: true, subtree: true })
return observer
}
// If the element is <body>, set up a container and cache. If it has a hyphen,
// then its a custom: we must fetch it if we don't have done it already.
// This is a recursive function, so it will allow deeply parsing (necessary
// for use in React).
function parseElements (elementList) {
elementList.forEach(el => {
if (el.addedNodes) el = el.addedNodes[0]
if (el instanceof HTMLElement) parseElements( el.childNodes )
else return false
if( !el.tagName.includes('-')) return false
const tagName = el.tagName.toLowerCase()
if( loadedComponents.includes(tagName)) return
setUpContainer()
markDefined(tagName)
getMarkUp(tagName)
.then(markUp => injectMarkup(markUp))
.then(frag => recreateScripts(frag))
.then(frag => addToDOM(frag))
.then(() => el.shadowRoot && parseElements(el.shadowRoot.childNodes))
})
}
// A hidden div is needed to hold the components' templates. Let's check if it
// exists, and create it if it doesn't.
function setUpContainer () {
if( !document.querySelector(`#${containerId}`) && document.querySelector('body') ){
const container = document.createElement('div')
container.id = containerId
document.body.appendChild(container)
}
}
// Checks if component hasn't been loaded yet.
function isNew( tagName ){
return !loadedComponents.includes( tagName.toLowerCase() )
}
// Make a note of loaded web component
function markDefined( tagName ){
loadedComponents.push( tagName )
}
// Fetches the needed markup from the server
function getMarkUp( tagName ){
return fetch(`${componentsPath}/${ tagName }.html`)
.then( res => {
if( !res.ok )
console.warn(`Component <${ tagName }> not found. Is it a subcomponent?`)
else
return res.text()
})
}
// Append the html text to a DOM element. This will parse the element, but
// the scripts won't run.
function injectMarkup( markUp ){
if( !markUp ) return false
let markUpFragment = document.createDocumentFragment()
let container = document.createElement('div')
container.innerHTML = markUp
while( container.firstChild ) markUpFragment.appendChild( container.firstChild )
return markUpFragment
}
// Oftentimes scripts are inserted/cloned with an "already started" flag on,
// so they just don't run. To deal with that, we recreate them as new
// elements.
function recreateScripts( element ){
if( !element ) return false
element.querySelectorAll( 'script' ).forEach( script => {
let scriptElement = document.createElement( 'script' )
scriptElement.appendChild(document.createTextNode(script.innerHTML))
let container = script.parentElement || element
container.appendChild( scriptElement )
container.removeChild( script )
})
return element
}
// Adds the component to the DOM
function addToDOM( fragment ){
if( !fragment ) return false
document.querySelector(`#${containerId}`).append( fragment )
}
// Scans elements parsed before the observer was in effect and
// reparse them
function backSearch(){
const elList = Array.from( document.getElementsByTagName("*") )
.filter( el => el.tagName.includes("-"))
elList.forEach( el => reparseElement( el ))
}
// Remove an element and attach it to the same position, forcing it to be reparsed.
function reparseElement( el ){
let parent = el.parentNode
let sibling = el.nextSibling
el.parentNode.removeChild(el)
if( sibling )
parent.insertBefore( el, sibling )
else
parent.appendChild( el )
}
// Extends customElements.define to keep synchrounous tag on the custom elements
const originalCustomElementDefinition = customElements.define
customElements.define = function(){
loadedComponents.push( arguments[0] )
return originalCustomElementDefinition.apply( customElements, arguments )
}
setUpContainer()
let watcher = setUpObserver()
backSearch()
return watcher
})()