├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json └── src ├── namespaces.js ├── serializer.js ├── undom-ng.js ├── undom.js └── utils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,.*rc,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true 7 | }, 8 | "globals": { 9 | "process": true, 10 | "globalThis": true, 11 | }, 12 | "extends": "eslint:recommended", 13 | "parserOptions": { 14 | "ecmaVersion": 2022, 15 | "sourceType": "module" 16 | }, 17 | "rules": { 18 | "accessor-pairs": "error", 19 | "array-bracket-spacing": [ 20 | "error", 21 | "never" 22 | ], 23 | "array-callback-return": "error", 24 | "arrow-body-style": "error", 25 | "arrow-parens": [ 26 | "off", 27 | "as-needed", 28 | { 29 | "requireForBlockBody": true 30 | } 31 | ], 32 | "arrow-spacing": [ 33 | "error", 34 | { 35 | "after": true, 36 | "before": true 37 | } 38 | ], 39 | "block-scoped-var": "error", 40 | "block-spacing": "error", 41 | "brace-style": [ 42 | "error", 43 | "1tbs" 44 | ], 45 | "callback-return": "error", 46 | "camelcase": "warn", 47 | "class-methods-use-this": "error", 48 | "comma-dangle": [ 49 | "error", 50 | "only-multiline" 51 | ], 52 | "comma-spacing": "off", 53 | "comma-style": [ 54 | "error", 55 | "last" 56 | ], 57 | "complexity": "error", 58 | "computed-property-spacing": [ 59 | "error", 60 | "never" 61 | ], 62 | "consistent-return": "off", 63 | "consistent-this": "error", 64 | "curly": "off", 65 | "default-case": "error", 66 | "dot-location": "off", 67 | "dot-notation": "error", 68 | "eol-last": "off", 69 | "eqeqeq": "error", 70 | "func-call-spacing": "error", 71 | "func-names": [ 72 | "error", 73 | "never" 74 | ], 75 | "func-style": [ 76 | "off", 77 | "expression" 78 | ], 79 | "generator-star-spacing": "error", 80 | "global-require": "error", 81 | "guard-for-in": "off", 82 | "handle-callback-err": "error", 83 | "id-blacklist": "error", 84 | "id-length": "off", 85 | "id-match": "error", 86 | "indent": "off", 87 | "init-declarations": "error", 88 | "jsx-quotes": "error", 89 | "key-spacing": "error", 90 | "keyword-spacing": [ 91 | "error", 92 | { 93 | "after": true, 94 | "before": true 95 | } 96 | ], 97 | "line-comment-position": "error", 98 | "linebreak-style": [ 99 | "off" 100 | ], 101 | "lines-around-comment": "error", 102 | "lines-around-directive": "off", 103 | "max-depth": "error", 104 | "max-len": "off", 105 | "max-lines": "off", 106 | "max-nested-callbacks": "error", 107 | "max-params": "error", 108 | "max-statements": "off", 109 | "max-statements-per-line": "error", 110 | "multiline-ternary": "off", 111 | "new-parens": "error", 112 | "newline-after-var": "off", 113 | "newline-before-return": "off", 114 | "newline-per-chained-call": "error", 115 | "no-alert": "error", 116 | "no-array-constructor": "error", 117 | "no-bitwise": "error", 118 | "no-caller": "error", 119 | "no-catch-shadow": "error", 120 | "no-confusing-arrow": "error", 121 | "no-console": "off", 122 | "no-continue": "error", 123 | "no-div-regex": "error", 124 | "no-duplicate-imports": "error", 125 | "no-else-return": "off", 126 | "no-empty-function": "error", 127 | "no-eq-null": "error", 128 | "no-eval": "error", 129 | "no-extend-native": "error", 130 | "no-extra-bind": "error", 131 | "no-extra-label": "error", 132 | "no-extra-parens": "off", 133 | "no-floating-decimal": "error", 134 | "no-global-assign": "error", 135 | "no-implicit-globals": "error", 136 | "no-implied-eval": "error", 137 | "no-inline-comments": "error", 138 | "no-invalid-this": "off", 139 | "no-iterator": "error", 140 | "no-label-var": "error", 141 | "no-labels": "error", 142 | "no-lone-blocks": "error", 143 | "no-lonely-if": "error", 144 | "no-loop-func": "error", 145 | "no-magic-numbers": "off", 146 | "no-mixed-operators": "off", 147 | "no-mixed-requires": "error", 148 | "no-multi-spaces": "error", 149 | "no-multi-str": "error", 150 | "no-multiple-empty-lines": "error", 151 | "no-negated-condition": "error", 152 | "no-nested-ternary": "error", 153 | "no-new": "error", 154 | "no-new-func": "error", 155 | "no-new-object": "error", 156 | "no-new-require": "error", 157 | "no-new-wrappers": "error", 158 | "no-octal-escape": "error", 159 | "no-param-reassign": "off", 160 | "no-path-concat": "error", 161 | "no-plusplus": [ 162 | "error", 163 | { 164 | "allowForLoopAfterthoughts": true 165 | } 166 | ], 167 | "no-process-env": "off", 168 | "no-process-exit": "error", 169 | "no-proto": "error", 170 | "no-prototype-builtins": "error", 171 | "no-restricted-globals": "error", 172 | "no-restricted-imports": "error", 173 | "no-restricted-modules": "error", 174 | "no-restricted-properties": "error", 175 | "no-restricted-syntax": "error", 176 | "no-return-assign": "error", 177 | "no-script-url": "error", 178 | "no-self-compare": "error", 179 | "no-sequences": "error", 180 | "no-shadow": "off", 181 | "no-shadow-restricted-names": "error", 182 | "no-spaced-func": "error", 183 | "no-sync": "error", 184 | "no-tabs": "off", 185 | "no-template-curly-in-string": "error", 186 | "no-ternary": "off", 187 | "no-throw-literal": "error", 188 | "no-trailing-spaces": "error", 189 | "no-undef-init": "error", 190 | "no-undefined": "off", 191 | "no-underscore-dangle": "off", 192 | "no-unmodified-loop-condition": "error", 193 | "no-unneeded-ternary": "error", 194 | "no-unsafe-negation": "error", 195 | "no-unused-expressions": "error", 196 | "no-use-before-define": "off", 197 | "no-useless-call": "error", 198 | "no-useless-computed-key": "error", 199 | "no-useless-concat": "error", 200 | "no-useless-constructor": "error", 201 | "no-useless-escape": "error", 202 | "no-useless-rename": "error", 203 | "no-var": "error", 204 | "no-void": "error", 205 | "no-warning-comments": "error", 206 | "no-whitespace-before-property": "error", 207 | "no-with": "error", 208 | "object-curly-newline": "off", 209 | "object-curly-spacing": [ 210 | "off", 211 | "never" 212 | ], 213 | "object-property-newline": "off", 214 | "object-shorthand": "off", 215 | "one-var": "off", 216 | "one-var-declaration-per-line": "error", 217 | "operator-assignment": "error", 218 | "operator-linebreak": "error", 219 | "padded-blocks": "off", 220 | "prefer-arrow-callback": "error", 221 | "prefer-const": "off", 222 | "prefer-numeric-literals": "error", 223 | "prefer-reflect": "off", 224 | "prefer-rest-params": "error", 225 | "prefer-spread": "error", 226 | "prefer-template": "error", 227 | "quote-props": "off", 228 | "quotes": "off", 229 | "radix": "error", 230 | "require-jsdoc": "off", 231 | "rest-spread-spacing": [ 232 | "error", 233 | "never" 234 | ], 235 | "semi": [ 236 | "warn", 237 | "never" 238 | ], 239 | "semi-spacing": [ 240 | "error", 241 | { 242 | "after": true, 243 | "before": false 244 | } 245 | ], 246 | "sort-imports": "off", 247 | "sort-keys": "off", 248 | "sort-vars": "off", 249 | "space-before-blocks": "error", 250 | "space-before-function-paren": "off", 251 | "space-in-parens": [ 252 | "error", 253 | "never" 254 | ], 255 | "space-infix-ops": "error", 256 | "space-unary-ops": [ 257 | "error", 258 | { 259 | "nonwords": false, 260 | "words": false 261 | } 262 | ], 263 | "spaced-comment": [ 264 | "error", 265 | "always" 266 | ], 267 | "strict": "off", 268 | "symbol-description": "error", 269 | "template-curly-spacing": [ 270 | "error", 271 | "never" 272 | ], 273 | "unicode-bom": [ 274 | "error", 275 | "never" 276 | ], 277 | "valid-jsdoc": "error", 278 | "vars-on-top": "error", 279 | "wrap-iife": "error", 280 | "wrap-regex": "error", 281 | "yield-star-spacing": "error", 282 | "yoda": [ 283 | "error", 284 | "never" 285 | ], 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /npm-debug.log 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !/src 2 | !/dist 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jason Miller 4 | Copyright (c) 2022 Yukino Song 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # undom-ng 2 | 3 | [![NPM](https://img.shields.io/npm/v/undom-ng.svg?style=flat)](https://www.npmjs.org/package/undom-ng) 4 | 5 | ### **The Next Gen minimally viable DOM Document implementation** 6 | 7 | **NOTE** THIS IS A FORK OF UNDOM WITH SOME HUGE CHANGES THAT MIGHT NOT FIT THE GOAL OF THE [ORIGINAL PROJECT](https://github.com/developit/undom). 8 | 9 | > A bare-bones HTML DOM in a box. If you want the DOM but not a parser, this might be for you. 10 | > 11 | > Works in Node and browsers, plugin ready! 12 | 13 | --- 14 | 15 | 16 | ## Project Goals 17 | 18 | Undom aims to find a sweet spot between size/performance and utility. The goal is to provide the simplest possible implementation of a DOM Document, such that libraries relying on the DOM can run in places where there isn't one available. 19 | 20 | The intent to keep things as simple as possible means undom lacks some DOM features like HTML parsing & serialization, Web Components, etc. These features can be added through additional libraries. 21 | 22 | 23 | --- 24 | 25 | 26 | ## Installation 27 | 28 | Via npm: 29 | 30 | `npm install undom-ng` 31 | 32 | 33 | --- 34 | 35 | ## Derivatives 36 | 37 | [DOMiNATIVE](https://github.com/SudoMaker/DOMiNATIVE) Generic DOM implementation for [NativeScript](https://nativescript.org/) 38 | 39 | 40 | --- 41 | 42 | 43 | ## Usage 44 | 45 | ```js 46 | import { createEnvironment, HTMLNS } from 'undom-ng' 47 | 48 | const { createDocument } = createEnvironment() 49 | 50 | const document = createDocument(HTMLNS, 'html') 51 | 52 | const foo = document.createElementNS(HTMLNS, 'foo') 53 | foo.appendChild(document.createTextNode('Hello, World!')) 54 | document.body.appendChild(foo); 55 | ``` 56 | 57 | --- 58 | 59 | 60 | ## Serialize to HTML 61 | 62 | ```js 63 | import { serialize } from 'undom-ng' 64 | 65 | console.log(serialize(element)) 66 | ``` 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "undom-ng", 3 | "amdName": "undom-ng", 4 | "version": "1.1.2", 5 | "description": "The Next Gen minimally viable DOM Document implementation", 6 | "main": "src/undom-ng.js", 7 | "type": "module", 8 | "scripts": {}, 9 | "keywords": [ 10 | "dom", 11 | "document", 12 | "shim" 13 | ], 14 | "repository": "ClassicOldSong/undom-ng", 15 | "author": "Yukino Song ", 16 | "license": "MIT", 17 | "bugs": "https://github.com/ClassicOldSong/undom-ng/issues", 18 | "homepage": "https://github.com/ClassicOldSong/undom-ng" 19 | } 20 | -------------------------------------------------------------------------------- /src/namespaces.js: -------------------------------------------------------------------------------- 1 | export const HTMLNS = 'http://www.w3.org/1999/xhtml' 2 | export const SVGNS = 'http://www.w3.org/2000/svg' 3 | -------------------------------------------------------------------------------- /src/serializer.js: -------------------------------------------------------------------------------- 1 | const selfClosingTags = { 2 | area: true, 3 | base: true, 4 | br: true, 5 | col: true, 6 | command: true, 7 | embed: true, 8 | hr: true, 9 | img: true, 10 | input: true, 11 | keygen: true, 12 | link: true, 13 | menuitem: true, 14 | meta: true, 15 | param: true, 16 | source: true, 17 | track: true, 18 | wbr: true 19 | } 20 | 21 | // const serializerRegexp = /[&'"<>\u00a0-\u00b6\u00b8-\u00ff\u0152\u0153\u0160\u0161\u0178\u0192\u02c6\u02dc\u0391-\u03a1\u03a3-\u03a9\u03b1-\u03c9\u03d1\u03d2\u03d6\u2002\u2003\u2009\u200c-\u200f\u2013\u2014\u2018-\u201a\u201c-\u201e\u2020-\u2022\u2026\u2030\u2032\u2033\u2039\u203a\u203e\u20ac\u2122\u2190-\u2194\u21b5\u2200\u2202\u2203\u2205\u2207-\u2209\u220b\u220f\u2211\u2212\u2217\u221a\u221d\u221e\u2220\u2227-\u222b\u2234\u223c\u2245\u2248\u2260\u2261\u2264\u2265\u2282-\u2284\u2286\u2287\u2295\u2297\u22a5\u22c5\u2308-\u230b\u25ca\u2660\u2663\u2665\u2666]/g 22 | const serializerRegexp = /[&'"<>]/g 23 | 24 | const enc = s => `${s}`.replace(serializerRegexp, a => `&#${a.codePointAt(0)};`) 25 | 26 | const attr = (a) => { 27 | if (a.value !== '') { 28 | if (a.ns) { 29 | return ` ${a.ns}:${a.name}="${enc(a.value)}"` 30 | } 31 | return ` ${a.name}="${enc(a.value)}"` 32 | } 33 | return ` ${a.name}` 34 | } 35 | 36 | const serialize = (el, useRawName) => { 37 | switch (el.nodeType) { 38 | case 3: { 39 | if (el.data) { 40 | if (el.parentNode && ['SCRIPT', 'STYLE'].indexOf(el.parentNode.nodeName) > -1) return el.data 41 | return enc(el.data) 42 | } 43 | return '' 44 | } 45 | 46 | case 8: { 47 | if (el.data) return `` 48 | return '' 49 | } 50 | 51 | default: { 52 | if (!el.nodeName) return '' 53 | 54 | const {nodeName, localName, attributes, firstChild} = el 55 | const xmlStringFrags = [] 56 | 57 | let tag = nodeName 58 | 59 | if (useRawName) tag = localName 60 | else tag = nodeName.toLowerCase() 61 | 62 | if (tag && tag[0] === '#') tag = tag.substring(1) 63 | 64 | if (tag) xmlStringFrags.push(`<${tag}`) 65 | if (attributes) xmlStringFrags.push(...attributes.map(attr)) 66 | if (firstChild) { 67 | if (tag) xmlStringFrags.push('>') 68 | 69 | let currentNode = firstChild 70 | while (currentNode) { 71 | xmlStringFrags.push(serialize(currentNode, useRawName)) 72 | currentNode = currentNode.nextSibling 73 | } 74 | 75 | if (tag) xmlStringFrags.push(``) 76 | } else if (tag) { 77 | if (selfClosingTags[tag]) xmlStringFrags.push('/>') 78 | else xmlStringFrags.push(`>`) 79 | } 80 | 81 | return ''.concat(...xmlStringFrags) 82 | } 83 | } 84 | } 85 | 86 | export default serialize 87 | -------------------------------------------------------------------------------- /src/undom-ng.js: -------------------------------------------------------------------------------- 1 | export * from './undom.js' 2 | export * from './namespaces.js' 3 | export { default as serialize } from './serializer.js' 4 | -------------------------------------------------------------------------------- /src/undom.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | import { 4 | splice, 5 | findWhere, 6 | createAttributeFilter, 7 | named 8 | } from './utils.js' 9 | import serialize from './serializer.js' 10 | 11 | import { HTMLNS, SVGNS } from './namespaces.js' 12 | 13 | /* 14 | const NODE_TYPES = { 15 | ELEMENT_NODE: 1, 16 | ATTRIBUTE_NODE: 2, 17 | TEXT_NODE: 3, 18 | CDATA_SECTION_NODE: 4, 19 | ENTITY_REFERENCE_NODE: 5, 20 | PROCESSING_INSTRUCTION_NODE: 7, 21 | COMMENT_NODE: 8, 22 | DOCUMENT_NODE: 9, 23 | DOCUMENT_FRAGMENT_NODE: 11 24 | } 25 | */ 26 | 27 | class Event { 28 | constructor(type, {bubbles, captures, cancelable} = {}) { 29 | this.initEvent(type, bubbles, cancelable, captures) 30 | } 31 | // eslint-disable-next-line max-params 32 | initEvent(type, bubbles, cancelable = true, captures) { 33 | this.type = type 34 | this.bubbles = !!bubbles 35 | this.cancelable = !!cancelable 36 | this.captures = !!captures 37 | } 38 | stopPropagation() { 39 | this.__undom_event_stop = true 40 | } 41 | stopImmediatePropagation() { 42 | this.__undom_event_end = this.__undom_event_stop = true 43 | } 44 | preventDefault() { 45 | if (this.__undom_event_passive) { 46 | console.error('[UNDOM-NG] Unable to preventDefault inside passive event listener invocation.') 47 | return 48 | } 49 | 50 | this.defaultPrevented = true 51 | } 52 | } 53 | 54 | const createEvent = (eventName, options) => new Event(eventName, options) 55 | 56 | // eslint-disable-next-line max-params 57 | const getEventDescriptor = (target, type, handler, options) => { 58 | if (typeof option === 'object') { 59 | const { capture, once, passive, signal } = options 60 | return { target, capture, once, passive, signal, handler } 61 | } 62 | 63 | return { target, capture: !!options, type, handler } 64 | } 65 | 66 | 67 | const runEventHandlers = (store, event, cancelable) => { 68 | for (let descriptor of [...store.values()]) { 69 | const { target, handler, removed } = descriptor 70 | if (!removed) { 71 | event.__undom_event_passive = !cancelable || !!descriptor.passive 72 | event.currentTarget = target 73 | handler.call(target, event) 74 | if (event.__undom_event_end) return 75 | } 76 | } 77 | } 78 | 79 | const isElement = node => node && node.__undom_is_Element || false 80 | 81 | const isNode = node => node && node.__undom_is_Node || false 82 | 83 | const setData = (self, data) => { 84 | self.__undom_data = data 85 | } 86 | 87 | const defaultInitDocument = (document) => { 88 | document.head = document.createElement('head') 89 | document.body = document.createElement('body') 90 | 91 | document.documentElement.appendChild(document.head) 92 | document.documentElement.appendChild(document.body) 93 | } 94 | 95 | // eslint-disable-next-line max-params 96 | const updateAttributeNS = (self, ns, name, value) => { 97 | let attr = findWhere(self.attributes, createAttributeFilter(ns, name), false, false) 98 | if (!attr) self.attributes.push(attr = { ns, name }) 99 | attr.value = value 100 | } 101 | 102 | const getOwnerDocumentSetter = (onChangeOwnerDocument) => { 103 | if (onChangeOwnerDocument) return (node, ownerDocument) => { 104 | if (node.__undom_owner_document === ownerDocument) return 105 | if (onChangeOwnerDocument) onChangeOwnerDocument.call(node, ownerDocument, node.ownerDocument) 106 | node.__undom_owner_document = ownerDocument 107 | } 108 | 109 | return (node, ownerDocument) => { 110 | node.__undom_owner_document = ownerDocument 111 | } 112 | } 113 | 114 | const createEnvironment = ({ 115 | silent = true, 116 | commonAncestors = {}, 117 | initDocument = defaultInitDocument, 118 | preserveClassNameOnRegister = false, 119 | onGetData, 120 | onSetData, 121 | onCreateNode, 122 | onInsertBefore, 123 | onRemoveChild, 124 | onSetInnerHTML, 125 | onSetOuterHTML, 126 | onSetTextContent, 127 | onGetTextContent, 128 | onSetAttributeNS, 129 | onGetAttributeNS, 130 | onRemoveAttributeNS, 131 | onChangeOwnerDocument, 132 | onAddEventListener, 133 | onAddedEventListener, 134 | onRemoveEventListener, 135 | onRemovedEventListener 136 | } = {}) => { 137 | 138 | const scope = {} 139 | 140 | const createElement = (ownerDocument, type, ...args) => { 141 | if (scope[type]) return new scope[type](ownerDocument, ...args) 142 | if (!silent) console.warn(`[UNDOM-NG] Element type '${type}' is not registered.`) 143 | return new scope.HTMLElement(ownerDocument, null, type) 144 | } 145 | 146 | const setOwnerDocument = getOwnerDocumentSetter(onChangeOwnerDocument) 147 | 148 | const makeEventTarget = named( 149 | 'EventTarget', 150 | (_ = commonAncestors.EventTarget || Object) => { 151 | class EventTarget extends _ { 152 | constructor(...args) { 153 | super(...args) 154 | 155 | this.__undom_eventHandlers = { 156 | capturePhase: {}, 157 | bubblePhase: {} 158 | } 159 | } 160 | 161 | addEventListener(...args) { 162 | // Method could be called before constructor 163 | if (!this.__undom_eventHandlers) { 164 | return super.addEventListener(...args) 165 | } 166 | 167 | const [type, handler, options] = args 168 | 169 | let skip = false 170 | if (onAddEventListener) { 171 | skip = onAddEventListener.call(this, ...args) 172 | } 173 | 174 | if (!skip) { 175 | const descriptor = getEventDescriptor(this, type, handler, options) 176 | 177 | const phase = descriptor.capture && 'capturePhase' || 'bubblePhase' 178 | 179 | let store = this.__undom_eventHandlers[phase][type] 180 | if (!store) store = this.__undom_eventHandlers[phase][type] = new Map() 181 | else if (store.has(handler)) return 182 | 183 | store.set(handler, descriptor) 184 | 185 | const abortHandler = () => { 186 | if (!descriptor.removed) this.removeEventListener(...args) 187 | } 188 | 189 | descriptor.abortHandler = abortHandler 190 | 191 | if (descriptor.once) { 192 | descriptor.handler = function (...handlerArgs) { 193 | abortHandler() 194 | handler.call(this, ...handlerArgs) 195 | } 196 | } 197 | 198 | if (descriptor.signal) { 199 | descriptor.signal.addEventListener('abort', abortHandler) 200 | } 201 | 202 | if (onAddedEventListener) { 203 | onAddedEventListener.call(this, ...args) 204 | } 205 | } 206 | } 207 | 208 | removeEventListener(...args) { 209 | // Method could be called before constructor 210 | if (!this.__undom_eventHandlers) { 211 | return super.removeEventListener(...args) 212 | } 213 | 214 | const [type, handler, options] = args 215 | 216 | let skip = false 217 | if (onRemoveEventListener) { 218 | skip = onRemoveEventListener.call(this, ...args) 219 | } 220 | 221 | if (!skip) { 222 | let capture = false 223 | if (typeof options === 'object') capture = !!options.capture 224 | else capture = !!options 225 | 226 | const phase = capture && 'capturePhase' || 'bubblePhase' 227 | 228 | const store = this.__undom_eventHandlers[phase][type] 229 | if (!store) return 230 | 231 | const descriptor = store.get(handler) 232 | if (!descriptor) return 233 | 234 | if (descriptor.signal) descriptor.signal.removeEventListener('abort', descriptor.abortHandler) 235 | store.delete(handler) 236 | 237 | descriptor.remove = true 238 | 239 | if (!store.size) delete this.__undom_eventHandlers[phase][type] 240 | 241 | if (onRemovedEventListener) { 242 | onRemovedEventListener.call(this, ...args) 243 | } 244 | } 245 | } 246 | 247 | // eslint-disable-next-line complexity 248 | dispatchEvent(event) { 249 | const { cancelable, bubbles, captures, type } = event 250 | event.target = this 251 | 252 | const capturePhase = [] 253 | const bubblePhase = [] 254 | 255 | if (bubbles || captures) { 256 | // eslint-disable-next-line consistent-this 257 | let currentNode = this 258 | while (currentNode) { 259 | if (captures && currentNode.__undom_eventHandlers.capturePhase[type]) capturePhase.unshift(currentNode.__undom_eventHandlers.capturePhase[type]) 260 | if (bubbles && currentNode.__undom_eventHandlers.bubblePhase[type]) bubblePhase.push(currentNode.__undom_eventHandlers.bubblePhase[type]) 261 | currentNode = currentNode.parentNode 262 | } 263 | } 264 | 265 | if (!captures && this.__undom_eventHandlers.capturePhase[type]) capturePhase.push(this.__undom_eventHandlers.capturePhase[type]) 266 | if (!bubbles && this.__undom_eventHandlers.bubblePhase[type]) bubblePhase.push(this.__undom_eventHandlers.bubblePhase[type]) 267 | 268 | for (let i of capturePhase) { 269 | runEventHandlers(i, event, cancelable) 270 | if (!event.bubbles || event.__undom_event_stop) return !event.defaultPrevented 271 | } 272 | 273 | for (let i of bubblePhase) { 274 | runEventHandlers(i, event, cancelable) 275 | if (!event.bubbles || event.__undom_event_stop) return !event.defaultPrevented 276 | } 277 | 278 | return !event.defaultPrevented 279 | } 280 | } 281 | 282 | return EventTarget 283 | } 284 | ) 285 | 286 | const makeNode = named( 287 | 'Node', 288 | (_ = commonAncestors.Node || scope.EventTarget) => { 289 | class Node extends makeEventTarget(_) { 290 | // eslint-disable-next-line max-params 291 | constructor(ownerDocument = null, nodeType, localName, ...args) { 292 | super(ownerDocument, ...args) 293 | 294 | this.nodeType = nodeType 295 | if (localName) this.nodeName = localName[0] === '#' ? localName : String(localName).toUpperCase() 296 | 297 | this.parentNode = null 298 | this.nextSibling = null 299 | this.previousSibling = null 300 | 301 | this.__undom_owner_document = ownerDocument 302 | 303 | if (onCreateNode) { 304 | onCreateNode.call(this, nodeType, localName) 305 | } 306 | } 307 | 308 | get ownerDocument() { 309 | return this.__undom_owner_document 310 | } 311 | 312 | get previousElementSibling() { 313 | let currentNode = this.previousSibling 314 | while (currentNode) { 315 | if (isElement(currentNode)) return currentNode 316 | currentNode = currentNode.previousSibling 317 | } 318 | 319 | return null 320 | } 321 | get nextElementSibling() { 322 | let currentNode = this.nextSibling 323 | while (currentNode) { 324 | if (isElement(currentNode)) return currentNode 325 | currentNode = currentNode.nextSibling 326 | } 327 | 328 | return null 329 | } 330 | 331 | remove() { 332 | if (this.parentNode) this.parentNode.removeChild(this) 333 | } 334 | 335 | replaceWith(...nodes) { 336 | if (!this.parentNode) return 337 | 338 | const ref = this.nextSibling 339 | const parent = this.parentNode 340 | for (let i of nodes) { 341 | i.remove() 342 | parent.insertBefore(i, ref) 343 | } 344 | } 345 | 346 | cloneNode(deep) { 347 | let clonedNode = null 348 | 349 | if (this.__undom_is_ParentNode) { 350 | if (this.nodeType === 9) clonedNode = new scope.Document(...this.__undom_document_init_args) 351 | else if (this.nodeType === 11) clonedNode = new scope.DocumentFragment(this.ownerDocument) 352 | else { 353 | clonedNode = createElement(this.ownerDocument, this.localName) 354 | const sourceAttrs = this.attributes 355 | for (let {ns, name, value} of sourceAttrs) { 356 | clonedNode.setAttributeNS(ns, name, value) 357 | } 358 | } 359 | 360 | if (deep) { 361 | let currentNode = this.firstChild 362 | while (currentNode) { 363 | clonedNode.appendChild(currentNode.cloneNode(deep)) 364 | currentNode = currentNode.nextSibling 365 | } 366 | } 367 | } else if (this.nodeType === 3) clonedNode = new scope.Text(this.ownerDocument, this.nodeValue) 368 | else if (this.nodeType === 8) clonedNode = new scope.Comment(this.ownerDocument, this.nodeValue) 369 | 370 | return clonedNode 371 | } 372 | 373 | hasChildNodes() { 374 | return !!this.firstChild 375 | } 376 | } 377 | 378 | // Fix buble: https://github.com/bublejs/buble/blob/bac51a9c2793011987d1d17efcda03f70e4b540a/src/program/types/ClassBody.js#L134 379 | Object.defineProperty(Node, Symbol.toStringTag, { 380 | get() { 381 | return this.constructor.name 382 | } 383 | }) 384 | 385 | return Node 386 | } 387 | ) 388 | 389 | 390 | const makeCharacterData = named( 391 | 'CharacterData', 392 | (_ = commonAncestors.CharacterData || scope.Node) => class CharacterData extends makeNode(_) { 393 | get data() { 394 | if (onGetData) onGetData.call(this, data => setData(this, data)) 395 | 396 | return `${this.__undom_data}` 397 | } 398 | set data(data) { 399 | setData(this, data) 400 | 401 | if (onSetData) onSetData.call(this, data) 402 | } 403 | get length() { 404 | return this.data.length 405 | } 406 | 407 | get nodeValue() { 408 | return this.data 409 | } 410 | set nodeValue(data) { 411 | this.data = data 412 | } 413 | 414 | get textContent() { 415 | return this.data 416 | } 417 | set textContent(text) { 418 | this.data = text 419 | } 420 | 421 | appendData(data) { 422 | this.data += data 423 | } 424 | } 425 | ) 426 | 427 | 428 | const makeComment = named( 429 | 'Comment', 430 | (_ = commonAncestors.Comment || scope.CharacterData) => class Comment extends makeCharacterData(_) { 431 | constructor(ownerDocument, data, ...args) { 432 | super(ownerDocument, 8, '#comment', ...args) 433 | this.data = data 434 | } 435 | } 436 | ) 437 | 438 | 439 | const makeText = named( 440 | 'Text', 441 | (_ = commonAncestors.Text || scope.CharacterData) => class Text extends makeCharacterData(_) { 442 | constructor(ownerDocument, text, ...args) { 443 | super(ownerDocument, 3, '#text', ...args) 444 | this.data = text 445 | } 446 | } 447 | ) 448 | 449 | const clearChildren = (self) => { 450 | if (!self.hasChildNodes()) return 451 | 452 | let currentNode = self.firstChild 453 | 454 | while (currentNode) { 455 | const nextSibling = currentNode.nextSibling 456 | currentNode.remove() 457 | currentNode = nextSibling 458 | } 459 | } 460 | 461 | 462 | const makeParentNode = named( 463 | 'ParentNode', 464 | (_ = scope.Node) => class ParentNode extends makeNode(_) { 465 | 466 | get firstElementChild() { 467 | const currentNode = this.firstChild 468 | if (!currentNode) return null 469 | if (isElement(currentNode)) return currentNode 470 | return currentNode.nextElementSibling 471 | } 472 | get lastElementChild() { 473 | const currentNode = this.lastChild 474 | if (!currentNode) return null 475 | if (isElement(currentNode)) return currentNode 476 | return currentNode.previousElementSibling 477 | } 478 | 479 | get childNodes() { 480 | const childNodes = [] 481 | 482 | let currentNode = this.firstChild 483 | while (currentNode) { 484 | childNodes.push(currentNode) 485 | currentNode = currentNode.nextSibling 486 | } 487 | 488 | return childNodes 489 | } 490 | get children() { 491 | const children = [] 492 | 493 | let currentNode = this.firstElementChild 494 | while (currentNode) { 495 | children.push(currentNode) 496 | currentNode = currentNode.nextElementSibling 497 | } 498 | 499 | return children 500 | } 501 | get childElementCount() { 502 | let count = 0 503 | 504 | let currentNode = this.firstElementChild 505 | while (currentNode) { 506 | count += 1 507 | currentNode = currentNode.nextElementSibling 508 | } 509 | 510 | return count 511 | } 512 | 513 | get textContent() { 514 | if (onGetTextContent) { 515 | const textContent = onGetTextContent.call(this) 516 | if (textContent) return textContent 517 | } 518 | 519 | const textArr = [] 520 | 521 | let currentNode = this.firstChild 522 | while (currentNode) { 523 | if (currentNode.nodeType !== 8) { 524 | const textContent = currentNode.textContent 525 | if (textContent) textArr.push(textContent) 526 | } 527 | currentNode = currentNode.nextSibling 528 | } 529 | 530 | return ''.concat(...textArr) 531 | } 532 | set textContent(val) { 533 | clearChildren(this) 534 | 535 | if (onSetTextContent) { 536 | const skipDefault = onSetTextContent.call(this, val) 537 | if (skipDefault) return 538 | } 539 | 540 | if (val !== '') this.appendChild(new scope.Text(this.onwnerDocument, val)) 541 | } 542 | 543 | // eslint-disable-next-line complexity 544 | insertBefore(child, ref) { 545 | if (!child.__undom_is_Node) { 546 | if (onInsertBefore) onInsertBefore.call(this, child, ref) 547 | return 548 | } 549 | 550 | if (ref && ref.parentNode !== this) throw new Error(`[UNDOM-NG] Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node.`) 551 | if (child === ref) return 552 | 553 | let ownerDocument = this.ownerDocument 554 | // eslint-disable-next-line consistent-this 555 | if (this.nodeType === 9) ownerDocument = this 556 | 557 | if (child.nodeType === 11) { 558 | const {firstChild, lastChild} = child 559 | 560 | if (firstChild && lastChild) { 561 | const insertedChildList = [] 562 | let currentNode = firstChild 563 | while (currentNode) { 564 | const nextSibling = currentNode.nextSibling 565 | 566 | currentNode.parentNode = this 567 | if (onRemoveChild) onRemoveChild.call(child, currentNode) 568 | if (onInsertBefore) insertedChildList.push(currentNode) 569 | setOwnerDocument(currentNode, ownerDocument) 570 | 571 | currentNode = nextSibling 572 | } 573 | 574 | if (ref) { 575 | firstChild.previousSibling = ref.previousSibling 576 | lastChild.nextSibling = ref 577 | ref.previousSibling = lastChild 578 | } else { 579 | firstChild.previousSibling = this.lastChild 580 | lastChild.nextSibling = null 581 | } 582 | 583 | if (firstChild.previousSibling) firstChild.previousSibling.nextSibling = firstChild 584 | else this.firstChild = firstChild 585 | 586 | if (lastChild.nextSibling) lastChild.nextSibling.previousSibling = lastChild 587 | else this.lastChild = lastChild 588 | 589 | child.firstChild = null 590 | child.lastChild = null 591 | 592 | if (insertedChildList.length) { 593 | for (let currentNode of insertedChildList) { 594 | onInsertBefore.call(this, currentNode, ref) 595 | } 596 | } 597 | } 598 | } else { 599 | child.remove() 600 | child.parentNode = this 601 | 602 | if (ref) { 603 | child.previousSibling = ref.previousSibling 604 | child.nextSibling = ref 605 | ref.previousSibling = child 606 | } else { 607 | child.previousSibling = this.lastChild 608 | this.lastChild = child 609 | } 610 | 611 | if (child.previousSibling) child.previousSibling.nextSibling = child 612 | else this.firstChild = child 613 | } 614 | 615 | setOwnerDocument(child, ownerDocument) 616 | 617 | if (onInsertBefore) onInsertBefore.call(this, child, ref) 618 | 619 | return child 620 | } 621 | 622 | appendChild(child) { 623 | return this.insertBefore(child) 624 | } 625 | 626 | append(...children) { 627 | for (let i of children) { 628 | this.appendChild(i) 629 | } 630 | } 631 | 632 | replaceChild(child, oldChild) { 633 | if (oldChild.parentNode !== this) throw new Error(`[UNDOM-NG] Failed to execute 'replaceChild' on 'Node': The node to be replaced is not a child of this node.`) 634 | 635 | const ref = oldChild.nextSibling 636 | oldChild.remove() 637 | 638 | this.insertBefore(child, ref) 639 | 640 | return oldChild 641 | } 642 | 643 | removeChild(child) { 644 | if (!child.__undom_is_Node || child.parentNode !== this) { 645 | if (onRemoveChild) onRemoveChild.call(this, child) 646 | return 647 | } 648 | 649 | if (this.firstChild === child) this.firstChild = child.nextSibling 650 | if (this.lastChild === child) this.lastChild = child.previousSibling 651 | 652 | if (child.previousSibling) child.previousSibling.nextSibling = child.nextSibling 653 | if (child.nextSibling) child.nextSibling.previousSibling = child.previousSibling 654 | 655 | child.parentNode = null 656 | child.previousSibling = null 657 | child.nextSibling = null 658 | 659 | if (onRemoveChild) onRemoveChild.call(this, child) 660 | 661 | return child 662 | } 663 | } 664 | ) 665 | 666 | 667 | const makeDocumentFragment = named( 668 | 'DocumentFragment', 669 | (_ = scope.ParentNode) => class DocumentFragment extends makeParentNode(_) { 670 | constructor(ownerDocument, ...args) { 671 | super(ownerDocument, 11, '#document-fragment', ...args) 672 | } 673 | } 674 | ) 675 | 676 | 677 | const makeElement = named( 678 | 'Element', 679 | (_ = commonAncestors.Element || scope.ParentNode, name) => { 680 | const protoHasInnerHTML = 'innerHTML' in _.prototype 681 | const protoHasOuterHTML = 'outerHTML' in _.prototype 682 | 683 | class Element extends makeParentNode(_) { 684 | // eslint-disable-next-line max-params 685 | constructor(ownerDocument, nodeType, localName, ...args) { 686 | super(ownerDocument, nodeType || 1, localName || name, ...args) 687 | this.localName = localName || name 688 | this.attributes = [] 689 | if (!this.style) this.style = {} 690 | this.__undom_namespace = null 691 | } 692 | 693 | get tagName() { 694 | return this.nodeName 695 | } 696 | 697 | get namespaceURI() { 698 | return this.__undom_namespace 699 | } 700 | 701 | get className() { 702 | return this.getAttribute('class') 703 | } 704 | set className(val) { 705 | this.setAttribute('class', val) 706 | } 707 | 708 | // Actually innerHTML and outerHTML is out of DOM's spec 709 | // But we just put it here for some frameworks to work 710 | // Or warn people not trying to treat undom like a browser 711 | get innerHTML() { 712 | if (protoHasInnerHTML) return super.innerHTML 713 | 714 | const serializedChildren = [] 715 | let currentNode = this.firstChild 716 | while (currentNode) { 717 | serializedChildren.push(serialize(currentNode, true)) 718 | currentNode = currentNode.nextSibling 719 | } 720 | return ''.concat(...serializedChildren) 721 | } 722 | set innerHTML(value) { 723 | if (protoHasInnerHTML) { 724 | super.innerHTML = value 725 | return 726 | } 727 | 728 | // Setting innerHTML with an empty string just clears the element's children 729 | if (value === '') { 730 | clearChildren(this) 731 | return 732 | } 733 | 734 | if (onSetInnerHTML) { 735 | onSetInnerHTML(this, value) 736 | return 737 | } 738 | 739 | throw new Error(`[UNDOM-NG] Failed to set 'innerHTML' on '${this.localName}': Not implemented.`) 740 | } 741 | 742 | get outerHTML() { 743 | if (protoHasOuterHTML) return super.outerHTML 744 | 745 | return serialize(this, true) 746 | } 747 | set outerHTML(value) { 748 | if (protoHasOuterHTML) { 749 | super.outerHTML = value 750 | return 751 | } 752 | 753 | // Setting outehHTMO with an empty string just removes the element form it's parent 754 | if (value === '') { 755 | this.remove() 756 | return 757 | } 758 | if (onSetOuterHTML) { 759 | onSetOuterHTML.call(this, value) 760 | return 761 | } 762 | throw new Error(`[UNDOM-NG] Failed to set 'outerHTML' on '${this.localName}': Not implemented.`) 763 | } 764 | 765 | get cssText() { 766 | return this.getAttribute('style') 767 | } 768 | set cssText(val) { 769 | this.setAttribute('style', val) 770 | } 771 | 772 | setAttribute(key, value) { 773 | this.setAttributeNS(null, key, value) 774 | } 775 | getAttribute(key) { 776 | return this.getAttributeNS(null, key) 777 | } 778 | removeAttribute(key) { 779 | this.removeAttributeNS(null, key) 780 | } 781 | 782 | setAttributeNS(ns, name, value) { 783 | updateAttributeNS(this, ns, name, value) 784 | 785 | if (onSetAttributeNS) { 786 | onSetAttributeNS.call(this, ns, name, value) 787 | } 788 | } 789 | getAttributeNS(ns, name) { 790 | if (onGetAttributeNS) { 791 | onGetAttributeNS.call(this, ns, name, value => updateAttributeNS(this, ns, name, value)) 792 | } 793 | 794 | let attr = findWhere(this.attributes, createAttributeFilter(ns, name), false, false) 795 | return attr && attr.value 796 | } 797 | removeAttributeNS(ns, name) { 798 | splice(this.attributes, createAttributeFilter(ns, name), false, false) 799 | 800 | if (onRemoveAttributeNS) { 801 | onRemoveAttributeNS.call(this, ns, name) 802 | } 803 | } 804 | } 805 | 806 | return Element 807 | } 808 | ) 809 | 810 | const makeHTMLElement = named( 811 | 'HTMLElement', 812 | (_ = commonAncestors.HTMLElement || scope.Element, name) => class HTMLElement extends makeElement(_, name) { 813 | constructor(...args) { 814 | super(...args) 815 | this.__undom_namespace = HTMLNS 816 | } 817 | } 818 | ) 819 | 820 | 821 | const makeSVGElement = named( 822 | 'SVGElement', 823 | (_ = commonAncestors.SVGElement || scope.Element, name) => class SVGElement extends makeElement(_, name) { 824 | constructor(...args) { 825 | super(...args) 826 | this.__undom_namespace = SVGNS 827 | } 828 | } 829 | ) 830 | 831 | 832 | const createDocument = (namespaceURI, qualifiedNameStr, ...args) => { 833 | const document = new scope.Document(...args) 834 | 835 | document.__undom_namespace = namespaceURI 836 | 837 | const documentElement = createElement(document, qualifiedNameStr) 838 | document.appendChild(documentElement) 839 | 840 | if (initDocument) initDocument(document) 841 | 842 | return document 843 | } 844 | 845 | const DOMImplementation = class DOMImplementation { 846 | createDocument 847 | } 848 | 849 | const makeDocument = named( 850 | 'Document', 851 | (_ = commonAncestors.Document || scope.ParentNode) => class Document extends makeParentNode(_) { 852 | constructor(...args) { 853 | super(null, 9, '#document', ...args) 854 | this.__undom_implementation = new DOMImplementation() 855 | this.__undom_document_init_args = args 856 | } 857 | 858 | insertBefore(child, ref) { 859 | if (this.firstChild || 860 | (isNode(child) && 861 | child.nodeType === 11 && 862 | (child.firstChild !== child.lastChild) 863 | ) 864 | ) throw new Error('[UNDOM-NG] Only one element on document allowed.') 865 | super.insertBefore(child, ref) 866 | } 867 | 868 | createDocumentFragment() { 869 | return new scope.DocumentFragment(this) 870 | } 871 | 872 | createElement(type, ...args) { 873 | return createElement(this, type, ...args) 874 | } 875 | 876 | createElementNS(ns, type, ...args) { 877 | const element = this.createElement(type, ...args) 878 | element.__undom_namespace = ns 879 | return element 880 | } 881 | 882 | createComment(data) { 883 | return new scope.Comment(this, data) 884 | } 885 | 886 | createTextNode(text) { 887 | return new scope.Text(this, text) 888 | } 889 | 890 | // eslint-disable-next-line class-methods-use-this 891 | createEvent(type, options) { 892 | return createEvent(type, options) 893 | } 894 | 895 | get documentElement() { 896 | return this.firstChild 897 | } 898 | 899 | // eslint-disable-next-line class-methods-use-this 900 | get defaultView() { 901 | return scope 902 | } 903 | 904 | get implementation() { 905 | return this.__undom_implementation 906 | } 907 | } 908 | ) 909 | 910 | scope.Event = Event 911 | scope.EventTarget = makeEventTarget.master() 912 | scope.Node = makeNode.master() 913 | scope.CharacterData = makeCharacterData.master() 914 | scope.Text = makeText.master() 915 | scope.Comment = makeComment.master() 916 | scope.ParentNode = makeParentNode.master() 917 | scope.DocumentFragment = makeDocumentFragment.master() 918 | scope.Element = makeElement.master() 919 | scope.HTMLElement = makeHTMLElement.master() 920 | scope.SVGElement = makeSVGElement.master() 921 | scope.Document = makeDocument.master() 922 | 923 | // eslint-disable-next-line max-params 924 | const registerElement = (name, value, isSVG, isHTML) => { 925 | if (scope[name]) throw new Error(`[UNDOM-NG] Element type '${name}' has already been registered.`) 926 | // eslint-disable-next-line no-nested-ternary 927 | const element = isSVG ? makeSVGElement(value, name) : isHTML ? makeHTMLElement(value, name) : makeElement(value, name) 928 | scope[name] = element 929 | if (preserveClassNameOnRegister) Object.defineProperty(element, 'name', { value: name }) 930 | 931 | return element 932 | } 933 | 934 | return {scope, createEvent, createDocument, makeEventTarget, makeNode, makeParentNode, makeText, makeComment, makeDocumentFragment, makeElement, makeHTMLElement, makeSVGElement, makeDocument, registerElement, DOMImplementation} 935 | } 936 | 937 | export {createEnvironment, createEvent, Event, isElement, isNode} 938 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const toLower = str => String(str).toLowerCase() 2 | 3 | // eslint-disable-next-line max-params 4 | export const findWhere = (arr, fn, returnIndex, byValue) => { 5 | let i = arr.length 6 | while (i) { 7 | i -= 1 8 | const val = arr[i] 9 | if (byValue) { 10 | if (val === fn) return returnIndex ? i : val 11 | } else if (fn(val)) return returnIndex ? i : val 12 | } 13 | } 14 | 15 | // eslint-disable-next-line max-params 16 | export const splice = (arr, item, add, byValue) => { 17 | let i = arr ? findWhere(arr, item, true, byValue) : -1 18 | if (i > -1) { 19 | if (add) arr.splice(i, 0, add) 20 | else arr.splice(i, 1) 21 | } 22 | return i 23 | } 24 | 25 | export const createAttributeFilter = (ns, name) => o => o.ns === ns && toLower(o.name) === toLower(name) 26 | 27 | export const named = (key, extender) => { 28 | key = `__undom_is_${key}` 29 | 30 | const maker = (_, ...args) => { 31 | if (_ && _.prototype[key]) return _ 32 | const extendedClass = extender(_, ...args) 33 | Object.defineProperty(extendedClass.prototype, key, { 34 | enumerable: false, 35 | value: true 36 | }) 37 | return extendedClass 38 | } 39 | 40 | maker.master = (...args) => { 41 | const extendedClass = maker(...args) 42 | 43 | Object.defineProperty(extendedClass, Symbol.hasInstance, { 44 | value(instance) { 45 | return instance && instance[key] 46 | } 47 | }) 48 | 49 | return extendedClass 50 | } 51 | 52 | return maker 53 | } 54 | --------------------------------------------------------------------------------