omis - init

This commit is contained in:
dntzhang 2019-07-20 18:53:17 +08:00
parent 9475c989a9
commit b54bef42d4
36 changed files with 4047 additions and 0 deletions

18
packages/omis/.babelrc Normal file
View File

@ -0,0 +1,18 @@
{
"presets": [
[
"env",
{
"loose": true,
"exclude": ["transform-es2015-typeof-symbol"],
"targets": {
"browsers": ["last 2 versions", "IE >= 9"]
}
}
]
],
"plugins": [
"transform-class-properties",
["transform-react-jsx", { "pragma": "Omi.h" }]
]
}

View File

@ -0,0 +1,15 @@
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,.*rc,*.yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,2 @@
examples/*/b.js

14
packages/omis/.eslintrc Normal file
View File

@ -0,0 +1,14 @@
{
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"semi": 2,
"indent": ["error", 2]
}
}

10
packages/omis/.flowconfig Normal file
View File

@ -0,0 +1,10 @@
[ignore]
<PROJECT_ROOT>/dist/.*
[include]
[libs]
[lints]
[options]

3
packages/omis/README.md Normal file
View File

@ -0,0 +1,3 @@
# Omis
> Web Components Supporting + Scoped Style + Store + Function Component

View File

@ -0,0 +1,43 @@
/* eslint no-console:0 */
/** Find constants (identified by ALL_CAPS_DECLARATIONS), and inline them globally.
* This is safe because Omi *only* uses global constants.
*/
export default (file, api) => {
let j = api.jscodeshift,
code = j(file.source),
constants = {},
found = 0;
code
.find(j.VariableDeclaration)
.filter(decl => {
for (let i = decl.value.declarations.length; i--; ) {
let node = decl.value.declarations[i],
name = node.id && node.id.name,
init = node.init;
if (name && init && name.match(/^[A-Z0-9_$]+$/g) && !init.regex) {
if (init.type === "Literal") {
// console.log(`Inlining constant: ${name}=${init.raw}`);
found++;
constants[name] = init;
// remove declaration
decl.value.declarations.splice(i, 1);
// if it's the last, we'll remove the whole statement
return !decl.value.declarations.length;
}
}
}
return false;
})
.remove();
code
.find(j.Identifier)
.filter(
path => path.value.name && constants.hasOwnProperty(path.value.name)
)
.replaceWith(path => (found++, constants[path.value.name]));
return found ? code.toSource({ quote: "single" }) : null;
};

View File

@ -0,0 +1,16 @@
/**
* Restores var names transformed by babel's let block scoping
*/
export default (file, api) => {
let j = api.jscodeshift;
let code = j(file.source);
// @TODO unsafe, but without it we gain 20b gzipped: https://www.diffchecker.com/bVrOJWTO
code
.findVariableDeclarators()
.filter(d => /^_i/.test(d.value.id.name))
.renameTo("i");
code.findVariableDeclarators("_key").renameTo("key");
return code.toSource({ quote: "single" });
};

View File

@ -0,0 +1,62 @@
/* eslint no-console:0 */
// parent node types that we don't want to remove pointless initializations from (because it breaks hoisting)
const BLOCKED = ["ForStatement", "WhileStatement"]; // 'IfStatement', 'SwitchStatement'
/** Removes var initialization to `void 0`, which Babel adds for TDZ strictness. */
export default (file, api) => {
let { jscodeshift } = api,
found = 0;
let code = jscodeshift(file.source)
.find(jscodeshift.VariableDeclaration)
.forEach(handleDeclaration);
function handleDeclaration(decl) {
let p = decl,
remove = true;
while ((p = p.parentPath)) {
if (~BLOCKED.indexOf(p.value.type)) {
remove = false;
break;
}
}
decl.value.declarations.filter(isPointless).forEach(node => {
if (remove === false) {
console.log(
`> Skipping removal of undefined init for "${node.id.name}": within ${
p.value.type
}`
);
} else {
removeNodeInitialization(node);
}
});
}
function removeNodeInitialization(node) {
node.init = null;
found++;
}
function isPointless(node) {
let { init } = node;
if (init) {
if (
init.type === "UnaryExpression" &&
init.operator === "void" &&
init.argument.value == 0
) {
return true;
}
if (init.type === "Identifier" && init.name === "undefined") {
return true;
}
}
return false;
}
return found ? code.toSource({ quote: "single" }) : null;
};

View File

@ -0,0 +1,66 @@
module.exports = {
parser: "babel-eslint",
// extends: "eslint:recommended",
// plugins: ["react"],
"extends": ["prettier"],
"plugins": ["prettier"],
env: {
browser: true,
mocha: true,
node: true,
es6: true
},
parserOptions: {
ecmaFeatures: {
modules: true,
jsx: true
}
},
globals: {
sinon: true,
expect: true
},
rules: {
"prettier/prettier": "error",
"no-unused-vars": [1, { varsIgnorePattern: "^h$" }],
"no-cond-assign": 1,
"no-empty": 0,
"no-console": 1,
semi: [1, "never"],
camelcase: 0,
"comma-style": 2,
"comma-dangle": [2, "never"],
indent: ["error", 2],
"no-mixed-spaces-and-tabs": [2, "smart-tabs"],
"no-trailing-spaces": [2, { skipBlankLines: true }],
"max-nested-callbacks": [2, 3],
"no-eval": 2,
"no-implied-eval": 2,
"no-new-func": 2,
"guard-for-in": 0,
eqeqeq: 0,
"no-else-return": 2,
"no-redeclare": 2,
"no-dupe-keys": 2,
radix: 2,
strict: [2, "never"],
"no-shadow": 0,
"callback-return": [1, ["callback", "cb", "next", "done"]],
"no-delete-var": 2,
"no-undef-init": 2,
"no-shadow-restricted-names": 2,
"handle-callback-err": 0,
"no-lonely-if": 2,
"keyword-spacing": 2,
"constructor-super": 2,
"no-this-before-super": 2,
"no-dupe-class-members": 2,
"no-const-assign": 2,
"prefer-spread": 2,
"no-useless-concat": 2,
"no-var": 2,
"object-shorthand": 2,
"prefer-arrow-callback": 2,
"quotes": [1, "single"]
}
};

View File

@ -0,0 +1,51 @@
{
"props": {
"cname": 42,
"props": {
"$_dirty": "__d",
"$_disable": "__x",
"$_listeners": "__l",
"$_renderCallbacks": "__h",
"$__key": "__k",
"$__ref": "__r",
"$normalizedNodeName": "__n",
"$nextBase": "__b",
"$prevContext": "__c",
"$prevProps": "__p",
"$prevState": "__s",
"$_parentComponent": "__u",
"$_componentConstructor": "_componentConstructor",
"$__html": "__html",
"$_component": "_component",
"$__omiattr_": "__omiattr_",
"$_preStyle": "r",
"$_id": "s",
"$__preactattr_": "t",
"$__config__": "v",
"$_isMockDocument": "w",
"$__omiSsrData": "x",
"$_vd": "y",
"$_css": "z",
"$_proxifyObjectTreeRecursively": "A",
"$_isInstalled": "B",
"$_useId": "C",
"$_useMap": "D",
"$___touchX": "F",
"$___touchY": "G",
"$___scrollTop": "H",
"$__elementId": "I",
"$_willUpdate": "J",
"$_preCss": "K",
"$_host": "L",
"$_updatePath": "M",
"$_customStyleElement": "N",
"$_customStyleContent": "O",
"$__hasChildren": "P",
"$__prevProps": "Q"
}
},
"vars": {
"cname": -1,
"props": {}
}
}

View File

@ -0,0 +1,10 @@
import config from "./rollup.config";
// ES output
config.output.format = "es";
config.output.file = "dist/omi.esm.js";
// remove memory() plugin
config.plugins.splice(0, 1);
export default config;

View File

@ -0,0 +1,58 @@
import nodeResolve from "rollup-plugin-node-resolve";
import babel from "rollup-plugin-babel";
import memory from "rollup-plugin-memory";
const license = require("rollup-plugin-license");
const pkg = require("../package.json");
const licensePlugin = license({
banner:
" omi v" +
pkg.version +
" http://omijs.org\r\nOmi === Preact + Scoped CSS + Store System + Native Support in 3kb javascript.\r\nBy dntzhang https://github.com/dntzhang \r\n Github: https://github.com/Tencent/omi\r\n MIT Licensed."
});
export default {
input: "src/omi.js",
output: {
format: "iife",
file: "dist/omi.dev.js",
name: "omi",
sourcemap: true,
strict: true
},
plugins: [
memory({
path: "src/omi.js",
contents: `
import Omi from './omi';
if (typeof module!='undefined') module.exports = Omi;
else self.Omi = Omi;
`
}),
nodeResolve({
main: true
}),
babel({
sourceMap: true,
exclude: "node_modules/**",
babelrc: false,
presets: [
[
"env",
{
modules: false,
loose: true,
exclude: ["transform-es2015-typeof-symbol"],
targets: {
browsers: ["last 2 versions", "IE >= 9"]
}
}
]
],
plugins: [
"transform-class-properties"
]
}),
licensePlugin
]
};

View File

@ -0,0 +1,56 @@
import nodeResolve from "rollup-plugin-node-resolve";
import babel from "rollup-plugin-babel";
import memory from "rollup-plugin-memory";
import commonjs from "rollup-plugin-commonjs";
var ENV = process.env.npm_lifecycle_event;
export default {
input: "examples/" + ENV + "/main.js",
output: {
format: "iife",
file: "examples/" + ENV + "/b.js",
name: "omi",
sourcemap: true,
strict: true
},
plugins: [
memory({
path: "src/omi.js",
contents: `
import Omi from './omi';
if (typeof module!='undefined') module.exports = Omi;
else self.Omi = Omi;
`
}),
nodeResolve({
main: true
}),
commonjs({
include: 'node_modules/**'
}),
babel({
sourceMap: true,
exclude: "node_modules/**",
babelrc: false,
presets: [
[
"env",
{
modules: false,
loose: true,
exclude: ["transform-es2015-typeof-symbol"],
targets: {
browsers: ["last 2 versions", "IE >= 9"]
}
}
]
],
plugins: [
"transform-decorators-legacy",
"transform-class-properties",
["transform-react-jsx", { pragma: "Omi.h" }]
]
})
]
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
<html>
<head></head>
<body>
<script src="b.js"></script>
</body>
</html>

View File

@ -0,0 +1,37 @@
import { render } from '../../src/omi'
//逻辑store外置UI只负责渲染
const Counter = (props, store) => {
return <div>
<button onClick={store.sub}>sub</button>
<text>{store.data.count}</text>
<button onClick={store.add}>add</button>
</div>
}
Counter.store = _ => {
return {
data: {
count: 1
},
add() {
this.data.count++
},
sub() {
this.data.count--
}
}
}
const App = (props, store) => {
return <div>
<Counter></Counter>
</div>
}
App.store = _ => {
}
render(<App />, 'body')

189
packages/omis/package.json Normal file
View File

@ -0,0 +1,189 @@
{
"name": "omi",
"version": "6.6.9",
"description": "Front End Cross-Frameworks Framework.",
"main": "dist/omi.js",
"jsnext:main": "dist/omi.esm.js",
"module": "dist/omi.esm.js",
"dev:main": "dist/omi.dev.js",
"minified:main": "dist/omi.min.js",
"types": "dist/omi.d.ts",
"scripts": {
"clean": "rimraf test/ts/**/*.js",
"copy-typescript-definition": "copyfiles -f src/omi.d.ts dist",
"build": "npm-run-all --silent clean transpile copy-typescript-definition strip optimize minify size",
"slot": "rollup -c config/rollup.example.js --watch",
"perfs": "rollup -c config/rollup.example.js --watch",
"css": "rollup -c config/rollup.example.js --watch",
"markdown": "rollup -c config/rollup.example.js --watch",
"clock": "rollup -c config/rollup.example.js --watch",
"nest": "rollup -c config/rollup.example.js --watch",
"event": "rollup -c config/rollup.example.js --watch",
"observer": "rollup -c config/rollup.example.js --watch",
"hook": "rollup -c config/rollup.example.js --watch",
"decorators": "rollup -c config/rollup.example.js --watch",
"to": "rollup -c config/rollup.example.js --watch",
"m-to": "rollup -c config/rollup.example.js --watch",
"mobx": "rollup -c config/rollup.example.js --watch",
"todomvc": "rollup -c config/rollup.example.js --watch",
"mvvm": "rollup -c config/rollup.example.js --watch",
"receive-props": "rollup -c config/rollup.example.js --watch",
"rpx": "rollup -c config/rollup.example.js --watch",
"children-bug": "rollup -c config/rollup.example.js --watch",
"tree": "rollup -c config/rollup.example.js --watch",
"tree2": "rollup -c config/rollup.example.js --watch",
"store-tree": "rollup -c config/rollup.example.js --watch",
"todo-app": "rollup -c config/rollup.example.js --watch",
"tick": "rollup -c config/rollup.example.js --watch",
"observe": "rollup -c config/rollup.example.js --watch",
"template": "rollup -c config/rollup.example.js --watch",
"children": "rollup -c config/rollup.example.js --watch",
"timer": "rollup -c config/rollup.example.js --watch",
"react-style": "rollup -c config/rollup.example.js --watch",
"store-old": "rollup -c config/rollup.example.js --watch",
"store-counter": "rollup -c config/rollup.example.js --watch",
"store-todo": "rollup -c config/rollup.example.js --watch",
"store-demo": "rollup -c config/rollup.example.js --watch",
"pure": "rollup -c config/rollup.example.js --watch",
"counter": "rollup -c config/rollup.example.js --watch",
"render-array": "rollup -c config/rollup.example.js --watch",
"css3transform": "rollup -c config/rollup.example.js --watch",
"tap": "rollup -c config/rollup.example.js --watch",
"tap2": "rollup -c config/rollup.example.js --watch",
"simple": "rollup -c config/rollup.example.js --watch",
"transpile:main": "rollup -c config/rollup.config.js",
"transpile:esm": "rollup -c config/rollup.config.esm.js",
"transpile": "npm-run-all transpile:main transpile:esm",
"optimize": "uglifyjs dist/omi.dev.js -c conditionals=false,sequences=false,loops=false,join_vars=false,collapse_vars=false --pure-funcs=Object.defineProperty --mangle-props --mangle-regex=\"/^(_|nextBase|prev[CPS]|_parentC)/\" --name-cache config/properties.json -b width=120,quote_style=3 -o dist/omi.js -p relative --in-source-map dist/omi.dev.js.map --source-map dist/omi.js.map",
"minify": "uglifyjs dist/omi.js -c collapse_vars,evaluate,screw_ie8,unsafe,loops=false,keep_fargs=false,pure_getters,unused,dead_code -m -o dist/omi.min.js -p relative --in-source-map dist/omi.js.map --source-map dist/omi.min.js.map",
"strip:main": "jscodeshift --run-in-band -s -t config/codemod-strip-tdz.js dist/omi.dev.js && jscodeshift --run-in-band -s -t config/codemod-const.js dist/omi.dev.js && jscodeshift --run-in-band -s -t config/codemod-let-name.js dist/omi.dev.js",
"strip:esm": "jscodeshift --run-in-band -s -t config/codemod-strip-tdz.js dist/omi.esm.js && jscodeshift --run-in-band -s -t config/codemod-const.js dist/omi.esm.js && jscodeshift --run-in-band -s -t config/codemod-let-name.js dist/omi.esm.js",
"strip": "npm-run-all strip:main strip:esm",
"size": "node -e \"process.stdout.write('gzip size: ')\" && gzip-size --raw dist/omi.min.js",
"test": "karma start test/karma.conf.js --single-run",
"fix": "eslint src --fix",
"fix-e": "eslint examples --fix",
"lint": "eslint src test",
"smart-release": "npm run build && npm test && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish",
"release": "cross-env npm run smart-release"
},
"eslintConfig": {
"extends": "./config/eslint-config.js"
},
"typings": "./dist/omi.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/Tencent/omi.git"
},
"files": [
"src",
"dist",
"typings.json"
],
"keywords": [
"omi",
"webcomponents",
"jsx",
"proxy",
"preact",
"react",
"virtual dom",
"vdom",
"components",
"virtual",
"dom"
],
"author": "dntzhang <dntzhang@qq.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/Tencent/omi/issues"
},
"homepage": "https://github.com/Tencent/omi",
"devDependencies": {
"@types/chai": "^4.1.2",
"@types/mocha": "^5.0.0",
"@types/node": "^9.4.7",
"babel-cli": "^6.24.1",
"babel-core": "^6.24.1",
"babel-eslint": "^8.2.2",
"babel-loader": "^7.0.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-plugin-transform-object-rest-spread": "^6.23.0",
"babel-plugin-transform-react-jsx": "^6.24.1",
"babel-preset-env": "^1.6.1",
"bundlesize": "^0.17.0",
"chai": "^4.1.2",
"copyfiles": "^2.0.0",
"core-js": "^2.4.1",
"coveralls": "^3.0.0",
"cross-env": "^5.1.4",
"diff": "^3.0.0",
"eslint": "^4.18.2",
"eslint-config-prettier": "^3.1.0",
"eslint-plugin-prettier": "^3.0.0",
"eslint-plugin-react": "^7.7.0",
"flow-bin": "^0.67.1",
"gzip-size-cli": "^2.0.0",
"htm": "^2.1.1",
"istanbul-instrumenter-loader": "^3.0.0",
"jest": "^23.6.0",
"jscodeshift": "^0.5.0",
"karma": "^2.0.0",
"karma-babel-preprocessor": "^7.0.0",
"karma-chai-sinon": "^0.1.5",
"karma-chrome-launcher": "^2.0.0",
"karma-coverage": "^1.0.0",
"karma-mocha": "^1.1.1",
"karma-mocha-reporter": "^2.0.4",
"karma-sauce-launcher": "^1.2.0",
"karma-sinon": "^1.0.5",
"karma-source-map-support": "^1.2.0",
"karma-sourcemap-loader": "^0.3.6",
"karma-webpack": "^3.0.0",
"mappingjs": "latest",
"mobx": "^4.5.1",
"mocha": "^5.0.4",
"npm-run-all": "^4.0.0",
"omi-mobx": "^0.2.2",
"omi-tap": "^2.0.2",
"omi-transform": "^2.0.2",
"prettier": "^1.14.3",
"remarkable": "^1.7.1",
"rimraf": "^2.5.3",
"rollup": "^0.57.1",
"rollup-plugin-babel": "^3.0.2",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-license": "^0.6.0",
"rollup-plugin-memory": "^3.0.0",
"rollup-plugin-node-resolve": "^3.0.0",
"sinon": "^4.4.2",
"sinon-chai": "^3.0.0",
"typescript": "^2.8.1",
"to2to": "^1.0.2",
"uglify-js": "^2.7.5",
"webpack": "^4.3.0"
},
"greenkeeper": {
"ignore": [
"babel-cli",
"babel-core",
"babel-eslint",
"babel-loader",
"jscodeshift",
"rollup-plugin-babel"
]
},
"bundlesize": [
{
"path": "./dist/omi.min.js",
"threshold": "4Kb"
}
],
"prettier": {
"singleQuote": true,
"semi": false,
"tabWidth": 2,
"useTabs": false
}
}

View File

@ -0,0 +1,18 @@
import { extend } from './util';
import { h } from './h';
/**
* Clones the given VNode, optionally adding attributes/props and replacing its
* children.
* @param {import('./vnode').VNode} vnode The virtual DOM element to clone
* @param {object} props Attributes/props to add when cloning
* @param {Array<import('./vnode').VNode>} [rest] Any additional arguments will be used as replacement
* children.
*/
export function cloneElement(vnode, props) {
return h(
vnode.nodeName,
extend(extend({}, vnode.attributes), props),
arguments.length>2 ? [].slice.call(arguments, 2) : vnode.children
);
}

90
packages/omis/src/component.js Executable file
View File

@ -0,0 +1,90 @@
import { FORCE_RENDER } from './constants';
import { extend } from './util';
import { renderComponent } from './vdom/component';
import { enqueueRender } from './render-queue';
/**
* Base Component class.
* Provides `setState()` and `forceUpdate()`, which trigger rendering.
* @typedef {object} Component
* @param {object} props The initial component props
* @param {object} context The initial context from parent components' getChildContext
* @public
*
* @example
* class MyFoo extends Component {
* render(props, state) {
* return <div />;
* }
* }
*/
export function Component(props, context) {
this._dirty = true;
/**
* @public
* @type {object}
*/
this.context = context;
/**
* @public
* @type {object}
*/
this.props = props;
/**
* @public
* @type {object}
*/
this.state = this.state || {};
this._renderCallbacks = [];
}
extend(Component.prototype, {
/**
* Update component state and schedule a re-render.
* @param {object} state A dict of state properties to be shallowly merged
* into the current state, or a function that will produce such a dict. The
* function is called with the current state and props.
* @param {() => void} callback A function to be called once component state is
* updated
*/
setState(state, callback) {
if (!this.prevState) this.prevState = this.state;
this.state = extend(
extend({}, this.state),
typeof state === 'function' ? state(this.state, this.props) : state
);
if (callback) this._renderCallbacks.push(callback);
enqueueRender(this);
},
/**
* Immediately perform a synchronous re-render of the component.
* @param {() => void} callback A function to be called after component is
* re-rendered.
* @private
*/
forceUpdate(callback) {
if (callback) this._renderCallbacks.push(callback);
renderComponent(this, FORCE_RENDER);
},
/**
* Accepts `props` and `state`, and returns a new Virtual DOM tree to build.
* Virtual DOM is generally constructed via [JSX](http://jasonformat.com/wtf-is-jsx).
* @param {object} props Props (eg: JSX attributes) received from parent
* element/component
* @param {object} state The component's current state
* @param {object} context Context object, as returned by the nearest
* ancestor's `getChildContext()`
* @returns {import('./vnode').VNode | void}
*/
render() {}
});

17
packages/omis/src/constants.js Executable file
View File

@ -0,0 +1,17 @@
// render modes
/** Do not re-render a component */
export const NO_RENDER = 0;
/** Synchronously re-render a component and its children */
export const SYNC_RENDER = 1;
/** Synchronously re-render a component, even if its lifecycle methods attempt to prevent it. */
export const FORCE_RENDER = 2;
/** Queue asynchronous re-render of a component and it's children */
export const ASYNC_RENDER = 3;
export const ATTR_KEY = '__preactattr_';
/** DOM properties that should NOT have "px" added when numeric */
export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i;

139
packages/omis/src/dom/index.js Executable file
View File

@ -0,0 +1,139 @@
import { IS_NON_DIMENSIONAL } from '../constants';
import { applyRef } from '../util';
import options from '../options';
/**
* A DOM event listener
* @typedef {(e: Event) => void} EventListner
*/
/**
* A mapping of event types to event listeners
* @typedef {Object.<string, EventListener>} EventListenerMap
*/
/**
* Properties Preact adds to elements it creates
* @typedef PreactElementExtensions
* @property {string} [normalizedNodeName] A normalized node name to use in diffing
* @property {EventListenerMap} [_listeners] A map of event listeners added by components to this DOM node
* @property {import('../component').Component} [_component] The component that rendered this DOM node
* @property {function} [_componentConstructor] The constructor of the component that rendered this DOM node
*/
/**
* A DOM element that has been extended with Preact properties
* @typedef {Element & ElementCSSInlineStyle & PreactElementExtensions} PreactElement
*/
/**
* Create an element with the given nodeName.
* @param {string} nodeName The DOM node to create
* @param {boolean} [isSvg=false] If `true`, creates an element within the SVG
* namespace.
* @returns {PreactElement} The created DOM node
*/
export function createNode(nodeName, isSvg) {
/** @type {PreactElement} */
let node = isSvg ? document.createElementNS('http://www.w3.org/2000/svg', nodeName) : document.createElement(nodeName);
node.normalizedNodeName = nodeName;
return node;
}
/**
* Remove a child node from its parent if attached.
* @param {Node} node The node to remove
*/
export function removeNode(node) {
let parentNode = node.parentNode;
if (parentNode) parentNode.removeChild(node);
}
/**
* Set a named attribute on the given Node, with special behavior for some names
* and event handlers. If `value` is `null`, the attribute/handler will be
* removed.
* @param {PreactElement} node An element to mutate
* @param {string} name The name/key to set, such as an event or attribute name
* @param {*} old The last value that was set for this name/node pair
* @param {*} value An attribute value, such as a function to be used as an
* event handler
* @param {boolean} isSvg Are we currently diffing inside an svg?
* @private
*/
export function setAccessor(node, name, old, value, isSvg) {
if (name==='className') name = 'class';
if (name==='key') {
// ignore
}
else if (name==='ref') {
applyRef(old, null);
applyRef(value, node);
}
else if (name==='class' && !isSvg) {
node.className = value || '';
}
else if (name==='style') {
if (!value || typeof value==='string' || typeof old==='string') {
node.style.cssText = value || '';
}
if (value && typeof value==='object') {
if (typeof old!=='string') {
for (let i in old) if (!(i in value)) node.style[i] = '';
}
for (let i in value) {
node.style[i] = typeof value[i]==='number' && IS_NON_DIMENSIONAL.test(i)===false ? (value[i]+'px') : value[i];
}
}
}
else if (name==='dangerouslySetInnerHTML') {
if (value) node.innerHTML = value.__html || '';
}
else if (name[0]=='o' && name[1]=='n') {
let useCapture = name !== (name=name.replace(/Capture$/, ''));
name = name.toLowerCase().substring(2);
if (value) {
if (!old) node.addEventListener(name, eventProxy, useCapture);
}
else {
node.removeEventListener(name, eventProxy, useCapture);
}
(node._listeners || (node._listeners = {}))[name] = value;
}
else if (name!=='list' && name!=='type' && !isSvg && name in node) {
// Attempt to set a DOM property to the given value.
// IE & FF throw for certain property-value combinations.
try {
node[name] = value==null ? '' : value;
} catch (e) { }
if ((value==null || value===false) && name!='spellcheck') node.removeAttribute(name);
}
else {
let ns = isSvg && (name !== (name = name.replace(/^xlink:?/, '')));
// spellcheck is treated differently than all other boolean values and
// should not be removed when the value is `false`. See:
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-spellcheck
if (value==null || value===false) {
if (ns) node.removeAttributeNS('http://www.w3.org/1999/xlink', name.toLowerCase());
else node.removeAttribute(name);
}
else if (typeof value!=='function') {
if (ns) node.setAttributeNS('http://www.w3.org/1999/xlink', name.toLowerCase(), value);
else node.setAttribute(name, value);
}
}
}
/**
* Proxy an event to hooked event handlers
* @param {Event} e The event object from the browser
* @private
*/
function eventProxy(e) {
return this._listeners[e.type](options.event && options.event(e) || e);
}

86
packages/omis/src/h.js Executable file
View File

@ -0,0 +1,86 @@
import { VNode } from './vnode';
import options from './options';
const stack = [];
const EMPTY_CHILDREN = [];
/**
* JSX/hyperscript reviver.
* @see http://jasonformat.com/wtf-is-jsx
* Benchmarks: https://esbench.com/bench/57ee8f8e330ab09900a1a1a0
*
* Note: this is exported as both `h()` and `createElement()` for compatibility
* reasons.
*
* Creates a VNode (virtual DOM element). A tree of VNodes can be used as a
* lightweight representation of the structure of a DOM tree. This structure can
* be realized by recursively comparing it against the current _actual_ DOM
* structure, and applying only the differences.
*
* `h()`/`createElement()` accepts an element name, a list of attributes/props,
* and optionally children to append to the element.
*
* @example The following DOM tree
*
* `<div id="foo" name="bar">Hello!</div>`
*
* can be constructed using this function as:
*
* `h('div', { id: 'foo', name : 'bar' }, 'Hello!');`
*
* @param {string | function} nodeName An element name. Ex: `div`, `a`, `span`, etc.
* @param {object | null} attributes Any attributes/props to set on the created element.
* @param {VNode[]} [rest] Additional arguments are taken to be children to
* append. Can be infinitely nested Arrays.
*
* @public
*/
export function h(nodeName, attributes) {
let children=EMPTY_CHILDREN, lastSimple, child, simple, i;
for (i=arguments.length; i-- > 2; ) {
stack.push(arguments[i]);
}
if (attributes && attributes.children!=null) {
if (!stack.length) stack.push(attributes.children);
delete attributes.children;
}
while (stack.length) {
if ((child = stack.pop()) && child.pop!==undefined) {
for (i=child.length; i--; ) stack.push(child[i]);
}
else {
if (typeof child==='boolean') child = null;
if ((simple = typeof nodeName!=='function')) {
if (child==null) child = '';
else if (typeof child==='number') child = String(child);
else if (typeof child!=='string') simple = false;
}
if (simple && lastSimple) {
children[children.length-1] += child;
}
else if (children===EMPTY_CHILDREN) {
children = [child];
}
else {
children.push(child);
}
lastSimple = simple;
}
}
let p = new VNode();
p.nodeName = nodeName;
p.children = children;
p.attributes = attributes==null ? undefined : attributes;
p.key = attributes==null ? undefined : attributes.key;
// if a "vnode hook" is defined, pass every created VNode to it
if (options.vnode!==undefined) options.vnode(p);
return p;
}

940
packages/omis/src/omi.d.ts vendored Executable file
View File

@ -0,0 +1,940 @@
export = omi;
export as namespace omi;
declare namespace omi {
type Key = string | number;
type RefObject<T> = { current?: T | null };
type RefCallback<T> = (instance: T | null) => void;
type Ref<T> = RefObject<T> | RefCallback<T>;
type ComponentChild = VNode<any> | object | string | number | boolean | null;
type ComponentChildren = ComponentChild[] | ComponentChild;
/**
* @deprecated
*
* Use Attributes instead
*/
type ComponentProps = Attributes;
/**
* @deprecated
*
* Use ClassAttributes instead
*/
type PreactHTMLAttributes = ClassAttributes<any>;
interface Attributes {
key?: Key;
jsx?: boolean;
}
interface ClassAttributes<T> extends Attributes {
ref?: Ref<T>;
}
interface PreactDOMAttributes {
children?: ComponentChildren;
dangerouslySetInnerHTML?: {
__html: string;
};
}
type ComponentFactory<P> = ComponentConstructor<P> | FunctionalComponent<P>;
/**
* Define the contract for a virtual node in omi.
*
* A virtual node has a name, a map of attributes, an array
* of child {VNode}s and a key. The key is used by omi for
* internal purposes.
*/
interface VNode<P = any> {
nodeName: ComponentFactory<P> | string;
attributes: P;
children: Array<VNode<any> | string>;
key?: Key | null;
}
type RenderableProps<P, RefType = any> = Readonly<
P & Attributes & { children?: ComponentChildren; ref?: Ref<RefType> }
>;
interface FunctionalComponent<P = {}> {
(props: RenderableProps<P>, context?: any): VNode<any> | null;
displayName?: string;
defaultProps?: Partial<P>;
}
interface ComponentConstructor<P = {}, S = {}> {
new (props: P, context?: any): Component<P, S>;
displayName?: string;
defaultProps?: Partial<P>;
}
// Type alias for a component considered generally, whether stateless or stateful.
type AnyComponent<P = {}, S = {}> = FunctionalComponent<P> | ComponentConstructor<P, S>;
interface Component<P = {}, S = {}> {
componentWillMount?(): void;
componentDidMount?(): void;
componentWillUnmount?(): void;
getChildContext?(): object;
componentWillReceiveProps?(nextProps: Readonly<P>, nextContext: any): void;
shouldComponentUpdate?(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean;
componentWillUpdate?(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): void;
componentDidUpdate?(previousProps: Readonly<P>, previousState: Readonly<S>, previousContext: any): void;
}
abstract class Component<P, S> {
constructor(props?: P, context?: any);
static displayName?: string;
static defaultProps?: any;
state: Readonly<S>;
props: RenderableProps<P>;
context: any;
base?: HTMLElement;
// From https://github.com/DefinitelyTyped/DefinitelyTyped/blob/e836acc75a78cf0655b5dfdbe81d69fdd4d8a252/types/react/index.d.ts#L402
// // We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
// // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
// // Also, the ` | S` allows intellisense to not be dumbisense
setState<K extends keyof S>(
state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
callback?: () => void
): void;
forceUpdate(callback?: () => void): void;
abstract render(props?: RenderableProps<P>, state?: Readonly<S>, context?: any): ComponentChild;
// Add these variables to avoid descendants shadowing them (some from properties.json for minification)
private __key;
private __ref;
private _component;
private _dirty;
private _disable;
private nextBase;
private prevContext;
private prevProps;
private prevState;
private __d;
private __x;
private __l;
private __h;
private __k;
private __r;
private __n;
private __b;
private __c;
private __p;
private __s;
private __u;
}
function h(
node: string,
params: JSX.HTMLAttributes & JSX.SVGAttributes & Record<string, any> | null,
...children: ComponentChildren[]
): VNode<any>;
function h<P>(
node: ComponentFactory<P>,
params: Attributes & P | null,
...children: ComponentChildren[]
): VNode<any>;
function render(node: ComponentChild, parent: Element | Document | ShadowRoot | DocumentFragment, mergeWith?: Element): Element;
function rerender(): void;
function cloneElement(element: JSX.Element, props: any, ...children: ComponentChildren[]): JSX.Element;
function createRef<T=any>(): RefObject<T>;
var options: {
syncComponentUpdates?: boolean;
debounceRendering?: (render: () => void) => void;
vnode?: (vnode: VNode<any>) => void;
event?: (event: Event) => Event;
};
}
type Defaultize<Props, Defaults> =
// Distribute over unions
Props extends any
? // Make any properties included in Default optional
& Partial<Pick<Props, Extract<keyof Props, keyof Defaults>>>
// Include the remaining properties from Props
& Pick<Props, Exclude<keyof Props, keyof Defaults>>
: never;
declare global {
namespace JSX {
interface Element extends omi.VNode<any> {
}
interface ElementClass extends omi.Component<any, any> {
}
interface ElementAttributesProperty {
props: any;
}
interface ElementChildrenAttribute {
children: any;
}
type LibraryManagedAttributes<Component, Props> =
Component extends { defaultProps: infer Defaults }
? Defaultize<Props, Defaults>
: Props;
interface SVGAttributes extends HTMLAttributes {
accentHeight?: number | string;
accumulate?: "none" | "sum";
additive?: "replace" | "sum";
alignmentBaseline?: "auto" | "baseline" | "before-edge" | "text-before-edge" | "middle" | "central" | "after-edge" | "text-after-edge" | "ideographic" | "alphabetic" | "hanging" | "mathematical" | "inherit";
allowReorder?: "no" | "yes";
alphabetic?: number | string;
amplitude?: number | string;
arabicForm?: "initial" | "medial" | "terminal" | "isolated";
ascent?: number | string;
attributeName?: string;
attributeType?: string;
autoReverse?: number | string;
azimuth?: number | string;
baseFrequency?: number | string;
baselineShift?: number | string;
baseProfile?: number | string;
bbox?: number | string;
begin?: number | string;
bias?: number | string;
by?: number | string;
calcMode?: number | string;
capHeight?: number | string;
clip?: number | string;
clipPath?: string;
clipPathUnits?: number | string;
clipRule?: number | string;
colorInterpolation?: number | string;
colorInterpolationFilters?: "auto" | "sRGB" | "linearRGB" | "inherit";
colorProfile?: number | string;
colorRendering?: number | string;
contentScriptType?: number | string;
contentStyleType?: number | string;
cursor?: number | string;
cx?: number | string;
cy?: number | string;
d?: string;
decelerate?: number | string;
descent?: number | string;
diffuseConstant?: number | string;
direction?: number | string;
display?: number | string;
divisor?: number | string;
dominantBaseline?: number | string;
dur?: number | string;
dx?: number | string;
dy?: number | string;
edgeMode?: number | string;
elevation?: number | string;
enableBackground?: number | string;
end?: number | string;
exponent?: number | string;
externalResourcesRequired?: number | string;
fill?: string;
fillOpacity?: number | string;
fillRule?: "nonzero" | "evenodd" | "inherit";
filter?: string;
filterRes?: number | string;
filterUnits?: number | string;
floodColor?: number | string;
floodOpacity?: number | string;
focusable?: number | string;
fontFamily?: string;
fontSize?: number | string;
fontSizeAdjust?: number | string;
fontStretch?: number | string;
fontStyle?: number | string;
fontVariant?: number | string;
fontWeight?: number | string;
format?: number | string;
from?: number | string;
fx?: number | string;
fy?: number | string;
g1?: number | string;
g2?: number | string;
glyphName?: number | string;
glyphOrientationHorizontal?: number | string;
glyphOrientationVertical?: number | string;
glyphRef?: number | string;
gradientTransform?: string;
gradientUnits?: string;
hanging?: number | string;
horizAdvX?: number | string;
horizOriginX?: number | string;
ideographic?: number | string;
imageRendering?: number | string;
in2?: number | string;
in?: string;
intercept?: number | string;
k1?: number | string;
k2?: number | string;
k3?: number | string;
k4?: number | string;
k?: number | string;
kernelMatrix?: number | string;
kernelUnitLength?: number | string;
kerning?: number | string;
keyPoints?: number | string;
keySplines?: number | string;
keyTimes?: number | string;
lengthAdjust?: number | string;
letterSpacing?: number | string;
lightingColor?: number | string;
limitingConeAngle?: number | string;
local?: number | string;
markerEnd?: string;
markerHeight?: number | string;
markerMid?: string;
markerStart?: string;
markerUnits?: number | string;
markerWidth?: number | string;
mask?: string;
maskContentUnits?: number | string;
maskUnits?: number | string;
mathematical?: number | string;
mode?: number | string;
numOctaves?: number | string;
offset?: number | string;
opacity?: number | string;
operator?: number | string;
order?: number | string;
orient?: number | string;
orientation?: number | string;
origin?: number | string;
overflow?: number | string;
overlinePosition?: number | string;
overlineThickness?: number | string;
paintOrder?: number | string;
panose1?: number | string;
pathLength?: number | string;
patternContentUnits?: string;
patternTransform?: number | string;
patternUnits?: string;
pointerEvents?: number | string;
points?: string;
pointsAtX?: number | string;
pointsAtY?: number | string;
pointsAtZ?: number | string;
preserveAlpha?: number | string;
preserveAspectRatio?: string;
primitiveUnits?: number | string;
r?: number | string;
radius?: number | string;
refX?: number | string;
refY?: number | string;
renderingIntent?: number | string;
repeatCount?: number | string;
repeatDur?: number | string;
requiredExtensions?: number | string;
requiredFeatures?: number | string;
restart?: number | string;
result?: string;
rotate?: number | string;
rx?: number | string;
ry?: number | string;
scale?: number | string;
seed?: number | string;
shapeRendering?: number | string;
slope?: number | string;
spacing?: number | string;
specularConstant?: number | string;
specularExponent?: number | string;
speed?: number | string;
spreadMethod?: string;
startOffset?: number | string;
stdDeviation?: number | string;
stemh?: number | string;
stemv?: number | string;
stitchTiles?: number | string;
stopColor?: string;
stopOpacity?: number | string;
strikethroughPosition?: number | string;
strikethroughThickness?: number | string;
string?: number | string;
stroke?: string;
strokeDasharray?: string | number;
strokeDashoffset?: string | number;
strokeLinecap?: "butt" | "round" | "square" | "inherit";
strokeLinejoin?: "miter" | "round" | "bevel" | "inherit";
strokeMiterlimit?: string;
strokeOpacity?: number | string;
strokeWidth?: number | string;
surfaceScale?: number | string;
systemLanguage?: number | string;
tableValues?: number | string;
targetX?: number | string;
targetY?: number | string;
textAnchor?: string;
textDecoration?: number | string;
textLength?: number | string;
textRendering?: number | string;
to?: number | string;
transform?: string;
u1?: number | string;
u2?: number | string;
underlinePosition?: number | string;
underlineThickness?: number | string;
unicode?: number | string;
unicodeBidi?: number | string;
unicodeRange?: number | string;
unitsPerEm?: number | string;
vAlphabetic?: number | string;
values?: string;
vectorEffect?: number | string;
version?: string;
vertAdvY?: number | string;
vertOriginX?: number | string;
vertOriginY?: number | string;
vHanging?: number | string;
vIdeographic?: number | string;
viewBox?: string;
viewTarget?: number | string;
visibility?: number | string;
vMathematical?: number | string;
widths?: number | string;
wordSpacing?: number | string;
writingMode?: number | string;
x1?: number | string;
x2?: number | string;
x?: number | string;
xChannelSelector?: string;
xHeight?: number | string;
xlinkActuate?: string;
xlinkArcrole?: string;
xlinkHref?: string;
xlinkRole?: string;
xlinkShow?: string;
xlinkTitle?: string;
xlinkType?: string;
xmlBase?: string;
xmlLang?: string;
xmlns?: string;
xmlnsXlink?: string;
xmlSpace?: string;
y1?: number | string;
y2?: number | string;
y?: number | string;
yChannelSelector?: string;
z?: number | string;
zoomAndPan?: string;
}
interface PathAttributes {
d: string;
}
interface EventHandler<E extends Event> {
(event: E): void;
}
type ClipboardEventHandler = EventHandler<ClipboardEvent>;
type CompositionEventHandler = EventHandler<CompositionEvent>;
type DragEventHandler = EventHandler<DragEvent>;
type FocusEventHandler = EventHandler<FocusEvent>;
type KeyboardEventHandler = EventHandler<KeyboardEvent>;
type MouseEventHandler = EventHandler<MouseEvent>;
type TouchEventHandler = EventHandler<TouchEvent>;
type UIEventHandler = EventHandler<UIEvent>;
type WheelEventHandler = EventHandler<WheelEvent>;
type AnimationEventHandler = EventHandler<AnimationEvent>;
type TransitionEventHandler = EventHandler<TransitionEvent>;
type GenericEventHandler = EventHandler<Event>;
type PointerEventHandler = EventHandler<PointerEvent>;
interface DOMAttributes extends omi.PreactDOMAttributes {
// Image Events
onLoad?: GenericEventHandler;
onError?: GenericEventHandler;
onLoadCapture?: GenericEventHandler;
// Clipboard Events
onCopy?: ClipboardEventHandler;
onCopyCapture?: ClipboardEventHandler;
onCut?: ClipboardEventHandler;
onCutCapture?: ClipboardEventHandler;
onPaste?: ClipboardEventHandler;
onPasteCapture?: ClipboardEventHandler;
// Composition Events
onCompositionEnd?: CompositionEventHandler;
onCompositionEndCapture?: CompositionEventHandler;
onCompositionStart?: CompositionEventHandler;
onCompositionStartCapture?: CompositionEventHandler;
onCompositionUpdate?: CompositionEventHandler;
onCompositionUpdateCapture?: CompositionEventHandler;
// Focus Events
onFocus?: FocusEventHandler;
onFocusCapture?: FocusEventHandler;
onBlur?: FocusEventHandler;
onBlurCapture?: FocusEventHandler;
// Form Events
onChange?: GenericEventHandler;
onChangeCapture?: GenericEventHandler;
onInput?: GenericEventHandler;
onInputCapture?: GenericEventHandler;
onSearch?: GenericEventHandler;
onSearchCapture?: GenericEventHandler;
onSubmit?: GenericEventHandler;
onSubmitCapture?: GenericEventHandler;
onInvalid?: GenericEventHandler;
// Keyboard Events
onKeyDown?: KeyboardEventHandler;
onKeyDownCapture?: KeyboardEventHandler;
onKeyPress?: KeyboardEventHandler;
onKeyPressCapture?: KeyboardEventHandler;
onKeyUp?: KeyboardEventHandler;
onKeyUpCapture?: KeyboardEventHandler;
// Media Events
onAbort?: GenericEventHandler;
onAbortCapture?: GenericEventHandler;
onCanPlay?: GenericEventHandler;
onCanPlayCapture?: GenericEventHandler;
onCanPlayThrough?: GenericEventHandler;
onCanPlayThroughCapture?: GenericEventHandler;
onDurationChange?: GenericEventHandler;
onDurationChangeCapture?: GenericEventHandler;
onEmptied?: GenericEventHandler;
onEmptiedCapture?: GenericEventHandler;
onEncrypted?: GenericEventHandler;
onEncryptedCapture?: GenericEventHandler;
onEnded?: GenericEventHandler;
onEndedCapture?: GenericEventHandler;
onLoadedData?: GenericEventHandler;
onLoadedDataCapture?: GenericEventHandler;
onLoadedMetadata?: GenericEventHandler;
onLoadedMetadataCapture?: GenericEventHandler;
onLoadStart?: GenericEventHandler;
onLoadStartCapture?: GenericEventHandler;
onPause?: GenericEventHandler;
onPauseCapture?: GenericEventHandler;
onPlay?: GenericEventHandler;
onPlayCapture?: GenericEventHandler;
onPlaying?: GenericEventHandler;
onPlayingCapture?: GenericEventHandler;
onProgress?: GenericEventHandler;
onProgressCapture?: GenericEventHandler;
onRateChange?: GenericEventHandler;
onRateChangeCapture?: GenericEventHandler;
onSeeked?: GenericEventHandler;
onSeekedCapture?: GenericEventHandler;
onSeeking?: GenericEventHandler;
onSeekingCapture?: GenericEventHandler;
onStalled?: GenericEventHandler;
onStalledCapture?: GenericEventHandler;
onSuspend?: GenericEventHandler;
onSuspendCapture?: GenericEventHandler;
onTimeUpdate?: GenericEventHandler;
onTimeUpdateCapture?: GenericEventHandler;
onVolumeChange?: GenericEventHandler;
onVolumeChangeCapture?: GenericEventHandler;
onWaiting?: GenericEventHandler;
onWaitingCapture?: GenericEventHandler;
// MouseEvents
onClick?: MouseEventHandler;
onClickCapture?: MouseEventHandler;
onContextMenu?: MouseEventHandler;
onContextMenuCapture?: MouseEventHandler;
onDblClick?: MouseEventHandler;
onDblClickCapture?: MouseEventHandler;
onDrag?: DragEventHandler;
onDragCapture?: DragEventHandler;
onDragEnd?: DragEventHandler;
onDragEndCapture?: DragEventHandler;
onDragEnter?: DragEventHandler;
onDragEnterCapture?: DragEventHandler;
onDragExit?: DragEventHandler;
onDragExitCapture?: DragEventHandler;
onDragLeave?: DragEventHandler;
onDragLeaveCapture?: DragEventHandler;
onDragOver?: DragEventHandler;
onDragOverCapture?: DragEventHandler;
onDragStart?: DragEventHandler;
onDragStartCapture?: DragEventHandler;
onDrop?: DragEventHandler;
onDropCapture?: DragEventHandler;
onMouseDown?: MouseEventHandler;
onMouseDownCapture?: MouseEventHandler;
onMouseEnter?: MouseEventHandler;
onMouseEnterCapture?: MouseEventHandler;
onMouseLeave?: MouseEventHandler;
onMouseLeaveCapture?: MouseEventHandler;
onMouseMove?: MouseEventHandler;
onMouseMoveCapture?: MouseEventHandler;
onMouseOut?: MouseEventHandler;
onMouseOutCapture?: MouseEventHandler;
onMouseOver?: MouseEventHandler;
onMouseOverCapture?: MouseEventHandler;
onMouseUp?: MouseEventHandler;
onMouseUpCapture?: MouseEventHandler;
// Selection Events
onSelect?: GenericEventHandler;
onSelectCapture?: GenericEventHandler;
// Touch Events
onTouchCancel?: TouchEventHandler;
onTouchCancelCapture?: TouchEventHandler;
onTouchEnd?: TouchEventHandler;
onTouchEndCapture?: TouchEventHandler;
onTouchMove?: TouchEventHandler;
onTouchMoveCapture?: TouchEventHandler;
onTouchStart?: TouchEventHandler;
onTouchStartCapture?: TouchEventHandler;
// Pointer Events
onPointerOver?: PointerEventHandler;
onPointerOverCapture?: PointerEventHandler;
onPointerEnter?: PointerEventHandler;
onPointerEnterCapture?: PointerEventHandler;
onPointerDown?: PointerEventHandler;
onPointerDownCapture?: PointerEventHandler;
onPointerMove?: PointerEventHandler;
onPointerMoveCapture?: PointerEventHandler;
onPointerUp?: PointerEventHandler;
onPointerUpCapture?: PointerEventHandler;
onPointerCancel?: PointerEventHandler;
onPointerCancelCapture?: PointerEventHandler;
onPointerOut?: PointerEventHandler;
onPointerOutCapture?: PointerEventHandler;
onPointerLeave?: PointerEventHandler;
onPointerLeaveCapture?: PointerEventHandler;
onGotPointerCapture?: PointerEventHandler;
onGotPointerCaptureCapture?: PointerEventHandler;
onLostPointerCapture?: PointerEventHandler;
onLostPointerCaptureCapture?: PointerEventHandler;
// UI Events
onScroll?: UIEventHandler;
onScrollCapture?: UIEventHandler;
// Wheel Events
onWheel?: WheelEventHandler;
onWheelCapture?: WheelEventHandler;
// Animation Events
onAnimationStart?: AnimationEventHandler;
onAnimationStartCapture?: AnimationEventHandler;
onAnimationEnd?: AnimationEventHandler;
onAnimationEndCapture?: AnimationEventHandler;
onAnimationIteration?: AnimationEventHandler;
onAnimationIterationCapture?: AnimationEventHandler;
// Transition Events
onTransitionEnd?: TransitionEventHandler;
onTransitionEndCapture?: TransitionEventHandler;
}
interface HTMLAttributes extends omi.PreactHTMLAttributes, DOMAttributes {
// Standard HTML Attributes
accept?: string;
acceptCharset?: string;
accessKey?: string;
action?: string;
allowFullScreen?: boolean;
allowTransparency?: boolean;
alt?: string;
async?: boolean;
autocomplete?: string;
autofocus?: boolean;
autoPlay?: boolean;
capture?: boolean;
cellPadding?: number | string;
cellSpacing?: number | string;
charSet?: string;
challenge?: string;
checked?: boolean;
class?: string;
className?: string;
cols?: number;
colSpan?: number;
content?: string;
contentEditable?: boolean;
contextMenu?: string;
controls?: boolean;
controlsList?: string;
coords?: string;
crossOrigin?: string;
data?: string;
dateTime?: string;
default?: boolean;
defer?: boolean;
dir?: string;
disabled?: boolean;
download?: any;
draggable?: boolean;
encType?: string;
form?: string;
formAction?: string;
formEncType?: string;
formMethod?: string;
formNoValidate?: boolean;
formTarget?: string;
frameBorder?: number | string;
headers?: string;
height?: number | string;
hidden?: boolean;
high?: number;
href?: string;
hrefLang?: string;
for?: string;
httpEquiv?: string;
icon?: string;
id?: string;
inputMode?: string;
integrity?: string;
is?: string;
keyParams?: string;
keyType?: string;
kind?: string;
label?: string;
lang?: string;
list?: string;
loop?: boolean;
low?: number;
manifest?: string;
marginHeight?: number;
marginWidth?: number;
max?: number | string;
maxLength?: number;
media?: string;
mediaGroup?: string;
method?: string;
min?: number | string;
minLength?: number;
multiple?: boolean;
muted?: boolean;
name?: string;
noValidate?: boolean;
open?: boolean;
optimum?: number;
pattern?: string;
placeholder?: string;
playsInline?: boolean;
poster?: string;
preload?: string;
radioGroup?: string;
readOnly?: boolean;
rel?: string;
required?: boolean;
role?: string;
rows?: number;
rowSpan?: number;
sandbox?: string;
scope?: string;
scoped?: boolean;
scrolling?: string;
seamless?: boolean;
selected?: boolean;
shape?: string;
size?: number;
sizes?: string;
slot?: string;
span?: number;
spellcheck?: boolean;
src?: string;
srcset?: string;
srcDoc?: string;
srcLang?: string;
srcSet?: string;
start?: number;
step?: number | string;
style?: any;
summary?: string;
tabIndex?: number;
target?: string;
title?: string;
type?: string;
useMap?: string;
value?: string | string[] | number;
width?: number | string;
wmode?: string;
wrap?: string;
// RDFa Attributes
about?: string;
datatype?: string;
inlist?: any;
prefix?: string;
property?: string;
resource?: string;
typeof?: string;
vocab?: string;
}
interface IntrinsicElements {
// HTML
a: HTMLAttributes;
abbr: HTMLAttributes;
address: HTMLAttributes;
area: HTMLAttributes;
article: HTMLAttributes;
aside: HTMLAttributes;
audio: HTMLAttributes;
b: HTMLAttributes;
base: HTMLAttributes;
bdi: HTMLAttributes;
bdo: HTMLAttributes;
big: HTMLAttributes;
blockquote: HTMLAttributes;
body: HTMLAttributes;
br: HTMLAttributes;
button: HTMLAttributes;
canvas: HTMLAttributes;
caption: HTMLAttributes;
cite: HTMLAttributes;
code: HTMLAttributes;
col: HTMLAttributes;
colgroup: HTMLAttributes;
data: HTMLAttributes;
datalist: HTMLAttributes;
dd: HTMLAttributes;
del: HTMLAttributes;
details: HTMLAttributes;
dfn: HTMLAttributes;
dialog: HTMLAttributes;
div: HTMLAttributes;
dl: HTMLAttributes;
dt: HTMLAttributes;
em: HTMLAttributes;
embed: HTMLAttributes;
fieldset: HTMLAttributes;
figcaption: HTMLAttributes;
figure: HTMLAttributes;
footer: HTMLAttributes;
form: HTMLAttributes;
h1: HTMLAttributes;
h2: HTMLAttributes;
h3: HTMLAttributes;
h4: HTMLAttributes;
h5: HTMLAttributes;
h6: HTMLAttributes;
head: HTMLAttributes;
header: HTMLAttributes;
hr: HTMLAttributes;
html: HTMLAttributes;
i: HTMLAttributes;
iframe: HTMLAttributes;
img: HTMLAttributes;
input: HTMLAttributes;
ins: HTMLAttributes;
kbd: HTMLAttributes;
keygen: HTMLAttributes;
label: HTMLAttributes;
legend: HTMLAttributes;
li: HTMLAttributes;
link: HTMLAttributes;
main: HTMLAttributes;
map: HTMLAttributes;
mark: HTMLAttributes;
menu: HTMLAttributes;
menuitem: HTMLAttributes;
meta: HTMLAttributes;
meter: HTMLAttributes;
nav: HTMLAttributes;
noscript: HTMLAttributes;
object: HTMLAttributes;
ol: HTMLAttributes;
optgroup: HTMLAttributes;
option: HTMLAttributes;
output: HTMLAttributes;
p: HTMLAttributes;
param: HTMLAttributes;
picture: HTMLAttributes;
pre: HTMLAttributes;
progress: HTMLAttributes;
q: HTMLAttributes;
rp: HTMLAttributes;
rt: HTMLAttributes;
ruby: HTMLAttributes;
s: HTMLAttributes;
samp: HTMLAttributes;
script: HTMLAttributes;
section: HTMLAttributes;
select: HTMLAttributes;
slot: HTMLAttributes;
small: HTMLAttributes;
source: HTMLAttributes;
span: HTMLAttributes;
strong: HTMLAttributes;
style: HTMLAttributes;
sub: HTMLAttributes;
summary: HTMLAttributes;
sup: HTMLAttributes;
table: HTMLAttributes;
tbody: HTMLAttributes;
td: HTMLAttributes;
textarea: HTMLAttributes;
tfoot: HTMLAttributes;
th: HTMLAttributes;
thead: HTMLAttributes;
time: HTMLAttributes;
title: HTMLAttributes;
tr: HTMLAttributes;
track: HTMLAttributes;
u: HTMLAttributes;
ul: HTMLAttributes;
"var": HTMLAttributes;
video: HTMLAttributes;
wbr: HTMLAttributes;
//SVG
svg: SVGAttributes;
animate: SVGAttributes;
circle: SVGAttributes;
clipPath: SVGAttributes;
defs: SVGAttributes;
desc: SVGAttributes;
ellipse: SVGAttributes;
feBlend: SVGAttributes;
feColorMatrix: SVGAttributes;
feComponentTransfer: SVGAttributes;
feComposite: SVGAttributes;
feConvolveMatrix: SVGAttributes;
feDiffuseLighting: SVGAttributes;
feDisplacementMap: SVGAttributes;
feFlood: SVGAttributes;
feGaussianBlur: SVGAttributes;
feImage: SVGAttributes;
feMerge: SVGAttributes;
feMergeNode: SVGAttributes;
feMorphology: SVGAttributes;
feOffset: SVGAttributes;
feSpecularLighting: SVGAttributes;
feTile: SVGAttributes;
feTurbulence: SVGAttributes;
filter: SVGAttributes;
foreignObject: SVGAttributes;
g: SVGAttributes;
image: SVGAttributes;
line: SVGAttributes;
linearGradient: SVGAttributes;
marker: SVGAttributes;
mask: SVGAttributes;
path: SVGAttributes;
pattern: SVGAttributes;
polygon: SVGAttributes;
polyline: SVGAttributes;
radialGradient: SVGAttributes;
rect: SVGAttributes;
stop: SVGAttributes;
symbol: SVGAttributes;
text: SVGAttributes;
tspan: SVGAttributes;
use: SVGAttributes;
}
}
}

45
packages/omis/src/omi.js Executable file
View File

@ -0,0 +1,45 @@
import { h, h as createElement } from './h';
import { cloneElement } from './clone-element';
import { Component } from './component';
import { render } from './render';
import { rerender } from './render-queue';
import options from './options';
function createRef() {
return {};
}
export default {
h,
createElement,
cloneElement,
createRef,
Component,
render,
rerender,
options
};
export {
h,
createElement,
cloneElement,
createRef,
Component,
render,
rerender,
options
};
if(typeof window !== 'undefined'){
window.Omi = {
h,
createElement,
cloneElement,
createRef,
Component,
render,
rerender,
options
};
}

25
packages/omis/src/omi.js.flow Executable file
View File

@ -0,0 +1,25 @@
/* @flow */
import { createElement, cloneElement, createRef, Component, type Node } from 'react';
declare var h: typeof createElement;
declare function render(vnode: Node, parent: Element, toReplace?: Element): Element;
export { h, createElement, cloneElement, createRef, Component, render };
export default { h, createElement, cloneElement, createRef, Component, render };
declare type VNode<P> = {
nodeName: string | Function,
children: Array<VNode<P> | string>,
key?: string | number | void,
attributes: P,
};
declare export function rerender(): void;
declare export var options: {
syncComponentUpdates?: boolean,
vnode?: (vnode: VNode<any>) => void,
debounceRendering?: (rerender: () => void) => void,
event?: (event: Event) => Event | void,
};

22
packages/omis/src/options.js Executable file
View File

@ -0,0 +1,22 @@
/**
* @typedef {import('./component').Component} Component
* @typedef {import('./vnode').VNode} VNode
*/
/**
* Global options
* @public
* @typedef Options
* @property {boolean} [syncComponentUpdates] If `true`, `prop` changes trigger synchronous component updates. Defaults to true.
* @property {(vnode: VNode) => void} [vnode] Processes all created VNodes.
* @property {(component: Component) => void} [afterMount] Hook invoked after a component is mounted.
* @property {(component: Component) => void} [afterUpdate] Hook invoked after the DOM is updated with a component's latest render.
* @property {(component: Component) => void} [beforeUnmount] Hook invoked immediately before a component is unmounted.
* @property {(rerender: function) => void} [debounceRendering] Hook invoked whenever a rerender is requested. Can be used to debounce rerenders.
* @property {(event: Event) => Event | void} [event] Hook invoked before any Preact event listeners. The return value (if any) replaces the native browser event given to event listeners
*/
/** @type {Options} */
const options = {};
export default options;

View File

@ -0,0 +1,27 @@
import options from './options';
import { defer } from './util';
import { renderComponent } from './vdom/component';
/**
* Managed queue of dirty components to be re-rendered
* @type {Array<import('./component').Component>}
*/
let items = [];
/**
* Enqueue a rerender of a component
* @param {import('./component').Component} component The component to rerender
*/
export function enqueueRender(component) {
if (!component._dirty && (component._dirty = true) && items.push(component)==1) {
(options.debounceRendering || defer)(rerender);
}
}
/** Rerender all enqueued dirty components */
export function rerender() {
let p;
while ( (p = items.pop()) ) {
if (p._dirty) renderComponent(p);
}
}

22
packages/omis/src/render.js Executable file
View File

@ -0,0 +1,22 @@
import { diff } from './vdom/diff';
/**
* Render JSX into a `parent` Element.
* @param {import('./vnode').VNode} vnode A (JSX) VNode to render
* @param {import('./dom').PreactElement} parent DOM element to render into
* @param {import('./dom').PreactElement} [merge] Attempt to re-use an existing DOM tree rooted at
* `merge`
* @public
*
* @example
* // render a div into <body>:
* render(<div id="hello">hello!</div>, document.body);
*
* @example
* // render a "Thing" component into #foo:
* const Thing = ({ name }) => <span>{ name }</span>;
* render(<Thing name="one" />, document.querySelector('#foo'));
*/
export function render(vnode, parent, merge) {
return diff(merge, vnode, {}, false, parent, false);
}

30
packages/omis/src/util.js Executable file
View File

@ -0,0 +1,30 @@
/**
* Copy all properties from `props` onto `obj`.
* @param {object} obj Object onto which properties should be copied.
* @param {object} props Object from which to copy properties.
* @returns {object}
* @private
*/
export function extend(obj, props) {
for (let i in props) obj[i] = props[i];
return obj;
}
/** Invoke or update a ref, depending on whether it is a function or object ref.
* @param {object|function} [ref=null]
* @param {any} [value]
*/
export function applyRef(ref, value) {
if (ref) {
if (typeof ref=='function') ref(value);
else ref.current = value;
}
}
/**
* Call a function asynchronously, as soon as possible. Makes
* use of HTML Promise to schedule the callback if available,
* otherwise falling back to `setTimeout` (mainly for IE<11).
* @type {(callback: function) => void}
*/
export const defer = typeof Promise=='function' ? Promise.resolve().then.bind(Promise.resolve()) : setTimeout;

View File

@ -0,0 +1,48 @@
import { Component } from '../component';
/**
* Retains a pool of Components for re-use.
* @type {Component[]}
* @private
*/
export const recyclerComponents = [];
/**
* Create a component. Normalizes differences between PFC's and classful
* Components.
* @param {function} Ctor The constructor of the component to create
* @param {object} props The initial props of the component
* @param {object} context The initial context of the component
* @returns {import('../component').Component}
*/
export function createComponent(Ctor, props, context) {
let inst, i = recyclerComponents.length;
if (Ctor.prototype && Ctor.prototype.render) {
inst = new Ctor(props, context);
Component.call(inst, props, context);
}
else {
inst = new Component(props, context);
inst.constructor = Ctor;
inst.render = doRender;
}
while (i--) {
if (recyclerComponents[i].constructor===Ctor) {
inst.nextBase = recyclerComponents[i].nextBase;
recyclerComponents.splice(i, 1);
return inst;
}
}
return inst;
}
/** The `.render()` method for a PFC backing instance. */
function doRender(props, state, context) {
return this.constructor(props, context);
}

View File

@ -0,0 +1,296 @@
import { SYNC_RENDER, NO_RENDER, FORCE_RENDER, ASYNC_RENDER, ATTR_KEY } from '../constants';
import options from '../options';
import { extend, applyRef } from '../util';
import { enqueueRender } from '../render-queue';
import { getNodeProps } from './index';
import { diff, mounts, diffLevel, flushMounts, recollectNodeTree, removeChildren } from './diff';
import { createComponent, recyclerComponents } from './component-recycler';
import { removeNode } from '../dom/index';
/**
* Set a component's `props` and possibly re-render the component
* @param {import('../component').Component} component The Component to set props on
* @param {object} props The new props
* @param {number} renderMode Render options - specifies how to re-render the component
* @param {object} context The new context
* @param {boolean} mountAll Whether or not to immediately mount all components
*/
export function setComponentProps(component, props, renderMode, context, mountAll) {
if (component._disable) return;
component._disable = true;
component.__ref = props.ref;
component.__key = props.key;
delete props.ref;
delete props.key;
if (typeof component.constructor.getDerivedStateFromProps === 'undefined') {
if (!component.base || mountAll) {
if (component.componentWillMount) component.componentWillMount();
}
else if (component.componentWillReceiveProps) {
component.componentWillReceiveProps(props, context);
}
}
if (context && context!==component.context) {
if (!component.prevContext) component.prevContext = component.context;
component.context = context;
}
if (!component.prevProps) component.prevProps = component.props;
component.props = props;
component._disable = false;
if (renderMode!==NO_RENDER) {
if (renderMode===SYNC_RENDER || options.syncComponentUpdates!==false || !component.base) {
renderComponent(component, SYNC_RENDER, mountAll);
}
else {
enqueueRender(component);
}
}
applyRef(component.__ref, component);
}
/**
* Render a Component, triggering necessary lifecycle events and taking
* High-Order Components into account.
* @param {import('../component').Component} component The component to render
* @param {number} [renderMode] render mode, see constants.js for available options.
* @param {boolean} [mountAll] Whether or not to immediately mount all components
* @param {boolean} [isChild] ?
* @private
*/
export function renderComponent(component, renderMode, mountAll, isChild) {
if (component._disable) return;
let props = component.props,
state = component.state,
context = component.context,
previousProps = component.prevProps || props,
previousState = component.prevState || state,
previousContext = component.prevContext || context,
isUpdate = component.base,
nextBase = component.nextBase,
initialBase = isUpdate || nextBase,
initialChildComponent = component._component,
skip = false,
snapshot = previousContext,
rendered, inst, cbase;
if (component.constructor.getDerivedStateFromProps) {
state = extend(extend({}, state), component.constructor.getDerivedStateFromProps(props, state));
component.state = state;
}
// if updating
if (isUpdate) {
component.props = previousProps;
component.state = previousState;
component.context = previousContext;
if (renderMode!==FORCE_RENDER
&& component.shouldComponentUpdate
&& component.shouldComponentUpdate(props, state, context) === false) {
skip = true;
}
else if (component.componentWillUpdate) {
component.componentWillUpdate(props, state, context);
}
component.props = props;
component.state = state;
component.context = context;
}
component.prevProps = component.prevState = component.prevContext = component.nextBase = null;
component._dirty = false;
if (!skip) {
rendered = component.render(props, state, context);
// context to pass to the child, can be updated via (grand-)parent component
if (component.getChildContext) {
context = extend(extend({}, context), component.getChildContext());
}
if (isUpdate && component.getSnapshotBeforeUpdate) {
snapshot = component.getSnapshotBeforeUpdate(previousProps, previousState);
}
let childComponent = rendered && rendered.nodeName,
toUnmount, base;
if (typeof childComponent==='function') {
// set up high order component link
let childProps = getNodeProps(rendered);
inst = initialChildComponent;
if (inst && inst.constructor===childComponent && childProps.key==inst.__key) {
setComponentProps(inst, childProps, SYNC_RENDER, context, false);
}
else {
toUnmount = inst;
component._component = inst = createComponent(childComponent, childProps, context);
inst.nextBase = inst.nextBase || nextBase;
inst._parentComponent = component;
setComponentProps(inst, childProps, NO_RENDER, context, false);
renderComponent(inst, SYNC_RENDER, mountAll, true);
}
base = inst.base;
}
else {
cbase = initialBase;
// destroy high order component link
toUnmount = initialChildComponent;
if (toUnmount) {
cbase = component._component = null;
}
if (initialBase || renderMode===SYNC_RENDER) {
if (cbase) cbase._component = null;
base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true);
}
}
if (initialBase && base!==initialBase && inst!==initialChildComponent) {
let baseParent = initialBase.parentNode;
if (baseParent && base!==baseParent) {
baseParent.replaceChild(base, initialBase);
if (!toUnmount) {
initialBase._component = null;
recollectNodeTree(initialBase, false);
}
}
}
if (toUnmount) {
unmountComponent(toUnmount);
}
component.base = base;
if (base && !isChild) {
let componentRef = component,
t = component;
while ((t=t._parentComponent)) {
(componentRef = t).base = base;
}
base._component = componentRef;
base._componentConstructor = componentRef.constructor;
}
}
if (!isUpdate || mountAll) {
mounts.push(component);
}
else if (!skip) {
// Ensure that pending componentDidMount() hooks of child components
// are called before the componentDidUpdate() hook in the parent.
// Note: disabled as it causes duplicate hooks, see https://github.com/developit/omi/issues/750
// flushMounts();
if (component.componentDidUpdate) {
component.componentDidUpdate(previousProps, previousState, snapshot);
}
if (options.afterUpdate) options.afterUpdate(component);
}
while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component);
if (!diffLevel && !isChild) flushMounts();
}
/**
* Apply the Component referenced by a VNode to the DOM.
* @param {import('../dom').PreactElement} dom The DOM node to mutate
* @param {import('../vnode').VNode} vnode A Component-referencing VNode
* @param {object} context The current context
* @param {boolean} mountAll Whether or not to immediately mount all components
* @returns {import('../dom').PreactElement} The created/mutated element
* @private
*/
export function buildComponentFromVNode(dom, vnode, context, mountAll) {
let c = dom && dom._component,
originalComponent = c,
oldDom = dom,
isDirectOwner = c && dom._componentConstructor===vnode.nodeName,
isOwner = isDirectOwner,
props = getNodeProps(vnode);
while (c && !isOwner && (c=c._parentComponent)) {
isOwner = c.constructor===vnode.nodeName;
}
if (c && isOwner && (!mountAll || c._component)) {
setComponentProps(c, props, ASYNC_RENDER, context, mountAll);
dom = c.base;
}
else {
if (originalComponent && !isDirectOwner) {
unmountComponent(originalComponent);
dom = oldDom = null;
}
c = createComponent(vnode.nodeName, props, context);
if (dom && !c.nextBase) {
c.nextBase = dom;
// passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L229:
oldDom = null;
}
setComponentProps(c, props, SYNC_RENDER, context, mountAll);
dom = c.base;
if (oldDom && dom!==oldDom) {
oldDom._component = null;
recollectNodeTree(oldDom, false);
}
}
return dom;
}
/**
* Remove a component from the DOM and recycle it.
* @param {import('../component').Component} component The Component instance to unmount
* @private
*/
export function unmountComponent(component) {
if (options.beforeUnmount) options.beforeUnmount(component);
let base = component.base;
component._disable = true;
if (component.componentWillUnmount) component.componentWillUnmount();
component.base = null;
// recursively tear down & recollect high-order component children:
let inner = component._component;
if (inner) {
unmountComponent(inner);
}
else if (base) {
if (base[ATTR_KEY]!=null) applyRef(base[ATTR_KEY].ref, null);
component.nextBase = base;
removeNode(base);
recyclerComponents.push(component);
removeChildren(base);
}
applyRef(component.__ref, null);
}

337
packages/omis/src/vdom/diff.js Executable file
View File

@ -0,0 +1,337 @@
import { ATTR_KEY } from '../constants';
import { isSameNodeType, isNamedNode } from './index';
import { buildComponentFromVNode } from './component';
import { createNode, setAccessor } from '../dom/index';
import { unmountComponent } from './component';
import options from '../options';
import { applyRef } from '../util';
import { removeNode } from '../dom/index';
/**
* Queue of components that have been mounted and are awaiting componentDidMount
* @type {Array<import('../component').Component>}
*/
export const mounts = [];
/** Diff recursion count, used to track the end of the diff cycle. */
export let diffLevel = 0;
/** Global flag indicating if the diff is currently within an SVG */
let isSvgMode = false;
/** Global flag indicating if the diff is performing hydration */
let hydrating = false;
/** Invoke queued componentDidMount lifecycle methods */
export function flushMounts() {
let c;
while ((c = mounts.shift())) {
if (options.afterMount) options.afterMount(c);
if (c.componentDidMount) c.componentDidMount();
}
}
/**
* Apply differences in a given vnode (and it's deep children) to a real DOM Node.
* @param {import('../dom').PreactElement} dom A DOM node to mutate into the shape of a `vnode`
* @param {import('../vnode').VNode} vnode A VNode (with descendants forming a tree) representing
* the desired DOM structure
* @param {object} context The current context
* @param {boolean} mountAll Whether or not to immediately mount all components
* @param {Element} parent ?
* @param {boolean} componentRoot ?
* @returns {import('../dom').PreactElement} The created/mutated element
* @private
*/
export function diff(dom, vnode, context, mountAll, parent, componentRoot) {
// diffLevel having been 0 here indicates initial entry into the diff (not a subdiff)
if (!diffLevel++) {
// when first starting the diff, check if we're diffing an SVG or within an SVG
isSvgMode = parent!=null && parent.ownerSVGElement!==undefined;
// hydration is indicated by the existing element to be diffed not having a prop cache
hydrating = dom!=null && !(ATTR_KEY in dom);
}
let ret = idiff(dom, vnode, context, mountAll, componentRoot);
// append the element if its a new parent
if (parent && ret.parentNode!==parent) parent.appendChild(ret);
// diffLevel being reduced to 0 means we're exiting the diff
if (!--diffLevel) {
hydrating = false;
// invoke queued componentDidMount lifecycle methods
if (!componentRoot) flushMounts();
}
return ret;
}
/**
* Internals of `diff()`, separated to allow bypassing diffLevel / mount flushing.
* @param {import('../dom').PreactElement} dom A DOM node to mutate into the shape of a `vnode`
* @param {import('../vnode').VNode} vnode A VNode (with descendants forming a tree) representing the desired DOM structure
* @param {object} context The current context
* @param {boolean} mountAll Whether or not to immediately mount all components
* @param {boolean} [componentRoot] ?
* @private
*/
function idiff(dom, vnode, context, mountAll, componentRoot) {
let out = dom,
prevSvgMode = isSvgMode;
// empty values (null, undefined, booleans) render as empty Text nodes
if (vnode==null || typeof vnode==='boolean') vnode = '';
// Fast case: Strings & Numbers create/update Text nodes.
if (typeof vnode==='string' || typeof vnode==='number') {
// update if it's already a Text node:
if (dom && dom.splitText!==undefined && dom.parentNode && (!dom._component || componentRoot)) {
/* istanbul ignore if */ /* Browser quirk that can't be covered: https://github.com/developit/omi/commit/fd4f21f5c45dfd75151bd27b4c217d8003aa5eb9 */
if (dom.nodeValue!=vnode) {
dom.nodeValue = vnode;
}
}
else {
// it wasn't a Text node: replace it with one and recycle the old Element
out = document.createTextNode(vnode);
if (dom) {
if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
recollectNodeTree(dom, true);
}
}
out[ATTR_KEY] = true;
return out;
}
// If the VNode represents a Component, perform a component diff:
let vnodeName = vnode.nodeName;
if (typeof vnodeName==='function') {
return buildComponentFromVNode(dom, vnode, context, mountAll);
}
// Tracks entering and exiting SVG namespace when descending through the tree.
isSvgMode = vnodeName==='svg' ? true : vnodeName==='foreignObject' ? false : isSvgMode;
// If there's no existing element or it's the wrong type, create a new one:
vnodeName = String(vnodeName);
if (!dom || !isNamedNode(dom, vnodeName)) {
out = createNode(vnodeName, isSvgMode);
if (dom) {
// move children into the replacement node
while (dom.firstChild) out.appendChild(dom.firstChild);
// if the previous Element was mounted into the DOM, replace it inline
if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
// recycle the old element (skips non-Element node types)
recollectNodeTree(dom, true);
}
}
let fc = out.firstChild,
props = out[ATTR_KEY],
vchildren = vnode.children;
if (props==null) {
props = out[ATTR_KEY] = {};
for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value;
}
// Optimization: fast-path for elements containing a single TextNode:
if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {
if (fc.nodeValue!=vchildren[0]) {
fc.nodeValue = vchildren[0];
}
}
// otherwise, if there are existing or new children, diff them:
else if (vchildren && vchildren.length || fc!=null) {
innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null);
}
// Apply attributes/props from VNode to the DOM Element:
diffAttributes(out, vnode.attributes, props);
// restore previous SVG mode: (in case we're exiting an SVG namespace)
isSvgMode = prevSvgMode;
return out;
}
/**
* Apply child and attribute changes between a VNode and a DOM Node to the DOM.
* @param {import('../dom').PreactElement} dom Element whose children should be compared & mutated
* @param {Array<import('../vnode').VNode>} vchildren Array of VNodes to compare to `dom.childNodes`
* @param {object} context Implicitly descendant context object (from most
* recent `getChildContext()`)
* @param {boolean} mountAll Whether or not to immediately mount all components
* @param {boolean} isHydrating if `true`, consumes externally created elements
* similar to hydration
*/
function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) {
let originalChildren = dom.childNodes,
children = [],
keyed = {},
keyedLen = 0,
min = 0,
len = originalChildren.length,
childrenLen = 0,
vlen = vchildren ? vchildren.length : 0,
j, c, f, vchild, child;
// Build up a map of keyed children and an Array of unkeyed children:
if (len!==0) {
for (let i=0; i<len; i++) {
let child = originalChildren[i],
props = child[ATTR_KEY],
key = vlen && props ? child._component ? child._component.__key : props.key : null;
if (key!=null) {
keyedLen++;
keyed[key] = child;
}
else if (props || (child.splitText!==undefined ? (isHydrating ? child.nodeValue.trim() : true) : isHydrating)) {
children[childrenLen++] = child;
}
}
}
if (vlen!==0) {
for (let i=0; i<vlen; i++) {
vchild = vchildren[i];
child = null;
// attempt to find a node based on key matching
let key = vchild.key;
if (key!=null) {
if (keyedLen && keyed[key]!==undefined) {
child = keyed[key];
keyed[key] = undefined;
keyedLen--;
}
}
// attempt to pluck a node of the same type from the existing children
else if (min<childrenLen) {
for (j=min; j<childrenLen; j++) {
if (children[j]!==undefined && isSameNodeType(c = children[j], vchild, isHydrating)) {
child = c;
children[j] = undefined;
if (j===childrenLen-1) childrenLen--;
if (j===min) min++;
break;
}
}
}
// morph the matched/found/created DOM child to match vchild (deep)
child = idiff(child, vchild, context, mountAll);
f = originalChildren[i];
if (child && child!==dom && child!==f) {
if (f==null) {
dom.appendChild(child);
}
else if (child===f.nextSibling) {
removeNode(f);
}
else {
dom.insertBefore(child, f);
}
}
}
}
// remove unused keyed children:
if (keyedLen) {
for (let i in keyed) if (keyed[i]!==undefined) recollectNodeTree(keyed[i], false);
}
// remove orphaned unkeyed children:
while (min<=childrenLen) {
if ((child = children[childrenLen--])!==undefined) recollectNodeTree(child, false);
}
}
/**
* Recursively recycle (or just unmount) a node and its descendants.
* @param {import('../dom').PreactElement} node DOM node to start
* unmount/removal from
* @param {boolean} [unmountOnly=false] If `true`, only triggers unmount
* lifecycle, skips removal
*/
export function recollectNodeTree(node, unmountOnly) {
let component = node._component;
if (component) {
// if node is owned by a Component, unmount that component (ends up recursing back here)
unmountComponent(component);
}
else {
// If the node's VNode had a ref function, invoke it with null here.
// (this is part of the React spec, and smart for unsetting references)
if (node[ATTR_KEY]!=null) applyRef(node[ATTR_KEY].ref, null);
if (unmountOnly===false || node[ATTR_KEY]==null) {
removeNode(node);
}
removeChildren(node);
}
}
/**
* Recollect/unmount all children.
* - we use .lastChild here because it causes less reflow than .firstChild
* - it's also cheaper than accessing the .childNodes Live NodeList
*/
export function removeChildren(node) {
node = node.lastChild;
while (node) {
let next = node.previousSibling;
recollectNodeTree(node, true);
node = next;
}
}
/**
* Apply differences in attributes from a VNode to the given DOM Element.
* @param {import('../dom').PreactElement} dom Element with attributes to diff `attrs` against
* @param {object} attrs The desired end-state key-value attribute pairs
* @param {object} old Current/previous attributes (from previous VNode or
* element's prop cache)
*/
function diffAttributes(dom, attrs, old) {
let name;
// remove attributes no longer present on the vnode by setting them to undefined
for (name in old) {
if (!(attrs && attrs[name]!=null) && old[name]!=null) {
setAccessor(dom, name, old[name], old[name] = undefined, isSvgMode);
}
}
// add new & update changed attributes
for (name in attrs) {
if (name!=='children' && name!=='innerHTML' && (!(name in old) || attrs[name]!==(name==='value' || name==='checked' ? dom[name] : old[name]))) {
setAccessor(dom, name, old[name], old[name] = attrs[name], isSvgMode);
}
}
}

54
packages/omis/src/vdom/index.js Executable file
View File

@ -0,0 +1,54 @@
import { extend } from '../util';
/**
* Check if two nodes are equivalent.
* @param {import('../dom').PreactElement} node DOM Node to compare
* @param {import('../vnode').VNode} vnode Virtual DOM node to compare
* @param {boolean} [hydrating=false] If true, ignores component constructors
* when comparing.
* @private
*/
export function isSameNodeType(node, vnode, hydrating) {
if (typeof vnode==='string' || typeof vnode==='number') {
return node.splitText!==undefined;
}
if (typeof vnode.nodeName==='string') {
return !node._componentConstructor && isNamedNode(node, vnode.nodeName);
}
return hydrating || node._componentConstructor===vnode.nodeName;
}
/**
* Check if an Element has a given nodeName, case-insensitively.
* @param {import('../dom').PreactElement} node A DOM Element to inspect the name of.
* @param {string} nodeName Unnormalized name to compare against.
*/
export function isNamedNode(node, nodeName) {
return node.normalizedNodeName===nodeName || node.nodeName.toLowerCase()===nodeName.toLowerCase();
}
/**
* Reconstruct Component-style `props` from a VNode.
* Ensures default/fallback values from `defaultProps`:
* Own-properties of `defaultProps` not present in `vnode.attributes` are added.
* @param {import('../vnode').VNode} vnode The VNode to get props for
* @returns {object} The props to use for this VNode
*/
export function getNodeProps(vnode) {
let props = extend({}, vnode.attributes);
props.children = vnode.children;
let defaultProps = vnode.nodeName.defaultProps;
if (defaultProps!==undefined) {
for (let i in defaultProps) {
if (props[i]===undefined) {
props[i] = defaultProps[i];
}
}
}
return props;
}

9
packages/omis/src/vnode.js Executable file
View File

@ -0,0 +1,9 @@
/**
* Virtual DOM Node
* @typedef VNode
* @property {string | function} nodeName The string of the DOM node to create or Component constructor to render
* @property {Array<VNode | string>} children The children of node
* @property {string | number | undefined} key The key used to identify this VNode in a list
* @property {object} attributes The properties of this VNode
*/
export const VNode = function VNode() {};