├── .gitignore ├── .npmignore ├── README.md ├── package-lock.json ├── package.json └── src ├── nativescript-preact.js ├── undom.js └── unwindow.js /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | .DS_Store 3 | 4 | *.js.map 5 | hooks/ 6 | lib/ 7 | node_modules/ 8 | platforms/ 9 | tmp/ 10 | typings/ 11 | .idea 12 | .tern-port 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo-app/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NativeScript Preact 2 | 3 | This plugin integrates [Preact](https://preactjs.com/) and [NativeScript](https://www.nativescript.org/), allowing you to build cross-platform iOS and Android apps using Preact. 4 | 5 | Why? Because I prefer the (P)React pattern of building UIs over what Angular offers, and find that NativeScript has [several technical advantages](https://www.quora.com/What-are-the-key-difference-between-ReactNative-and-NativeScript) over ReactNative. 6 | 7 | ## Getting Started 8 | 9 | 1. [Install NativeScript CLI](https://docs.nativescript.org/start/quick-setup) 10 | 2. Generate a new repo based on [NativeScript Preact Template](https://github.com/staydecent/nativescript-preact-template/generate) 11 | 3. Clone your new repo. 12 | 4. `npm install` and `nativescript run android` (or `ios`) 13 | 14 | ## This is Alpha Software! 15 | 16 | This is a very early working example, and should not be used for a production app unless you really know what you're doing. I hope to build a side-project using this as well as add unit tests for all NativeScript components. 17 | 18 | ## How it Works 19 | 20 | This was made possible by [undom](https://github.com/developit/undom) library, allowing Preact to rending into a pure JavaScript DOM, within the NativeScript runtime. Currently, I'm shipping a modified undom with basic MutationObserver API implemented which is what nativescript-preact uses to sync changes from the DOM to the NativeScript widgets. I aimed to keep this code generalized (and hopefully small), so the bridge code is easier to maintain and less prone to bugs. 21 | 22 | ## Get Involved! 23 | 24 | [File issues!](https://github.com/staydecent/nativescript-preact/issues) Feel free to post questions as issues here or look for the `#preact` channel on the [NativeScript Slack Community](https://www.nativescript.org/slack-invitation-form). 25 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nativescript-preact", 3 | "version": "0.0.3", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "check-arg-types": { 8 | "version": "1.1.2", 9 | "resolved": "https://registry.npmjs.org/check-arg-types/-/check-arg-types-1.1.2.tgz", 10 | "integrity": "sha512-JlPlbqz/TcRGJYR0rSe/YhuqjOP1VoIePuaQHKHPM+GoxsLbjSCFc3N93Iiar+KNs9KcmLTYhmR15Mlhl4OgfA==" 11 | }, 12 | "preact": { 13 | "version": "10.0.0-rc.3", 14 | "resolved": "https://registry.npmjs.org/preact/-/preact-10.0.0-rc.3.tgz", 15 | "integrity": "sha512-IvDc2AGvHJncEtORciLDzpluDF2MsZqf9eo6xHt7HVY4E6OvxZzAePYJtv3siVdEntxmB9NciQpbToT1APqJYQ==" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nativescript-preact", 3 | "version": "0.0.3", 4 | "description": "Native mobile applications using Preact and NativeScript.", 5 | "main": "src/nativescript-preact", 6 | "author": "Adrian Unger ", 7 | "license": "MIT", 8 | "homepage": "https://github.com/staydecent/nativescript-preact#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/staydecent/nativescript-preact.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/staydecent/nativescript-preact/issues" 15 | }, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "dependencies": { 20 | "check-arg-types": "1.1.2", 21 | "preact": "10.0.0-rc.3" 22 | }, 23 | "keywords": [ 24 | "nativescript", 25 | "integration", 26 | "mobile", 27 | "native", 28 | "preact" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/nativescript-preact.js: -------------------------------------------------------------------------------- 1 | require('./unwindow') 2 | 3 | const undom = require('./undom') 4 | 5 | const check = require('check-arg-types') 6 | 7 | const { Component, h, render: mount } = require('preact') 8 | const hooks = require('preact/hooks') 9 | 10 | const PREACT_WIDGET_REF = '__preact_widget_ref__' 11 | 12 | const modules = { 13 | // UI Modules 14 | animation: () => require('tns-core-modules/ui/animation'), 15 | formattedString: () => require('tns-core-modules/text/formatted-string'), 16 | frame: () => require('tns-core-modules/ui/frame'), 17 | page: () => require('tns-core-modules/ui/page'), 18 | contentView: () => require('tns-core-modules/ui/content-view'), 19 | 20 | // Layouts 21 | absoluteLayout: () => require('tns-core-modules/ui/layouts/absolute-layout'), 22 | dockLayout: () => require('tns-core-modules/ui/layouts/dock-layout'), 23 | flexboxLayout: () => require('tns-core-modules/ui/layouts/flexbox-layout'), 24 | gridLayout: () => require('tns-core-modules/ui/layouts/grid-layout'), 25 | layoutBase: () => require('tns-core-modules/ui/layouts/layout-base'), 26 | stackLayout: () => require('tns-core-modules/ui/layouts/stack-layout'), 27 | wrapLayout: () => require('tns-core-modules/ui/layouts/wrap-layout'), 28 | 29 | // Widgets 30 | actionBar: () => require('tns-core-modules/ui/action-bar'), 31 | activityIndicator: () => require('tns-core-modules/ui/activity-indicator'), 32 | button: () => require('tns-core-modules/ui/button'), 33 | datePicker: () => require('tns-core-modules/ui/date-picker'), 34 | dialogs: () => require('tns-core-modules/ui/dialogs'), 35 | editableTextBase: () => require('tns-core-modules/ui/editable-text-base'), 36 | htmlView: () => require('tns-core-modules/ui/html-view'), 37 | image: () => require('tns-core-modules/ui/image'), 38 | label: () => require('tns-core-modules/ui/label'), 39 | listPicker: () => require('tns-core-modules/ui/list-picker'), 40 | listView: () => require('tns-core-modules/ui/list-view'), 41 | placeholder: () => require('tns-core-modules/ui/placeholder'), 42 | progress: () => require('tns-core-modules/ui/progress'), 43 | scrollView: () => require('tns-core-modules/ui/scroll-view'), 44 | searchBar: () => require('tns-core-modules/ui/search-bar'), 45 | slider: () => require('tns-core-modules/ui/slider'), 46 | switchNS: () => require('tns-core-modules/ui/switch'), 47 | tabView: () => require('tns-core-modules/ui/tab-view'), 48 | textField: () => require('tns-core-modules/ui/text-field'), 49 | textView: () => require('tns-core-modules/ui/text-view'), 50 | timePicker: () => require('tns-core-modules/ui/time-picker'), 51 | webView: () => require('tns-core-modules/ui/web-view') 52 | } 53 | 54 | const widgets = [ 55 | 'AbsoluteLayout', 56 | 'ActionBar', 57 | 'ActionItem', 58 | 'ActivityIndicator', 59 | 'Button', 60 | 'ContainerView', 61 | 'ContentView', 62 | 'CustomLayoutView', 63 | 'DatePicker', 64 | 'DockLayout', 65 | 'EditableTextBase', 66 | 'FlexboxLayout', 67 | 'FormattedString', 68 | 'Frame', 69 | 'GridLayout', 70 | 'HtmlView', 71 | 'Image', 72 | 'Label', 73 | 'LayoutBase', 74 | 'ListPicker', 75 | 'ListView', 76 | 'NavigationButton', 77 | 'Observable', 78 | 'Page', 79 | 'Placeholder', 80 | 'Progress', 81 | 'Repeater', 82 | 'ScrollView', 83 | 'SearchBar', 84 | 'SegmentedBar', 85 | 'SegmentedBarItem', 86 | 'Slider', 87 | 'Span', 88 | 'StackLayout', 89 | 'Switch', 90 | 'TabView', 91 | 'TabViewItem', 92 | 'TextBase', 93 | 'TextField', 94 | 'TextView', 95 | 'TimePicker', 96 | 'View', 97 | 'ViewBase', 98 | 'WebView', 99 | 'WrapLayout' 100 | ] 101 | 102 | // nodeName => module 103 | // Ex. TEXTVIEW => 'textView' 104 | const modMap = Object.fromEntries(widgets.map(w => [w.toUpperCase(), w[0].toLowerCase() + w.slice(1)])) 105 | 106 | // nodeName => class 107 | // Ex. TEXTVIEW => 'TextView' 108 | const classMap = Object.fromEntries(widgets.map(w => [w.toUpperCase(), w])) 109 | 110 | // handler mappings 111 | const handlerMap = { 112 | input: 'textChange', 113 | press: 'tap', 114 | click: 'tap' 115 | } 116 | 117 | // Map NativeScript components to createElement calls 118 | const makeComponent = componentName => { 119 | function ComponentWrapper ({ children, ...props }) { 120 | return h(componentName, props, children) 121 | } 122 | ComponentWrapper.displayName = componentName 123 | return ComponentWrapper 124 | } 125 | let components = {} 126 | Object.values(classMap).map(componentName => { 127 | components[componentName] = makeComponent(componentName) 128 | }) 129 | 130 | // Create and attach NativeScript UI widget to Element 131 | const attachWidget = (el) => { 132 | const type = check.toType(el) 133 | if (type === 'string') { 134 | return el 135 | } 136 | 137 | const modName = modMap[el.nodeName] || el.nodeName.toLowerCase() 138 | const module = modules[modName]() 139 | const className = classMap[el.nodeName] 140 | const widget = new module[className]() 141 | 142 | const attrs = el.attributes 143 | const attrKeys = Object.keys(attrs) 144 | for (let x = 0; x < attrKeys.length; x++) { 145 | const k = attrKeys[x] 146 | const v = attrs[k] 147 | widget[k] = v 148 | } 149 | 150 | el[PREACT_WIDGET_REF] = widget 151 | return el 152 | } 153 | 154 | // Mock the DOM 155 | const document = undom() 156 | global.document = document 157 | const superCreateElement = document.createElement 158 | global.document.createElement = (type) => { 159 | const el = superCreateElement(type) 160 | return attachWidget(el) 161 | } 162 | 163 | // Safely get value or undefined without exception 164 | const getAttr = (attributes, name = 'value') => { 165 | for (let x = 0; x < attributes.length; x++) { 166 | if (attributes[x].name && attributes[x].name === name) { 167 | return attributes[x].value 168 | } 169 | } 170 | } 171 | 172 | const build = (parentNode, target) => { 173 | const widget = parentNode[PREACT_WIDGET_REF] 174 | 175 | // textContent 176 | if (!widget && parentNode.nodeType === 3 && target) { 177 | return build(target) 178 | } 179 | 180 | // Build Page 181 | if (widget instanceof modules.page().Page) { 182 | const children = parentNode.childNodes 183 | const len = children.length 184 | for (let x = 0; x < len; x++) { 185 | if (children[x].localName === 'ACTIONBAR') { 186 | widget.actionBar = build(children[x]) 187 | } else { 188 | widget.content = build(children[x]) 189 | break 190 | } 191 | } 192 | } else if (widget instanceof modules.contentView().ContentView) { 193 | widget.content = build(parentNode.childNodes[0]) 194 | } 195 | 196 | // Build Layouts 197 | if (widget instanceof modules.layoutBase().LayoutBase) { 198 | for (let x = 0; x < parentNode.childNodes.length; x++) { 199 | const childWidget = build(parentNode.childNodes[x]) 200 | widget.addChild(childWidget) 201 | } 202 | } 203 | 204 | // Label / Button 205 | if (widget instanceof modules.label().Label || widget instanceof modules.button().Button) { 206 | widget.text = parentNode.childNodes && parentNode.childNodes.length 207 | ? parentNode.childNodes[0].data 208 | : getAttr(parentNode.attributes, 'text') 209 | } 210 | 211 | // Sync Element attributes on NativeScript widget 212 | const attrs = parentNode.attributes 213 | const attrsLen = attrs.length 214 | for (let x = 0; x < attrsLen; x++) { 215 | const attr = attrs[x] 216 | widget[attr.name] = attr.value 217 | } 218 | 219 | // Bind any found handlers 220 | const eventNames = Object.keys(parentNode.__handlers || {}) 221 | if (eventNames.length) { 222 | const len = eventNames.length 223 | for (let x = 0; x < len; x++) { 224 | const name = eventNames[x] 225 | widget.on(handlerMap[eventNames[x]] || eventNames[x], function (ev) { 226 | ev.type = name[0].toUpperCase() + name.slice(1) 227 | parentNode.__handlers[name][0](ev) 228 | }) 229 | } 230 | } 231 | 232 | // Ensure the widget exists in the NS tree 233 | if (target && target[PREACT_WIDGET_REF] && !widget.parent) { 234 | const targetWidget = target[PREACT_WIDGET_REF] 235 | let pos = -1 236 | for (let x = 0; x < target.children.length; x++) { 237 | if (target.children[x] === parentNode) { 238 | pos = x 239 | break 240 | } 241 | } 242 | if (pos !== -1) { 243 | console.log('ensure tree', pos, targetWidget, widget) 244 | targetWidget.insertChild(widget, pos) 245 | } 246 | } 247 | 248 | return widget 249 | } 250 | 251 | const destroy = (parentNode, nodes) => { 252 | const parentWidget = parentNode[PREACT_WIDGET_REF] 253 | if (!parentWidget) return 254 | const len = nodes.length 255 | console.log('destroy', parentWidget, { len }) 256 | for (let x = 0; x < len; x++) { 257 | const node = nodes[x] 258 | console.log('_removeView', parentWidget, node[PREACT_WIDGET_REF]) 259 | parentWidget._removeView(node[PREACT_WIDGET_REF]) 260 | } 261 | } 262 | 263 | const update = (target, attributeName) => { 264 | const widget = target[PREACT_WIDGET_REF] 265 | 266 | const newVal = getAttr(target.attributes.slice(0), attributeName) 267 | console.log('update', target.localName, { attributeName, newVal }) 268 | widget[attributeName] = newVal 269 | 270 | if (attributeName === 'value') { 271 | widget.text = newVal 272 | } 273 | } 274 | 275 | // Observe (un)DOM changes 276 | const MutationObserver = document.defaultView.MutationObserver 277 | const observer = new MutationObserver(function (mutations) { 278 | const len = mutations.length 279 | for (let x = 0; x < len; x++) { 280 | const mutation = mutations[x] 281 | const { addedNodes, removedNodes, type, target } = mutation 282 | console.log('mutation', type) 283 | 284 | if (type === 'attributes') { 285 | update(target, mutation.attributeName) 286 | } 287 | 288 | if (addedNodes && addedNodes.length) { 289 | console.log('build', target.localName, addedNodes[0].localName) 290 | build(addedNodes[0], target) 291 | } 292 | 293 | if (target && removedNodes && removedNodes.length) { 294 | destroy(target, removedNodes) 295 | } 296 | } 297 | }) 298 | 299 | observer.observe(document.body, { 300 | attributes: true, 301 | characterData: true, // needed 302 | childList: true, // children, includes text 303 | subtree: true // descendants 304 | }) 305 | 306 | function render (Component) { 307 | // Build default NS frame 308 | const frameModule = modules.frame() 309 | let topmost = frameModule.topmost() 310 | if (!topmost) topmost = new frameModule.Frame() 311 | 312 | // Mount to (un)DOM 313 | mount(h(Component), document.body) 314 | 315 | // The first child of body is our top-level widget. 316 | const page = document.body.childNodes[0][PREACT_WIDGET_REF] 317 | 318 | // navigate our frame to the given Component widget counterpart. 319 | topmost.navigate({ create: () => page }) 320 | 321 | // return frame for application.run.create 322 | return topmost 323 | } 324 | 325 | module.exports = { 326 | render, 327 | Component, 328 | h, 329 | ...hooks, 330 | ...components 331 | } 332 | -------------------------------------------------------------------------------- /src/undom.js: -------------------------------------------------------------------------------- 1 | const assign = (target, source) => Object.assign(target, source) 2 | const toLower = str => String(str).toLowerCase() 3 | const findWhere = (arr, fn, returnIndex, byValue) => { 4 | let x = arr.length 5 | while (x--) if (byValue ? arr[x] === fn : fn(arr[x])) break 6 | return returnIndex ? x : arr[x] 7 | } 8 | const splice = (arr, item, add, byValue) => { 9 | let x = arr ? findWhere(arr, item, true, byValue) : -1 10 | console.log('splice', x, ~x) 11 | if (~x) add ? arr.splice(x, 0, add) : arr.splice(x, 1) 12 | return x 13 | } 14 | const createAttributeFilter = (ns, name) => 15 | attr => attr.ns === ns && toLower(attr.name) === toLower(name) 16 | 17 | /* 18 | const NODE_TYPES = { 19 | ELEMENT_NODE: 1, 20 | ATTRIBUTE_NODE: 2, 21 | TEXT_NODE: 3, 22 | CDATA_SECTION_NODE: 4, 23 | ENTITY_REFERENCE_NODE: 5, 24 | COMMENT_NODE: 6, 25 | PROCESSING_INSTRUCTION_NODE: 7, 26 | DOCUMENT_NODE: 9 27 | }; 28 | */ 29 | 30 | function createEnvironment () { 31 | const isElement = node => node.nodeType === 1 32 | let observers = [] 33 | let pendingMutations = false 34 | 35 | class Node { 36 | constructor (nodeType, nodeName) { 37 | this.nodeType = nodeType 38 | this.nodeName = nodeName 39 | this.localName = nodeName 40 | this.childNodes = [] 41 | } 42 | get nextSibling () { 43 | let p = this.parentNode 44 | if (p) return p.childNodes[findWhere(p.childNodes, this, true, true) + 1] 45 | } 46 | get previousSibling () { 47 | let p = this.parentNode 48 | if (p) return p.childNodes[findWhere(p.childNodes, this, true, true) - 1] 49 | } 50 | get firstChild () { 51 | return this.childNodes[0] 52 | } 53 | get lastChild () { 54 | return this.childNodes[this.childNodes.length - 1] 55 | } 56 | appendChild (child) { 57 | this.insertBefore(child) 58 | return child 59 | } 60 | insertBefore (child, ref) { 61 | child.remove() 62 | child.parentNode = this 63 | if (ref) splice(this.childNodes, ref, child, true) 64 | else this.childNodes.push(child) 65 | mutation(this, 'childList', { 66 | addedNodes: [child], 67 | previousSibling: !ref && this.childNodes[this.childNodes.length - 2], 68 | nextSibling: ref 69 | }) 70 | return child 71 | } 72 | replaceChild (child, ref) { 73 | if (ref.parentNode === this) { 74 | this.insertBefore(child, ref) 75 | ref.remove() 76 | return ref 77 | } 78 | } 79 | removeChild (child) { 80 | const x = splice(this.childNodes, child, false, true) 81 | mutation(this, 'childList', { 82 | removedNodes: [child], 83 | previousSibling: this.childNodes[x - 1], 84 | nextSibling: this.childNodes[x] 85 | }) 86 | return child 87 | } 88 | remove () { 89 | if (this.parentNode) this.parentNode.removeChild(this) 90 | } 91 | } 92 | 93 | class Text extends Node { 94 | constructor (text) { 95 | super(3, '#text') // TEXT_NODE 96 | this.nodeValue = text 97 | } 98 | set textContent (text) { 99 | this.nodeValue = text 100 | } 101 | get textContent () { 102 | return this.nodeValue 103 | } 104 | set data (text) { 105 | // Trigger mutation for nativescript-preact to handle 106 | // @TODO: Rather not have this tied to nativescript-preact implementation; 107 | // How can we update textContent/nodeValue and trigger a DOM standard mutation? 108 | this.parentNode.setAttribute('value', text) 109 | this.nodeValue = text 110 | } 111 | get data () { 112 | return this.nodeValue 113 | } 114 | } 115 | 116 | class Element extends Node { 117 | constructor (nodeType, nodeName) { 118 | super(nodeType || 1, nodeName) // ELEMENT_NODE 119 | this.attributes = [] 120 | this.__handlers = {} 121 | this.style = {} 122 | } 123 | 124 | get className () { return this.getAttribute('class') } 125 | set className (val) { this.setAttribute('class', val) } 126 | 127 | get cssText () { return this.getAttribute('style') } 128 | set cssText (val) { this.setAttribute('style', val) } 129 | 130 | get children () { 131 | return this.childNodes.filter(isElement) 132 | } 133 | 134 | setAttribute (key, value) { 135 | this.setAttributeNS(null, key, value) 136 | } 137 | getAttribute (key) { 138 | return this.getAttributeNS(null, key) 139 | } 140 | removeAttribute (key) { 141 | this.removeAttributeNS(null, key) 142 | } 143 | 144 | setAttributeNS (ns, name, value) { 145 | let attr = findWhere(this.attributes, createAttributeFilter(ns, name), false, false) 146 | let oldValue = attr && attr.value 147 | if (!attr) this.attributes.push(attr = { ns, name }) 148 | attr.value = String(value) 149 | mutation(this, 'attributes', { attributeName: name, attributeNamespace: ns, oldValue: oldValue }) 150 | } 151 | getAttributeNS (ns, name) { 152 | let attr = findWhere(this.attributes, createAttributeFilter(ns, name), false, false) 153 | return attr && attr.value 154 | } 155 | removeAttributeNS (ns, name) { 156 | splice(this.attributes, createAttributeFilter(ns, name), false, false) 157 | mutation(this, 'attributes', { attributeName: name, attributeNamespace: ns, oldValue: this.getAttributeNS(ns, name) }) 158 | } 159 | 160 | addEventListener (type, handler) { 161 | (this.__handlers[toLower(type)] || (this.__handlers[toLower(type)] = [])).push(handler.bind(this)) 162 | } 163 | removeEventListener (type, handler) { 164 | splice(this.__handlers[toLower(type)], handler, false, true) 165 | } 166 | dispatchEvent (event) { 167 | let t = event.target = this 168 | let l 169 | let i 170 | do { 171 | event.currentTarget = t 172 | l = t.__handlers && t.__handlers[toLower(event.type)] 173 | if (l) { 174 | for (i = l.length; i--;) { 175 | if ((l[i].call(t, event) === false || event._end) && event.cancelable) { 176 | event.defaultPrevented = true 177 | } 178 | } 179 | } 180 | } while (event.bubbles && !(event.cancelable && event._stop) && (t = t.parentNode)) 181 | return l != null 182 | } 183 | } 184 | 185 | class Document extends Element { 186 | constructor () { 187 | super(9, '#document') // DOCUMENT_NODE 188 | } 189 | 190 | createElement (type) { 191 | return new Element(null, String(type).toUpperCase()) 192 | } 193 | 194 | createElementNS (ns, type) { 195 | let element = this.createElement(type) 196 | element.namespace = ns 197 | return element 198 | } 199 | 200 | createTextNode (text) { 201 | return new Text(text) 202 | } 203 | } 204 | 205 | class Event { 206 | constructor (type, opts) { 207 | this.type = type 208 | this.bubbles = !!(opts && opts.bubbles) 209 | this.cancelable = !!(opts && opts.cancelable) 210 | } 211 | stopPropagation () { 212 | this._stop = true 213 | } 214 | stopImmediatePropagation () { 215 | this._end = this._stop = true 216 | } 217 | preventDefault () { 218 | this.defaultPrevented = true 219 | } 220 | } 221 | 222 | class MutationObserver { 223 | constructor (callback) { 224 | this.callback = callback 225 | this._records = [] 226 | } 227 | 228 | observe (target, options) { 229 | this.disconnect() 230 | this._target = target 231 | this._options = options || {} 232 | observers.push(this) 233 | } 234 | 235 | disconnect () { 236 | this._target = null 237 | splice(observers, this) 238 | } 239 | 240 | takeRecords () { 241 | return this._records.splice(0, this._records.length) 242 | } 243 | } 244 | 245 | function mutation (target, type, record) { 246 | record.target = target 247 | record.type = type 248 | 249 | for (let x = observers.length; x--;) { 250 | const ob = observers[x] 251 | let match = target === ob._target 252 | 253 | if (!match && ob._options.subtree) { 254 | do { 255 | if ((match = target === ob._target)) break 256 | } while ((target = target.parentNode)) 257 | } 258 | if (match) { 259 | ob._records.push(record) 260 | if (!pendingMutations) { 261 | pendingMutations = true 262 | setTimeout(flushMutations, 0) 263 | } 264 | } 265 | } 266 | } 267 | 268 | function flushMutations () { 269 | pendingMutations = false 270 | for (let x = observers.length; x--;) { 271 | const ob = observers[x] 272 | if (ob._records.length) { 273 | try { 274 | ob.callback(ob.takeRecords()) 275 | } catch (err) { 276 | console.error(err) 277 | } 278 | } 279 | } 280 | } 281 | 282 | /** 283 | * Create a minimally viable DOM Document 284 | * 285 | * @returns {Document} document 286 | */ 287 | function createDocument () { 288 | let document = new Document() 289 | assign(document, document.defaultView = { 290 | document, 291 | Document, 292 | Node, 293 | Text, 294 | Element, 295 | SVGElement: Element, 296 | Event, 297 | MutationObserver 298 | }) 299 | document.appendChild( 300 | document.documentElement = document.createElement('html') 301 | ) 302 | document.documentElement.appendChild( 303 | document.head = document.createElement('head') 304 | ) 305 | document.documentElement.appendChild( 306 | document.body = document.createElement('body') 307 | ) 308 | return document 309 | } 310 | 311 | createDocument.env = createEnvironment 312 | return createDocument 313 | } 314 | 315 | module.exports = createEnvironment() 316 | -------------------------------------------------------------------------------- /src/unwindow.js: -------------------------------------------------------------------------------- 1 | const window = {} 2 | let lastTime = 0 3 | 4 | window.setTimeout = global.setTimeout 5 | window.clearTimeout = global.clearTimeout 6 | 7 | window.requestAnimationFrame = function (fn, element) { 8 | const currTime = new Date().getTime() 9 | const timeToCall = Math.max(0, 16 - (currTime - lastTime)) 10 | const id = window.setTimeout(function () { 11 | fn(currTime + timeToCall) 12 | }, timeToCall) 13 | lastTime = currTime + timeToCall 14 | return id 15 | } 16 | 17 | window.cancelAnimationFrame = function (id) { 18 | clearTimeout(id) 19 | } 20 | 21 | global.requestAnimationFrame = window.requestAnimationFrame 22 | global.cancelAnimationFrame = window.cancelAnimationFrame 23 | global.window = window 24 | --------------------------------------------------------------------------------