UNPKG

shadow-function

Version:

ioing lib - shadow Function, worker Function

499 lines (459 loc) 14.9 kB
'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 }