├── .npmignore ├── src ├── patchVNode │ ├── index.ts │ ├── updateElement.ts │ ├── replacePreviousElement.ts │ ├── hooks.ts │ ├── patchVNodeChildren.ts │ ├── patchVNode.ts │ └── updateElement.test.ts ├── hyperscript │ ├── hasCssSelector │ │ ├── index.ts │ │ ├── matchBasicCssSelector.ts │ │ ├── matchesSelector.ts │ │ ├── matchAttribute.ts │ │ ├── matchPsuedoSelector.ts │ │ ├── hasCssSelector.ts │ │ └── hasCssSelector.test.ts │ ├── index.ts │ ├── parseSelector.ts │ ├── querySelector.ts │ ├── querySelectorAll.ts │ ├── h.test.tsx │ ├── parseSelector.test.ts │ ├── querySelectorAll.test.ts │ ├── VNode.ts │ ├── h.ts │ ├── h.test.ts │ ├── svg-helpers.ts │ └── hyperscript-helpers.ts ├── dom-scope-attribute.ts ├── index.ts ├── types │ ├── index.ts │ ├── VNodeEventTypes.ts │ ├── hooks.ts │ ├── VirtualNode.ts │ ├── HtmlTagNames.ts │ └── HtmlProperties.ts ├── modules │ ├── index.ts │ ├── emptyVNode.ts │ ├── BaseModule.ts │ ├── focus.ts │ ├── class.ts │ ├── props.test.ts │ ├── events.ts │ ├── props.ts │ ├── attributes.ts │ ├── ModuleCallbacks.ts │ └── styles.ts ├── helpers.ts ├── addVNodes.ts ├── elementToVNode.ts ├── removeVNodes.ts ├── init.ts ├── createElement.ts ├── createElement.test.ts ├── updateChildren.ts └── patch.test.ts ├── .vscode └── settings.json ├── .travis.yml ├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── CONTRIBUTING.md ├── .config ├── tsconfig.commonjs.json └── tsconfig.es2015.json ├── tsconfig.json ├── tslint.json ├── .gitignore ├── README.md ├── karma.conf.js ├── LICENSE.md ├── package.json └── CHANGELOG.md /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/patchVNode/index.ts: -------------------------------------------------------------------------------- 1 | export * from './patchVNode' 2 | -------------------------------------------------------------------------------- /src/hyperscript/hasCssSelector/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hasCssSelector' 2 | -------------------------------------------------------------------------------- /src/dom-scope-attribute.ts: -------------------------------------------------------------------------------- 1 | export const SCOPE_ATTRIBUTE = 'data-mostly-dom-scope' 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | 4 | "editor.insertSpaces": true, 5 | 6 | "typescript.tsdk": "node_modules/typescript/lib" 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './hyperscript' 3 | export * from './init' 4 | export * from './elementToVNode' 5 | export * from './modules' 6 | export * from './dom-scope-attribute' 7 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CSS' 2 | export * from './HtmlProperties' 3 | export * from './HtmlTagNames' 4 | export * from './VirtualNode' 5 | export * from './hooks' 6 | export * from './VNodeEventTypes' 7 | -------------------------------------------------------------------------------- /src/hyperscript/index.ts: -------------------------------------------------------------------------------- 1 | export * from './VNode' 2 | export * from './h' 3 | export * from './hyperscript-helpers' 4 | export * from './svg-helpers' 5 | export * from './hasCssSelector' 6 | export * from './parseSelector' 7 | export * from './querySelectorAll' 8 | export * from './querySelector' 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | dist: trusty 4 | sudo: false 5 | 6 | addons: 7 | firefox: latest 8 | 9 | node_js: 10 | - 7 11 | 12 | cache: yarn 13 | 14 | before_install: 15 | - export CHROME_BIN=chromium-browser 16 | - export DISPLAY=:99.0 17 | - sh -e /etc/init.d/xvfb start 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | insert_final_newline = false 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | - [ ] I added new tests for the issue I fixed or the feature I built 7 | - [ ] I ran `npm test` for the package I'm modifying 8 | - [ ] I used `npm run commit` instead of `git commit` -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { BaseModule } from './BaseModule' 2 | export { createAttributesModule } from './attributes' 3 | export { createClassModule } from './class' 4 | export { createEventsModule } from './events' 5 | export { createFocusModule } from './focus' 6 | export { createPropsModule } from './props' 7 | export { createStylesModule } from './styles' 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, thank you so much, we need your help. 4 | 5 | ## Contributing a fix or feature 6 | 7 | 1. Fork the repository 8 | 2. Switch to a new branch `git checkout -b [branchName]` 9 | 3. Produce your fix or feature 10 | 4. Use `npm run commit` instead of `git commit` PLEASE! 11 | 5. Submit a pull request for review 12 | -------------------------------------------------------------------------------- /src/modules/emptyVNode.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, MostlyVNode, VNodeProps } from '../' 2 | 3 | export const emptyVNode: ElementVNode> = new MostlyVNode( 4 | undefined, 5 | {}, 6 | undefined, 7 | undefined, 8 | undefined, 9 | undefined, 10 | undefined, 11 | undefined 12 | ) as ElementVNode> 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | **Code to reproduce the issue:** 8 | 9 | 10 | **Expected behavior:** 11 | 12 | 13 | **Actual behavior:** 14 | 15 | 16 | **Versions of packages used:** 17 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from './' 2 | 3 | export function isString(x: any): x is string { 4 | return typeof x === 'string' 5 | } 6 | 7 | export function isNumber(x: any): x is number { 8 | return typeof x === 'number' 9 | } 10 | 11 | export function isPrimitive(x: any): x is (string | number) { 12 | return isString(x) || isNumber(x) 13 | } 14 | 15 | export function vNodesAreEqual(formerVNode: VNode, vNode: VNode) { 16 | return formerVNode.key === vNode.key && formerVNode.tagName === vNode.tagName 17 | } 18 | -------------------------------------------------------------------------------- /.config/tsconfig.commonjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "dom", 6 | "es5", 7 | "es2015" 8 | ], 9 | "types": [], 10 | "baseUrl": "../", 11 | "declaration": true, 12 | "noImplicitAny": true, 13 | "noUnusedParameters": true, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "strict": true, 17 | "sourceMap": true, 18 | "target": "es5", 19 | "module": "commonjs", 20 | "outDir": "../lib" 21 | }, 22 | "exclude": [ 23 | "../src/**/*.test.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/BaseModule.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, Module, VNode } from '../types' 2 | 3 | export class BaseModule implements Module { 4 | public pre(_: VNode) {} 5 | 6 | public post(_: VNode) {} 7 | 8 | public init(_: VNode) {} 9 | 10 | public create(_: ElementVNode) {} 11 | 12 | public update(_: ElementVNode, __: ElementVNode) {} 13 | 14 | public remove(_: ElementVNode, removeElement: Function) { 15 | removeElement() 16 | } 17 | 18 | public destroy(_: ElementVNode) {} 19 | 20 | public prepatch(_: VNode, __: VNode) {} 21 | 22 | public postpatch(_: VNode, __: VNode) {} 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "lib": [ 8 | "dom", 9 | "es5", 10 | "es2015" 11 | ], 12 | "noImplicitAny": true, 13 | "sourceMap": true, 14 | "noUnusedParameters": true, 15 | "strictNullChecks": true, 16 | "strictFunctionTypes": true, 17 | "strict": true, 18 | "types": [ 19 | "mocha", 20 | "node" 21 | ] 22 | }, 23 | "include": [ 24 | "src/**/*.ts" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/patchVNode/updateElement.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, SCOPE_ATTRIBUTE, VNode } from '../' 2 | 3 | export function updateElement(formerVNode: VNode, vNode: VNode): ElementVNode { 4 | const node = vNode.element = formerVNode.element as Node 5 | 6 | if (isElement(node)) { 7 | const { scope } = vNode 8 | 9 | if (scope) 10 | node.setAttribute(SCOPE_ATTRIBUTE, scope) 11 | else 12 | node.removeAttribute(SCOPE_ATTRIBUTE) 13 | } 14 | 15 | return vNode as ElementVNode 16 | } 17 | 18 | function isElement(node: Node): node is Element { 19 | return typeof (node as Element).setAttribute === 'function' 20 | } 21 | -------------------------------------------------------------------------------- /.config/tsconfig.es2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "dom", 6 | "es5", 7 | "es2015" 8 | ], 9 | "types": [], 10 | "baseUrl": "../", 11 | "declaration": true, 12 | "moduleResolution": "node", 13 | "noImplicitAny": true, 14 | "noUnusedParameters": true, 15 | "strictNullChecks": true, 16 | "strictFunctionTypes": true, 17 | "strict": true, 18 | "sourceMap": true, 19 | "target": "es5", 20 | "module": "es2015", 21 | "outDir": "../lib.es2015" 22 | }, 23 | "exclude": [ 24 | "../src/**/*.test.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/addVNodes.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode } from './' 2 | import { ModuleCallbacks } from './modules/ModuleCallbacks' 3 | import { createElement } from './createElement' 4 | 5 | export function addVNodes( 6 | parentNode: Node, 7 | referenceNode: Node | null, 8 | vNodes: Array, 9 | startIndex: number, 10 | endIndex: number, 11 | moduleCallbacks: ModuleCallbacks, 12 | insertedVNodeQueue: Array) 13 | { 14 | for (; startIndex <= endIndex; ++startIndex) 15 | parentNode.insertBefore( 16 | createElement(vNodes[startIndex], moduleCallbacks, insertedVNodeQueue).element, 17 | referenceNode, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/types/VNodeEventTypes.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode } from './VirtualNode' 2 | 3 | export type VNodeEvent = Ev & { currentTarget: T } 4 | 5 | export type EventHandler = ( 6 | event: VNodeEvent, 7 | vNode: ElementVNode 8 | ) => any 9 | 10 | export type VNodeEvents = { 11 | [K in keyof EventMap]?: EventHandler 12 | } 13 | 14 | export type ElementEvents = VNodeEvents 15 | 16 | export type HtmlElementEvents = VNodeEvents 17 | -------------------------------------------------------------------------------- /src/modules/focus.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, Module } from '../' 2 | 3 | import { BaseModule } from './BaseModule' 4 | 5 | export function createFocusModule(): Module { 6 | return new FocusModule() 7 | } 8 | 9 | class FocusModule extends BaseModule { 10 | public insert(vNode: ElementVNode) { 11 | setFocus(vNode) 12 | } 13 | 14 | public update(_: ElementVNode, vNode: ElementVNode) { 15 | setFocus(vNode) 16 | } 17 | } 18 | 19 | function setFocus(vNode: ElementVNode) { 20 | const { props: { focus = false }, element } = vNode 21 | 22 | if (focus && typeof (element as HTMLElement).focus === 'function') 23 | (element as HTMLElement).focus() 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@motorcycle/tslint" 4 | ], 5 | "rules": { 6 | "no-bitwise": false, 7 | "readonly-indexer": false, 8 | "readonly-interface": false, 9 | "max-params": [true, 10], 10 | "no-empty": false, 11 | "prefer-function-over-method": [false], 12 | "no-param-reassign": false, 13 | "forin": false, 14 | "variable-name": [ 15 | true, 16 | "allow-leading-underscore" 17 | ], 18 | "no-parameter-properties": false, 19 | "unified-signatures": [false], 20 | "no-arguments": false, 21 | "prefer-for-of": false, 22 | "trailing-comma": [ 23 | false 24 | ], 25 | "no-unused-variable": [ 26 | false 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/patchVNode/replacePreviousElement.ts: -------------------------------------------------------------------------------- 1 | import { VNode, ElementVNode } from '../' 2 | import { ModuleCallbacks } from '../modules/ModuleCallbacks' 3 | import { createElement } from '../createElement' 4 | import { removeVNodes } from '../removeVNodes' 5 | 6 | export function replacePreviousElement( 7 | formerVNode: VNode, 8 | vNode: VNode, 9 | moduleCallbacks: ModuleCallbacks, 10 | insertedVNodeQueue: Array) 11 | { 12 | const parentNode = (formerVNode.element as Element).parentNode as Node 13 | 14 | const element = createElement(vNode, moduleCallbacks, insertedVNodeQueue).element as Element 15 | 16 | parentNode.insertBefore(element, formerVNode.element as Element) 17 | 18 | removeVNodes(parentNode, [ formerVNode as ElementVNode ], 0, 0, moduleCallbacks) 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # generated files 41 | lib 42 | lib.es2015 43 | .tmp 44 | -------------------------------------------------------------------------------- /src/hyperscript/parseSelector.ts: -------------------------------------------------------------------------------- 1 | const classIdSplit = /([\.#]?[a-zA-Z0-9\u007F-\uFFFF_:-]+)/ 2 | 3 | export function parseSelector(selector: string) { 4 | let tagName: string | void 5 | let id = '' 6 | const classes: Array = [] 7 | 8 | const tagParts = selector.split(classIdSplit) 9 | 10 | let part: string | void 11 | let type 12 | 13 | for (let i = 0; i < tagParts.length; i++) { 14 | part = tagParts[i] 15 | 16 | if (!part) 17 | continue 18 | 19 | type = part.charAt(0) 20 | 21 | if (!tagName) { 22 | tagName = part 23 | } else if (type === '.') { 24 | classes.push(part.substring(1, part.length)) 25 | } else if (type === '#') { 26 | id = part.substring(1, part.length) 27 | } 28 | } 29 | 30 | return { 31 | tagName: tagName as string, 32 | id, 33 | className: classes.join(' '), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mostly DOM 2 | 3 | 4 | > A Type-Safe virtual-dom implementation. A virtual-dom implementation that works for you. 5 | 6 | Mostly DOM is a virtual-dom implementation that provides strong types for everyday things 7 | like CSS values and HTML properties, you will be able to use itellisense to make your 8 | life easier. Say goodbye to a great deal of spelling mistakes. 9 | 10 | ## Let me have it 11 | ```sh 12 | npm install --save mostly-dom 13 | ``` 14 | 15 | ## Basic Usage 16 | 17 | ```typescript 18 | import { init, elementToVNode, h } from 'mostly-dom'; 19 | 20 | const patch = init([]); 21 | const rootElement = document.querySelector('#app') 22 | 23 | if (!rootElement) throw new Error('Unable to find root element') 24 | 25 | const initialVNode = elementToVNode(rootElement); 26 | const vNode = h('div', [ h('h1', 'Hello, World') ]); 27 | 28 | patch(initialVNode, vNode); 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /src/elementToVNode.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, MostlyVNode, VNode } from './' 2 | 3 | export function elementToVNode(element: T): ElementVNode { 4 | return new MostlyVNode( 5 | element.tagName && element.tagName.toLowerCase(), 6 | { 7 | id: element.id, 8 | className: element.className 9 | }, 10 | Array.prototype.slice.call(element.childNodes).map(nodeToVNode) || undefined, 11 | element, 12 | undefined, 13 | undefined, 14 | undefined, 15 | undefined, 16 | ) as ElementVNode 17 | } 18 | 19 | function nodeToVNode(node: Element | Text): VNode { 20 | if (isElement(node)) 21 | return elementToVNode(node) 22 | 23 | const textVNode = MostlyVNode.createText(node.textContent as string) 24 | 25 | textVNode.element = node 26 | 27 | return textVNode as VNode 28 | } 29 | 30 | function isElement(node: Node): node is Element { 31 | return node.nodeType === 1 32 | } 33 | -------------------------------------------------------------------------------- /src/hyperscript/hasCssSelector/matchBasicCssSelector.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from '../../' 2 | import { parseSelector } from '../parseSelector' 3 | 4 | export function matchBasicCssSelector(cssSelector: string, vNode: VNode) { 5 | const hasTagName = cssSelector[0] !== '#' && cssSelector[0] !== '.' 6 | 7 | const { tagName, className, id } = 8 | hasTagName ? 9 | parseSelector(cssSelector) : 10 | parseSelector(vNode.tagName + cssSelector) 11 | 12 | if (tagName !== vNode.tagName) 13 | return false 14 | 15 | const parsedClassNames = className && className.split(' ') || [] 16 | 17 | const { props: { className: propsClassName = '', id: propsId } } = vNode 18 | 19 | const vNodeClassNames = propsClassName.split(' ') 20 | 21 | for (let i = 0; i < parsedClassNames.length; ++i) { 22 | const parsedClassName = parsedClassNames[i] 23 | 24 | if (vNodeClassNames.indexOf(parsedClassName) === -1) 25 | return false 26 | } 27 | 28 | if (id) return propsId === id 29 | 30 | return true 31 | } 32 | -------------------------------------------------------------------------------- /src/hyperscript/querySelector.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from '../types' 2 | import { hasCssSelector } from './hasCssSelector' 3 | 4 | export const querySelector: QuerySelector = ((cssSelector: string, vNode?: VNode) => vNode 5 | ? _querySelector(cssSelector, vNode) 6 | : (_vNode: VNode) => _querySelector(cssSelector, _vNode)) as QuerySelector 7 | 8 | function _querySelector(cssSelector: string, vNode: VNode): VNode | null { 9 | const scope = vNode.scope 10 | 11 | const children: Array = [ vNode ] 12 | 13 | while (children.length > 0) { 14 | const child = children.shift() as VNode 15 | 16 | if (child.scope !== scope) 17 | continue 18 | 19 | if (hasCssSelector(cssSelector, child)) 20 | return child 21 | 22 | if (!child.children) continue 23 | 24 | children.push(...child.children) 25 | } 26 | 27 | return null 28 | } 29 | 30 | export interface QuerySelector { 31 | (cssSelector: string, vNode: VNode): VNode | null 32 | (cssSelector: string): (vNode: VNode) => VNode | null 33 | } 34 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const options = 2 | { 3 | frameworks: [ 4 | 'mocha', 5 | 'karma-typescript', 6 | ], 7 | 8 | files: [ 9 | { pattern: 'src/**/*.ts' }, 10 | ], 11 | 12 | preprocessors: { 13 | '**/*.ts': 'karma-typescript', 14 | }, 15 | 16 | reporters: [ 17 | 'progress', 18 | 'karma-typescript', 19 | ], 20 | 21 | customLaunchers: { 22 | Chrome_travis_ci: { 23 | base: 'Chrome', 24 | flags: ['--no-sandbox'] 25 | }, 26 | }, 27 | 28 | singleRun: true, 29 | 30 | browsers: [], 31 | 32 | karmaTypescriptConfig: { 33 | tsconfig: "./tsconfig.json", 34 | reports: { 35 | "html": "coverage", 36 | "lcovonly": "coverage", 37 | } 38 | }, 39 | }; 40 | 41 | module.exports = function (karma) { 42 | if (process.env.TRAVIS) 43 | options.browsers.push('Chrome_travis_ci', 'Firefox') 44 | 45 | if (options.browsers.length === 0) 46 | options.browsers.push('Chrome', 'Firefox') 47 | 48 | karma.set(options); 49 | }; 50 | -------------------------------------------------------------------------------- /src/modules/class.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, EventHandler, Module, VNode } from '../' 2 | 3 | import { BaseModule } from './BaseModule' 4 | import { emptyVNode } from './emptyVNode' 5 | 6 | export function createClassModule(): Module { 7 | return new ClassModule() 8 | } 9 | 10 | class ClassModule extends BaseModule { 11 | public create(vNode: ElementVNode) { 12 | updateClass(emptyVNode, vNode) 13 | } 14 | 15 | public update(formerVNode: ElementVNode, vNode: ElementVNode) { 16 | updateClass(formerVNode, vNode) 17 | } 18 | } 19 | 20 | function updateClass(formerVNode: ElementVNode, vNode: ElementVNode): void { 21 | const { props: { class: formerClass = {} }, element: formerElement } = formerVNode 22 | const { props: { class: klass = {} }, element } = vNode 23 | 24 | if (formerClass === klass) return 25 | 26 | for (const name in formerClass) 27 | if (!klass[name]) 28 | formerElement.classList.remove(name) 29 | 30 | for (const name in klass) 31 | if (klass[name] !== formerClass[name]) 32 | element.classList.toggle(name) 33 | } 34 | -------------------------------------------------------------------------------- /src/patchVNode/hooks.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, VNode } from '../' 2 | 3 | import { ModuleCallbacks } from '../modules/ModuleCallbacks' 4 | 5 | export function prepatchHooks( 6 | formerVNode: ElementVNode, 7 | vNode: VNode, 8 | moduleCallbacks: ModuleCallbacks) 9 | { 10 | const props = vNode.props 11 | 12 | moduleCallbacks.prepatch(formerVNode, vNode) 13 | 14 | if (props.prepatch) 15 | props.prepatch(formerVNode, vNode) 16 | } 17 | 18 | export function updateHooks( 19 | formerVNode: ElementVNode, 20 | vNode: ElementVNode, 21 | moduleCallbacks: ModuleCallbacks) 22 | { 23 | const props = vNode.props 24 | 25 | moduleCallbacks.update(formerVNode, vNode) 26 | 27 | if (props.update) props.update(formerVNode, vNode) 28 | } 29 | 30 | export function postpatchHooks( 31 | formerVNode: ElementVNode, 32 | vNode: ElementVNode, 33 | moduleCallbacks: ModuleCallbacks) 34 | { 35 | const props = vNode.props 36 | 37 | moduleCallbacks.postpatch(formerVNode, vNode) 38 | 39 | if (props.postpatch) 40 | props.postpatch(formerVNode, vNode) 41 | } 42 | -------------------------------------------------------------------------------- /src/hyperscript/hasCssSelector/matchesSelector.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from '../../' 2 | import { matchAttribute } from './matchAttribute' 3 | import { matchBasicCssSelector } from './matchBasicCssSelector' 4 | import { matchPsuedoSelector } from './matchPsuedoSelector' 5 | 6 | const EMPTY: any = Object.freeze({}) 7 | 8 | export function matchesSelector(cssSelector: string, vNode: VNode): boolean { 9 | cssSelector = cssSelector.trim() 10 | 11 | // if working with an ElementVNode return use native implementation 12 | if (vNode.element && (vNode.element as Element).matches) 13 | return (vNode.element as Element).matches(cssSelector) 14 | 15 | if (cssSelector[0] === '[' && cssSelector[cssSelector.length - 1] === ']') 16 | return matchAttribute(cssSelector.slice(1, -1), vNode.props.attrs || EMPTY) 17 | 18 | if (cssSelector.indexOf(':') > -1) 19 | return matchPsuedoSelector(cssSelector, vNode) 20 | 21 | if (cssSelector.indexOf(' ') > -1) 22 | throw new Error('Basic CSS selectors can not contain spaces') 23 | 24 | return matchBasicCssSelector(cssSelector, vNode) 25 | } 26 | -------------------------------------------------------------------------------- /src/hyperscript/querySelectorAll.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from '../types' 2 | import { hasCssSelector } from './hasCssSelector' 3 | 4 | export const querySelectorAll: QuerySelectorAll = function(cssSelector: string, vNode: VNode) { 5 | if (!vNode) return (_vNode: VNode) => _querySelectorAll(cssSelector, vNode) 6 | 7 | return _querySelectorAll(cssSelector, vNode) 8 | } as QuerySelectorAll 9 | 10 | function _querySelectorAll(cssSelector: string, vNode: VNode): Array { 11 | const matches: Array = [] 12 | const scope = vNode.scope 13 | 14 | const children: Array = [ vNode ] 15 | 16 | while (children.length > 0) { 17 | const child = children.shift() as VNode 18 | 19 | if (child.scope !== scope) continue 20 | 21 | if (hasCssSelector(cssSelector, child)) matches.push(child) 22 | 23 | if (!child.children) continue 24 | 25 | children.push(...child.children) 26 | } 27 | 28 | return matches 29 | } 30 | 31 | export interface QuerySelectorAll { 32 | (cssSelector: string, vNode: VNode): Array 33 | (cssSelector: string): (vNode: VNode) => Array 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tylor Steinberger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/modules/props.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import { ElementVNode, div } from '../' 4 | 5 | import { createPropsModule } from './props' 6 | 7 | describe(`Props Module`, () => { 8 | it(`removes previous id`, () => { 9 | const props = createPropsModule() 10 | 11 | const id = 'foo' 12 | const formerVNode = div({ id }, []) as ElementVNode 13 | formerVNode.element = document.createElement('div') 14 | formerVNode.element.id = id 15 | 16 | const vNode = div({}, []) as ElementVNode 17 | vNode.element = formerVNode.element 18 | 19 | props.update(formerVNode, vNode) 20 | 21 | assert.strictEqual(vNode.element.id, '') 22 | }) 23 | 24 | it(`removes previous className`, () => { 25 | const props = createPropsModule() 26 | 27 | const className = 'foo' 28 | const formerVNode = div({ className }, []) as ElementVNode 29 | formerVNode.element = document.createElement('div') 30 | formerVNode.element.className = className 31 | 32 | const vNode = div({}, []) as ElementVNode 33 | vNode.element = formerVNode.element 34 | 35 | props.update(formerVNode, vNode) 36 | 37 | assert.strictEqual(vNode.element.className, '') 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/hyperscript/h.test.tsx: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import { h, HyperscriptChildren } from './h' 4 | 5 | describe('h jsx support', () => { 6 | it('accepts a function', () => { 7 | function MyComponent() { 8 | return
9 | } 10 | 11 | const vNode = 12 | assert.strictEqual(vNode.tagName, 'div') 13 | }) 14 | 15 | it('supports destructuration', () => { 16 | function MyComponent({ className }: { className: string }) { 17 | return
18 | } 19 | 20 | const vNode = 21 | assert.strictEqual(vNode.tagName, 'div') 22 | }) 23 | 24 | it('supports children', () => { 25 | function MyComponent(_: any, children: HyperscriptChildren) { 26 | return
{children}
27 | } 28 | 29 | const vNode = ( 30 | 31 |
32 | 33 | 34 | ) 35 | 36 | if (!vNode.children) throw new Error(`VNode should have children`) 37 | 38 | assert.strictEqual(vNode.tagName, 'div') 39 | assert.strictEqual(vNode.children[0].tagName, 'div') 40 | assert.strictEqual(vNode.children[1].tagName, 'span') 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/hyperscript/parseSelector.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { parseSelector } from './parseSelector' 3 | 4 | describe('parseSelector', () => { 5 | it('parses selectors', () => { 6 | let result = parseSelector('p') 7 | assert.deepEqual(result, { tagName: 'p', id: '', className: '' }) 8 | 9 | result = parseSelector('p#foo') 10 | assert.deepEqual(result, { tagName: 'p', id: 'foo', className: '' }) 11 | 12 | result = parseSelector('p.bar') 13 | assert.deepEqual(result, { tagName: 'p', id: '', className: 'bar' }) 14 | 15 | result = parseSelector('p.bar.baz') 16 | assert.deepEqual(result, { tagName: 'p', id: '', className: 'bar baz' }) 17 | 18 | result = parseSelector('p#foo.bar.baz') 19 | assert.deepEqual(result, { tagName: 'p', id: 'foo', className: 'bar baz' }) 20 | 21 | result = parseSelector('div#foo') 22 | assert.deepEqual(result, { tagName: 'div', id: 'foo', className: '' }) 23 | 24 | result = parseSelector('div#foo.bar.baz') 25 | assert.deepEqual(result, { tagName: 'div', id: 'foo', className: 'bar baz' }) 26 | 27 | result = parseSelector('div.bar.baz') 28 | assert.deepEqual(result, { tagName: 'div', id: '', className: 'bar baz' }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/patchVNode/patchVNodeChildren.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, VNode } from '../' 2 | 3 | import { ModuleCallbacks } from '../modules/ModuleCallbacks' 4 | import { addVNodes } from '../addVNodes' 5 | import { removeVNodes } from '../removeVNodes' 6 | import { updateChildren } from '../updateChildren' 7 | 8 | export function patchVNodeChildren( 9 | formerVNode: VNode, 10 | vNode: VNode, 11 | moduleCallbacks: ModuleCallbacks, 12 | insertedVNodeQueue: Array) 13 | { 14 | const element = vNode.element as Element 15 | const formerChildren = formerVNode.children as Array 16 | const children = vNode.children as Array 17 | 18 | if (formerVNode.text) 19 | element.textContent = '' 20 | 21 | if (formerChildren && children && formerChildren !== children) 22 | updateChildren(element, formerChildren, children, moduleCallbacks, insertedVNodeQueue) 23 | else if (children) 24 | addVNodes(element, null, children, 0, endIndex(children), moduleCallbacks, insertedVNodeQueue) 25 | else if (formerChildren) 26 | removeVNodes(element, formerChildren, 0, endIndex(formerChildren), moduleCallbacks) 27 | } 28 | 29 | function endIndex(vNodeChildren: Array) { 30 | return vNodeChildren.length - 1 31 | } 32 | -------------------------------------------------------------------------------- /src/hyperscript/hasCssSelector/matchAttribute.ts: -------------------------------------------------------------------------------- 1 | // ~ and * : value contains 2 | // | and ^ : value starts with 3 | // $ : value ends with 4 | 5 | const attrModifiers: Array = 6 | [ '~', '*', '|', '^', '$' ] 7 | 8 | export function matchAttribute(cssSelector: string, attrs: any) { 9 | // tslint:disable-next-line:prefer-const 10 | let [ attribute, value ] = cssSelector.split('=') 11 | 12 | const attributeLength = attribute.length - 1 13 | 14 | const modifier = attribute[attributeLength] 15 | const modifierIndex = attrModifiers.indexOf(modifier) 16 | 17 | if (modifierIndex > -1) { 18 | attribute = attribute.slice(0, attributeLength) 19 | const attrModifier = attrModifiers[modifierIndex] 20 | 21 | const attrValue = String(attrs[attribute]) 22 | 23 | if (!attrValue) return false 24 | 25 | switch (attrModifier) { 26 | case '~': return attrValue.indexOf(value) > -1 27 | case '*': return attrValue.indexOf(value) > -1 28 | case '|': return attrValue.indexOf(value) === 0 29 | case '^': return attrValue.indexOf(value) === 0 30 | case '$': return attrValue.slice(-value.length) === value 31 | default: return false 32 | } 33 | } 34 | 35 | if (value) 36 | return value === attrs[attribute] 37 | 38 | return !!attrs[attribute] 39 | } 40 | -------------------------------------------------------------------------------- /src/patchVNode/patchVNode.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, TextVNode, VNode } from '../' 2 | import { postpatchHooks, prepatchHooks, updateHooks } from './hooks' 3 | 4 | import { ModuleCallbacks } from '../modules/ModuleCallbacks' 5 | import { patchVNodeChildren } from './patchVNodeChildren' 6 | import { replacePreviousElement } from './replacePreviousElement' 7 | import { updateElement } from './updateElement' 8 | import { vNodesAreEqual } from '../helpers' 9 | 10 | export function patchVNode( 11 | formerVNode: VNode, 12 | vNode: VNode, 13 | moduleCallbacks: ModuleCallbacks, 14 | insertedVNodeQueue: Array): void 15 | { 16 | prepatchHooks(formerVNode as ElementVNode, vNode as VNode, moduleCallbacks) 17 | 18 | vNode = updateElement(formerVNode, vNode) 19 | 20 | if (formerVNode === vNode) return 21 | 22 | if (!vNodesAreEqual(formerVNode, vNode)) 23 | return replacePreviousElement(formerVNode, vNode, moduleCallbacks, insertedVNodeQueue) 24 | 25 | updateHooks(formerVNode as ElementVNode, vNode as ElementVNode, moduleCallbacks) 26 | 27 | if (!vNode.text) 28 | patchVNodeChildren(formerVNode, vNode, moduleCallbacks, insertedVNodeQueue) 29 | else if (formerVNode.text !== (vNode as TextVNode).text) 30 | (vNode.element as Element).textContent = vNode.text 31 | 32 | postpatchHooks(formerVNode as ElementVNode, vNode as ElementVNode, moduleCallbacks) 33 | } 34 | -------------------------------------------------------------------------------- /src/removeVNodes.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, VNode } from './' 2 | import { ModuleCallbacks } from './modules/ModuleCallbacks' 3 | 4 | export function removeVNodes( 5 | parentNode: Node, 6 | vNodes: Array, 7 | startIndex: number, 8 | endIndex: number, 9 | moduleCallbacks: ModuleCallbacks 10 | ) 11 | { 12 | for (; startIndex <= endIndex; ++startIndex) { 13 | const vNode = vNodes[startIndex] 14 | 15 | if (!vNode) continue 16 | 17 | if (isElementVNode(vNode)) { 18 | const { props } = vNode 19 | 20 | invokeDestroyHook(vNode, moduleCallbacks) 21 | 22 | const removeElement = moduleCallbacks.createRemoveElementFn(vNode.element) 23 | moduleCallbacks.remove(vNode, removeElement) 24 | 25 | if (props.remove) props.remove(vNode, removeElement) 26 | else removeElement() 27 | } else { 28 | parentNode.removeChild(vNode.element) 29 | } 30 | } 31 | } 32 | 33 | function isElementVNode(vNode: VNode): vNode is ElementVNode { 34 | return vNode.tagName && vNode.element 35 | } 36 | 37 | function invokeDestroyHook(vNode: VNode, moduleCallbacks: ModuleCallbacks) { 38 | const props = vNode.props 39 | 40 | if (props.destroy) props.destroy(vNode) 41 | 42 | if (isElementVNode(vNode)) moduleCallbacks.destroy(vNode) 43 | 44 | const children = vNode.children 45 | 46 | if (!children) return 47 | 48 | for (let i = 0; i < children.length; ++i) invokeDestroyHook(children[i], moduleCallbacks) 49 | } 50 | -------------------------------------------------------------------------------- /src/hyperscript/querySelectorAll.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import { h } from './h' 4 | import { querySelectorAll } from './querySelectorAll' 5 | 6 | describe('querySelectorAll', () => { 7 | describe('given a cssSelector and a vNode with children', () => { 8 | it('returns all vNodes that match given cssSelector', () => { 9 | const vNode = h('div', {}, [ 10 | h('a', { className: 'hi' }), 11 | h('b', { className: 'hi' }), 12 | h('br', { className: 'hi' }), 13 | ]) 14 | 15 | const matches = querySelectorAll('.hi', vNode) 16 | 17 | assert.strictEqual(matches.length, 3) 18 | const [ a, b, br ] = matches 19 | 20 | assert.strictEqual(a.tagName, 'a') 21 | assert.strictEqual(a.props.className, 'hi') 22 | assert.strictEqual(b.tagName, 'b') 23 | assert.strictEqual(b.props.className, 'hi') 24 | assert.strictEqual(br.tagName, 'br') 25 | assert.strictEqual(br.props.className, 'hi') 26 | }) 27 | 28 | it('returns only vNodes with a given scope', () => { 29 | const vNode = h('div', { scope: 'hi' }, [ 30 | h('a', { className: 'hi' }), 31 | h('b', { className: 'hi', scope: 'hello' }), 32 | h('br', { className: 'hi', scope: 'bye' }), 33 | ]) 34 | 35 | const matches = querySelectorAll('.hi', vNode) 36 | 37 | assert.strictEqual(matches.length, 1) 38 | 39 | const [ a ] = matches 40 | 41 | assert.strictEqual(a.tagName, 'a') 42 | assert.strictEqual(a.props.className, 'hi') 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/patchVNode/updateElement.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import { ElementVNode, SCOPE_ATTRIBUTE, h } from '../' 4 | 5 | import { updateElement } from './updateElement' 6 | 7 | describe('updateElement', () => { 8 | it('updates a change to scope', () => { 9 | const element = document.createElement('div') 10 | element.setAttribute(SCOPE_ATTRIBUTE, 'hi') 11 | 12 | const formerVNode = h('div', { scope: 'hi' }, []) as ElementVNode 13 | 14 | formerVNode.element = element 15 | 16 | const vNode = h('div', { scope: 'hello' }, []) 17 | 18 | const elementVNode = updateElement(formerVNode, vNode) 19 | 20 | assert.strictEqual(elementVNode.element.getAttribute(SCOPE_ATTRIBUTE), 'hello') 21 | }) 22 | 23 | it(`removes previous scope`, () => { 24 | const scope = 'hi' 25 | 26 | const formerVNode = h('div', { scope }, []) as ElementVNode 27 | formerVNode.element = document.createElement('div') 28 | formerVNode.element.setAttribute(SCOPE_ATTRIBUTE, scope) 29 | 30 | const vNode = h('div', {}, []) 31 | 32 | const { element } = updateElement(formerVNode, vNode) 33 | 34 | assert.ok(!element.hasAttribute(SCOPE_ATTRIBUTE)) 35 | }) 36 | 37 | it(`does not throw when updateing SVG element`, () => { 38 | const className = 'test' 39 | const formerVNode = h('svg', {}, []) as ElementVNode 40 | formerVNode.element = document.createElementNS('http://www.w3.org/2000/svg', 'svg') 41 | 42 | assert.doesNotThrow(() => { 43 | updateElement(formerVNode, h('svg', {}, [])) 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /src/init.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, InsertHook, Module, VNode, VNodeEvents, VNodeProps } from './' 2 | 3 | import { ModuleCallbacks } from './modules/ModuleCallbacks' 4 | import { createElement } from './createElement' 5 | import { patchVNode } from './patchVNode' 6 | import { removeVNodes } from './removeVNodes' 7 | import { vNodesAreEqual } from './helpers' 8 | 9 | export function init(modules: Array = []) { 10 | const moduleCallbacks = new ModuleCallbacks(modules) 11 | 12 | return function patch< 13 | T extends Element, 14 | Props extends VNodeProps> 15 | >(formerVNode: ElementVNode, vNode: VNode): ElementVNode 16 | { 17 | const insertedVNodeQueue: Array = [] 18 | 19 | moduleCallbacks.pre(vNode) 20 | 21 | if (vNodesAreEqual(formerVNode, vNode)) 22 | patchVNode(formerVNode, vNode, moduleCallbacks, insertedVNodeQueue) 23 | else { 24 | const element = formerVNode.element 25 | const parentNode = element.parentNode 26 | 27 | vNode = createElement(vNode, moduleCallbacks, insertedVNodeQueue) as VNode 28 | 29 | if (parentNode) { 30 | parentNode.insertBefore(vNode.element as Element, element.nextSibling) 31 | removeVNodes(parentNode, [ formerVNode ], 0, 0, moduleCallbacks) 32 | } 33 | } 34 | 35 | for (let i = 0; i < insertedVNodeQueue.length; ++i) 36 | (insertedVNodeQueue[i].props.insert as InsertHook)(insertedVNodeQueue[i]) 37 | 38 | moduleCallbacks.post(vNode as ElementVNode) 39 | 40 | return vNode as ElementVNode 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/createElement.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, TextVNode, VNode } from './' 2 | 3 | import { ModuleCallbacks } from './modules/ModuleCallbacks' 4 | import { SCOPE_ATTRIBUTE } from './dom-scope-attribute' 5 | 6 | export function createElement( 7 | vNode: VNode, 8 | moduleCallbacks: ModuleCallbacks, 9 | insertedVNodeQueue: Array): ElementVNode | TextVNode 10 | { 11 | const props = vNode.props 12 | 13 | moduleCallbacks.init(vNode) 14 | 15 | if (props.init) 16 | props.init(vNode) 17 | 18 | if (vNode.tagName) { 19 | const element = vNode.namespace 20 | ? document.createElementNS(vNode.namespace, vNode.tagName) 21 | : document.createElement(vNode.tagName) 22 | 23 | if (vNode.scope) 24 | element.setAttribute(SCOPE_ATTRIBUTE, vNode.scope) 25 | 26 | vNode.element = element 27 | 28 | const children = vNode.children 29 | 30 | if (children) { 31 | const childCount = children.length 32 | 33 | for (let i = 0; i < childCount; ++i) 34 | element.appendChild( 35 | createElement(children[i], moduleCallbacks, insertedVNodeQueue).element as Node) 36 | } 37 | 38 | if (vNode.text) 39 | element.appendChild(document.createTextNode(vNode.text)) 40 | 41 | moduleCallbacks.create(vNode as ElementVNode) 42 | 43 | if (props.create) 44 | props.create(vNode as ElementVNode) 45 | 46 | if (props.insert) 47 | insertedVNodeQueue.push(vNode as ElementVNode) 48 | 49 | return vNode as ElementVNode 50 | } 51 | 52 | vNode.element = document.createTextNode(vNode.text as string) 53 | 54 | return vNode as TextVNode 55 | } 56 | -------------------------------------------------------------------------------- /src/modules/events.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, EventHandler, Module } from '../' 2 | 3 | import { BaseModule } from './BaseModule' 4 | import { emptyVNode } from './emptyVNode' 5 | 6 | export function createEventsModule(): Module { 7 | return new EventsModule() 8 | } 9 | 10 | class EventsModule extends BaseModule { 11 | public create(vNode: ElementVNode) { 12 | updateEventHandlers(emptyVNode, vNode) 13 | } 14 | 15 | public update(formerVNode: ElementVNode, vNode: ElementVNode) { 16 | updateEventHandlers(formerVNode, vNode) 17 | } 18 | 19 | public destroy(vNode: ElementVNode) { 20 | updateEventHandlers(vNode, emptyVNode) 21 | } 22 | } 23 | 24 | function updateEventHandlers( 25 | formerVNode: ElementVNode, 26 | vNode: ElementVNode 27 | ) 28 | { 29 | const { 30 | element: formerElement, 31 | props: { on: formerOn = {}, listener: formerListener = createListener(vNode) } 32 | } = formerVNode 33 | 34 | const { element, props: { on = {} } } = vNode 35 | 36 | if (formerOn === on) return 37 | 38 | const listener = vNode.props.listener = formerListener 39 | 40 | for (const name in formerOn) 41 | if (!(on as any)[name]) formerElement.removeEventListener(name, listener, false) 42 | 43 | for (const name in on) 44 | if (!(formerOn as any)[name]) element.addEventListener(name, listener, false) 45 | } 46 | 47 | function createListener(vNode: ElementVNode): EventListener { 48 | return function(event: Event) { 49 | handleEvent(event, vNode) 50 | } 51 | } 52 | 53 | function handleEvent(event: Event, vNode: ElementVNode) { 54 | const { type } = event 55 | const { props: { on = {} } } = vNode 56 | const handler = (on as any)[type] as EventHandler 57 | 58 | if (handler) handler(event as any, vNode) 59 | } 60 | -------------------------------------------------------------------------------- /src/types/hooks.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, VNode } from './VirtualNode' 2 | 3 | export interface Module { 4 | pre: PreModuleHook 5 | post: PostModuleHook 6 | init: InitHook 7 | create: CreateHook 8 | update: UpdateHook 9 | remove: RemoveHook 10 | destroy: DestroyHook 11 | prepatch: PrepatchHook 12 | postpatch: PostpatchHook 13 | } 14 | 15 | // Only available to modules 16 | // before any diffing or patching 17 | export type PreModuleHook = (vNode: VNode) => any 18 | // before returning the just patched vNode 19 | export type PostModuleHook = (vNode: ElementVNode) => any 20 | 21 | // Available to both VNode and Modules 22 | // called just before created an element from a VirtualNode 23 | export type InitHook = (vNode: VNode) => any 24 | // called when a VirtualNode becomes an ElementVNode 25 | export type CreateHook = (vNode: ElementVNode) => any 26 | // called when a ElementVNode element is being updated 27 | export type UpdateHook = 28 | (formerVNode: ElementVNode, vNode: ElementVNode) => any 29 | // called when an ElementVNode is being removed from the DOM 30 | export type RemoveHook = 31 | (vNode: ElementVNode, removeElement: () => void) => any 32 | // called when an ElementVNode's parent is being removed from the DOM 33 | export type DestroyHook = (vNode: ElementVNode) => any 34 | // called just before an ElementVNode is about to be patched 35 | export type PrepatchHook = 36 | (formerVNode: VNode, vNode: VNode) => any 37 | // called just after an ElementVNode has been patched 38 | export type PostpatchHook = 39 | (formerVNode: ElementVNode, vNode: ElementVNode) => any 40 | 41 | // Available for only VNode hooks 42 | // called when an ElementVNode has been inserted into the DOM 43 | export type InsertHook = (vNode: ElementVNode) => any 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mostly-dom", 3 | "description": "A virtual-dom for TypeScript", 4 | "version": "6.0.0", 5 | "author": "Tylor Steinberger ", 6 | "main": "lib/index.js", 7 | "module": "lib.es2015/index.js", 8 | "jsnext:main": "lib.es2015/index.js", 9 | "typings": "lib/index.d.ts", 10 | "bugs": { 11 | "url": "https://github.com/TylorS/mostly-dom/issues" 12 | }, 13 | "devDependencies": { 14 | "@motorcycle/tslint": "3.1.0", 15 | "@types/mocha": "2.2.48", 16 | "@types/node": "^10.12.0", 17 | "conventional-changelog-cli": "1.3.16", 18 | "karma": "~3.0.0", 19 | "karma-chrome-launcher": "~2.2.0", 20 | "karma-firefox-launcher": "~1.1.0", 21 | "karma-mocha": "1.3.0", 22 | "karma-typescript": "3.0.12", 23 | "mocha": "5.0.2", 24 | "ts-node": "~7.0.0", 25 | "tslint": "~5.11.0", 26 | "typescript": "~3.1.1" 27 | }, 28 | "homepage": "https://github.com/TylorS/mostly-dom#readme", 29 | "keywords": [ 30 | "dom", 31 | "fast", 32 | "modular", 33 | "reconciliation", 34 | "virtual", 35 | "virtual-dom" 36 | ], 37 | "license": "MIT", 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/TylorS/mostly-dom.git" 41 | }, 42 | "scripts": { 43 | "build": "npm run build:commonjs && npm run build:es2015", 44 | "build:commonjs": "tsc -P .config/tsconfig.commonjs.json", 45 | "build:es2015": "tsc -P .config/tsconfig.es2015.json", 46 | "changelog": "conventional-changelog -i CHANGELOG.md -s -r 0 -p angular", 47 | "commitmsg": "validate-commit-msg", 48 | "postchangelog": "git add CHANGELOG.md && git commit -m 'docs(CHANGELOG): amend changelog'", 49 | "postversion": "npm run changelog && git push origin master --tags && npm publish", 50 | "preversion": "npm run test && npm run build", 51 | "release:major": "npm version major -m 'chore(package): v%s'", 52 | "release:minor": "npm version minor -m 'chore(package): v%s'", 53 | "test": "npm run test:lint && karma start --single-run", 54 | "test:lint": "tslint --project tsconfig.json" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/types/VirtualNode.ts: -------------------------------------------------------------------------------- 1 | import * as hooks from './hooks' 2 | 3 | import { CSSProperties } from './CSS' 4 | import { ElementProperties } from './HtmlProperties' 5 | import { VNodeEvents } from './VNodeEventTypes' 6 | import { HyperscriptChildren } from '../index' 7 | 8 | export interface VNode { 9 | tagName: string | undefined 10 | props: Props 11 | children: Array | undefined 12 | text: string | undefined 13 | key: string | number | undefined 14 | element: T | undefined 15 | namespace: string | undefined 16 | scope: string | undefined 17 | 18 | parent: VNode | undefined 19 | } 20 | 21 | // tslint:disable-next-line:max-line-length 22 | export interface ElementVNode extends VNode { 23 | tagName: string 24 | element: T 25 | namespace: string 26 | text: undefined 27 | } 28 | 29 | export interface TextVNode extends VNode { 30 | tagName: undefined 31 | children: undefined 32 | text: string 33 | key: undefined 34 | element: Text 35 | namespace: undefined 36 | scope: undefined 37 | } 38 | 39 | export interface VNodeProps< 40 | T extends Element = Element, 41 | EventMap extends VNodeEvents = VNodeEvents 42 | > extends ElementProperties { 43 | // key for dom diffing 44 | key?: string | number 45 | 46 | // classes 47 | class?: { [className: string]: Boolean } 48 | 49 | // attributes for setAttribute() 50 | attrs?: { [attributeName: string]: any } 51 | 52 | // styling 53 | style?: VNodeStyle 54 | 55 | // events 56 | on?: EventMap 57 | listener?: EventListener 58 | 59 | // declarative focusing 60 | focus?: boolean 61 | 62 | scope?: string 63 | 64 | // hooks 65 | init?: hooks.InitHook 66 | create?: hooks.CreateHook 67 | update?: hooks.UpdateHook 68 | insert?: hooks.InsertHook 69 | remove?: hooks.RemoveHook 70 | destroy?: hooks.DestroyHook 71 | prepatch?: hooks.PrepatchHook 72 | postpatch?: hooks.PostpatchHook 73 | } 74 | 75 | export interface VNodeStyle extends CSSProperties { 76 | delayed?: CSSProperties 77 | remove?: CSSProperties 78 | } 79 | -------------------------------------------------------------------------------- /src/modules/props.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, Module } from '../' 2 | 3 | import { BaseModule } from './BaseModule' 4 | import { emptyVNode } from './emptyVNode' 5 | 6 | export function createPropsModule(): Module { 7 | return new PropsModule() 8 | } 9 | 10 | export const PROPERTIES_TO_SKIP: Record = { 11 | class: true, 12 | on: true, 13 | listener: true, 14 | focus: true, 15 | style: true, 16 | attrs: true, 17 | key: true, 18 | module: true, 19 | init: true, 20 | create: true, 21 | update: true, 22 | insert: true, 23 | remove: true, 24 | destroy: true, 25 | prepatch: true, 26 | postpatch: true, 27 | } 28 | 29 | export const ATTRIBUTE_TO_REMOVE: Record = { 30 | id: true, 31 | } 32 | 33 | class PropsModule extends BaseModule { 34 | public create(vNode: ElementVNode) { 35 | updateProps(emptyVNode as ElementVNode, vNode) 36 | } 37 | 38 | public update(formerVNode: ElementVNode, vNode: ElementVNode) { 39 | updateProps(formerVNode, vNode) 40 | } 41 | } 42 | 43 | function updateProps(formerVNode: ElementVNode, vNode: ElementVNode): void { 44 | const element: any = vNode.element 45 | let formerProps: any = formerVNode.props 46 | let props: any = vNode.props 47 | 48 | if (!formerProps && !props) return 49 | 50 | formerProps = formerProps || {} 51 | props = props || {} 52 | 53 | for (const key in formerProps) { 54 | const propertyIsMissing = !PROPERTIES_TO_SKIP[key] && !props[key] 55 | const keyIsClassName = propertyIsMissing && key === 'className' 56 | const shouldRemoveAttribute = propertyIsMissing && ATTRIBUTE_TO_REMOVE[key] 57 | 58 | if (propertyIsMissing) delete element[key] 59 | 60 | if (keyIsClassName) removePreviousClassName(formerProps[key], element) 61 | 62 | if (shouldRemoveAttribute) element.removeAttribute(key) 63 | } 64 | 65 | for (const key in props) if (!PROPERTIES_TO_SKIP[key]) element[key] = props[key] 66 | } 67 | 68 | function removePreviousClassName(className: string, element: Element) { 69 | const shouldRemoveClassName = className && element.classList 70 | const shouldRemoveClassAttribute = shouldRemoveClassName && element.getAttribute('class') === '' 71 | 72 | if (shouldRemoveClassName) element.classList.remove(...className.split(' ')) 73 | 74 | if (shouldRemoveClassAttribute) element.removeAttribute('class') 75 | } 76 | -------------------------------------------------------------------------------- /src/hyperscript/VNode.ts: -------------------------------------------------------------------------------- 1 | import { VNode, VNodeProps, VNodeEvents } from '../types' 2 | 3 | export const SVG_NAMESPACE = `http://www.w3.org/2000/svg` 4 | 5 | const defaultTextNodeData: VNodeProps = {} 6 | 7 | export class MostlyVNode implements VNode { 8 | public parent: MostlyVNode | undefined = undefined 9 | 10 | constructor( 11 | public tagName: string | undefined, 12 | public props: VNodeProps, 13 | public children: Array | undefined, 14 | public element: T | undefined, 15 | public text: string | undefined, 16 | public key: string | number | undefined, 17 | public scope: string | undefined, 18 | public namespace: string | undefined 19 | ) {} 20 | 21 | public static create( 22 | tagName: string | undefined, 23 | props: VNodeProps, 24 | children: Array | undefined, 25 | text: string | undefined 26 | ) 27 | { 28 | return new MostlyVNode( 29 | tagName, 30 | props, 31 | children, 32 | undefined, 33 | text, 34 | props.key, 35 | props.scope, 36 | undefined 37 | ) 38 | } 39 | 40 | public static createText(text: string): MostlyVNode { 41 | return new MostlyVNode( 42 | undefined, 43 | defaultTextNodeData, 44 | undefined, 45 | undefined, 46 | text, 47 | undefined, 48 | undefined, 49 | undefined 50 | ) 51 | } 52 | 53 | public static createSvg( 54 | tagName: string | undefined, 55 | props: VNodeProps>, 56 | children: Array | undefined, 57 | text: string | undefined 58 | ) 59 | { 60 | return new MostlyVNode( 61 | tagName, 62 | props as any, 63 | children, 64 | undefined, 65 | text, 66 | props.key, 67 | props.scope, 68 | SVG_NAMESPACE 69 | ) 70 | } 71 | } 72 | 73 | export function addSvgNamespace(vNode: MostlyVNode): void { 74 | vNode.namespace = SVG_NAMESPACE 75 | 76 | if (Array.isArray(vNode.children)) { 77 | const children = vNode.children 78 | const childCount = children.length 79 | 80 | for (let i = 0; i < childCount; ++i) { 81 | const child = children[i] 82 | 83 | if (child.tagName !== 'foreignObject') addSvgNamespace(child as MostlyVNode) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/hyperscript/hasCssSelector/matchPsuedoSelector.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from '../../' 2 | 3 | const defaultParent = Object.freeze({ children: [] as Array }) 4 | 5 | export function matchPsuedoSelector(cssSelector: string, vNode: VNode): boolean { 6 | const parent = vNode.parent || defaultParent 7 | const children = parent.children as Array 8 | 9 | if (cssSelector.indexOf(`:nth-child`) === 0) 10 | return matchNthChild(cssSelector.slice(11).split(')')[0], vNode) 11 | 12 | if (cssSelector.indexOf(`:contains`) === 0) 13 | return vNodeContainsText(cssSelector.slice(10).split(')')[0], vNode) 14 | 15 | switch (cssSelector) { 16 | case ':first-child': return children && children[0] === vNode 17 | case ':last-child': return children && children[children.length - 1] === vNode 18 | case ':empty': return !vNode.children || vNode.children.length === 0 19 | case ':root': return isRoot(vNode) 20 | default: return false 21 | } 22 | } 23 | 24 | function vNodeContainsText(text: string, vNode: VNode) { 25 | if (vNode.text) return text === vNode.text 26 | 27 | const children = vNode.children 28 | 29 | if (!children || children.length === 0) return false 30 | 31 | for (let i = 0; i < children.length; ++i) { 32 | const child = children[i] 33 | 34 | if (child.text === text) return true 35 | } 36 | 37 | return false 38 | } 39 | 40 | function isRoot(vNode: VNode) { 41 | return !vNode.parent 42 | } 43 | 44 | function matchNthChild(index: string, vNode: VNode) { 45 | const parent = vNode.parent || defaultParent 46 | const children = parent.children 47 | 48 | if (!children || children.length === 0) return false 49 | 50 | if (index.indexOf('+') === -1 && !isNaN(parseInt(index, 10))) 51 | return children[parseInt(index, 10)] === vNode 52 | 53 | const childIndex = children.indexOf(vNode) 54 | 55 | if (index === 'odd') 56 | return childIndex % 2 !== 0 57 | 58 | if (index === 'even') 59 | return childIndex % 2 === 0 60 | 61 | if (index.indexOf('+') > -1) { 62 | const [ multipleString, offsetString ] = index.split('+') 63 | const multiple = parseInt(multipleString.split('n')[0], 10) 64 | const offset = parseInt(offsetString, 10) 65 | 66 | if (multiple === 0) 67 | return true 68 | 69 | return childIndex !== 0 && childIndex % (multiple + offset) === 0 70 | } 71 | 72 | return false 73 | } 74 | 75 | function isNaN(x: number): x is number { 76 | return (x | 0) !== x 77 | } 78 | -------------------------------------------------------------------------------- /src/types/HtmlTagNames.ts: -------------------------------------------------------------------------------- 1 | export type HtmlTagNames = 2 | 'a' | 'abbr' | 'acronym' | 'address' | 'applet' | 'area' | 'article' | 3 | 'aside' | 'audio' | 'b' | 'base' | 'basefont' | 'bdi' | 'bdo' | 'bgsound' | 4 | 'big' | 'blink' | 'blockquote' | 'body' | 'br' | 'button' | 'canvas' | 5 | 'caption' | 'center' | 'cite' | 'code' | 'col' | 'colgroup' | 'command' | 6 | 'content' | 'data' | 'datalist' | 'dd' | 'del' | 'details' | 'dfn' | 'dialog' | 7 | 'dir' | 'div' | 'dl' | 'dt' | 'element' | 'em' | 'embed' | 'fieldset' | 8 | 'figcaption' | 'figure' | 'font' | 'footer' | 'form' | 'frame' | 'frameset' | 9 | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'head' | 'header' | 'hgroup' | 'hr' | 10 | 'html' | 'i' | 'iframe' | 'image' | 'img' | 'input' | 'ins' | 'isindex' | 11 | 'kbd' | 'keygen' | 'label' | 'legend' | 'li' | 'link' | 'listing' | 'main' | 12 | 'map' | 'mark' | 'marquee' | 'math' | 'menu' | 'menuitem' | 'meta' | 'meter' | 13 | 'multicol' | 'nav' | 'nextid' | 'nobr' | 'noembed' | 'noframes' | 14 | 'noscript' | 'object' | 'ol' | 'optgroup' | 'option' | 'output' | 'p' | 15 | 'param' | 'picture' | 'plaintext' | 'pre' | 'progress' | 'q' | 'rb' | 'rbc' | 16 | 'rp' | 'rt' | 'rtc' | 'ruby' | 's' | 'samp' | 'script' | 'section' | 'select' | 17 | 'shadow' | 'slot' | 'small' | 'source' | 'spacer' | 'span' | 'strike' | 'strong' | 18 | 'style' | 'sub' | 'summary' | 'sup' | 'table' | 'tbody' | 'td' | 'template' | 19 | 'textarea' | 'tfoot' | 'th' | 'thead' | 'time' | 'title' | 'tr' | 'track' | 20 | 'tt' | 'u' | 'ul' | 'video' | 'wbr' | 'xmp' 21 | 22 | export type SvgTagNames = 'svg' | 23 | 'a' | 'altGlyph' | 'altGlyphDef' | 'altGlyphItem' | 'animate' | 'animateColor' | 24 | 'animateMotion' | 'animateTransform' | 'circle' | 'clipPath' | 'colorProfile' | 25 | 'cursor' | 'defs' | 'desc' | 'ellipse' | 'feBlend' | 'feColorMatrix' | 26 | 'feComponentTransfer' | 'feComposite' | 'feConvolveMatrix' | 'feDiffuseLighting' | 27 | 'feDisplacementMap' | 'feDistantLight' | 'feFlood' | 'feFuncA' | 'feFuncB' | 28 | 'feFuncG' | 'feFuncR' | 'feGaussianBlur' | 'feImage' | 'feMerge' | 'feMergeNode' | 29 | 'feMorphology' | 'feOffset' | 'fePointLight' | 'feSpecularLighting' | 30 | 'feSpotlight' | 'feTile' | 'feTurbulence' | 'filter' | 'font' | 'fontFace' | 31 | 'fontFaceFormat' | 'fontFaceName' | 'fontFaceSrc' | 'fontFaceUri' | 32 | 'foreignObject' | 'g' | 'glyph' | 'glyphRef' | 'hkern' | 'image' | 'line' | 33 | 'linearGradient' | 'marker' | 'mask' | 'metadata' | 'missingGlyph' | 'mpath' | 34 | 'path' | 'pattern' | 'polygon' | 'polyline' | 'radialGradient' | 'rect' | 'script' | 35 | 'set' | 'stop' | 'style' | 'switch' | 'symbol' | 'text' | 'textPath' | 'title' | 36 | 'tref' | 'tspan' | 'use' | 'view' | 'vkern' 37 | -------------------------------------------------------------------------------- /src/modules/attributes.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, Module } from '../' 2 | 3 | import { BaseModule } from './BaseModule' 4 | import { emptyVNode } from './emptyVNode' 5 | 6 | export function createAttributesModule(): Module { 7 | return new AttributesModule() 8 | } 9 | 10 | const NAMESPACE_URIS = { 11 | xlink: 'http://www.w3.org/1999/xlink', 12 | } 13 | 14 | const booleanAttributes: Array = [ 15 | 'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 16 | 'controls', 'declare', 'default', 'defaultchecked', 'defaultmuted', 17 | 'defaultselected', 'defer', 'disabled', 'draggable', 'enabled', 18 | 'formnovalidate', 'hidden', 'indeterminate', 'inert', 'ismap', 'itemscope', 19 | 'loop', 'multiple', 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 20 | 'nowrap', 'open', 'pauseonexit', 'readonly', 'required', 'reversed', 'scoped', 21 | 'seamless', 'selected', 'sortable', 'spellcheck', 'translate', 'truespeed', 22 | 'typemustmatch', 'visible', 23 | ] 24 | 25 | const booleanAttributeDictionary: any = Object.create(null) 26 | 27 | for (let i = 0, count = booleanAttributes.length; i < count; i++) 28 | booleanAttributeDictionary[booleanAttributes[i]] = true 29 | 30 | // attributes module 31 | class AttributesModule extends BaseModule { 32 | public create(vNode: ElementVNode) { 33 | updateAttributes(emptyVNode, vNode) 34 | } 35 | 36 | public update(formerVNode: ElementVNode, vNode: ElementVNode) { 37 | updateAttributes(formerVNode, vNode) 38 | } 39 | } 40 | 41 | function updateAttributes(formerVNode: ElementVNode, vNode: ElementVNode) { 42 | let attributeValue: any 43 | let formerAttributeValue: any 44 | const element: Element = vNode.element 45 | let formerAttributes: any = formerVNode.props.attrs 46 | let attributes: any = vNode.props.attrs 47 | let attributeParts: Array 48 | 49 | if (!formerAttributes && !attributes) return 50 | 51 | formerAttributes = formerAttributes || {} 52 | attributes = attributes || {} 53 | 54 | for (const key in attributes) { 55 | attributeValue = attributes[key] 56 | formerAttributeValue = formerAttributes[key] 57 | 58 | if (formerAttributeValue !== attributeValue) { 59 | if (!attributeValue && booleanAttributeDictionary[key]) 60 | element.removeAttribute(key) 61 | 62 | else { 63 | attributeParts = key.split(':') 64 | 65 | if (attributeParts.length > 1 && NAMESPACE_URIS.hasOwnProperty(attributeParts[0])) 66 | element.setAttributeNS((NAMESPACE_URIS as any)[attributeParts[0]], key, attributeValue) 67 | 68 | else 69 | element.setAttribute(key, attributeValue) 70 | } 71 | } 72 | } 73 | 74 | for (const key in formerAttributes) 75 | if (!(key in attributes)) 76 | element.removeAttribute(key) 77 | } 78 | -------------------------------------------------------------------------------- /src/modules/ModuleCallbacks.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, Module, VNode } from '../' 2 | 3 | export class ModuleCallbacks implements Module { 4 | private _modules: Array 5 | private _moduleCount: number 6 | 7 | constructor(modules: Array) { 8 | this._modules = modules 9 | this._moduleCount = modules.length 10 | } 11 | 12 | public createRemoveElementFn(element: Element) { 13 | let listeners = this._moduleCount + 1 14 | 15 | return function removeElement() { 16 | if (--listeners === 0) { 17 | const parent = element.parentNode as Node 18 | parent.removeChild(element) 19 | } 20 | } 21 | } 22 | 23 | // module hooks 24 | public pre(vNode: VNode) { 25 | const modules = this._modules 26 | const moduleCount = this._moduleCount 27 | 28 | for (let i = 0; i < moduleCount; ++i) 29 | modules[i].pre(vNode) 30 | } 31 | 32 | public post(vNode: ElementVNode) { 33 | const modules = this._modules 34 | const moduleCount = this._moduleCount 35 | 36 | for (let i = 0; i < moduleCount; ++i) 37 | modules[i].post(vNode) 38 | } 39 | 40 | public init(vNode: VNode) { 41 | const modules = this._modules 42 | const moduleCount = this._moduleCount 43 | 44 | for (let i = 0; i < moduleCount; ++i) 45 | modules[i].init(vNode) 46 | } 47 | 48 | public create(vNode: ElementVNode) { 49 | const modules = this._modules 50 | const moduleCount = this._moduleCount 51 | 52 | for (let i = 0; i < moduleCount; ++i) 53 | modules[i].create(vNode) 54 | } 55 | 56 | public update(formerVNode: ElementVNode, vNode: ElementVNode) { 57 | const modules = this._modules 58 | const moduleCount = this._moduleCount 59 | 60 | for (let i = 0; i < moduleCount; ++i) 61 | modules[i].update(formerVNode, vNode) 62 | } 63 | 64 | public remove(vNode: ElementVNode, removeElement: () => void) { 65 | const modules = this._modules 66 | const moduleCount = this._moduleCount 67 | 68 | for (let i = 0; i < moduleCount; ++i) 69 | modules[i].remove(vNode, removeElement) 70 | } 71 | 72 | public destroy(vNode: ElementVNode) { 73 | const modules = this._modules 74 | const moduleCount = this._moduleCount 75 | 76 | for (let i = 0; i < moduleCount; ++i) 77 | modules[i].destroy(vNode) 78 | } 79 | 80 | public prepatch(formerVNode: VNode, vNode: VNode) { 81 | const modules = this._modules 82 | const moduleCount = this._moduleCount 83 | 84 | for (let i = 0; i < moduleCount; ++i) 85 | modules[i].prepatch(formerVNode, vNode) 86 | } 87 | 88 | public postpatch(formerVNode: ElementVNode, vNode: ElementVNode) { 89 | const modules = this._modules 90 | const moduleCount = this._moduleCount 91 | 92 | for (let i = 0; i < moduleCount; ++i) 93 | modules[i].postpatch(formerVNode, vNode) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/hyperscript/hasCssSelector/hasCssSelector.ts: -------------------------------------------------------------------------------- 1 | import { VNode } from '../../' 2 | import { matchesSelector } from './matchesSelector' 3 | 4 | // tslint:disable-next-line:cyclomatic-complexity 5 | export function hasCssSelector(cssSelector: string, vNode: VNode): boolean { 6 | cssSelector = cssSelector.trim() 7 | 8 | if (cssSelector === '*') 9 | return true 10 | 11 | if (cssSelector.indexOf(',') > -1) { 12 | const cssSelectors = cssSelector.split(',').map((str) => str.trim()) 13 | 14 | for (let i = 0; i < cssSelectors.length; ++i) 15 | if (hasCssSelector(cssSelectors[i], vNode)) 16 | return true 17 | 18 | return false 19 | } else if (cssSelector.indexOf('>') > -1) { 20 | const [ parentSelector, childSelector ] = splitByLastIndex(cssSelector, '>') 21 | 22 | if (!vNode.parent) 23 | return false 24 | 25 | return hasCssSelector(parentSelector, vNode.parent) && 26 | hasCssSelector(childSelector, vNode) 27 | } else if (cssSelector.indexOf(' + ') > -1) { 28 | const [ siblingSelector, selector ] = splitByLastIndex(cssSelector, '+') 29 | 30 | const parent = vNode.parent 31 | 32 | if (!parent || !hasCssSelector(selector, vNode)) 33 | return false 34 | 35 | const children = parent.children 36 | 37 | if (!children) return false 38 | 39 | const index = children.indexOf(vNode) 40 | 41 | if (index === 0 || !hasCssSelector(siblingSelector, children[index - 1])) 42 | return false 43 | 44 | return true 45 | } else if (cssSelector.indexOf(' ~ ') > -1) { 46 | const [ siblingSelector, selector ] = splitByLastIndex(cssSelector, '~') 47 | 48 | const parent = vNode.parent 49 | 50 | if (!parent || !hasCssSelector(selector, vNode)) 51 | return false 52 | 53 | const children = parent.children 54 | 55 | if (!children) return false 56 | 57 | const index = children.indexOf(vNode) 58 | 59 | if (index === 0) 60 | return false 61 | 62 | for (let i = 0; i < index; ++i) 63 | if (hasCssSelector(siblingSelector, children[i])) 64 | return true 65 | 66 | return false 67 | } else if (cssSelector.indexOf(' ') > -1) { 68 | const cssSelectors: Array = 69 | cssSelector.split(' ').filter(Boolean).map((str) => str.trim()) 70 | 71 | let i = cssSelectors.length - 1 72 | 73 | if (!hasCssSelector(cssSelectors[i], vNode)) 74 | return false 75 | 76 | while (--i >= 0) { 77 | const parentMatches = 78 | traverseParentVNodes((parent) => hasCssSelector(cssSelectors[i], parent), vNode) 79 | 80 | if (!parentMatches) 81 | return false 82 | } 83 | 84 | return true 85 | } 86 | 87 | return matchesSelector(cssSelector, vNode) 88 | } 89 | 90 | function splitByLastIndex(cssSelector: string, token: string): [string, string] { 91 | const index = cssSelector.lastIndexOf(token) 92 | 93 | return [ 94 | cssSelector.substring(0, index).trim(), 95 | cssSelector.substring(index + 1).trim(), 96 | ] 97 | } 98 | 99 | function traverseParentVNodes( 100 | predicate: (vNode: VNode) => boolean, 101 | vNode: VNode): boolean 102 | { 103 | const parent = vNode.parent 104 | 105 | if (!parent) return false 106 | 107 | if (predicate(parent)) 108 | return true 109 | 110 | return traverseParentVNodes(predicate, parent) 111 | } 112 | -------------------------------------------------------------------------------- /src/createElement.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import { ElementVNode, MostlyVNode, h } from './' 4 | 5 | import { ModuleCallbacks } from './modules/ModuleCallbacks' 6 | import { createElement } from './createElement' 7 | 8 | const moduleCallbacks = new ModuleCallbacks([]) 9 | 10 | describe('createElement', () => { 11 | it('creates an basic element from VNode', () => { 12 | const vNode = h('div') 13 | 14 | const element = createElement(vNode, moduleCallbacks, []).element 15 | 16 | assert.strictEqual((element as Element).tagName, 'DIV') 17 | }) 18 | 19 | it('creates an element with children', () => { 20 | const vNode = h('div', {}, [ 21 | h('h1', 'Hello'), 22 | h('h2', 'World'), 23 | ]) 24 | 25 | const element = createElement(vNode, moduleCallbacks, []).element as HTMLDivElement 26 | 27 | assert.strictEqual(element.children.length, 2) 28 | }) 29 | 30 | it('adds VNodes to given array if insert hook is present', () => { 31 | const vNode = h('div', { insert() {} }, []) 32 | 33 | const vNodeQueue: Array = [] 34 | 35 | createElement(vNode, moduleCallbacks, vNodeQueue) 36 | 37 | assert.strictEqual(vNodeQueue.length, 1) 38 | }) 39 | 40 | it('creates text elements', () => { 41 | const vNode = MostlyVNode.createText('hello') 42 | 43 | const text = createElement(vNode, moduleCallbacks, []).element as Text 44 | 45 | assert.strictEqual(text.nodeName, '#text') 46 | }) 47 | 48 | it('creates an svg element', () => { 49 | const vNode = h('svg', {}, []) 50 | 51 | assert.strictEqual(vNode.namespace, 'http://www.w3.org/2000/svg') 52 | 53 | const element = createElement(vNode, moduleCallbacks, []).element as SVGElement 54 | 55 | assert.strictEqual(element.namespaceURI, 'http://www.w3.org/2000/svg') 56 | }) 57 | 58 | describe('given a vNode with scope', () => { 59 | it('creates an element with data-mostly-dom-scope attribute', () => { 60 | const vNode = h('div', { scope: 'hello' }, []) 61 | 62 | const element = createElement(vNode, moduleCallbacks, []).element as Element 63 | 64 | assert.strictEqual(element.getAttribute('data-mostly-dom-scope'), 'hello') 65 | }) 66 | 67 | it('creates children elements with data-mostly-dom-scope attribute', () => { 68 | const vNode = h('div', { scope: 'hello' }, [ h('h1', {}, []) ]) 69 | 70 | const elementVNode = createElement(vNode, moduleCallbacks, []) 71 | 72 | if (!elementVNode.children) 73 | throw new Error(`VNode should have children`) 74 | 75 | const element = elementVNode.children[0].element 76 | 77 | assert.strictEqual(element.getAttribute('data-mostly-dom-scope'), 'hello') 78 | }) 79 | }) 80 | 81 | describe('hooks', () => { 82 | describe('init', () => { 83 | it('calls init hook if present', () => { 84 | let called = 0 85 | 86 | function init() { 87 | ++called 88 | } 89 | 90 | const vNode = h('div', { init }) 91 | 92 | createElement(vNode, moduleCallbacks, []) 93 | 94 | assert.strictEqual(called, 1) 95 | }) 96 | }) 97 | 98 | describe('create', () => { 99 | it('calls create hook if present', () => { 100 | let called = 0 101 | 102 | function create() { 103 | ++called 104 | } 105 | 106 | const vNode = h('div', { create }) 107 | 108 | createElement(vNode, moduleCallbacks, []) 109 | 110 | assert.strictEqual(called, 1) 111 | }) 112 | }) 113 | }) 114 | }) 115 | -------------------------------------------------------------------------------- /src/modules/styles.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode, Module } from '../' 2 | 3 | import { BaseModule } from './BaseModule' 4 | import { emptyVNode } from './emptyVNode' 5 | 6 | export function createStylesModule(): Module { 7 | return new StylesModule() 8 | } 9 | 10 | class StylesModule extends BaseModule { 11 | public pre() { 12 | setRequestAnimationFrame() 13 | } 14 | 15 | public create(vNode: ElementVNode) { 16 | updateStyle(emptyVNode, vNode) 17 | } 18 | 19 | public update(formerVNode: ElementVNode, vNode: ElementVNode) { 20 | updateStyle(formerVNode, vNode) 21 | } 22 | 23 | public remove(vNode: ElementVNode, removeElement: Function) { 24 | applyRemoveStyle(vNode, removeElement) 25 | } 26 | 27 | public destroy(vNode: ElementVNode) { 28 | applyDestroyStyle(vNode) 29 | } 30 | } 31 | 32 | let requestAnimationFrame: any 33 | 34 | function setRequestAnimationFrame() { 35 | if (!requestAnimationFrame) 36 | requestAnimationFrame = 37 | (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout 38 | } 39 | 40 | function nextFrame(fn: any) { 41 | requestAnimationFrame(function() { 42 | requestAnimationFrame(fn) 43 | }) 44 | } 45 | 46 | function setValueOnNextFrame(obj: any, prop: string, value: any) { 47 | nextFrame(function() { 48 | obj[prop] = value 49 | }) 50 | } 51 | 52 | function updateStyle(formerVNode: ElementVNode, vNode: ElementVNode): void { 53 | let styleValue: any 54 | const element: any = vNode.element 55 | let formerStyle: any = formerVNode.props.style 56 | let style: any = vNode.props.style 57 | 58 | if (!formerStyle && !style) return 59 | 60 | formerStyle = formerStyle || {} 61 | style = style || {} 62 | 63 | const formerHasDelayedProperty: boolean = 64 | !!formerStyle.delayed 65 | 66 | // tslint:disable-next-line:prefer-const 67 | for (let key in formerStyle) 68 | if (!style[key]) 69 | element.style[key] = '' 70 | 71 | for (const key in style) { 72 | styleValue = style[key] 73 | 74 | if (key === 'delayed') { 75 | // tslint:disable-next-line:prefer-const 76 | for (let delayKey in style.delayed) { 77 | styleValue = style.delayed[delayKey] 78 | 79 | if (!formerHasDelayedProperty || styleValue !== formerStyle.delayed[delayKey]) 80 | setValueOnNextFrame((element as any).style, delayKey, styleValue) 81 | } 82 | } else if (key !== 'remove') { 83 | element.style[key] = styleValue 84 | } 85 | } 86 | } 87 | 88 | function applyDestroyStyle(vNode: ElementVNode) { 89 | const element: any = vNode.element 90 | const style: any = vNode.props.style 91 | 92 | if (!style || !style.destroy) return 93 | 94 | const destroy: any = style.destroy 95 | 96 | for (const key in destroy) 97 | element.style[key] = destroy[key] 98 | } 99 | 100 | function applyRemoveStyle(vNode: ElementVNode, callback: Function) { 101 | const style: any = vNode.props.style 102 | 103 | if (!style || !style.remove) 104 | return callback() 105 | 106 | const element: any = vNode.element 107 | let index = 0 108 | let computedStyle: any 109 | let listenerCount = 0 110 | const appliedStyles: Array = [] 111 | 112 | for (const key in style) { 113 | appliedStyles.push(key) 114 | element.style[key] = style[key] 115 | } 116 | 117 | computedStyle = getComputedStyle(element) 118 | 119 | const transitionProperties: Array = 120 | computedStyle['transition-property'].split(', ') 121 | 122 | for (; index < transitionProperties.length; ++index) 123 | if (appliedStyles.indexOf(transitionProperties[index]) !== -1) 124 | listenerCount++ 125 | 126 | element.addEventListener('transitionend', function(event: TransitionEvent) { 127 | if (event.target === element) 128 | --listenerCount 129 | 130 | if (listenerCount === 0) 131 | callback() 132 | }) 133 | } 134 | 135 | export { 136 | setRequestAnimationFrame as pre, 137 | updateStyle as create, 138 | updateStyle as update, 139 | applyDestroyStyle as destroy, 140 | applyRemoveStyle as remove, 141 | } 142 | -------------------------------------------------------------------------------- /src/hyperscript/h.ts: -------------------------------------------------------------------------------- 1 | import { HtmlTagNames, SvgTagNames, VNode, VNodeProps } from '../types' 2 | import { MostlyVNode, addSvgNamespace } from './VNode' 3 | import { isPrimitive, isString } from '../helpers' 4 | 5 | export const h: HyperscriptFn = function(): VNode { 6 | const tagName: string | ComponentFn = arguments[0] // required 7 | const childrenOrText: HyperscriptChildren = arguments[2] // optional 8 | 9 | let props: VNodeProps = {} 10 | let children: ArrayLike | undefined 11 | let text: string | undefined 12 | 13 | if (childrenOrText) { 14 | props = arguments[1] 15 | 16 | if (isArrayLike(childrenOrText)) children = flattenArrayLike(childrenOrText) as Array 17 | else if (isPrimitive(childrenOrText)) text = String(childrenOrText) 18 | } else if (arguments[1]) { 19 | const childrenOrTextOrProps = arguments[1] 20 | 21 | if (isArrayLike(childrenOrTextOrProps)) 22 | children = flattenArrayLike(childrenOrTextOrProps) as Array 23 | else if (isPrimitive(childrenOrTextOrProps)) text = String(childrenOrTextOrProps) 24 | else props = childrenOrTextOrProps 25 | } 26 | 27 | if (typeof tagName === 'function') { 28 | return tagName(props, Array.isArray(children) ? children : text ? [ text ] : []) 29 | } 30 | 31 | const isSvg = tagName === 'svg' 32 | 33 | const vNode = isSvg 34 | ? MostlyVNode.createSvg(tagName, props, undefined, text) 35 | : MostlyVNode.create(tagName, props, undefined, text) 36 | 37 | if (Array.isArray(children)) vNode.children = sanitizeChildren(children, vNode) 38 | 39 | if (isSvg) addSvgNamespace(vNode) 40 | 41 | return vNode 42 | } 43 | 44 | function isArrayLike(x: any): x is ArrayLike { 45 | const typeOf = typeof x 46 | 47 | return x && typeof x.length === 'number' && typeOf !== 'function' && typeOf !== 'string' 48 | } 49 | 50 | function flattenArrayLike(arrayLike: ArrayLike>, arr: Array = []): Array { 51 | forEach( 52 | (x: A | ArrayLike) => (isArrayLike(x) ? flattenArrayLike(x, arr) : arr.push(x)), 53 | arrayLike 54 | ) 55 | 56 | return arr 57 | } 58 | 59 | function forEach(fn: (value: A) => void, list: ArrayLike): void { 60 | for (let i = 0; i < list.length; ++i) fn(list[i]) 61 | } 62 | 63 | function sanitizeChildren(childrenOrText: Array, parent: VNode): Array { 64 | childrenOrText = childrenOrText.filter(Boolean) // remove possible null values 65 | const childCount: number = childrenOrText.length 66 | 67 | const children: Array = Array(childCount) 68 | 69 | for (let i = 0; i < childCount; ++i) { 70 | const vNodeOrText = childrenOrText[i] 71 | 72 | if (isString(vNodeOrText)) children[i] = MostlyVNode.createText(vNodeOrText) 73 | else children[i] = vNodeOrText 74 | 75 | if (parent.scope && !children[i].scope) children[i].scope = parent.scope 76 | 77 | children[i].parent = parent as VNode 78 | } 79 | 80 | return children 81 | } 82 | 83 | export type VNodeChildren = string | number | null | VNode 84 | export type HyperscriptChildren = 85 | | VNodeChildren 86 | | ArrayLike 87 | | ArrayLike> 88 | | ArrayLike> 89 | 90 | export interface ComponentFn { 91 | (props: VNodeProps, children: Array): VNode 92 | } 93 | 94 | export type ValidTagNames = HtmlTagNames | SvgTagNames | ComponentFn 95 | 96 | export interface HyperscriptFn { 97 | (tagName: ValidTagNames): VNode 98 | (tagName: ValidTagNames, props: VNodeProps): VNode 99 | (tagName: ValidTagNames, children: HyperscriptChildren): VNode 100 | (tagName: ValidTagNames, props: VNodeProps, children: HyperscriptChildren): VNode 101 | 102 | = VNodeProps>( 103 | tagName: ValidTagNames 104 | ): VNode 105 | = VNodeProps>( 106 | tagName: ValidTagNames, 107 | props: Props 108 | ): VNode 109 | = VNodeProps>( 110 | tagName: ValidTagNames, 111 | children: HyperscriptChildren 112 | ): VNode 113 | 114 | = VNodeProps>( 115 | tagName: ValidTagNames, 116 | props: Props, 117 | children: HyperscriptChildren 118 | ): VNode 119 | } 120 | -------------------------------------------------------------------------------- /src/hyperscript/h.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import { VNode } from '../types' 4 | import { h } from './h' 5 | 6 | describe('h', () => { 7 | 8 | describe('given a selector representing a tagName', () => { 9 | it('returns a vNode with tagName property of same value', () => { 10 | const tagName = 'div' 11 | 12 | const vNode = h(tagName) 13 | 14 | assert.strictEqual(vNode.tagName, tagName) 15 | }) 16 | }) 17 | 18 | describe('given a props object as second parameter', () => { 19 | it('correctly sets vNode.props', () => { 20 | const props = {} 21 | 22 | const vNode = h('div', props) 23 | 24 | assert.strictEqual(vNode.props, props) 25 | }) 26 | }) 27 | 28 | describe('given children as second parameter', () => { 29 | it('correctly sets vNode.children', () => { 30 | const children: Array = [ h('div') ] 31 | 32 | const vNode = h('div', children) 33 | 34 | assert.deepEqual(vNode.children, children) 35 | }) 36 | 37 | it('sets parent property on children to parent vNode', () => { 38 | const children: Array = [ h('div') ] 39 | 40 | const vNode = h('div', children) 41 | 42 | if (!vNode.children) 43 | throw new Error(`VNode should have children`) 44 | 45 | assert.strictEqual(vNode.children[0].parent, vNode) 46 | }) 47 | }) 48 | 49 | describe('given text as second parameter', () => { 50 | it('correctly sets vNode.text', () => { 51 | const text = 'hello' 52 | 53 | const vNode = h('div', text) 54 | 55 | assert.strictEqual(vNode.text, text) 56 | }) 57 | }) 58 | 59 | describe('given a selector props and children', () => { 60 | it('correctly sets children property', () => { 61 | const vNodeChildren = [ h('h1', 'Hi') ] 62 | const vNode = h('div', {}, vNodeChildren) 63 | 64 | assert.deepEqual(vNode.children, vNodeChildren) 65 | }) 66 | 67 | it('converts text to VNodes', () => { 68 | const vNodeChildren = [ 'hi' ] 69 | const vNode = h('div', {}, vNodeChildren) 70 | 71 | if (!vNode.children) 72 | throw new Error(`VNode should have children`) 73 | 74 | assert.deepEqual(vNode.children[0].text, 'hi') 75 | }) 76 | }) 77 | 78 | describe('given a selector, props, and text', () => { 79 | it('sets the text property', () => { 80 | const vNode = h('h1', {}, 'Hi') 81 | 82 | assert.strictEqual(vNode.text, 'Hi') 83 | }) 84 | }) 85 | 86 | describe('given a selector, props, and a number', () => { 87 | it('sets the text property', () => { 88 | const vNode = h('h1', {}, 4) 89 | 90 | assert.strictEqual(vNode.text, '4') 91 | }) 92 | }) 93 | 94 | describe('given svg tagName', () => { 95 | it('creates an vNode with SVG namespace', () => { 96 | const vNode = h('svg') 97 | 98 | assert.ok(vNode.namespace && vNode.namespace.indexOf('svg') > -1) 99 | }) 100 | 101 | it('sets the namespace on children', () => { 102 | const vNode = h('svg', {}, [ h('g', {}, []) ]) 103 | 104 | if (!vNode.children) 105 | throw new Error(`VNode should have children`) 106 | 107 | const child = vNode.children[0] 108 | 109 | assert.ok(child.namespace && child.namespace.indexOf('svg') > -1) 110 | }) 111 | 112 | it('does not set namespace under a foreignObject', () => { 113 | const foreignObject = h('foreignObject') 114 | const child = h('g', {}, []) 115 | const vNode = h('svg', {}, [ child, foreignObject ]) 116 | 117 | Function.prototype(vNode) 118 | 119 | assert.ok(child.namespace && child.namespace.indexOf('svg') > -1) 120 | assert.ok(!foreignObject.namespace) 121 | }) 122 | }) 123 | 124 | describe('given a scope', () => { 125 | it('adds scope to children', () => { 126 | const vNode = h('div', { scope: 'hello' }, [ h('h1', 'Hello') ]) 127 | 128 | if (!vNode.children) 129 | throw new Error(`VNode should have children`) 130 | 131 | assert.strictEqual(vNode.scope, 'hello') 132 | assert.strictEqual(vNode.children[0].scope, 'hello') 133 | }) 134 | 135 | it('does not overwrite nested scopes', () => { 136 | const vNode = h('div', { scope: 'hello' }, [ h('h1', { scope: 'other' }, 'Hello') ]) 137 | 138 | if (!vNode.children) 139 | throw new Error(`VNode should have children`) 140 | 141 | assert.strictEqual(vNode.scope, 'hello') 142 | assert.strictEqual(vNode.children[0].scope, 'other') 143 | }) 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /src/updateChildren.ts: -------------------------------------------------------------------------------- 1 | import { ElementVNode } from './types' 2 | import { ModuleCallbacks } from './modules/ModuleCallbacks' 3 | import { addVNodes } from './addVNodes' 4 | import { createElement } from './createElement' 5 | import { patchVNode } from './patchVNode' 6 | import { removeVNodes } from './removeVNodes' 7 | import { vNodesAreEqual } from './helpers' 8 | 9 | export function updateChildren( 10 | parentElement: Element, 11 | formerChildren: Array, 12 | children: Array, 13 | moduleCallbacks: ModuleCallbacks, 14 | insertedVNodeQueue: Array) 15 | { 16 | // indexes 17 | let formerStartIndex = 0 18 | let startIndex = 0 19 | let formerEndIndex = formerChildren.length - 1 20 | let endIndex = children.length - 1 21 | 22 | // VNodes 23 | let formerStartVNode = formerChildren[formerStartIndex] 24 | let startVNode = children[startIndex] 25 | let formerEndVNode = formerChildren[formerEndIndex] 26 | let endVNode = children[endIndex] 27 | 28 | // an object mapping keys to indexes in formerChildren array 29 | let mappedKeyToFormerIndex: any 30 | 31 | while (formerStartIndex <= formerEndIndex && startIndex <= endIndex) { 32 | if (!formerStartVNode) 33 | formerStartVNode = formerChildren[++formerStartIndex] 34 | 35 | else if (!formerEndVNode) 36 | formerEndVNode = formerChildren[--formerEndIndex] 37 | 38 | else if (vNodesAreEqual(formerStartVNode, startVNode)) { 39 | patchVNode(formerStartVNode, startVNode, moduleCallbacks, insertedVNodeQueue) 40 | formerStartVNode = formerChildren[++formerStartIndex] 41 | startVNode = children[++startIndex] 42 | } 43 | 44 | else if (vNodesAreEqual(formerEndVNode, endVNode)) { 45 | patchVNode(formerEndVNode, endVNode, moduleCallbacks, insertedVNodeQueue) 46 | formerEndVNode = formerChildren[--formerEndIndex] 47 | endVNode = children[--endIndex] 48 | } 49 | 50 | else if (vNodesAreEqual(formerStartVNode, endVNode)) { 51 | patchVNode(formerStartVNode, endVNode, moduleCallbacks, insertedVNodeQueue) 52 | parentElement.insertBefore(formerStartVNode.element, formerEndVNode.element.nextSibling) 53 | formerStartVNode = formerChildren[++formerStartIndex] 54 | endVNode = children[--endIndex] 55 | } 56 | 57 | else if (vNodesAreEqual(formerEndVNode, startVNode)) { 58 | patchVNode(formerEndVNode, startVNode, moduleCallbacks, insertedVNodeQueue) 59 | parentElement.insertBefore(formerEndVNode.element, formerStartVNode.element) 60 | formerEndVNode = formerChildren[--formerEndIndex] 61 | startVNode = children[++startIndex] 62 | } 63 | 64 | else { 65 | if (!mappedKeyToFormerIndex) mappedKeyToFormerIndex = 66 | mapKeyToFormerIndex(formerChildren, formerStartIndex, formerEndIndex) 67 | 68 | const formerIndexKey = mappedKeyToFormerIndex[startVNode.key as string | number] 69 | 70 | if (!formerIndexKey) { // new element 71 | const element = createElement(startVNode, moduleCallbacks, insertedVNodeQueue).element 72 | parentElement.insertBefore(element, formerStartVNode.element) 73 | startVNode = children[++startIndex] 74 | } 75 | 76 | else { 77 | const reorderableVNode = formerChildren[formerIndexKey] 78 | 79 | patchVNode(reorderableVNode, startVNode, moduleCallbacks, insertedVNodeQueue) 80 | 81 | // WARNING: hack for performance optimization 82 | formerChildren[formerIndexKey] = void 0 as any 83 | 84 | parentElement.insertBefore(reorderableVNode.element, formerStartVNode.element) 85 | 86 | startVNode = children[++startIndex] 87 | } 88 | 89 | } 90 | } 91 | 92 | if (formerStartIndex > formerEndIndex) { 93 | const referenceNode = children[endIndex + 1] ? children[endIndex + 1].element : null 94 | 95 | addVNodes( 96 | parentElement, referenceNode, 97 | children, startIndex, endIndex, 98 | moduleCallbacks, insertedVNodeQueue, 99 | ) 100 | } 101 | else if (startIndex > endIndex) 102 | removeVNodes(parentElement, formerChildren, formerStartIndex, formerEndIndex, moduleCallbacks) 103 | } 104 | 105 | function mapKeyToFormerIndex( 106 | children: Array, 107 | startIndex: number, 108 | endIndex: number): any 109 | { 110 | let index: number = startIndex 111 | const map: any = {} 112 | let key: any 113 | 114 | for (; index <= endIndex; ++index) { 115 | key = children[index].key 116 | 117 | if (key) 118 | map[key] = index 119 | } 120 | 121 | return map 122 | } 123 | -------------------------------------------------------------------------------- /src/hyperscript/svg-helpers.ts: -------------------------------------------------------------------------------- 1 | import { HyperscriptChildren, h } from './h' 2 | import { SvgTagNames, VNode, VNodeProps } from '../' 3 | 4 | export interface SvgHyperscriptHelperFn { 5 | (): VNode 6 | (data: VNodeProps): VNode 7 | (data: VNodeProps, children: HyperscriptChildren): VNode 8 | (children: HyperscriptChildren): VNode 9 | } 10 | 11 | function hh (tagName: SvgTagNames): SvgHyperscriptHelperFn { 12 | return function(): VNode { 13 | const data = arguments[0] 14 | const children = arguments[1] 15 | 16 | if (Array.isArray(data)) 17 | return h(tagName, {}, data) 18 | 19 | if (typeof data === 'object') 20 | return h(tagName, data, children) 21 | 22 | return h(tagName, {}) 23 | } 24 | } 25 | 26 | function isValidString(param: any): boolean { 27 | return typeof param === 'string' && param.length > 0 28 | } 29 | 30 | function isSelector(param: any): boolean { 31 | return isValidString(param) && (param[0] === '.' || param[0] === '#') 32 | } 33 | 34 | export interface SVGHelperFn extends SvgHyperscriptHelperFn { 35 | a: SvgHyperscriptHelperFn 36 | altGlyph: SvgHyperscriptHelperFn 37 | altGlyphDef: SvgHyperscriptHelperFn 38 | altGlyphItem: SvgHyperscriptHelperFn 39 | animate: SvgHyperscriptHelperFn 40 | animateColor: SvgHyperscriptHelperFn 41 | animateMotion: SvgHyperscriptHelperFn 42 | animateTransform: SvgHyperscriptHelperFn 43 | circle: SvgHyperscriptHelperFn 44 | clipPath: SvgHyperscriptHelperFn 45 | colorProfile: SvgHyperscriptHelperFn 46 | cursor: SvgHyperscriptHelperFn 47 | defs: SvgHyperscriptHelperFn 48 | desc: SvgHyperscriptHelperFn 49 | ellipse: SvgHyperscriptHelperFn 50 | feBlend: SvgHyperscriptHelperFn 51 | feColorMatrix: SvgHyperscriptHelperFn 52 | feComponentTransfer: SvgHyperscriptHelperFn 53 | feComposite: SvgHyperscriptHelperFn 54 | feConvolveMatrix: SvgHyperscriptHelperFn 55 | feDiffuseLighting: SvgHyperscriptHelperFn 56 | feDisplacementMap: SvgHyperscriptHelperFn 57 | feDistantLight: SvgHyperscriptHelperFn 58 | feFlood: SvgHyperscriptHelperFn 59 | feFuncA: SvgHyperscriptHelperFn 60 | feFuncB: SvgHyperscriptHelperFn 61 | feFuncG: SvgHyperscriptHelperFn 62 | feFuncR: SvgHyperscriptHelperFn 63 | feGaussianBlur: SvgHyperscriptHelperFn 64 | feImage: SvgHyperscriptHelperFn 65 | feMerge: SvgHyperscriptHelperFn 66 | feMergeNode: SvgHyperscriptHelperFn 67 | feMorphology: SvgHyperscriptHelperFn 68 | feOffset: SvgHyperscriptHelperFn 69 | fePointLight: SvgHyperscriptHelperFn 70 | feSpecularLighting: SvgHyperscriptHelperFn 71 | feSpotlight: SvgHyperscriptHelperFn 72 | feTile: SvgHyperscriptHelperFn 73 | feTurbulence: SvgHyperscriptHelperFn 74 | filter: SvgHyperscriptHelperFn 75 | font: SvgHyperscriptHelperFn 76 | fontFace: SvgHyperscriptHelperFn 77 | fontFaceFormat: SvgHyperscriptHelperFn 78 | fontFaceName: SvgHyperscriptHelperFn 79 | fontFaceSrc: SvgHyperscriptHelperFn 80 | fontFaceUri: SvgHyperscriptHelperFn 81 | foreignObject: SvgHyperscriptHelperFn 82 | g: SvgHyperscriptHelperFn 83 | glyph: SvgHyperscriptHelperFn 84 | glyphRef: SvgHyperscriptHelperFn 85 | hkern: SvgHyperscriptHelperFn 86 | image: SvgHyperscriptHelperFn 87 | line: SvgHyperscriptHelperFn 88 | linearGradient: SvgHyperscriptHelperFn 89 | marker: SvgHyperscriptHelperFn 90 | mask: SvgHyperscriptHelperFn 91 | metadata: SvgHyperscriptHelperFn 92 | missingGlyph: SvgHyperscriptHelperFn 93 | mpath: SvgHyperscriptHelperFn 94 | path: SvgHyperscriptHelperFn 95 | pattern: SvgHyperscriptHelperFn 96 | polygon: SvgHyperscriptHelperFn 97 | polyline: SvgHyperscriptHelperFn 98 | radialGradient: SvgHyperscriptHelperFn 99 | rect: SvgHyperscriptHelperFn 100 | script: SvgHyperscriptHelperFn 101 | set: SvgHyperscriptHelperFn 102 | stop: SvgHyperscriptHelperFn 103 | style: SvgHyperscriptHelperFn 104 | switch: SvgHyperscriptHelperFn 105 | symbol: SvgHyperscriptHelperFn 106 | text: SvgHyperscriptHelperFn 107 | textPath: SvgHyperscriptHelperFn 108 | title: SvgHyperscriptHelperFn 109 | tref: SvgHyperscriptHelperFn 110 | tspan: SvgHyperscriptHelperFn 111 | use: SvgHyperscriptHelperFn 112 | view: SvgHyperscriptHelperFn 113 | vkern: SvgHyperscriptHelperFn 114 | } 115 | 116 | function createSVGHelper(): SVGHelperFn { 117 | // tslint:disable:no-shadowed-variable 118 | const svg: any = hh('svg') 119 | 120 | svg.a = hh('a') 121 | svg.altGlyph = hh('altGlyph') 122 | svg.altGlyphDef = hh('altGlyphDef') 123 | svg.altGlyphItem = hh('altGlyphItem') 124 | svg.animate = hh('animate') 125 | svg.animateColor = hh('animateColor') 126 | svg.animateMotion = hh('animateMotion') 127 | svg.animateTransform = hh('animateTransform') 128 | svg.circle = hh('circle') 129 | svg.clipPath = hh('clipPath') 130 | svg.colorProfile = hh('colorProfile') 131 | svg.cursor = hh('cursor') 132 | svg.defs = hh('defs') 133 | svg.desc = hh('desc') 134 | svg.ellipse = hh('ellipse') 135 | svg.feBlend = hh('feBlend') 136 | svg.feColorMatrix = hh('feColorMatrix') 137 | svg.feComponentTransfer = hh('feComponentTransfer') 138 | svg.feComposite = hh('feComposite') 139 | svg.feConvolveMatrix = hh('feConvolveMatrix') 140 | svg.feDiffuseLighting = hh('feDiffuseLighting') 141 | svg.feDisplacementMap = hh('feDisplacementMap') 142 | svg.feDistantLight = hh('feDistantLight') 143 | svg.feFlood = hh('feFlood') 144 | svg.feFuncA = hh('feFuncA') 145 | svg.feFuncB = hh('feFuncB') 146 | svg.feFuncG = hh('feFuncG') 147 | svg.feFuncR = hh('feFuncR') 148 | svg.feGaussianBlur = hh('feGaussianBlur') 149 | svg.feImage = hh('feImage') 150 | svg.feMerge = hh('feMerge') 151 | svg.feMergeNode = hh('feMergeNode') 152 | svg.feMorphology = hh('feMorphology') 153 | svg.feOffset = hh('feOffset') 154 | svg.fePointLight = hh('fePointLight') 155 | svg.feSpecularLighting = hh('feSpecularLighting') 156 | svg.feSpotlight = hh('feSpotlight') 157 | svg.feTile = hh('feTile') 158 | svg.feTurbulence = hh('feTurbulence') 159 | svg.filter = hh('filter') 160 | svg.font = hh('font') 161 | svg.fontFace = hh('fontFace') 162 | svg.fontFaceFormat = hh('fontFaceFormat') 163 | svg.fontFaceName = hh('fontFaceName') 164 | svg.fontFaceSrc = hh('fontFaceSrc') 165 | svg.fontFaceUri = hh('fontFaceUri') 166 | svg.foreignObject = hh('foreignObject') 167 | svg.g = hh('g') 168 | svg.glyph = hh('glyph') 169 | svg.glyphRef = hh('glyphRef') 170 | svg.hkern = hh('hkern') 171 | svg.image = hh('image') 172 | svg.linearGradient = hh('linearGradient') 173 | svg.marker = hh('marker') 174 | svg.mask = hh('mask') 175 | svg.metadata = hh('metadata') 176 | svg.missingGlyph = hh('missingGlyph') 177 | svg.mpath = hh('mpath') 178 | svg.path = hh('path') 179 | svg.pattern = hh('pattern') 180 | svg.polygon = hh('polygon') 181 | svg.polyline = hh('polyline') 182 | svg.radialGradient = hh('radialGradient') 183 | svg.rect = hh('rect') 184 | svg.script = hh('script') 185 | svg.set = hh('set') 186 | svg.stop = hh('stop') 187 | svg.style = hh('style') 188 | svg.switch = hh('switch') 189 | svg.symbol = hh('symbol') 190 | svg.text = hh('text') 191 | svg.textPath = hh('textPath') 192 | svg.title = hh('title') 193 | svg.tref = hh('tref') 194 | svg.tspan = hh('tspan') 195 | svg.use = hh('use') 196 | svg.view = hh('view') 197 | svg.vkern = hh('vkern') 198 | 199 | return svg as SVGHelperFn 200 | } 201 | 202 | export const svg: SVGHelperFn = createSVGHelper() 203 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # [6.0.0](https://github.com/TylorS/mostly-dom/compare/v5.0.0...v6.0.0) (2018-10-22) 3 | 4 | 5 | ### Features 6 | 7 | * upgrade to typescript 3.1.1 ([21923a6](https://github.com/TylorS/mostly-dom/commit/21923a6)) 8 | 9 | 10 | 11 | 12 | # [5.0.0](https://github.com/TylorS/mostly-dom/compare/v4.4.0...v5.0.0) (2018-03-06) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * ensure importing from correct directory ([fe041df](https://github.com/TylorS/mostly-dom/commit/fe041df)) 18 | * use Element instead of Node ([0e3aebd](https://github.com/TylorS/mostly-dom/commit/0e3aebd)) 19 | * use undefined ([aeae37e](https://github.com/TylorS/mostly-dom/commit/aeae37e)) 20 | 21 | 22 | ### Features 23 | 24 | * allow extending props to skip and attributes to remove ([22d3c5b](https://github.com/TylorS/mostly-dom/commit/22d3c5b)) 25 | * JSX support, target -> currentTarget, no default modules ([50aab85](https://github.com/TylorS/mostly-dom/commit/50aab85)) 26 | * remove dependencies ([e505cb3](https://github.com/TylorS/mostly-dom/commit/e505cb3)) 27 | * upgrade CSS types ([183e9d3](https://github.com/TylorS/mostly-dom/commit/183e9d3)) 28 | * upgrade to typescript 2.7 + strict mode ([f2f5b09](https://github.com/TylorS/mostly-dom/commit/f2f5b09)) 29 | 30 | 31 | 32 | 33 | # [4.4.0](https://github.com/TylorS/mostly-dom/compare/v4.3.0...v4.4.0) (2018-03-06) 34 | 35 | 36 | 37 | 38 | # [4.3.0](https://github.com/TylorS/mostly-dom/compare/v4.2.0...v4.3.0) (2017-09-02) 39 | 40 | 41 | ### Features 42 | 43 | * **HtmlProperties:** convert VNodeProperties from type alias to interface ([7b7ebdb](https://github.com/TylorS/mostly-dom/commit/7b7ebdb)) 44 | 45 | 46 | 47 | 48 | # [4.2.0](https://github.com/TylorS/mostly-dom/compare/v4.1.0...v4.2.0) (2017-08-29) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * **props:** fix props module ([f69be9c](https://github.com/TylorS/mostly-dom/commit/f69be9c)) 54 | 55 | 56 | 57 | 58 | # [4.1.0](https://github.com/TylorS/mostly-dom/compare/v4.0.0...v4.1.0) (2017-08-24) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * **updateElement:** remove previous className and id from element ([b13051c](https://github.com/TylorS/mostly-dom/commit/b13051c)) 64 | 65 | 66 | 67 | 68 | # [4.0.0](https://github.com/TylorS/mostly-dom/compare/v3.6.2...v4.0.0) (2017-08-18) 69 | 70 | 71 | ### Features 72 | 73 | * **hyperscript:** remove the ability to define className/id in selector ([f61c008](https://github.com/TylorS/mostly-dom/commit/f61c008)) 74 | * **modules:** expose module factory functions ([f2cd4cb](https://github.com/TylorS/mostly-dom/commit/f2cd4cb)) 75 | 76 | 77 | ### BREAKING CHANGES 78 | 79 | * **hyperscript:** h and various hyperscript-helpers no longer accept a className/id selector 80 | * **modules:** No longer are the modules concatenated, but instead they can be overridden. 81 | before init required an array of modules, but now it is optional. To continue 82 | having the same behavior as before use init() with no arguments 83 | 84 | 85 | 86 | 87 | ## [3.6.2](https://github.com/TylorS/mostly-dom/compare/v3.6.0...v3.6.2) (2017-08-07) 88 | 89 | 90 | ### Bug Fixes 91 | 92 | * **updateElement:** correctly remove previous properties ([06456fd](https://github.com/TylorS/mostly-dom/commit/06456fd)) 93 | 94 | 95 | 96 | 97 | # [3.5.0](https://github.com/TylorS/mostly-dom/compare/v3.4.0...v3.5.0) (2017-06-22) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * **hasCssSelector:** support className and id set using props ([1e26434](https://github.com/TylorS/mostly-dom/commit/1e26434)) 103 | 104 | 105 | 106 | 107 | # [3.4.0](https://github.com/TylorS/mostly-dom/compare/v3.3.0...v3.4.0) (2017-06-09) 108 | 109 | 110 | 111 | 112 | # [3.3.0](https://github.com/TylorS/mostly-dom/compare/v3.2.0...v3.3.0) (2017-06-09) 113 | 114 | 115 | ### Bug Fixes 116 | 117 | * **hh:** missing import ([e06d256](https://github.com/TylorS/mostly-dom/commit/e06d256)) 118 | 119 | 120 | ### Features 121 | 122 | * **events:** make each event optional ([c45dd7c](https://github.com/TylorS/mostly-dom/commit/c45dd7c)) 123 | * **modules:** implement events module ([f34a97c](https://github.com/TylorS/mostly-dom/commit/f34a97c)) 124 | * **props:** skip over on property for props ([00d7fe9](https://github.com/TylorS/mostly-dom/commit/00d7fe9)) 125 | * **types:** parameterize each hh function with event types ([49d9f32](https://github.com/TylorS/mostly-dom/commit/49d9f32)) 126 | 127 | 128 | 129 | 130 | # [3.2.0](https://github.com/TylorS/mostly-dom/compare/v3.1.0...v3.2.0) (2017-06-08) 131 | 132 | 133 | ### Bug Fixes 134 | 135 | * **src:** cleanup tslint error ([03ee4e3](https://github.com/TylorS/mostly-dom/commit/03ee4e3)) 136 | * **types:** add slot to tagnames ([62d0e7c](https://github.com/TylorS/mostly-dom/commit/62d0e7c)) 137 | 138 | 139 | 140 | 141 | # [3.1.0](https://github.com/TylorS/mostly-dom/compare/v3.0.0...v3.1.0) (2017-05-30) 142 | 143 | 144 | ### Features 145 | 146 | * **hyperscript:** export parseSelector function ([1fdf3d0](https://github.com/TylorS/mostly-dom/commit/1fdf3d0)) 147 | 148 | 149 | 150 | 151 | # [3.0.0](https://github.com/TylorS/mostly-dom/compare/v2.1.1...v3.0.0) (2017-05-30) 152 | 153 | 154 | ### Features 155 | 156 | * **types:** use VNode with default type parameters ([40ac337](https://github.com/TylorS/mostly-dom/commit/40ac337)) 157 | 158 | 159 | ### BREAKING CHANGES 160 | 161 | * **types:** deletes VirtualNode and ElementVirtualNode types 162 | 163 | 164 | 165 | 166 | ## [2.1.1](https://github.com/TylorS/mostly-dom/compare/v2.1.0...v2.1.1) (2017-05-08) 167 | 168 | 169 | ### Bug Fixes 170 | 171 | * **types:** correct HTMLTitleElementProperties ([3384818](https://github.com/TylorS/mostly-dom/commit/3384818)) 172 | 173 | 174 | 175 | 176 | # [2.1.0](https://github.com/TylorS/mostly-dom/compare/v2.0.3...v2.1.0) (2017-05-08) 177 | 178 | 179 | ### Features 180 | 181 | * **elementToVNode:** parameterize elementToVNode ([fdb7ce6](https://github.com/TylorS/mostly-dom/commit/fdb7ce6)) 182 | 183 | 184 | 185 | 186 | ## [2.0.3](https://github.com/TylorS/mostly-dom/compare/v2.0.2...v2.0.3) (2017-05-08) 187 | 188 | 189 | ### Bug Fixes 190 | 191 | * **init:** parameterize init instead of patch ([1368dc1](https://github.com/TylorS/mostly-dom/commit/1368dc1)) 192 | 193 | 194 | 195 | 196 | ## [2.0.2](https://github.com/TylorS/mostly-dom/compare/v2.0.1...v2.0.2) (2017-05-06) 197 | 198 | 199 | ### Bug Fixes 200 | 201 | * **hyperscript:** improve typings ([1d6db71](https://github.com/TylorS/mostly-dom/commit/1d6db71)) 202 | 203 | 204 | 205 | 206 | ## [2.0.1](https://github.com/TylorS/mostly-dom/compare/v2.0.0...v2.0.1) (2017-05-06) 207 | 208 | 209 | ### Bug Fixes 210 | 211 | * **patch:** allow parameterizing patch with element type ([351d8b1](https://github.com/TylorS/mostly-dom/commit/351d8b1)) 212 | 213 | 214 | 215 | 216 | # [2.0.0](https://github.com/TylorS/mostly-dom/compare/v1.5.0...v2.0.0) (2017-05-06) 217 | 218 | 219 | ### Features 220 | 221 | * **hyperscript:** create element-specific hyperscript-helpers ([24fde72](https://github.com/TylorS/mostly-dom/commit/24fde72)) 222 | * **mostly-dom:** parameterize VNodeProps and Hooks ([d94cd68](https://github.com/TylorS/mostly-dom/commit/d94cd68)) 223 | 224 | 225 | ### BREAKING CHANGES 226 | 227 | * **hyperscript:** can no longer set properties that do no exist for a given element type 228 | 229 | 230 | 231 | 232 | # [1.5.0](https://github.com/TylorS/mostly-dom/compare/v1.4.2...v1.5.0) (2017-04-29) 233 | 234 | 235 | ### Features 236 | 237 | * **types:** support ReadonlyArray children ([db0d296](https://github.com/TylorS/mostly-dom/commit/db0d296)) 238 | 239 | 240 | 241 | 242 | ## [1.4.2](https://github.com/TylorS/mostly-dom/compare/v1.4.1...v1.4.2) (2017-03-01) 243 | 244 | 245 | ### Bug Fixes 246 | 247 | * **modules:** fix module bugs ([98c5934](https://github.com/TylorS/mostly-dom/commit/98c5934)) 248 | 249 | 250 | 251 | 252 | ## [1.4.1](https://github.com/TylorS/mostly-dom/compare/v1.4.0...v1.4.1) (2017-02-28) 253 | 254 | 255 | ### Bug Fixes 256 | 257 | * **styles:** always set updated styles ([b845124](https://github.com/TylorS/mostly-dom/commit/b845124)) 258 | 259 | 260 | 261 | 262 | # [1.4.0](https://github.com/TylorS/mostly-dom/compare/v1.3.1...v1.4.0) (2017-02-14) 263 | 264 | 265 | ### Features 266 | 267 | * **mostly-dom:** add focus module ([a457faf](https://github.com/TylorS/mostly-dom/commit/a457faf)) 268 | 269 | 270 | 271 | 272 | ## [1.3.1](https://github.com/TylorS/mostly-dom/compare/v1.3.0...v1.3.1) (2017-02-07) 273 | 274 | 275 | ### Bug Fixes 276 | 277 | * **hyperscript:** fix typescript 2.1.5 compiler errors ([e85b786](https://github.com/TylorS/mostly-dom/commit/e85b786)) 278 | * **patchVNode:** check element type before setting properties ([84ea0dc](https://github.com/TylorS/mostly-dom/commit/84ea0dc)), closes [motorcyclejs/motorcyclejs#27](https://github.com/motorcyclejs/motorcyclejs/issues/27) 279 | * **props:** add \\`class\\` to PROPERTIES_TO_SKIP ([3c5056c](https://github.com/TylorS/mostly-dom/commit/3c5056c)) 280 | 281 | 282 | 283 | 284 | # [1.3.0](https://github.com/TylorS/mostly-dom/compare/v1.2.1...v1.3.0) (2017-02-06) 285 | 286 | 287 | ### Features 288 | 289 | * **hh:** add i helper ([ee6cf30](https://github.com/TylorS/mostly-dom/commit/ee6cf30)) 290 | * **VNodeProps:** Add ability to give in extra classes as a key-value dictionary where the key is th ([629075f](https://github.com/TylorS/mostly-dom/commit/629075f)) 291 | 292 | 293 | 294 | 295 | ## [1.2.1](https://github.com/TylorS/mostly-dom/compare/v1.2.0...v1.2.1) (2017-01-10) 296 | 297 | 298 | ### Bug Fixes 299 | 300 | * **package:** add missing dep ([217d966](https://github.com/TylorS/mostly-dom/commit/217d966)) 301 | 302 | 303 | 304 | 305 | # [1.2.0](https://github.com/TylorS/mostly-dom/compare/v1.1.2...v1.2.0) (2017-01-10) 306 | 307 | 308 | ### Bug Fixes 309 | 310 | * **types:** correct VirtualNode types ([32a32e3](https://github.com/TylorS/mostly-dom/commit/32a32e3)) 311 | 312 | 313 | ### Features 314 | 315 | * **hasCssSelector:** match more complex css selectors ([7c3e8e7](https://github.com/TylorS/mostly-dom/commit/7c3e8e7)) 316 | * **hyperscript:** export new functions ([fb1de4d](https://github.com/TylorS/mostly-dom/commit/fb1de4d)) 317 | * **querySelector:** implement vNode querySelector ([015a7a7](https://github.com/TylorS/mostly-dom/commit/015a7a7)) 318 | * **querySelectorAll:** implement vNode querySelectorAll ([6088521](https://github.com/TylorS/mostly-dom/commit/6088521)) 319 | 320 | 321 | 322 | 323 | ## [1.1.2](https://github.com/TylorS/mostly-dom/compare/v1.1.1...v1.1.2) (2017-01-03) 324 | 325 | 326 | ### Bug Fixes 327 | 328 | * **elementToVNode:** add text element to textVNode ([3de43ea](https://github.com/TylorS/mostly-dom/commit/3de43ea)) 329 | 330 | 331 | 332 | 333 | ## [1.1.1](https://github.com/TylorS/mostly-dom/compare/v1.1.0...v1.1.1) (2017-01-02) 334 | 335 | 336 | ### Bug Fixes 337 | 338 | * **mostly-dom:** add a fix for adding scopes to children ([9027d81](https://github.com/TylorS/mostly-dom/commit/9027d81)) 339 | 340 | 341 | 342 | 343 | # [1.1.0](https://github.com/TylorS/mostly-dom/compare/v1.0.0...v1.1.0) (2017-01-02) 344 | 345 | 346 | ### Features 347 | 348 | * **h:** create function variations with template types ([5fb8b7f](https://github.com/TylorS/mostly-dom/commit/5fb8b7f)) 349 | * **hasCssSelector:** add utility function that checks for CSS classes on VNode. ([4ba729a](https://github.com/TylorS/mostly-dom/commit/4ba729a)) 350 | * **hyperscript:** add hyperscript helper functions ([40af759](https://github.com/TylorS/mostly-dom/commit/40af759)) 351 | * **types:** add HTML tag name types ([b36080a](https://github.com/TylorS/mostly-dom/commit/b36080a)) 352 | 353 | 354 | 355 | 356 | # [1.0.0](https://github.com/TylorS/mostly-dom/compare/0.0.0...v1.0.0) (2017-01-02) 357 | 358 | 359 | ### Features 360 | 361 | * **mostly-dom:** complete implementation ([a07539a](https://github.com/TylorS/mostly-dom/commit/a07539a)) 362 | 363 | 364 | ### BREAKING CHANGES 365 | 366 | * **mostly-dom:** initial implementation 367 | 368 | 369 | 370 | 371 | # [0.0.0](https://github.com/TylorS/mostly-dom/compare/2aa8a52...0.0.0) (2016-12-17) 372 | 373 | 374 | ### Features 375 | 376 | * **mostly-dom:** initial commit ([2aa8a52](https://github.com/TylorS/mostly-dom/commit/2aa8a52)) 377 | 378 | 379 | 380 | -------------------------------------------------------------------------------- /src/hyperscript/hyperscript-helpers.ts: -------------------------------------------------------------------------------- 1 | import { ElementEvents, HtmlTagNames, VNode, VNodeEvents, VNodeProps, VNodeProperties } from '../' 2 | import { 3 | HTMLAnchorElementProperties, 4 | HTMLAppletElementProperties, 5 | HTMLAreaElementProperties, 6 | HTMLAudioElementProperties, 7 | HTMLBRElementProperties, 8 | HTMLBaseElementProperties, 9 | HTMLBaseFontElementProperties, 10 | HTMLBodyElementProperties, 11 | HTMLButtonElementProperties, 12 | HTMLCanvasElementProperties, 13 | HTMLDListElementProperties, 14 | HTMLDataElementProperties, 15 | HTMLDataListElementProperties, 16 | HTMLDirectoryElementProperties, 17 | HTMLDivElementProperties, 18 | HTMLEmbedElementProperties, 19 | HTMLFieldSetElementProperties, 20 | HTMLFontElementProperties, 21 | HTMLFormElementProperties, 22 | HTMLFrameElementProperties, 23 | HTMLFrameSetElementProperties, 24 | HTMLHRElementProperties, 25 | HTMLHeadElementProperties, 26 | HTMLHeadingElementProperties, 27 | HTMLHtmlElementProperties, 28 | HTMLIFrameElementProperties, 29 | HTMLImageElementProperties, 30 | HTMLInputElementProperties, 31 | HTMLLIElementProperties, 32 | HTMLLabelElementProperties, 33 | HTMLLegendElementProperties, 34 | HTMLLinkElementProperties, 35 | HTMLMapElementProperties, 36 | HTMLMarqueeElementProperties, 37 | HTMLMenuElementProperties, 38 | HTMLMetaElementProperties, 39 | HTMLMeterElementProperties, 40 | HTMLOListElementProperteis, 41 | HTMLObjectElementProperties, 42 | HTMLOptGroupElementProperties, 43 | HTMLOptionElementProperties, 44 | HTMLOutputElementProperties, 45 | HTMLParagraphElementProperties, 46 | HTMLParamElementProperties, 47 | HTMLPictureElementProperties, 48 | HTMLPreElementProperties, 49 | HTMLProgressElementProperties, 50 | HTMLQuoteElementProperties, 51 | HTMLScriptElementProperties, 52 | HTMLSelectElementProperties, 53 | HTMLSourceElementProperties, 54 | HTMLSpanElementProperties, 55 | HTMLStyleElementProperties, 56 | HTMLTableElementProperties, 57 | HTMLTableRowElementProperties, 58 | HTMLTemplateElementProperties, 59 | HTMLTextAreaElementProperties, 60 | HTMLTimeElementProperties, 61 | HTMLTitleElementProperties, 62 | HTMLTrackElementProperties, 63 | HTMLUListElementProperties, 64 | HTMLVideoElementProperties 65 | } from '../types/HtmlProperties' 66 | import { HyperscriptChildren, h } from './h' 67 | 68 | // tslint:disable:max-line-length 69 | // tslint:disable:no-shadowed-variable 70 | export interface HyperscriptHelperFn< 71 | T extends Element, 72 | Props extends VNodeProps = VNodeProps 73 | > { 74 | (): VNode 75 | (data: Props): VNode> 76 | (data: Props, children: HyperscriptChildren): VNode 77 | (children: HyperscriptChildren): VNode 78 | } 79 | // tslint:enable:max-line-length 80 | 81 | export function hh = VNodeProps>( 82 | tagName: HtmlTagNames 83 | ): HyperscriptHelperFn 84 | { 85 | return function(): VNode { 86 | const data = arguments[0] 87 | const children = arguments[1] 88 | 89 | if (Array.isArray(data)) return h(tagName, {}, data) 90 | 91 | if (typeof data === 'object') return h(tagName, data, children) 92 | 93 | return h(tagName, data || {}) 94 | } 95 | } 96 | 97 | function isValidString(param: any): boolean { 98 | return typeof param === 'string' && param.length > 0 99 | } 100 | 101 | function isSelector(param: any): boolean { 102 | return isValidString(param) && (param[0] === '.' || param[0] === '#') 103 | } 104 | 105 | export const a = hh('a') 106 | export const abbr = hh('abbr') 107 | export const acronym = hh('acronym') 108 | export const address = hh('address') 109 | export const applet = hh('applet') 110 | export const area = hh('area') 111 | export const article = hh('article') 112 | export const aside = hh('aside') 113 | export const audio = hh('audio') 114 | export const b = hh('b') 115 | export const base = hh('base') 116 | export const basefont = hh('basefont') 117 | export const bdi = hh('bdi') 118 | export const bdo = hh('bdo') 119 | export const bgsound = hh('bgsound') 120 | export const big = hh('big') 121 | export const blink = hh('blink') 122 | export const blockquote = hh('blockquote') 123 | export const body = hh('body') 124 | export const br = hh('br') 125 | export const button = hh('button') 126 | export const canvas = hh('canvas') 127 | export const caption = hh('caption') 128 | export const center = hh('center') 129 | export const cite = hh('cite') 130 | export const code = hh('code') 131 | export const col = hh('col') 132 | export const colgroup = hh('colgroup') 133 | export const command = hh('command') 134 | export const content = hh('content') 135 | export const data = hh('data') 136 | export const datalist = hh('datalist') 137 | export const dd = hh('dd') 138 | export const del = hh('del') 139 | export const details = hh('details') 140 | export const dfn = hh('dfn') 141 | export const dialog = hh('dialog') 142 | export const dir = hh('dir') 143 | export const div = hh('div') 144 | export const dl = hh('dl') 145 | export const dt = hh('dt') 146 | export const element = hh('element') 147 | export const em = hh('em') 148 | export const embed = hh('embed') 149 | export const fieldset = hh('fieldset') 150 | export const figcaption = hh('figcaption') 151 | export const figure = hh('figure') 152 | export const font = hh('font') 153 | export const form = hh('form') 154 | export const footer = hh('footer') 155 | export const frame = hh('frame') 156 | export const frameset = hh('frameset') 157 | export const h1 = hh('h1') 158 | export const h2 = hh('h2') 159 | export const h3 = hh('h3') 160 | export const h4 = hh('h4') 161 | export const h5 = hh('h5') 162 | export const h6 = hh('h6') 163 | export const head = hh('head') 164 | export const header = hh('header') 165 | export const hgroup = hh('hgroup') 166 | export const hr = hh('hr') 167 | export const html = hh('html') 168 | export const i = hh('i') 169 | export const iframe = hh('iframe') 170 | export const img = hh('img') 171 | export const input = hh('input') 172 | export const ins = hh('ins') 173 | export const isindex = hh('isindex') 174 | export const kbd = hh('kbd') 175 | export const keygen = hh('keygen') 176 | export const label = hh('label') 177 | export const legend = hh('legend') 178 | export const li = hh('li') 179 | export const link = hh('link') 180 | export const listing = hh('listing') 181 | export const main = hh('main') 182 | export const map = hh('map') 183 | export const mark = hh('mark') 184 | export const marquee = hh('marquee') 185 | export const math = hh('math') 186 | export const menu = hh('menu') 187 | export const menuitem = hh('menuitem') 188 | export const meta = hh('meta') 189 | export const meter = hh('meter') 190 | export const multicol = hh('multicol') 191 | export const nav = hh('nav') 192 | export const nextid = hh('nextid') 193 | export const nobr = hh('nobr') 194 | export const noembed = hh('noembed') 195 | export const noframes = hh('noframes') 196 | export const noscript = hh('noscript') 197 | export const object = hh('object') 198 | export const ol = hh('ol') 199 | export const optgroup = hh('optgroup') 200 | export const option = hh('option') 201 | export const output = hh('output') 202 | export const p = hh('p') 203 | export const param = hh('param') 204 | export const picture = hh('picture') 205 | export const plaintext = hh('plaintext') 206 | export const pre = hh('pre') 207 | export const progress = hh('progress') 208 | export const q = hh('q') 209 | export const rb = hh('rb') 210 | export const rbc = hh('rbc') 211 | export const rp = hh('rp') 212 | export const rt = hh('rt') 213 | export const rtc = hh('rtc') 214 | export const ruby = hh('ruby') 215 | export const s = hh('s') 216 | export const samp = hh('samp') 217 | export const script = hh('script') 218 | export const section = hh('section') 219 | export const select = hh('select') 220 | export const shadow = hh('shadow') 221 | export const small = hh('small') 222 | export const source = hh('source') 223 | export const spacer = hh('spacer') 224 | export const span = hh('span') 225 | export const strike = hh('strike') 226 | export const strong = hh('strong') 227 | export const style = hh('style') 228 | export const sub = hh('sub') 229 | export const summary = hh('summary') 230 | export const sup = hh('sup') 231 | export const slot = hh('slot') 232 | export const table = hh('table') 233 | export const tbody = hh('tbody') 234 | export const td = hh('td') 235 | export const template = hh('template') 236 | export const textarea = hh('textarea') 237 | export const tfoot = hh('tfoot') 238 | export const th = hh('th') 239 | export const time = hh('time') 240 | export const title = hh('title') 241 | export const tr = hh('tr') 242 | export const track = hh('track') 243 | export const tt = hh('tt') 244 | export const u = hh('u') 245 | export const ul = hh('ul') 246 | export const video = hh('video') 247 | export const wbr = hh('wbr') 248 | export const xmp = hh('xmp') 249 | 250 | // tslint:disable:no-mixed-interface 251 | declare global { 252 | namespace JSX { 253 | interface Element extends VNode {} 254 | interface IntrinsicElements { 255 | a: HTMLAnchorElementProperties 256 | applet: HTMLAppletElementProperties 257 | area: HTMLAreaElementProperties 258 | audio: HTMLAudioElementProperties 259 | base: HTMLBaseElementProperties 260 | basefont: HTMLBaseFontElementProperties 261 | body: HTMLBodyElementProperties 262 | br: HTMLBRElementProperties 263 | button: HTMLButtonElementProperties 264 | canvas: HTMLCanvasElementProperties 265 | data: HTMLDataElementProperties 266 | datalist: HTMLDataListElementProperties 267 | dir: HTMLDirectoryElementProperties 268 | div: HTMLDivElementProperties 269 | dl: HTMLDListElementProperties 270 | embed: HTMLEmbedElementProperties 271 | fieldset: HTMLFieldSetElementProperties 272 | font: HTMLFontElementProperties 273 | form: HTMLFormElementProperties 274 | frame: HTMLFrameElementProperties 275 | frameset: HTMLFrameSetElementProperties 276 | h1: HTMLHeadingElementProperties 277 | h2: HTMLHeadingElementProperties 278 | h3: HTMLHeadingElementProperties 279 | h4: HTMLHeadingElementProperties 280 | h5: HTMLHeadingElementProperties 281 | h6: HTMLHeadingElementProperties 282 | head: HTMLHeadElementProperties 283 | hr: HTMLHRElementProperties 284 | html: HTMLHtmlElementProperties 285 | i: HTMLHtmlElementProperties 286 | iframe: HTMLIFrameElementProperties 287 | img: HTMLImageElementProperties 288 | input: HTMLInputElementProperties 289 | label: HTMLLabelElementProperties 290 | legend: HTMLLegendElementProperties 291 | li: HTMLLIElementProperties 292 | link: HTMLLinkElementProperties 293 | map: HTMLMapElementProperties 294 | marquee: HTMLMarqueeElementProperties 295 | menu: HTMLMenuElementProperties 296 | meta: HTMLMetaElementProperties 297 | meter: HTMLMeterElementProperties 298 | object: HTMLObjectElementProperties 299 | ol: HTMLOListElementProperteis 300 | optgroup: HTMLOptGroupElementProperties 301 | option: HTMLOptionElementProperties 302 | output: HTMLOutputElementProperties 303 | p: HTMLParagraphElementProperties 304 | param: HTMLParamElementProperties 305 | picture: HTMLPictureElementProperties 306 | pre: HTMLPreElementProperties 307 | progress: HTMLProgressElementProperties 308 | q: HTMLQuoteElementProperties 309 | script: HTMLScriptElementProperties 310 | select: HTMLSelectElementProperties 311 | source: HTMLSourceElementProperties 312 | span: HTMLSpanElementProperties 313 | style: HTMLStyleElementProperties 314 | table: HTMLTableElementProperties 315 | template: HTMLTemplateElementProperties 316 | textarea: HTMLTextAreaElementProperties 317 | time: HTMLTimeElementProperties 318 | title: HTMLTitleElementProperties 319 | tr: HTMLTableRowElementProperties 320 | track: HTMLTrackElementProperties 321 | ul: HTMLUListElementProperties 322 | video: HTMLVideoElementProperties 323 | } 324 | } 325 | // tslint:disable-next-line:max-file-line-count 326 | } 327 | -------------------------------------------------------------------------------- /src/hyperscript/hasCssSelector/hasCssSelector.test.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-file-line-count 2 | import * as assert from 'assert' 3 | 4 | import { div, p } from '../hyperscript-helpers' 5 | 6 | import { hasCssSelector } from './hasCssSelector' 7 | 8 | describe('hasCssSelector', () => { 9 | // wildcard 10 | describe('given * as selector and a VNode', () => { 11 | it('returns true', () => { 12 | assert.ok(hasCssSelector('*', div({ className: 'foo' }))) 13 | }) 14 | }) 15 | 16 | // tagName, className, and id matching 17 | describe('given a class selector .foo and VNode with class foo', () => { 18 | it('returns true', () => { 19 | assert.ok(hasCssSelector('.foo', div({ className: 'foo' }))) 20 | }) 21 | }) 22 | 23 | describe('given a class selector .bar and VNode with class foo', () => { 24 | it('returns false', () => { 25 | assert.ok(!hasCssSelector('.bar', div({ className: 'foo' }))) 26 | }) 27 | }) 28 | 29 | describe('given a class selector xbar and VNode with class bar', () => { 30 | it('returns false', () => { 31 | assert.ok(!hasCssSelector('xbar', div({ className: 'bar' }))) 32 | }) 33 | }) 34 | 35 | describe('given a class selector .foo.bar and VNode with classes foo and bar', () => { 36 | it('returns true', () => { 37 | assert.ok(hasCssSelector('.foo.bar', div({ className: 'foo bar' }))) 38 | }) 39 | }) 40 | 41 | describe('given a class selector .foo.bar and VNode with classes bar and foo', () => { 42 | it('returns true', () => { 43 | assert.ok(hasCssSelector('.foo.bar', div({ className: 'bar foo' }))) 44 | }) 45 | }) 46 | 47 | describe('given a class selector .foo.bar.baz and VNode with classes bar foo and baz', () => { 48 | it('returns true', () => { 49 | assert.ok(hasCssSelector('.foo.bar.baz', div({ className: 'baz bar foo' }))) 50 | }) 51 | }) 52 | 53 | describe('given a class selector .foo and VNode with no classes', () => { 54 | it('returns false', () => { 55 | assert.ok(!hasCssSelector('.foo', div())) 56 | }) 57 | }) 58 | 59 | describe('given an id selector #foo and VNode with id foo', () => { 60 | it('returns true', () => { 61 | assert.ok(hasCssSelector('#foo', div({ id: 'foo' }))) 62 | }) 63 | }) 64 | 65 | describe('given an id selector #bar and VNode with id foo', () => { 66 | it('returns false', () => { 67 | assert.ok(!hasCssSelector('#bar', div({ id: 'foo' }))) 68 | }) 69 | }) 70 | 71 | describe('given an id selector #foo#bar and VNode with id foo', () => { 72 | it('returns false', () => { 73 | assert.ok(!hasCssSelector('#foo#bar', div({ id: 'foo' }))) 74 | }) 75 | }) 76 | 77 | describe('given a cssSelector .foo#bar and VNode with class foo and id bar', () => { 78 | it('returns true', () => { 79 | assert.ok(hasCssSelector('.foo#bar', div({ className: 'foo', id: 'bar' }))) 80 | }) 81 | }) 82 | 83 | describe('given a cssSelector .foo#bar and VNode with class foo', () => { 84 | it('returns false', () => { 85 | assert.ok(!hasCssSelector('.foo#bar', div({ className: 'foo' }))) 86 | }) 87 | }) 88 | 89 | describe('given a cssSelector #bar.foo and VNode with class foo', () => { 90 | it('returns false', () => { 91 | assert.ok(!hasCssSelector('#bar.foo', div({ className: 'foo' }))) 92 | }) 93 | }) 94 | 95 | // describe('given a cssSelector .foo .bar and VNode with classes foo and bar', () => { 96 | // it('throws error', () => { 97 | // assert.throws(() => { 98 | // hasCssSelector('.foo .bar', div({ className: 'foo bar' })); 99 | // }, /CSS selectors can not contain spaces/); 100 | // }); 101 | // }); 102 | 103 | describe('given a cssSelector .foo#bar.baz and VNode with classes foo and baz and id bar', () => { 104 | it('returns true', () => { 105 | assert.ok(hasCssSelector('.foo#bar.baz', div({ className: 'foo baz', id: 'bar' }))) 106 | }) 107 | }) 108 | 109 | describe('given cssSelector .foo#bar.baz and VNode with class foo and id bar', () => { 110 | it('returns false', () => { 111 | assert.ok(!hasCssSelector('.foo#bar.baz', div({ id: 'bar', className: 'foo' }))) 112 | }) 113 | }) 114 | 115 | describe('given cssSelector .foo#bar.baz and VNode with class baz and id bar', () => { 116 | it('returns false', () => { 117 | assert.ok(!hasCssSelector('.foo#bar.baz', div({ className: 'baz', id: 'bar' }))) 118 | }) 119 | }) 120 | 121 | describe('given a cssSelector div and VNode with tagName div', () => { 122 | it('returns true', () => { 123 | assert.ok(hasCssSelector('div', div())) 124 | }) 125 | }) 126 | 127 | describe('given a cssSelector div.foo and VNode with tagName div and class foo', () => { 128 | it('returns true', () => { 129 | assert.ok(hasCssSelector('div.foo', div({ className: 'foo' }))) 130 | }) 131 | }) 132 | 133 | describe('given a cssSelector h2.foo and VNode with tagName div and class foo', () => { 134 | it('returns false', () => { 135 | assert.ok(!hasCssSelector('h2.foo', div({ className: 'foo' }))) 136 | }) 137 | }) 138 | 139 | describe('given a cssSelector div.foo and VNode with tagName div', () => { 140 | it('returns false', () => { 141 | assert.ok(!hasCssSelector('div.foo', div())) 142 | }) 143 | }) 144 | 145 | describe('given a cssSelector div.foo and VNode with tagName div and id foo', () => { 146 | it('returns false', () => { 147 | assert.ok(!hasCssSelector('div.foo', div({ id: 'foo' }))) 148 | }) 149 | }) 150 | 151 | describe( 152 | 'given a cssSelector div#foo.bar and VNode with' + 153 | ' tagName div and id foo and class bar', function() 154 | { 155 | it('returns true', () => { 156 | assert.ok(hasCssSelector('div#foo.bar', div({ id: 'foo', className: 'bar' }))) 157 | }) 158 | }, 159 | ) 160 | 161 | // attribute matching 162 | describe('given a cssSelector [data-foo]', () => { 163 | it('returns true given a vNode with data-foo attribute', () => { 164 | assert.ok(hasCssSelector('[data-foo]', div({ attrs: { 'data-foo': 'foo' } }))) 165 | }) 166 | 167 | it('returns false given a vNode with no attributes', () => { 168 | assert.ok(!hasCssSelector('[data-foo]', div({}))) 169 | }) 170 | }) 171 | 172 | describe(`given a cssSelector [data-foo=foo]`, () => { 173 | it('returns true given a vNode with data-foo attribute of foo', () => { 174 | assert.ok(hasCssSelector('[data-foo=foo]', div({ attrs: { 'data-foo': 'foo' } }))) 175 | }) 176 | 177 | it('returns false given a vNode with no attributes', () => { 178 | assert.ok(!hasCssSelector('[data-foo=foo]', div({}))) 179 | }) 180 | }) 181 | 182 | describe(`given a cssSelector [data-foo~=foo]`, () => { 183 | describe('returns true when', () => { 184 | it('is given vNode with attribute data-foo value of foobar', () => { 185 | assert.ok(hasCssSelector('[data-foo~=foo]', div({ attrs: { 'data-foo': 'foobar' } }))) 186 | }) 187 | }) 188 | 189 | describe('returns false when', () => { 190 | it('is given a vNode with attribute data-foo value of bar', () => { 191 | assert.ok(!hasCssSelector('[data-foo~=foo]', div({ attrs: { 'data-foo': 'bar' } }))) 192 | }) 193 | }) 194 | }) 195 | 196 | describe(`given a cssSelector [data-foo|=foo]`, () => { 197 | describe(`returns true when`, () => { 198 | it(`is given a vNode with attribute data-foo with value of foo`, () => { 199 | assert.ok(hasCssSelector('[data-foo|=foo]', div({ attrs: { 'data-foo': 'foo' } }))) 200 | }) 201 | 202 | it(`is given a vNode with attribute data-foo with value of foobar`, () => { 203 | assert.ok(hasCssSelector('[data-foo|=foo]', div({ attrs: { 'data-foo': 'foobar' } }))) 204 | }) 205 | }) 206 | 207 | describe(`returns false when`, () => { 208 | it(`is given a vNode with attribute data-foo with value of barfoo`, () => { 209 | assert.ok(!hasCssSelector('[data-foo|=foo]', div({ attrs: { 'data-foo': 'barfoo' } }))) 210 | }) 211 | }) 212 | }) 213 | 214 | describe(`given a cssSelector of [data-foo$=foo]`, () => { 215 | describe(`returns true when`, () => { 216 | it(`is given a vNode with attribute data-foo with value of foo`, () => { 217 | assert.ok(hasCssSelector('[data-foo$=foo]', div({ attrs: { 'data-foo': 'foo' } }))) 218 | }) 219 | 220 | it(`is given a vNode with attribute data-foo with value of barfoo`, () => { 221 | assert.ok(hasCssSelector('[data-foo$=foo]', div({ attrs: { 'data-foo': 'barfoo' } }))) 222 | }) 223 | }) 224 | 225 | describe(`returns false when`, () => { 226 | it(`is given a vNode with attribute data-foo with value of foobar`, () => { 227 | assert.ok( 228 | !hasCssSelector('[data-foo$=foo]', div({ attrs: { 'data-foo': 'foobar' } }, [])), 229 | ) 230 | }) 231 | }) 232 | }) 233 | 234 | // psuedo-selector matching 235 | describe('psuedo-selectors', () => { 236 | describe(':first-child', () => { 237 | it('returns true when a vNode is the first child of its parent vNode', () => { 238 | const child = div({ className: 'hi' }) 239 | div({ className: 'parent' }, [ child, div({}, []) ]) 240 | 241 | assert.ok(hasCssSelector(':first-child', child)) 242 | }) 243 | 244 | it('returns false when a vNode is not its parents first child', () => { 245 | const child = div({ className: 'hi' }) 246 | div({ className: 'parent' }, [ div({}, []), child ]) 247 | 248 | assert.ok(!hasCssSelector(':first-child', child)) 249 | }) 250 | }) 251 | 252 | describe(':last-child', () => { 253 | it('returns true when a vNode is the last child of its parent vNode', () => { 254 | const child = div({ className: 'hi' }) 255 | div({ className: 'parent' }, [ div({}, []), child ]) 256 | 257 | assert.ok(hasCssSelector(':last-child', child)) 258 | }) 259 | 260 | it('returns false when a vNode is not its parents last child', () => { 261 | const child = div({ className: 'hi' }) 262 | div({ className: 'parent' }, [ child, div({}, []) ]) 263 | 264 | assert.ok(!hasCssSelector(':last-child', child)) 265 | }) 266 | }) 267 | 268 | describe('nth:child', () => { 269 | describe('given :nth-child(1)', () => { 270 | it('returns true when a vNode is the at index `n`', () => { 271 | const child = div({ className: 'hi' }) 272 | div({ className: 'parent' }, [ div({}, []), child ]) 273 | 274 | assert.ok(hasCssSelector(':nth-child(1)', child)) 275 | }) 276 | 277 | it('returns false when a vNode is not is not at index `n`', () => { 278 | const child = div({ className: 'hi' }) 279 | div({ className: 'parent' }, [ child, div({}, []) ]) 280 | 281 | assert.ok(!hasCssSelector(':nth-child(1)', child)) 282 | }) 283 | }) 284 | 285 | describe('given :nth-child(odd)', () => { 286 | it('returns true when a vNode is the at odd index', () => { 287 | const child = div({ className: 'hi' }) 288 | div({ className: 'parent' }, [ div({}, []), child ]) 289 | 290 | assert.ok(hasCssSelector(':nth-child(odd)', child)) 291 | }) 292 | 293 | it('returns false when a vNode is not is not at odd index', () => { 294 | const child = div({ className: 'hi' }) 295 | div({ className: 'parent' }, [ child, div({}, []) ]) 296 | 297 | assert.ok(!hasCssSelector(':nth-child(odd)', child)) 298 | }) 299 | }) 300 | 301 | describe('given :nth-child(even)', () => { 302 | it('returns true when a vNode is the at even index', () => { 303 | const child = div({ className: 'hi' }) 304 | div({ className: 'parent' }, [ child, div({}, []) ]) 305 | 306 | assert.ok(hasCssSelector(':nth-child(even)', child)) 307 | }) 308 | 309 | it('returns false when a vNode is not is not at odd index', () => { 310 | const child = div({ className: 'hi' }) 311 | div({ className: 'parent' }, [ div({}, []), child ]) 312 | 313 | assert.ok(!hasCssSelector(':nth-child(even)', child)) 314 | }) 315 | }) 316 | 317 | describe('given :nth-child(3n+0)', () => { 318 | it('returns true when a vNode is at a children index multiples of 3', () => { 319 | const child = div({ className: 'hi' }) 320 | div({ className: 'parent' }, [ 321 | div({}), 322 | div({}), 323 | div({}), 324 | child, 325 | div({}), 326 | ]) 327 | 328 | assert.ok(hasCssSelector(':nth-child(3n+0)', child)) 329 | }) 330 | 331 | it('returns false when a vNode is not at a children index multiples of 3', () => { 332 | const child = div({ className: 'hi' }) 333 | div({ className: 'parent' }, [ 334 | div({}), 335 | child, 336 | div({}), 337 | div({}), 338 | ]) 339 | 340 | assert.ok(!hasCssSelector(':nth-child(3n+0)', child)) 341 | }) 342 | }) 343 | }) 344 | 345 | describe(':empty', () => { 346 | it('returns true when no children are defined', () => { 347 | assert.ok(hasCssSelector(':empty', div())) 348 | }) 349 | 350 | it('returns true when children array is empty', () => { 351 | assert.ok(hasCssSelector(':empty', div([]))) 352 | }) 353 | 354 | it('return false when given children', () => { 355 | assert.ok(!hasCssSelector(':empty', div([ div() ]))) 356 | }) 357 | }) 358 | 359 | describe(':root', () => { 360 | it('returns true when root of vNode graph', () => { 361 | assert.ok(hasCssSelector(':root', div())) 362 | }) 363 | 364 | it('returns false when not root of vNode graph', () => { 365 | const child = div() 366 | div([ child ]) 367 | 368 | assert.ok(!hasCssSelector(':root', child)) 369 | }) 370 | }) 371 | 372 | describe(':contains(text)', () => { 373 | it('returns true when a vNode has text property', () => { 374 | assert.ok(hasCssSelector(':contains(hi)', div('hi'))) 375 | }) 376 | 377 | it('returns true when vNode contains textVNode', () => { 378 | assert.ok(hasCssSelector(':contains(hi)', div([ 'hi' ]))) 379 | }) 380 | 381 | it('returns false when vNode does not contain text', () => { 382 | assert.ok(!hasCssSelector(':contains(hi)', div())) 383 | }) 384 | }) 385 | }) 386 | 387 | describe('given cssSelectors separated by commas', () => { 388 | it('matches vNode that satisfies left selector', () => { 389 | const vNode = div() 390 | 391 | assert.ok(hasCssSelector('div, p', vNode)) 392 | }) 393 | 394 | it('matches vNode that satisfies right selector', () => { 395 | const vNode = div() 396 | 397 | assert.ok(hasCssSelector('p, div', vNode)) 398 | }) 399 | 400 | it('returns false if vNode does not satisfy selector', () => { 401 | const vNode = div() 402 | 403 | assert.ok(!hasCssSelector('p, a', vNode)) 404 | }) 405 | }) 406 | 407 | describe('cssSelectors separated by whitespace', () => { 408 | describe('given cssSelector `div .foo`', () => { 409 | it('returns true given a vNode with className .foo' + 410 | ' that is descendant of a div', () => 411 | { 412 | const child = div({ className: 'foo' }) 413 | div([ child ]) 414 | 415 | assert.ok(hasCssSelector('div .foo', child)) 416 | }) 417 | 418 | it('returns false given a vNode of className .bar', () => { 419 | const vNode = div({ className: 'bar' }) 420 | assert.ok(!hasCssSelector('div .foo', vNode)) 421 | }) 422 | }) 423 | 424 | describe('given cssSelector `div .foo .bar`', () => { 425 | it('returns true given a vNode with tagName `div` with a child with ' + 426 | 'className .foo containing a child with className .bar', () => 427 | { 428 | const child = div({ className: 'bar' }) 429 | div([ div({ className: 'foo' }, [ child ]) ]) 430 | 431 | assert.ok(hasCssSelector('div .foo .bar', child)) 432 | }) 433 | }) 434 | 435 | describe('given cssSelector `a .foo .bar`', () => { 436 | it('returns false given a vNode with tagName `div` with a child with ' + 437 | 'className .foo containing a child with className .bar', () => 438 | { 439 | const child = div({ className: 'bar' }) 440 | div([ div({ className: 'foo' }, [ child ]) ]) 441 | 442 | assert.ok(!hasCssSelector('a .foo .bar', child)) 443 | }) 444 | }) 445 | 446 | describe('given cssSelector `div > .foo`', () => { 447 | it('returns true given a vNode with tagName `div` with a child with ' + 448 | 'className .foo', () => 449 | { 450 | const child = div({ className: 'foo' }) 451 | div([ child ]) 452 | assert.ok(hasCssSelector('div > .foo', child)) 453 | }) 454 | 455 | it('returns false given a vNode with tagName `div` with a child with ' + 456 | 'className .bar', () => 457 | { 458 | const child = div({ className: 'bar' }) 459 | div([ child ]) 460 | 461 | assert.ok(!hasCssSelector('div > .foo', child)) 462 | }) 463 | }) 464 | 465 | describe('given cssSelector `.foo + .bar`', () => { 466 | it('returns true when given a vNode with className bar preceded ' + 467 | 'by vNode with className foo', () => 468 | { 469 | const vNode = div({ className: 'bar' }) 470 | div([ div({ className: 'foo' }), vNode ]) 471 | 472 | assert.ok(hasCssSelector('.foo + .bar', vNode)) 473 | }) 474 | 475 | it('returns false when given a vNode with className bar preceded ' + 476 | 'by vNode with className baz', () => 477 | { 478 | const vNode = div({ className: 'bar' }) 479 | div([ div('.baz'), vNode ]) 480 | 481 | assert.ok(!hasCssSelector('.foo + .bar', vNode)) 482 | }) 483 | }) 484 | 485 | describe('given cssSelector `.foo ~ .bar`', () => { 486 | it('returns true when given a vNode with className bar preceded ' + 487 | 'by vNode with className foo', () => 488 | { 489 | const vNode = div({ className: 'bar' }) 490 | div([ div({ className: 'foo' }), div(), div(), vNode ]) 491 | 492 | assert.ok(hasCssSelector('.foo ~ .bar', vNode)) 493 | }) 494 | 495 | it('returns false when given a vNode with className bar preceded ' + 496 | 'by vNode with className baz', () => 497 | { 498 | const vNode = div({ className: 'bar' }) 499 | div([ div('.baz'), vNode ]) 500 | 501 | assert.ok(!hasCssSelector('.foo ~ .bar', vNode)) 502 | }) 503 | }) 504 | 505 | describe('complicated selectors', () => { 506 | it('should return true given `div > .parent > .foo ~ .bar`', () => { 507 | const child = div({ className: 'bar' }) 508 | const sibling = div({ className: 'foo' }) 509 | const parent = div({ className: 'parent' }, [ sibling, child ]) 510 | div({ className: 'grandparent' }, [ parent ]) 511 | 512 | assert.ok(hasCssSelector(`div > .parent > .foo ~ .bar`, child)) 513 | }) 514 | 515 | it('should return true given `p > .foo, div .bar`', () => { 516 | const child = div({ className: 'bar' }) 517 | div([ child ]) 518 | 519 | const selector = `p > .foo, div .bar` 520 | 521 | assert.ok(hasCssSelector(selector, child)) 522 | 523 | const child2 = div({ className: 'foo' }) 524 | p([ child2 ]) 525 | 526 | assert.ok(hasCssSelector(selector, child2)) 527 | }) 528 | 529 | it('should return true given `div .bar, .parent > .foo`', () => { 530 | const child = div({ className: 'bar' }) 531 | div([ child ]) 532 | 533 | const selector = `div .bar, .parent > .foo` 534 | 535 | assert.ok(hasCssSelector(selector, child)) 536 | 537 | const child2 = div({ className: 'foo' }) 538 | p({ className: 'parent' }, [ child2 ]) 539 | 540 | assert.ok(hasCssSelector(selector, child2)) 541 | }) 542 | }) 543 | }) 544 | 545 | describe(`given a css selector of .foo and a vNode with className foo set in props`, () => { 546 | it(`returns true`, () => { 547 | const cssSelector = `.foo` 548 | const vNode = div({ className: `foo` }) 549 | 550 | assert.ok(hasCssSelector(cssSelector, vNode)) 551 | }) 552 | }) 553 | 554 | describe(`given a css selector of #foo and a vNode with id foo set in props`, () => { 555 | it(`returns true`, () => { 556 | const cssSelector = `#foo` 557 | const vNode = div({ id: `foo` }) 558 | 559 | assert.ok(hasCssSelector(cssSelector, vNode)) 560 | }) 561 | }) 562 | }) 563 | -------------------------------------------------------------------------------- /src/types/HtmlProperties.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:max-file-line-count 2 | import * as hooks from './hooks' 3 | 4 | import { HtmlElementEvents, VNodeEvents } from './VNodeEventTypes' 5 | 6 | import { VNodeStyle } from './VirtualNode' 7 | import { VNode } from '../index' 8 | 9 | export interface NodeProperties { 10 | nodeValue?: string | null 11 | textContent?: string | null 12 | } 13 | 14 | export interface ElementProperties extends NodeProperties { 15 | className?: string 16 | id?: string 17 | innerHTML?: string 18 | scrollLeft?: number 19 | scrollTop?: number 20 | slot?: string 21 | } 22 | 23 | export interface HTMLElementProperties extends ElementProperties { 24 | accessKey?: string 25 | contentEditable?: string 26 | dir?: string 27 | draggable?: boolean 28 | hidden?: boolean 29 | hideFocus?: boolean 30 | innerText?: string 31 | lang?: string 32 | outerText?: string 33 | spellcheck?: boolean 34 | tabIndex?: boolean 35 | title?: string 36 | } 37 | 38 | export interface VNodeProperties< 39 | T extends HTMLElement, 40 | EventMap extends VNodeEvents = VNodeEvents 41 | > extends HTMLElementProperties { 42 | // key for dom diffing 43 | key?: string | number 44 | 45 | // classes 46 | class?: { [className: string]: Boolean } 47 | 48 | // attributes for setAttribute() 49 | attrs?: { [attributeName: string]: any } 50 | 51 | // styling 52 | style?: VNodeStyle 53 | 54 | // events 55 | on?: EventMap 56 | listener?: EventListener 57 | 58 | // declarative focusing 59 | focus?: boolean 60 | 61 | scope?: string 62 | 63 | // hooks 64 | init?: hooks.InitHook 65 | create?: hooks.CreateHook 66 | update?: hooks.UpdateHook 67 | insert?: hooks.InsertHook 68 | remove?: hooks.RemoveHook 69 | destroy?: hooks.DestroyHook 70 | prepatch?: hooks.PrepatchHook 71 | postpatch?: hooks.PostpatchHook 72 | } 73 | 74 | export interface HTMLAnchorElementProperties 75 | extends VNodeProperties> { 76 | Methods?: string 77 | charset?: string 78 | coords?: string 79 | download?: string 80 | hash?: string 81 | host?: string 82 | hostname?: string 83 | href?: string 84 | hreflang?: string 85 | name?: string 86 | pathname?: string 87 | port?: string 88 | protocol?: string 89 | rel?: string 90 | rev?: string 91 | search?: string 92 | shape?: string 93 | target?: string 94 | text?: string 95 | type?: string 96 | urn?: string 97 | } 98 | 99 | export interface HTMLAppletElementProperties 100 | extends VNodeProperties> { 101 | align?: string 102 | alt?: string 103 | altHtml?: string 104 | archive?: string 105 | border?: string 106 | code?: string 107 | codeBase?: string 108 | codeType?: string 109 | data?: string 110 | declare?: boolean 111 | height?: string 112 | hspace?: number 113 | name?: string 114 | object?: string | null 115 | standby?: string 116 | type?: string 117 | vspace?: number 118 | width?: number 119 | } 120 | 121 | export interface HTMLAreaElementProperties 122 | extends VNodeProperties> { 123 | alt?: string 124 | coords?: string 125 | download?: string 126 | hash?: string 127 | host?: string 128 | hostname?: string 129 | href?: string 130 | noHref?: string 131 | pathname?: string 132 | port?: string 133 | protocol?: string 134 | rel?: string 135 | search?: string 136 | shape?: string 137 | target?: string 138 | } 139 | 140 | export interface HTMLAudioElementProperties 141 | extends VNodeProperties> {} 142 | 143 | export interface HTMLBRElementProperties 144 | extends VNodeProperties> { 145 | clear?: string 146 | } 147 | 148 | export interface HTMLBaseElementProperties 149 | extends VNodeProperties> { 150 | href?: string 151 | target?: string 152 | } 153 | 154 | export interface HTMLBaseFontElementProperties 155 | extends VNodeProperties> { 156 | face?: string 157 | size?: number 158 | } 159 | 160 | export interface HTMLBodyElementProperties 161 | extends VNodeProperties> { 162 | aLink?: any 163 | background?: string 164 | bgColor?: any 165 | bgProperties?: string 166 | link?: any 167 | noWrap?: boolean 168 | text?: any 169 | vLink?: any 170 | } 171 | 172 | export interface HTMLButtonElementProperties 173 | extends VNodeProperties> { 174 | autofocus?: boolean 175 | disabled?: boolean 176 | formAction?: string 177 | formEnctype?: string 178 | formMethod?: string 179 | formNoValidate?: string 180 | formTarget?: string 181 | name?: string 182 | status?: any 183 | type?: string 184 | value?: string 185 | } 186 | 187 | export interface HTMLCanvasElementProperties 188 | extends VNodeProperties> { 189 | height?: number 190 | width?: number 191 | } 192 | 193 | export interface HTMLDListElementProperties 194 | extends VNodeProperties> { 195 | compact?: boolean 196 | } 197 | 198 | export interface HTMLDataElementProperties 199 | extends VNodeProperties> { 200 | value?: string 201 | } 202 | 203 | export interface HTMLDataListElementProperties 204 | extends VNodeProperties> { 205 | options?: HTMLCollectionOf 206 | } 207 | 208 | export interface HTMLDirectoryElementProperties extends VNodeProperties { 209 | compact?: boolean 210 | } 211 | 212 | export interface HTMLDivElementProperties extends VNodeProperties { 213 | align?: string 214 | noWrap?: boolean 215 | } 216 | 217 | export interface HTMLEmbedElementProperties extends VNodeProperties { 218 | height?: string 219 | hidden?: any 220 | msPlayToDisabled?: boolean 221 | msPlayToPreferredSourceUri?: string 222 | msPlayToPrimary?: boolean 223 | name?: string 224 | src?: string 225 | units?: string 226 | width?: string 227 | } 228 | 229 | export interface HTMLFieldSetElementProperties extends VNodeProperties { 230 | align?: string 231 | disabled?: boolean 232 | name?: string 233 | } 234 | 235 | export interface HTMLFontElementProperties extends VNodeProperties { 236 | face?: string 237 | } 238 | 239 | export interface HTMLFormElementProperties extends VNodeProperties { 240 | acceptCharset?: string 241 | action?: string 242 | autocomplete?: string 243 | encoding?: string 244 | enctype?: string 245 | method?: string 246 | name?: string 247 | noValidate?: boolean 248 | target?: string 249 | } 250 | 251 | export interface HTMLFrameElementProperties 252 | extends VNodeProperties< 253 | HTMLFrameElement, 254 | VNodeEvents 255 | > { 256 | border?: string 257 | borderColor?: any 258 | frameBorder?: string 259 | frameSpacing?: any 260 | height?: string | number 261 | longDesc?: string 262 | marginHeight?: string 263 | marginWidth?: string 264 | name?: string 265 | noResize?: boolean 266 | scrolling?: string 267 | src?: string 268 | width?: string | number 269 | } 270 | 271 | export interface HTMLFrameSetElementProperties 272 | extends VNodeProperties< 273 | HTMLFrameSetElement, 274 | VNodeEvents 275 | > { 276 | border?: string 277 | borderColor?: string 278 | cols?: string 279 | frameBorder?: string 280 | frameSpacing?: any 281 | name?: string 282 | rows?: string 283 | } 284 | 285 | export interface HTMLHRElementProperties extends VNodeProperties { 286 | align?: string 287 | noShade?: boolean 288 | width?: number 289 | } 290 | 291 | export interface HTMLHeadElementProperties extends VNodeProperties { 292 | profile?: string 293 | } 294 | 295 | export interface HTMLHeadingElementProperties extends VNodeProperties { 296 | align?: string 297 | } 298 | 299 | export interface HTMLHtmlElementProperties extends VNodeProperties { 300 | version?: string 301 | } 302 | 303 | export interface HTMLIFrameElementProperties 304 | extends VNodeProperties< 305 | HTMLIFrameElement, 306 | VNodeEvents 307 | > { 308 | align?: string 309 | allowFullscreen?: boolean 310 | allowPaymentRequest?: boolean 311 | border?: string 312 | frameBorder?: string 313 | frameSpacing?: any 314 | height?: string 315 | hspace?: number 316 | longDesc?: string 317 | marginHeight?: string 318 | marginWidth?: string 319 | name?: string 320 | noResize?: boolean 321 | scrolling?: string 322 | src?: string 323 | vspace?: number 324 | width?: string 325 | } 326 | 327 | export interface HTMLImageElementProperties extends VNodeProperties { 328 | align?: string 329 | alt?: string 330 | border?: string 331 | crossOrigin?: string | null 332 | height?: number 333 | hspace?: number 334 | isMap?: boolean 335 | longDesc?: string 336 | lowsrc?: string 337 | msPlayToDisabled?: boolean 338 | msPlayToPreferredSourceUri?: string 339 | msPlayToPrimary?: boolean 340 | name?: string 341 | sizes?: string 342 | src?: string 343 | srcset?: string 344 | useMap?: string 345 | vspace?: number 346 | width?: number 347 | } 348 | 349 | export interface HTMLInputElementProperties extends VNodeProperties { 350 | accept?: string 351 | align?: string 352 | alt?: string 353 | autocomplete?: string 354 | autofocus?: boolean 355 | border?: string 356 | checked?: boolean 357 | defaultChecked?: boolean 358 | defaultValue?: string 359 | disabled?: boolean 360 | formAction?: string 361 | formEnctype?: string 362 | formMethod?: string 363 | formNoValidate?: string 364 | formTarget?: string 365 | height?: string 366 | hspace?: number 367 | indeterminate?: boolean 368 | max?: string 369 | maxLength?: number 370 | min?: string 371 | minLength?: number 372 | multiple?: boolean 373 | name?: string 374 | pattern?: string 375 | placeholder?: string 376 | readOnly?: string 377 | required?: boolean 378 | selectionDirection?: string 379 | selectionEnd?: number 380 | selectionStart?: number 381 | size?: number 382 | src?: string 383 | status?: boolean 384 | step?: string 385 | type?: string 386 | useMap?: string 387 | value?: string 388 | valueAsDate?: Date 389 | valueAsNumber?: number 390 | vspace?: number 391 | webkitdirectory?: boolean 392 | width?: string 393 | } 394 | 395 | export interface HTMLLIElementProperties extends VNodeProperties { 396 | type?: string 397 | value?: number 398 | } 399 | 400 | export interface HTMLLabelElementProperties extends VNodeProperties { 401 | htmlFor?: string 402 | } 403 | 404 | export interface HTMLLegendElementProperties extends VNodeProperties { 405 | align?: string 406 | } 407 | 408 | export interface HTMLLinkElementProperties extends VNodeProperties { 409 | charset?: string 410 | disabled?: boolean 411 | href?: string 412 | hreflang?: string 413 | media?: string 414 | rel?: string 415 | rev?: string 416 | target?: string 417 | type?: string 418 | import?: Document 419 | integrity?: string 420 | } 421 | 422 | export interface HTMLMapElementProperties extends VNodeProperties { 423 | name?: string 424 | } 425 | 426 | export interface HTMLMarqueeElementProperties 427 | extends VNodeProperties< 428 | HTMLMarqueeElement, 429 | VNodeEvents 430 | > { 431 | behavior?: string 432 | bgColor?: any 433 | direction?: string 434 | height?: string 435 | hspace?: number 436 | loop?: number 437 | scrollAmount?: number 438 | scrollDelay?: number 439 | trueSpeed?: boolean 440 | vspace?: number 441 | width?: string 442 | } 443 | 444 | export interface HTMLMediaElementProperties 445 | extends VNodeProperties< 446 | HTMLMediaElement, 447 | VNodeEvents 448 | > { 449 | autoplay?: boolean 450 | controls?: boolean 451 | crossOrigin?: string | null 452 | currentTime?: number 453 | defaultMuted?: boolean 454 | defaultPlaybackRate?: number 455 | loop?: boolean 456 | msAudioCategory?: string 457 | msAudioDeviceType?: string 458 | msPlayToDisabled?: boolean 459 | msPlayToPreferredSourceUri?: string 460 | msPlayToPrimary?: string 461 | msRealTime?: boolean 462 | muted?: boolean 463 | playbackRate?: number 464 | preload?: string 465 | readyState?: number 466 | src?: string 467 | srcObject?: MediaStream | null 468 | volume?: number 469 | } 470 | 471 | export interface HTMLMenuElementProperties extends VNodeProperties { 472 | compact?: boolean 473 | type?: string 474 | } 475 | 476 | export interface HTMLMetaElementProperties extends VNodeProperties { 477 | charset?: string 478 | content?: string 479 | httpEquiv?: string 480 | name?: string 481 | scheme?: string 482 | url?: string 483 | } 484 | 485 | export interface HTMLMeterElementProperties extends VNodeProperties { 486 | high?: number 487 | low?: number 488 | max?: number 489 | min?: number 490 | optimum?: number 491 | value?: number 492 | } 493 | 494 | export interface HTMLModElementProperties extends VNodeProperties { 495 | cite?: string 496 | dateTime?: string 497 | } 498 | 499 | export interface HTMLOListElementProperteis extends VNodeProperties { 500 | compact?: boolean 501 | start?: number 502 | type?: string 503 | } 504 | 505 | export interface HTMLObjectElementProperties extends VNodeProperties { 506 | align?: string 507 | alt?: string 508 | altHtml?: string 509 | archive?: string 510 | border?: string 511 | code?: string 512 | codeBase?: string 513 | codeType?: string 514 | data?: string 515 | declare?: boolean 516 | height?: string 517 | hspace?: number 518 | msPlayToDisabled?: boolean 519 | msPlayToPreferredSourceUri?: string 520 | msPlayToPrimary?: boolean 521 | name?: string 522 | standby?: string 523 | type?: string 524 | useMap?: string 525 | vspace?: number 526 | width?: string 527 | } 528 | 529 | export interface HTMLOptGroupElementProperties extends VNodeProperties { 530 | defaultSelected?: boolean 531 | disabled?: boolean 532 | label?: string 533 | selected?: boolean 534 | value?: string 535 | } 536 | 537 | export interface HTMLOptionElementProperties extends VNodeProperties { 538 | defaultSelected?: boolean 539 | disabled?: boolean 540 | label?: string 541 | selected?: boolean 542 | text?: string 543 | value?: string 544 | } 545 | 546 | export interface HTMLOutputElementProperties extends VNodeProperties { 547 | defaultValue?: string 548 | name?: string 549 | value?: string 550 | } 551 | 552 | export interface HTMLParagraphElementProperties extends VNodeProperties { 553 | align?: string 554 | clear?: string 555 | } 556 | 557 | export interface HTMLParamElementProperties extends VNodeProperties { 558 | name?: string 559 | type?: string 560 | value?: string 561 | valueType?: string 562 | } 563 | 564 | export interface HTMLPictureElementProperties extends VNodeProperties {} 565 | 566 | export interface HTMLPreElementProperties extends VNodeProperties { 567 | width?: number 568 | } 569 | 570 | export interface HTMLProgressElementProperties extends VNodeProperties { 571 | max?: number 572 | value?: number 573 | } 574 | 575 | export interface HTMLQuoteElementProperties extends VNodeProperties { 576 | cite?: string 577 | } 578 | 579 | export interface HTMLScriptElementProperties extends VNodeProperties { 580 | async?: boolean 581 | charset?: string 582 | crossOrigin?: string | null 583 | defer?: boolean 584 | event?: string 585 | htmlFor?: string 586 | src?: string 587 | text?: string 588 | type?: string 589 | integrity?: string 590 | } 591 | 592 | export interface HTMLSelectElementProperties extends VNodeProperties { 593 | autofocus?: boolean 594 | disabled?: boolean 595 | length?: number 596 | multiple?: boolean 597 | name?: string 598 | required?: boolean 599 | selectedIndex?: number 600 | selectedOptions?: HTMLCollectionOf 601 | size?: number 602 | value?: string 603 | } 604 | 605 | export interface HTMLSourceElementProperties extends VNodeProperties { 606 | media?: string 607 | msKeySystem?: string 608 | sizes?: string 609 | src?: string 610 | srcset?: string 611 | type?: string 612 | } 613 | 614 | export interface HTMLSpanElementProperties extends VNodeProperties {} 615 | 616 | export interface HTMLStyleElementProperties extends VNodeProperties { 617 | disabled?: boolean 618 | media?: string 619 | type?: string 620 | } 621 | 622 | export interface HTMLTableCaptionElementProperties 623 | extends VNodeProperties { 624 | align?: string 625 | vAlign?: string 626 | } 627 | 628 | export interface HTMLTableCellElementProperties extends VNodeProperties { 629 | abbr?: string 630 | align?: string 631 | axis?: string 632 | bgColor?: any 633 | colSpan?: number 634 | headers?: string 635 | height?: any 636 | noWrap?: boolean 637 | rowSpan?: number 638 | scope?: string 639 | width?: string 640 | } 641 | 642 | export interface HTMLTableColElementProperties extends VNodeProperties { 643 | align?: string 644 | span?: number 645 | width?: any 646 | } 647 | 648 | export interface HTMLTableDataCellElementProperties 649 | extends VNodeProperties {} 650 | 651 | export interface HTMLTableElementProperties extends VNodeProperties { 652 | align?: string 653 | bgColor?: any 654 | border?: string 655 | borderColor?: any 656 | caption?: HTMLTableCaptionElement 657 | cellPadding?: string 658 | cellSpacing?: string 659 | cols?: number 660 | frame?: string 661 | height?: any 662 | rows?: HTMLCollectionOf 663 | rules?: string 664 | summary?: string 665 | tBodies?: HTMLCollectionOf 666 | tFoot?: HTMLTableSectionElement 667 | tHead?: HTMLTableSectionElement 668 | width?: string 669 | } 670 | 671 | export interface HTMLTableHeaderCellElementProperties 672 | extends VNodeProperties { 673 | scope?: string 674 | } 675 | 676 | export interface HTMLTableRowElementProperties extends VNodeProperties { 677 | align?: string 678 | bgColor?: any 679 | cells?: HTMLCollectionOf 680 | height?: any 681 | } 682 | 683 | export interface HTMLTableSectionElementProperties 684 | extends VNodeProperties { 685 | align?: string 686 | rows?: HTMLCollectionOf 687 | } 688 | 689 | export interface HTMLTemplateElementProperties extends VNodeProperties {} 690 | 691 | export interface HTMLTextAreaElementProperties extends VNodeProperties { 692 | autofocus?: boolean 693 | cols?: number 694 | defaultValue?: string 695 | disabled?: boolean 696 | maxLength?: number 697 | minLength?: number 698 | name?: string 699 | placeholder?: string 700 | readOnly?: boolean 701 | required?: boolean 702 | rows?: number 703 | selectionEnd?: number 704 | selectedStart?: number 705 | status?: any 706 | value?: string 707 | wrap?: string 708 | } 709 | 710 | export interface HTMLTimeElementProperties extends VNodeProperties { 711 | dateTime?: string 712 | } 713 | 714 | export interface HTMLTitleElementProperties extends VNodeProperties { 715 | text?: string 716 | } 717 | 718 | export interface HTMLTrackElementProperties extends VNodeProperties { 719 | default?: boolean 720 | kind?: string 721 | label?: string 722 | src?: string 723 | srclang?: string 724 | } 725 | 726 | export interface HTMLUListElementProperties extends VNodeProperties { 727 | compact?: boolean 728 | type?: string 729 | } 730 | 731 | export interface HTMLUnknownElementProperties extends VNodeProperties {} 732 | 733 | export interface HTMLVideoElementProperties extends VNodeProperties { 734 | height?: number 735 | msHorizontalMirror?: boolean 736 | msStereo3DPackingMode?: string 737 | msStereo3DRenderMode?: string 738 | msZoom?: boolean 739 | poster?: string 740 | width?: number 741 | } 742 | -------------------------------------------------------------------------------- /src/patch.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import { BaseModule, ElementVNode, VNode, a, b, div, h, i, init, span } from './' 4 | 5 | import { elementToVNode } from './elementToVNode' 6 | import { createPropsModule } from './modules/props' 7 | 8 | const patch = init() 9 | 10 | function prop(name: string) { 11 | return function(obj: any): any { 12 | return obj[name] 13 | } 14 | } 15 | 16 | // tslint:disable:no-shadowed-variable 17 | 18 | function map(fn: (x: any) => any, list: Array | ArrayLike): Array { 19 | const ret: Array = [] 20 | 21 | for (let i = 0; i < list.length; ++i) ret[i] = fn(list[i]) 22 | 23 | return ret 24 | } 25 | 26 | function getChild(vnode: VNode, index: number): VNode { 27 | return ( 28 | (vnode && 29 | vnode.children && 30 | (vnode.children as Array).length >= index + 1 && 31 | ((vnode.children as Array)[index] as VNode)) 32 | ) as VNode 33 | } 34 | 35 | const inner = prop('innerHTML') 36 | 37 | describe('mostly-dom', function() { 38 | let elm: HTMLElement 39 | let vnode0: ElementVNode 40 | 41 | beforeEach(function() { 42 | elm = document.createElement('div') 43 | vnode0 = elementToVNode(elm) 44 | }) 45 | 46 | describe('patching an element', function() { 47 | it('changes an elements props', function() { 48 | const vnode1 = a({ href: 'http://other/' }) as ElementVNode 49 | const vnode2 = a({ href: 'http://localhost/' }) 50 | 51 | const patch = init([ createPropsModule() ]) 52 | 53 | patch(vnode0, vnode1) 54 | 55 | elm = patch(vnode1, vnode2).element as HTMLAnchorElement 56 | assert.equal((elm as any).href, 'http://localhost/') 57 | }) 58 | 59 | it('removes an elements props', function() { 60 | const vnode1 = a({ href: 'http://other/' }) as ElementVNode 61 | const vnode2 = a() 62 | 63 | const patch = init([ createPropsModule() ]) 64 | 65 | patch(vnode0, vnode1) 66 | patch(vnode1, vnode2) 67 | 68 | assert.equal((elm as any).href, undefined) 69 | }) 70 | 71 | describe('updating children with keys', function() { 72 | function spanNum(n: string | number) { 73 | if (typeof n === 'string') { 74 | return span({}, n) 75 | } else { 76 | return span({ key: n }, n.toString()) 77 | } 78 | } 79 | 80 | describe('addition of elements', function() { 81 | it('appends elements', function() { 82 | const vnode1 = span([ 1 ].map(spanNum)) as ElementVNode 83 | 84 | const vnode2 = span([ 1, 2, 3 ].map(spanNum)) 85 | 86 | elm = patch(vnode0, vnode1).element as HTMLElement 87 | 88 | assert.equal(elm.children.length, 1) 89 | 90 | elm = patch(vnode1, vnode2).element as HTMLElement 91 | 92 | assert.equal(elm.children.length, 3) 93 | assert.equal(elm.children[1].innerHTML, '2') 94 | assert.equal(elm.children[2].innerHTML, '3') 95 | }) 96 | 97 | it('prepends elements', function() { 98 | const vnode1 = span([ 4, 5 ].map(spanNum)) as ElementVNode 99 | const vnode2 = span([ 1, 2, 3, 4, 5 ].map(spanNum)) 100 | 101 | elm = patch(vnode0, vnode1).element as HTMLElement 102 | 103 | assert.equal(elm.children.length, 2) 104 | 105 | elm = patch(vnode1, vnode2).element as HTMLElement 106 | 107 | assert.deepEqual(map(inner, elm.children), [ '1', '2', '3', '4', '5' ]) 108 | }) 109 | 110 | it('add elements in the middle', function() { 111 | const vnode1 = span([ 1, 2, 4, 5 ].map(spanNum)) as ElementVNode 112 | const vnode2 = span([ 1, 2, 3, 4, 5 ].map(spanNum)) 113 | 114 | elm = patch(vnode0, vnode1).element as HTMLElement 115 | 116 | assert.equal(elm.children.length, 4) 117 | assert.equal(elm.children.length, 4) 118 | 119 | elm = patch(vnode1, vnode2).element as HTMLElement 120 | 121 | assert.deepEqual(map(inner, elm.children), [ '1', '2', '3', '4', '5' ]) 122 | }) 123 | 124 | it('add elements at begin and end', function() { 125 | const vnode1 = span([ 2, 3, 4 ].map(spanNum)) as ElementVNode 126 | const vnode2 = span([ 1, 2, 3, 4, 5 ].map(spanNum)) 127 | 128 | elm = patch(vnode0, vnode1).element as HTMLElement 129 | 130 | assert.equal(elm.children.length, 3) 131 | 132 | elm = patch(vnode1, vnode2).element as HTMLElement 133 | 134 | assert.deepEqual(map(inner, elm.children), [ '1', '2', '3', '4', '5' ]) 135 | }) 136 | 137 | it('adds children to parent with no children', function() { 138 | const vnode1 = span({ key: 'span' }) as ElementVNode 139 | const vnode2 = span({ key: 'span' }, [ 1, 2, 3 ].map(spanNum)) 140 | 141 | elm = patch(vnode0, vnode1).element as HTMLElement 142 | 143 | assert.equal(elm.children.length, 0) 144 | 145 | elm = patch(vnode1, vnode2).element as HTMLElement 146 | 147 | assert.deepEqual(map(inner, elm.children), [ '1', '2', '3' ]) 148 | }) 149 | 150 | it('removes all children from parent', function() { 151 | const vnode1 = span({ key: 'span' }, [ 1, 2, 3 ].map(spanNum)) as ElementVNode 152 | 153 | const vnode2 = span({ key: 'span' }) 154 | 155 | elm = patch(vnode0, vnode1).element as HTMLElement 156 | 157 | assert.deepEqual(map(inner, elm.children), [ '1', '2', '3' ]) 158 | 159 | elm = patch(vnode1, vnode2).element as HTMLElement 160 | 161 | assert.equal(elm.children.length, 0) 162 | }) 163 | }) 164 | 165 | describe('removal of elements', function() { 166 | it('removes elements from the beginning', function() { 167 | const vnode1 = span([ 1, 2, 3, 4, 5 ].map(spanNum)) as ElementVNode 168 | const vnode2 = span([ 3, 4, 5 ].map(spanNum)) 169 | 170 | elm = patch(vnode0, vnode1).element as HTMLElement 171 | 172 | assert.equal(elm.children.length, 5) 173 | 174 | elm = patch(vnode1, vnode2).element as HTMLElement 175 | 176 | assert.deepEqual(map(inner, elm.children), [ '3', '4', '5' ]) 177 | }) 178 | 179 | it('removes elements from the end', function() { 180 | const vnode1 = span([ 1, 2, 3, 4, 5 ].map(spanNum)) as ElementVNode 181 | const vnode2 = span([ 1, 2, 3 ].map(spanNum)) 182 | 183 | elm = patch(vnode0, vnode1).element as HTMLElement 184 | 185 | assert.equal(elm.children.length, 5) 186 | 187 | elm = patch(vnode1, vnode2).element as HTMLElement 188 | 189 | assert.equal(elm.children.length, 3) 190 | assert.equal(elm.children[0].innerHTML, '1') 191 | assert.equal(elm.children[1].innerHTML, '2') 192 | assert.equal(elm.children[2].innerHTML, '3') 193 | }) 194 | 195 | it('removes elements from the middle', function() { 196 | const vnode1 = span([ 1, 2, 3, 4, 5 ].map(spanNum)) as ElementVNode 197 | const vnode2 = span([ 1, 2, 4, 5 ].map(spanNum)) 198 | 199 | elm = patch(vnode0, vnode1).element as HTMLElement 200 | 201 | assert.equal(elm.children.length, 5) 202 | elm = patch(vnode1, vnode2).element as HTMLElement 203 | 204 | assert.equal(elm.children.length, 4) 205 | assert.deepEqual(elm.children[0].innerHTML, '1') 206 | assert.equal(elm.children[0].innerHTML, '1') 207 | assert.equal(elm.children[1].innerHTML, '2') 208 | assert.equal(elm.children[2].innerHTML, '4') 209 | assert.equal(elm.children[3].innerHTML, '5') 210 | }) 211 | }) 212 | 213 | describe('element reordering', function() { 214 | it('moves element forward', function() { 215 | const vnode1 = span([ 1, 2, 3, 4 ].map(spanNum)) as ElementVNode 216 | const vnode2 = span([ 2, 3, 1, 4 ].map(spanNum)) 217 | 218 | elm = patch(vnode0, vnode1).element as HTMLElement 219 | 220 | assert.equal(elm.children.length, 4) 221 | 222 | elm = patch(vnode1, vnode2).element as HTMLElement 223 | 224 | assert.equal(elm.children.length, 4) 225 | assert.equal(elm.children[0].innerHTML, '2') 226 | assert.equal(elm.children[1].innerHTML, '3') 227 | assert.equal(elm.children[2].innerHTML, '1') 228 | assert.equal(elm.children[3].innerHTML, '4') 229 | }) 230 | 231 | it('moves element to end', function() { 232 | const vnode1 = span([ 1, 2, 3 ].map(spanNum)) as ElementVNode 233 | const vnode2 = span([ 2, 3, 1 ].map(spanNum)) 234 | 235 | elm = patch(vnode0, vnode1).element as HTMLElement 236 | 237 | assert.equal(elm.children.length, 3) 238 | 239 | elm = patch(vnode1, vnode2).element as HTMLElement 240 | 241 | assert.equal(elm.children.length, 3) 242 | assert.equal(elm.children[0].innerHTML, '2') 243 | assert.equal(elm.children[1].innerHTML, '3') 244 | assert.equal(elm.children[2].innerHTML, '1') 245 | }) 246 | 247 | it('moves element backwards', function() { 248 | const vnode1 = span([ 1, 2, 3, 4 ].map(spanNum)) as ElementVNode 249 | const vnode2 = span([ 1, 4, 2, 3 ].map(spanNum)) 250 | 251 | elm = patch(vnode0, vnode1).element as HTMLElement 252 | 253 | assert.equal(elm.children.length, 4) 254 | 255 | elm = patch(vnode1, vnode2).element as HTMLElement 256 | 257 | assert.equal(elm.children.length, 4) 258 | assert.equal(elm.children[0].innerHTML, '1') 259 | assert.equal(elm.children[1].innerHTML, '4') 260 | assert.equal(elm.children[2].innerHTML, '2') 261 | assert.equal(elm.children[3].innerHTML, '3') 262 | }) 263 | 264 | it('swaps first and last', function() { 265 | const vnode1 = span([ 1, 2, 3, 4 ].map(spanNum)) as ElementVNode 266 | const vnode2 = span([ 4, 2, 3, 1 ].map(spanNum)) 267 | 268 | elm = patch(vnode0, vnode1).element as HTMLElement 269 | 270 | assert.equal(elm.children.length, 4) 271 | 272 | elm = patch(vnode1, vnode2).element as HTMLElement 273 | 274 | assert.equal(elm.children.length, 4) 275 | assert.equal(elm.children[0].innerHTML, '4') 276 | assert.equal(elm.children[1].innerHTML, '2') 277 | assert.equal(elm.children[2].innerHTML, '3') 278 | assert.equal(elm.children[3].innerHTML, '1') 279 | }) 280 | }) 281 | 282 | describe('combinations of additions, removals and reorderings', function() { 283 | it('move to left and replace', function() { 284 | const vnode1 = span([ 1, 2, 3, 4, 5 ].map(spanNum)) as ElementVNode 285 | const vnode2 = span([ 4, 1, 2, 3, 6 ].map(spanNum)) 286 | 287 | elm = patch(vnode0, vnode1).element as HTMLElement 288 | 289 | assert.equal(elm.children.length, 5) 290 | 291 | elm = patch(vnode1, vnode2).element as HTMLElement 292 | 293 | assert.equal(elm.children.length, 5) 294 | assert.equal(elm.children[0].innerHTML, '4') 295 | assert.equal(elm.children[1].innerHTML, '1') 296 | assert.equal(elm.children[2].innerHTML, '2') 297 | assert.equal(elm.children[3].innerHTML, '3') 298 | assert.equal(elm.children[4].innerHTML, '6') 299 | }) 300 | 301 | it('moves to left and leaves hole', function() { 302 | const vnode1 = span([ 1, 4, 5 ].map(spanNum)) as ElementVNode 303 | const vnode2 = span([ 4, 6 ].map(spanNum)) 304 | 305 | elm = patch(vnode0, vnode1).element as HTMLElement 306 | 307 | assert.equal(elm.children.length, 3) 308 | 309 | elm = patch(vnode1, vnode2).element as HTMLElement 310 | 311 | assert.deepEqual(map(inner, elm.children), [ '4', '6' ]) 312 | }) 313 | 314 | it('handles moved and set to undefined element ending at the end', function() { 315 | const vnode1 = span([ 2, 4, 5 ].map(spanNum)) as ElementVNode 316 | const vnode2 = span([ 4, 5, 3 ].map(spanNum)) 317 | 318 | elm = patch(vnode0, vnode1).element as HTMLElement 319 | 320 | assert.equal(elm.children.length, 3) 321 | 322 | elm = patch(vnode1, vnode2).element as HTMLElement 323 | 324 | assert.equal(elm.children.length, 3) 325 | assert.equal(elm.children[0].innerHTML, '4') 326 | assert.equal(elm.children[1].innerHTML, '5') 327 | assert.equal(elm.children[2].innerHTML, '3') 328 | }) 329 | 330 | it('moves a key in non-keyed nodes with a size up', function() { 331 | const vnode1 = span([ 1, 'a', 'b', 'c' ].map(spanNum)) as ElementVNode 332 | const vnode2 = span([ 'd', 'a', 'b', 'c', 1, 'e' ].map(spanNum)) 333 | 334 | elm = patch(vnode0, vnode1).element as HTMLElement 335 | 336 | assert.equal(elm.childNodes.length, 4) 337 | assert.equal(elm.textContent, '1abc') 338 | 339 | elm = patch(vnode1, vnode2).element as HTMLElement 340 | 341 | assert.equal(elm.childNodes.length, 6) 342 | assert.equal(elm.textContent, 'dabc1e') 343 | }) 344 | }) 345 | 346 | it('reverses elements', function() { 347 | const vnode1 = span([ 1, 2, 3, 4, 5, 6, 7, 8 ].map(spanNum)) as ElementVNode 348 | const vnode2 = span([ 8, 7, 6, 5, 4, 3, 2, 1 ].map(spanNum)) 349 | 350 | elm = patch(vnode0, vnode1).element as HTMLElement 351 | 352 | assert.equal(elm.childNodes.length, 8) 353 | 354 | assert.deepEqual(map(inner, elm.childNodes), [ '1', '2', '3', '4', '5', '6', '7', '8' ]) 355 | 356 | elm = patch(vnode1, vnode2).element as HTMLElement 357 | 358 | assert.deepEqual(map(inner, elm.childNodes), [ '8', '7', '6', '5', '4', '3', '2', '1' ]) 359 | }) 360 | 361 | it('something', function() { 362 | const vnode1 = span([ 0, 1, 2, 3, 4, 5 ].map(spanNum)) as ElementVNode 363 | const vnode2 = span([ 4, 3, 2, 1, 5, 0 ].map(spanNum)) 364 | 365 | elm = patch(vnode0, vnode1).element as HTMLElement 366 | 367 | assert.equal(elm.children.length, 6) 368 | 369 | elm = patch(vnode1, vnode2).element as HTMLElement 370 | 371 | assert.deepEqual(map(inner, elm.children), [ '4', '3', '2', '1', '5', '0' ]) 372 | }) 373 | 374 | it('handles random shuffles', function() { 375 | let i: any 376 | const arr: Array = [] 377 | const opacities: Array = [] 378 | const elms = 14 379 | const samples = 5 380 | 381 | function spanNumWithOpacity(n: any, o: any) { 382 | return span({ key: n, style: { opacity: o } }, n.toString()) 383 | } 384 | 385 | for (let n = 0; n < elms; ++n) { 386 | arr[n] = n 387 | } 388 | 389 | for (let n = 0; n < samples; ++n) { 390 | const vnode1 = span( 391 | arr.map(function(num: number) { 392 | return spanNumWithOpacity(num, '1') 393 | }) 394 | ) as ElementVNode 395 | 396 | const shufArr: Array = shuffle(arr.slice(0)) 397 | 398 | let element = document.createElement('div') 399 | 400 | element = patch(elementToVNode(element), vnode1).element as HTMLDivElement 401 | 402 | for (i = 0; i < elms; ++i) { 403 | assert.equal(element.children[i].innerHTML, i.toString()) 404 | opacities[i] = Math.random().toFixed(5).toString() 405 | } 406 | 407 | const vnode2 = span( 408 | arr.map(function(num: number) { 409 | return spanNumWithOpacity(shufArr[num], opacities[num]) 410 | }) 411 | ) 412 | 413 | element = patch(vnode1, vnode2).element as HTMLDivElement 414 | 415 | for (i = 0; i < elms; ++i) { 416 | assert.equal(element.children[i].innerHTML, shufArr[i].toString()) 417 | assert.equal( 418 | opacities[i].indexOf((element.children[i] as HTMLElement).style.opacity), 419 | 0 420 | ) 421 | } 422 | } 423 | }) 424 | }) 425 | 426 | describe('updating children without keys', function() { 427 | it('appends elements', function() { 428 | const vnode1 = div([ span('Hello') ]) as ElementVNode 429 | const vnode2 = div([ span('Hello'), span('World') ]) 430 | 431 | elm = patch(vnode0, vnode1).element as HTMLElement 432 | 433 | assert.deepEqual(map(inner, elm.children), [ 'Hello' ]) 434 | 435 | elm = patch(vnode1, vnode2).element as HTMLElement 436 | 437 | assert.deepEqual(map(inner, elm.children), [ 'Hello', 'World' ]) 438 | }) 439 | 440 | it('handles unmoved text nodes', function() { 441 | const vnode1 = div([ 'Text', span('Span') ]) as ElementVNode 442 | const vnode2 = div([ 'Text', span('Span') ]) 443 | 444 | elm = patch(vnode0, vnode1).element as HTMLElement 445 | 446 | assert.equal(elm.childNodes[0].textContent, 'Text') 447 | 448 | elm = patch(vnode1, vnode2).element as HTMLElement 449 | 450 | assert.equal(elm.childNodes[0].textContent, 'Text') 451 | }) 452 | 453 | it('handles changing text children', function() { 454 | const vnode1 = div([ 'Text', span('Span') ]) as ElementVNode 455 | const vnode2 = div([ 'Text2', span('Span') ]) 456 | 457 | elm = patch(vnode0, vnode1).element as HTMLElement 458 | 459 | assert.equal(elm.childNodes[0].textContent, 'Text') 460 | 461 | elm = patch(vnode1, vnode2).element as HTMLElement 462 | 463 | assert.equal(elm.childNodes[0].textContent, 'Text2') 464 | }) 465 | 466 | it('prepends element', function() { 467 | const vnode1 = div([ span('World') ]) as ElementVNode 468 | const vnode2 = div([ span('Hello'), span('World') ]) 469 | 470 | elm = patch(vnode0, vnode1).element as HTMLElement 471 | 472 | assert.deepEqual(map(inner, elm.children), [ 'World' ]) 473 | 474 | elm = patch(vnode1, vnode2).element as HTMLElement 475 | 476 | const { childNodes } = elm 477 | const firstChild = childNodes[0] 478 | const secondChild = childNodes[1] 479 | 480 | assert.deepEqual([ firstChild.textContent, secondChild.textContent ], [ 'Hello', 'World' ]) 481 | }) 482 | 483 | it('prepends element of different tag type', function() { 484 | const vnode1 = div([ span('World') ]) as ElementVNode 485 | const vnode2 = div([ div('Hello'), span('World') ]) 486 | 487 | elm = patch(vnode0, vnode1).element as HTMLElement 488 | 489 | assert.deepEqual(map(inner, elm.children), [ 'World' ]) 490 | 491 | elm = patch(vnode1, vnode2).element as HTMLElement 492 | 493 | assert.deepEqual(map(prop('tagName'), elm.children), [ 'DIV', 'SPAN' ]) 494 | assert.deepEqual(map(inner, elm.children), [ 'Hello', 'World' ]) 495 | }) 496 | 497 | it('removes elements', function() { 498 | const vnode1 = div([ span('One'), span('Two'), span('Three') ]) as ElementVNode 499 | 500 | const vnode2 = div([ span('One'), span('Three') ]) 501 | 502 | elm = patch(vnode0, vnode1).element as HTMLElement 503 | 504 | const contents: Array = [] 505 | 506 | for (let i = 0; i < elm.childNodes.length; ++i) 507 | contents[i] = elm.childNodes[i].textContent as string 508 | 509 | assert.deepEqual(contents, [ 'One', 'Two', 'Three' ]) 510 | 511 | elm = patch(vnode1, vnode2).element as HTMLElement 512 | 513 | const secondContents: Array = [] 514 | 515 | for (let i = 0; i < elm.childNodes.length; ++i) 516 | secondContents[i] = elm.childNodes[i].textContent as string 517 | 518 | assert.deepEqual(secondContents, [ 'One', 'Three' ]) 519 | }) 520 | 521 | it('removes a single text node', function() { 522 | const vnode1 = div('One') as ElementVNode 523 | const vnode2 = div() 524 | 525 | patch(vnode0, vnode1) 526 | 527 | assert.equal(elm.textContent, 'One') 528 | 529 | patch(vnode1, vnode2) 530 | 531 | assert.equal(elm.textContent, '') 532 | }) 533 | 534 | it('removes a single text node when children are updated', function() { 535 | const vnode1 = div('One') as ElementVNode 536 | const vnode2 = div([ div('Two'), span('Three') ]) 537 | 538 | patch(vnode0, vnode1) 539 | 540 | assert.equal(elm.textContent, 'One') 541 | 542 | patch(vnode1, vnode2) 543 | 544 | assert.deepEqual(map(prop('textContent'), elm.childNodes), [ 'Two', 'Three' ]) 545 | }) 546 | 547 | it('removes a text node among other elements', function() { 548 | const vnode1 = div([ 'One', span('Two') ]) as ElementVNode 549 | const vnode2 = div([ div('Three') ]) 550 | 551 | patch(vnode0, vnode1) 552 | 553 | assert.deepEqual(map(prop('textContent'), elm.childNodes), [ 'One', 'Two' ]) 554 | 555 | patch(vnode1, vnode2) 556 | assert.equal(elm.childNodes.length, 1) 557 | assert.equal((elm.childNodes[0] as HTMLElement).tagName, 'DIV') 558 | assert.equal(elm.childNodes[0].textContent, 'Three') 559 | }) 560 | 561 | it('reorders elements', function() { 562 | const vnode1 = div([ span('One'), div('Two'), b('Three') ]) as ElementVNode 563 | const vnode2 = div([ b('Three'), span('One'), div('Two') ]) 564 | elm = patch(vnode0, vnode1).element as HTMLElement 565 | assert.deepEqual(map(inner, elm.children), [ 'One', 'Two', 'Three' ]) 566 | elm = patch(vnode1, vnode2).element as HTMLElement 567 | assert.deepEqual(map(prop('tagName'), elm.children), [ 'B', 'SPAN', 'DIV' ]) 568 | assert.deepEqual(map(inner, elm.children), [ 'Three', 'One', 'Two' ]) 569 | }) 570 | }) 571 | }) 572 | 573 | describe('hooks', function() { 574 | describe('element hooks', function() { 575 | it('calls `create` listener before inserted into parent but after children', function() { 576 | const result: Array = [] 577 | 578 | function cb(vnode: VNode) { 579 | assert.equal((vnode.element as HTMLSpanElement).children.length, 2) 580 | assert.strictEqual(vnode && vnode.element && vnode.element.parentNode, null) 581 | result.push(vnode) 582 | } 583 | 584 | const vnode1 = div([ 585 | span('First sibling'), 586 | div({ create: cb }, [ span('Child 1'), span('Child 2') ]), 587 | span("Can't touch me"), 588 | ]) 589 | 590 | patch(vnode0, vnode1) 591 | 592 | assert.equal(1, result.length) 593 | }) 594 | 595 | // tslint:disable-next-line 596 | it('calls `insert` listener after both parents, siblings and children have been inserted', function() { 597 | const result: Array = [] 598 | function cb(vnode: VNode) { 599 | assert(vnode.element instanceof Element) 600 | assert.equal((vnode.element as HTMLSpanElement).children.length, 2) 601 | assert.equal( 602 | ((vnode.element as HTMLSpanElement).parentNode as HTMLDivElement).children.length, 603 | 3 604 | ) 605 | result.push(vnode) 606 | } 607 | 608 | const vnode1 = div([ 609 | span('First sibling'), 610 | div({ insert: cb }, [ span('Child 1'), span('Child 2') ]), 611 | span('Can touch me'), 612 | ]) 613 | 614 | patch(vnode0, vnode1) 615 | assert.equal(1, result.length) 616 | }) 617 | 618 | it('calls `prepatch` listener', function() { 619 | const result: Array = [] 620 | function cb(oldVnode: VNode, vnode: VNode) { 621 | /* tslint:disable */ 622 | assert.strictEqual(oldVnode, getChild(vnode1, 1)) 623 | assert.strictEqual(vnode, getChild(vnode2, 1)) 624 | /* tslint:enable */ 625 | result.push(vnode) 626 | } 627 | 628 | const vnode1 = div([ 629 | span('First sibling'), 630 | div({ prepatch: cb }, [ span('Child 1'), span('Child 2') ]), 631 | ]) as ElementVNode 632 | 633 | const vnode2 = div([ 634 | span('First sibling'), 635 | div({ prepatch: cb }, [ span('Child 1'), span('Child 2') ]), 636 | ]) 637 | 638 | patch(vnode0, vnode1) 639 | patch(vnode1, vnode2) 640 | assert.equal(result.length, 1) 641 | }) 642 | 643 | it('calls `postpatch` after `prepatch` listener', function() { 644 | const pre: Array = [] 645 | const post: Array = [] 646 | function preCb() { 647 | pre.push(pre) 648 | } 649 | 650 | function postCb() { 651 | assert.equal(pre.length, post.length + 1) 652 | post.push(post) 653 | } 654 | 655 | const vnode1 = div([ 656 | span('First sibling'), 657 | div({ prepatch: preCb, postpatch: postCb }, [ span('Child 1'), span('Child 2') ]), 658 | ]) as ElementVNode 659 | 660 | const vnode2 = div([ 661 | span('First sibling'), 662 | div({ prepatch: preCb, postpatch: postCb }, [ span('Child 1'), span('Child 2') ]), 663 | ]) 664 | 665 | patch(vnode0, vnode1) 666 | patch(vnode1, vnode2) 667 | assert.equal(pre.length, 1) 668 | assert.equal(post.length, 1) 669 | }) 670 | 671 | it('calls `update` listener', function() { 672 | const result1: Array = [] 673 | const result2: Array = [] 674 | 675 | function cb(result: Array, oldVnode: VNode, vnode: VNode) { 676 | if (result.length > 0) { 677 | assert.strictEqual(result[result.length - 1], oldVnode) 678 | } 679 | result.push(vnode) 680 | } 681 | 682 | const vnode1 = div([ 683 | span('First sibling'), 684 | div({ update: cb.bind(null, result1) }, [ 685 | span('Child 1'), 686 | span({ update: cb.bind(null, result2) }, 'Child 2'), 687 | ]), 688 | ]) as ElementVNode 689 | 690 | const vnode2 = div([ 691 | span('First sibling'), 692 | div({ update: cb.bind(null, result1) }, [ 693 | span('Child 1'), 694 | span({ update: cb.bind(null, result2) }, 'Child 2'), 695 | ]), 696 | ]) 697 | 698 | patch(vnode0, vnode1) 699 | patch(vnode1, vnode2) 700 | assert.equal(result1.length, 1) 701 | assert.equal(result2.length, 1) 702 | }) 703 | 704 | it('calls `remove` listener', function() { 705 | const result: Array = [] 706 | function cb(vnode: ElementVNode, rm: Function) { 707 | const parent = vnode.element && (vnode.element.parentNode as Element) 708 | assert(vnode.element instanceof Element) 709 | assert.equal(vnode.element.children && vnode.element.children.length, 2) 710 | assert.equal(parent.children && parent.children.length, 2) 711 | result.push(vnode) 712 | rm() 713 | assert.equal(parent.children.length, 1) 714 | } 715 | 716 | const vnode1 = div([ 717 | span('First sibling'), 718 | div({ remove: cb }, [ span('Child 1'), span('Child 2') ]), 719 | ]) as ElementVNode 720 | 721 | const vnode2 = div([ span('First sibling') ]) 722 | 723 | patch(vnode0, vnode1) 724 | patch(vnode1, vnode2) 725 | assert.equal(1, result.length) 726 | }) 727 | 728 | it('calls `init` and `prepatch` listeners on root', function() { 729 | let count = 0 730 | /* tslint:disable */ 731 | 732 | function init(_: VNode) { 733 | count += 1 734 | } 735 | 736 | function prepatch(_: VNode, __: VNode) { 737 | count += 1 738 | } 739 | 740 | /* tslint:enable */ 741 | let vnode1 = div({ init, prepatch }) as ElementVNode 742 | vnode1 = patch(vnode0, vnode1) 743 | 744 | assert.equal(1, count) 745 | 746 | const vnode2 = span({ init, prepatch }) 747 | 748 | patch(vnode1, vnode2) 749 | assert.equal(2, count) 750 | }) 751 | 752 | it('removes element when all remove listeners are done', function() { 753 | let rm1: any 754 | let rm2: any 755 | let rm3: any 756 | 757 | class RemoveModule1 extends BaseModule { 758 | constructor() { 759 | super() 760 | } 761 | 762 | public remove(_: any, rm: Function) { 763 | rm1 = rm 764 | } 765 | } 766 | 767 | class RemoveModule2 extends BaseModule { 768 | constructor() { 769 | super() 770 | } 771 | 772 | public remove(_: any, rm: Function) { 773 | rm2 = rm 774 | } 775 | } 776 | 777 | const _patch = init([ new RemoveModule1(), new RemoveModule2() ]) 778 | 779 | const vnode1 = div([ 780 | a({ 781 | remove(_: any, rm: Function) { 782 | rm3 = rm 783 | }, 784 | }), 785 | ]) as ElementVNode 786 | 787 | const vnode2 = div([]) 788 | 789 | elm = _patch(vnode0, vnode1).element as HTMLElement 790 | 791 | assert.equal(elm.children.length, 1) 792 | 793 | elm = _patch(vnode1, vnode2).element as HTMLElement 794 | 795 | assert.equal(elm.children.length, 1) 796 | 797 | rm1() 798 | 799 | assert.equal(elm.children.length, 1) 800 | 801 | rm3() 802 | 803 | assert.equal(elm.children.length, 1) 804 | 805 | rm2() 806 | 807 | assert.equal(elm.children.length, 0) 808 | }) 809 | 810 | it('invokes remove hook on replaced root', function() { 811 | const result: Array = [] 812 | const parent = document.createElement('div') 813 | 814 | parent.appendChild(vnode0.element) 815 | 816 | function cb(vnode: VNode, rm: () => any) { 817 | result.push(vnode) 818 | rm() 819 | } 820 | 821 | const vnode1 = div({ remove: cb }, [ b('Child 1'), i('Child 2') ]) as ElementVNode 822 | 823 | const vnode2 = span([ b('Child 1'), i('Child 2') ]) 824 | 825 | patch(vnode0, vnode1) 826 | patch(vnode1, vnode2) 827 | 828 | assert.equal(1, result.length) 829 | }) 830 | }) 831 | 832 | describe('module hooks', function() { 833 | it('invokes `pre` and `post` hook', function() { 834 | const result: Array = [] 835 | 836 | class Module extends BaseModule { 837 | public pre() { 838 | result.push('pre') 839 | } 840 | 841 | public post() { 842 | result.push('post') 843 | } 844 | } 845 | 846 | const _patch = init([ new Module() ]) 847 | 848 | const vnode1 = div() 849 | 850 | _patch(vnode0, vnode1) 851 | 852 | assert.deepEqual(result, [ 'pre', 'post' ]) 853 | }) 854 | 855 | it('invokes global `destroy` hook for all removed children', function() { 856 | const result: Array = [] 857 | function cb(vnode: VNode) { 858 | result.push(vnode) 859 | } 860 | 861 | const vnode1 = div([ 862 | span('First sibling'), 863 | div([ span({ destroy: cb }, 'Child 1'), span('Child 2') ]), 864 | ]) as ElementVNode 865 | 866 | const vnode2 = div() 867 | 868 | patch(patch(vnode0, vnode1), vnode2) 869 | 870 | assert.equal(result.length, 1) 871 | }) 872 | 873 | it('handles text vnodes with `undefined` `data` property', function() { 874 | const vnode1 = div([ ' ' ]) as ElementVNode 875 | 876 | const vnode2 = div([]) 877 | 878 | patch(vnode0, vnode1) 879 | patch(vnode1, vnode2) 880 | }) 881 | 882 | it('invokes `destroy` module hook for all removed children', function() { 883 | let created = 0 884 | let destroyed = 0 885 | 886 | class Module extends BaseModule { 887 | public create() { 888 | created++ 889 | } 890 | 891 | public destroy() { 892 | destroyed++ 893 | } 894 | } 895 | 896 | const _patch = init([ new Module() ]) 897 | 898 | const vnode1 = div([ 899 | span('First sibling'), 900 | div([ span('Child 1'), span('Child 2') ]), 901 | ]) as ElementVNode 902 | 903 | const vnode2 = div() 904 | 905 | _patch(vnode0, vnode1) 906 | _patch(vnode1, vnode2) 907 | 908 | assert.equal(created, 4) 909 | assert.equal(destroyed, 4) 910 | }) 911 | 912 | it('does not invoke `create` and `remove` module hook for text nodes', function() { 913 | let created = 0 914 | let removed = 0 915 | 916 | class Module extends BaseModule { 917 | public create() { 918 | created++ 919 | } 920 | 921 | public remove() { 922 | removed++ 923 | } 924 | } 925 | 926 | const _patch = init([ new Module() ]) 927 | 928 | const vnode1 = div([ span('First child'), '', span('Third child') ]) as ElementVNode 929 | 930 | const vnode2 = div() 931 | 932 | _patch(vnode0, vnode1) 933 | _patch(vnode1, vnode2) 934 | 935 | assert.equal(created, 2) 936 | assert.equal(removed, 2) 937 | }) 938 | 939 | it('does not invoke `destroy` module hook for text nodes', function() { 940 | let created = 0 941 | let destroyed = 0 942 | 943 | const _patch = init([ 944 | // tslint:disable-next-line:max-classes-per-file 945 | new class extends BaseModule { 946 | public create() { 947 | created++ 948 | } 949 | public destroy() { 950 | destroyed++ 951 | } 952 | }(), 953 | ]) 954 | 955 | const vnode1 = div([ 956 | span('First sibling'), 957 | div([ span('Child 1'), span([ 'Text 1', 'Text 2' ]) ]), 958 | ]) as ElementVNode 959 | 960 | const vnode2 = div() 961 | 962 | _patch(vnode0, vnode1) 963 | _patch(vnode1, vnode2) 964 | 965 | assert.equal(created, 4) 966 | assert.equal(destroyed, 4) 967 | }) 968 | }) 969 | }) 970 | 971 | describe('short circuiting', function() { 972 | it('does not update strictly equal vnodes', function() { 973 | const result: Array = [] 974 | function cb(vnode: VNode) { 975 | result.push(vnode) 976 | } 977 | 978 | const vnode1 = div([ span({ update: cb }, 'Hello'), span('there') ]) as ElementVNode 979 | 980 | patch(vnode0, vnode1) 981 | patch(vnode1, vnode1) 982 | 983 | assert.equal(result.length, 0) 984 | }) 985 | 986 | it('does not update strictly equal children', function() { 987 | const result: Array = [] 988 | 989 | function cb(vnode: VNode) { 990 | result.push(vnode) 991 | } 992 | 993 | const vnode1 = div([ span({ update: cb }, 'Hello'), span('there') ]) as ElementVNode 994 | 995 | const vnode2 = div() 996 | 997 | vnode2.children = vnode1.children 998 | 999 | patch(vnode0, vnode1) 1000 | patch(vnode1, vnode2) 1001 | 1002 | assert.equal(result.length, 0) 1003 | }) 1004 | }) 1005 | }) 1006 | 1007 | export function shuffle(array: Array): Array { 1008 | let currentIndex = array.length 1009 | let temporaryValue: any 1010 | let randomIndex: any 1011 | 1012 | // While there remain elements to shuffle... 1013 | while (0 !== currentIndex) { 1014 | // Pick a remaining element... 1015 | randomIndex = Math.floor(Math.random() * currentIndex) 1016 | currentIndex -= 1 1017 | 1018 | // And swap it with the current element. 1019 | temporaryValue = array[currentIndex] 1020 | array[currentIndex] = array[randomIndex] 1021 | array[randomIndex] = temporaryValue 1022 | } 1023 | 1024 | return array 1025 | } 1026 | // tslint:disable:max-file-line-count 1027 | --------------------------------------------------------------------------------