shadow-function
Version:
ioing lib - shadow Function, worker Function
499 lines (459 loc) • 14.9 kB
text/typescript
'use strict'
import { ShadowFunction } from '../shadowFunction/index'
import { getObjectType } from '../objectType/index'
const DOCUMENT = document
let constructorId = 0
// ShadowDocument
class ShadowDocument {
public run: Function = () => null
private constructorId: number = 0
private TREE: object
private SHADOWTREE: object
private o: number = 0
private log: Function | undefined
private sandbox: Sandbox
private shadowWindow: ShadowWindow
private shadowDocumentBody: HTMLElement
private ShadowObject: ObjectConstructor
private ShadowNode: Node
private ShadowElement: Element
private shadowFunction: any
private shadowGetAttribute: Function
private allowTagName = {
'DIV': true,
'B': true,
'P': true,
'H1': true,
'H2': true,
'H3': true,
'H4': true,
'H5': true,
'DL': true,
'DT': true,
'DD': true,
'EM': true,
'HR': true,
'UL': true,
'LI': true,
'OL': true,
'TD': true,
'TH': true,
'TR': true,
'TT': true,
'NAV': true,
'SUP': true,
'SUB': true,
'SPAN': true,
'FONT': true,
'BR': true,
'STYLE': true,
'SMALL': true,
'LABEL': true,
'TABLE': true,
'TBODY': true,
'THEAD': true,
'TFOOT': true,
'BUTTON': true,
'FOOTER': true,
'HEADER': true,
'STRONG': true
}
private tracker = (e: object) => {
if (typeof (this.log) === 'function') {
this.log(e)
} else {
console.log('Event Log:', e)
}
}
constructor (root: any, template: string, setting = [{}, {}], log?: (e: object) => void) {
this.TREE = {
0: root.attachShadow ? root.attachShadow({ mode: 'open' }) : root
}
this.SHADOWTREE = {}
Object.assign(this.allowTagName, setting[0] || setting)
this.log = log
this.constructorId = constructorId++
this.shadowFunction = new ShadowFunction(setting[1] || {}, undefined, log)
this.shadowFunction = this.shadowFunction(this.injection())(this.empowerment())
this.sandbox = this.shadowFunction.sandbox
this.shadowWindow = this.sandbox.shadowWindow
this.shadowDocumentBody = this.shadowWindow.document.body
this.ShadowObject = this.shadowWindow.Object
this.ShadowNode = this.shadowWindow.Node
this.ShadowElement = this.shadowWindow.Element
this.shadowGetAttribute = this.ShadowElement['prototype'].getAttribute
this.setShadowObserver()
this.write(template)
this.run = this.shadowFunction.run.bind(this)
}
private getRealElement = (element: Element) => {
return this.TREE[element['uuid']] as HTMLElement || new Error(`ShadowFunction: Cannot synchronously read compiled style. Async read after the rendering ends / or use '<HTMLElement>.oncomputed = () => { element.offsetTop }'!`)
}
private empowerment () {
return {
getShadowElement: (element: Element) => {
return this.getRealElement(element)
},
getShadowElementProps: (element: Element, propsName: string) => {
return this.getRealElement(element)[propsName]
},
getComputedStyle: (element: Element, pseudoElt = null) => {
return getComputedStyle(this.getRealElement(element), pseudoElt)
},
setShadowEventListener: (listener: string, type: string, callback: Function, opt: boolean | AddEventListenerOptions | undefined) => {
let target: EventTarget | null = null
let eventName = 'on-shadow-' + type + '-' + this.constructorId
switch (listener) {
case '[object Window]':
target = window
break
case '[object HTMLDocument]':
target = document
break
case '[object HTMLBodyElement]':
target = document.body
break
case '[object HTMLHtmlElement]':
target = document.documentElement
break
}
if (!target) return
if (!callback) return target.removeEventListener(type, target[eventName])
Object.defineProperty(target, eventName, {
enumerable: false,
configurable: true,
writable: false,
value: (e: object) => {
callback.apply(callback, [this.createShadowEventObject(e)])
}
})
target.addEventListener(type, target[eventName], opt)
}
}
}
private isElementObject (value: any): boolean {
if (/HTML(\w+)?Element/.exec(getObjectType(value))) return true
return false
}
private createShadowEventObject (originEvent: object) {
const target = {}
for (let key in originEvent) {
let value = originEvent[key]
switch (typeof value) {
case 'string':
case 'number':
case 'boolean':
case 'undefined':
target[key] = value
break
case 'function':
target[key] = value.bind(originEvent)
break
case 'object':
switch (key) {
case 'changedTouches':
case 'sourceCapabilities':
case 'targetTouches':
case 'touches':
target[key] = this.createShadowEventObject(value)
break
case 'srcElement':
case 'target':
case 'toElement':
if (this.isElementObject(value)) target[key] = this.SHADOWTREE[value.uuid]
break
default:
if (!isNaN(Number(key))) {
target[key] = this.createShadowEventObject(value)
} else {
target[key] = value
}
break
}
break
}
}
return target
}
private injection () {
return `
window.addEventListener = EventTarget.prototype.addEventListener = function (type, callback, options) {
var target = this
var targetTypeName = Object.prototype.toString.call(target)
if (!target['onShadowEventNames']) {
Object.defineProperty(target, 'onShadowEventNames', {
enumerable: false,
configurable: true,
writable: true,
value: [type]
})
} else {
target['onShadowEventNames'].push(type)
}
if (!target['onshadow' + type]) {
Object.defineProperty(target, 'onshadow' + type, {
enumerable: false,
configurable: true,
writable: true,
value: {
type: type,
options: options,
callback: [callback]
}
})
setShadowEventListener(targetTypeName, type, function () {
var args = arguments
target['onshadow' + type].callback.map(function (fn) {
fn.apply(target, args)
})
}, options)
} else {
target['onshadow' + type].callback.push(callback)
}
}
window.removeEventListener = EventTarget.prototype.removeEventListener = function (type, callback, options) {
var target = this
var targetTypeName = Object.prototype.toString.call(target)
var callbackIndex = this['onshadow' + type].callback.indexOf(callback)
var eventNameIndex = this['onShadowEventNames'].indexOf(type)
if (eventNameIndex !== -1) {
this['onShadowEventNames'].splice(eventNameIndex, 1)
}
if (callbackIndex !== -1) {
this['onshadow' + type].callback.splice(callbackIndex, 1)
}
if (this['onshadow' + type].callback.length === 0) {
setShadowEventListener(targetTypeName, type)
}
}
shadowWindow.Object.defineProperties(shadowWindow.HTMLElement.prototype, {
'offsetHeight': {
get () {
return getShadowElementProps(this, 'offsetHeight')
}
},
'offsetWidth': {
get () {
return getShadowElementProps(this, 'offsetWidth')
}
},
'offsetTop': {
get () {
return getShadowElementProps(this, 'offsetTop')
}
},
'offsetLeft': {
get () {
return getShadowElementProps(this, 'offsetLeft')
}
},
'offsetParent': {
get () {
return getShadowElementProps(this, 'offsetParent')
}
},
'ref': {
get () {
return getShadowElement(this)
}
}
})
`
}
private setShadowObserver () {
new MutationObserver((records) => {
for (let record of records) {
let target = record.target
switch (record.type) {
case 'attributes':
this.setAttribute(record.attributeName as string, target)
break
case 'characterData':
this.setCharacterData(target)
break
case 'childList':
Array.prototype.forEach.call(record.removedNodes, (node: Node) => {
this.walker(this.iterator(node), target, true)
})
Array.prototype.forEach.call(record.addedNodes, (node: Node) => {
this.walker(this.iterator(node), target)
})
break
}
}
}).observe(this.shadowDocumentBody, {
subtree: true,
attributes: true,
childList: true,
characterData: true,
attributeOldValue: true,
characterDataOldValue: true
})
}
private write (template: string) {
this.shadowDocumentBody.innerHTML = `<div>${template}</div>`
}
private uuid (node: any, uuid?: number | string) {
uuid = parseInt(node.parentNode ? node.parentNode.uuid || 0 : 0, 10)
uuid++
this.o++
uuid = uuid + '.' + this.o
if (!node.uuid) {
this.ShadowObject.defineProperty(node, 'uuid', {
configurable: false,
enumerable: false,
value: uuid
})
this.SHADOWTREE[uuid] = node
}
return uuid
}
private iterator (nodes: any) {
if (nodes.nextNode) return nodes
return DOCUMENT.createNodeIterator(nodes, NodeFilter.SHOW_ALL, null)
}
private walker (nodes: TreeWalker, target: Node, del = false) {
let node = nodes.nextNode() as Element
while (node) {
this.uuid(node)
switch (node.nodeType) {
case Node.ELEMENT_NODE:
if (del) {
this.removeElement(node, target)
} else {
this.createElement(node, target)
}
break
case Node.TEXT_NODE:
if (del) {
this.removeTextNode(node, target)
} else {
this.createTextNode(node, target)
}
break
}
node = nodes.nextNode() as Element
if (!node) break
}
}
private getParentId (node: Node, target: Node) {
return (node.parentNode ? node.parentNode['uuid'] : target['uuid']) || 0
}
private createElement (node: Element, target: Node) {
let uuid = node['uuid']
let name = this.ShadowNode['prototype'].cloneNode.call(node).nodeName
let puuid = this.getParentId(node, target)
if (this.TREE[uuid]) return
switch (name) {
case this.allowTagName[name] ? name : null:
this.TREE[uuid] = DOCUMENT.createElement(name)
this.TREE[uuid].uuid = uuid
break
default:
this.tracker({
tagName: name,
action: 'createElement'
})
throw new Error(`ShadowFunction: The tag name provided ('${name}') is not a valid name of whitelist.`)
}
for (let i = 0; i < node.attributes.length; i++) {
this.setAttribute(node.attributes[i].name, node)
}
node['completedState'] = 'complete'
this.TREE[puuid].appendChild(this.TREE[uuid])
this.createEvent(node)
this.shadowFunction.run(`
typeof computed === 'function' && computed(el)
`)({
el: this.TREE[uuid],
computed: node['oncomputed']
})
}
private removeElement (node: Node, target: Node) {
let uuid = node['uuid']
let puuid = this.getParentId(node, target)
if (this.TREE[puuid] && this.TREE[uuid]) {
this.TREE[puuid].removeChild(this.TREE[uuid])
}
delete this.TREE[uuid]
}
private createTextNode (node: Node, target: Node) {
let uuid = node['uuid']
let puuid = this.getParentId(node, target)
let text = node.textContent || ''
if (this.TREE[uuid]) return
this.TREE[uuid] = DOCUMENT.createTextNode(text)
this.TREE[uuid].uuid = uuid
if (this.TREE[puuid]) {
this.TREE[puuid].appendChild(this.TREE[uuid])
}
}
private removeTextNode (node: Node, target: Node) {
let uuid = node['uuid']
let puuid = this.getParentId(node, target)
if (this.TREE[puuid] && this.TREE[uuid]) {
this.TREE[puuid].removeChild(this.TREE[uuid])
}
delete this.TREE[uuid]
}
private createEvent (node: Node) {
let onEvent = node['onShadowEventNames']
if (!onEvent) return
onEvent.map((type: string) => {
let shadowEvent = node['onshadow' + type]
if (shadowEvent) {
this.TREE[node['uuid']].addEventListener(shadowEvent.type, (event: object) => {
event = this.createShadowEventObject(event)
this.shadowFunction.run(`
for (let i = 0; i < events.length; i++) {
typeof(events[i]) === 'function' && events[i].apply(node, args)
}
`)({ events: shadowEvent.callback.map((fn: Function) => fn.bind(node)), node, args: [event] })
}, shadowEvent.options)
}
})
}
private setAttribute (name: string, node: Node) {
let whiteNode = this.TREE[node['uuid']]
let shadowNode = this.ShadowNode['prototype'].cloneNode.call(node)
let tagName = shadowNode.tagName
let allow = this.allowTagName[tagName]
let value = this.shadowGetAttribute.call(shadowNode, name)
let safeAttr = false
if (!whiteNode) return
switch (name) {
case 'id':
case 'name':
case 'style':
case 'class':
case 'width':
case 'height':
safeAttr = true
break
default:
if (typeof(allow) === 'function' && allow(name, value)) {
safeAttr = true
} else {
this.tracker({
tagName,
attributeName: name,
action: 'setAttribute',
value
})
throw new Error(`ShadowFunction: The attribute name provided ('${name} in <${tagName.toLocaleLowerCase()} />') is not a valid name of whitelist.`)
}
break
}
if (whiteNode && safeAttr) whiteNode.setAttribute(name, value)
}
private setCharacterData (node: any) {
let char = this.TREE[node.uuid]
if (char) char.textContent = node.textContent
}
}
const shadowDocument = (root: any, template: string, setting = [{}, {}], log?: (e: object) => void) => {
return new ShadowDocument(root, template, setting, log).run as unknown as void
}
export {
shadowDocument as ShadowDocument
}