Initial draft.
This commit is contained in:
Коммит
6498c51d0d
|
@ -0,0 +1,15 @@
|
|||
[ignore]
|
||||
.*/examples/.*
|
||||
.*/src/test/.*
|
||||
.*/node_modules/reflex/examples/.*
|
||||
.*/node_modules/reflex/lib/.*
|
||||
.*/node_modules/reflex/dist/.*
|
||||
.*/reflex-virtual-dom-renderer/lib/.*
|
||||
.*/reflex-virtual-dom-renderer/dist/.*
|
||||
|
||||
[libs]
|
||||
./node_modules/reflex/interfaces/
|
||||
|
||||
[include]
|
||||
|
||||
[options]
|
|
@ -0,0 +1,2 @@
|
|||
lib
|
||||
dist
|
|
@ -0,0 +1,2 @@
|
|||
*~
|
||||
~*
|
|
@ -0,0 +1,16 @@
|
|||
[ignore]
|
||||
.*/src/test/.*
|
||||
.*/dist/.*
|
||||
.*/node_modules/reflex/examples/.*
|
||||
.*/node_modules/reflex-virtual-dom-renderer/lib/.*
|
||||
.*/node_modules/reflex/lib/.*
|
||||
|
||||
[libs]
|
||||
./node_modules/reflex/interfaces/
|
||||
./node_modules/reflex-virtual-dom-renderer/interfaces/
|
||||
|
||||
[include]
|
||||
|
||||
[options]
|
||||
module.name_mapper='reflex-virtual-dom-renderer' -> 'reflex-virtual-dom-renderer/src/index'
|
||||
module.name_mapper='reflex' -> 'reflex/src/index'
|
|
@ -0,0 +1,135 @@
|
|||
'use strict';
|
||||
|
||||
import browserify from 'browserify';
|
||||
import gulp from 'gulp';
|
||||
import source from 'vinyl-source-stream';
|
||||
import buffer from 'vinyl-buffer';
|
||||
import uglify from 'gulp-uglify';
|
||||
import sourcemaps from 'gulp-sourcemaps';
|
||||
import gutil from 'gulp-util';
|
||||
import watchify from 'watchify';
|
||||
import child from 'child_process';
|
||||
import http from 'http';
|
||||
import path from 'path';
|
||||
import babelify from 'babelify';
|
||||
import sequencial from 'gulp-sequence';
|
||||
import ecstatic from 'ecstatic';
|
||||
import hmr from 'browserify-hmr';
|
||||
import hotify from 'hotify';
|
||||
|
||||
var settings = {
|
||||
port: process.env.DEV_PORT || '6061',
|
||||
cache: {},
|
||||
plugin: [],
|
||||
transform: [
|
||||
babelify.configure({
|
||||
"optional": [
|
||||
"spec.protoToAssign",
|
||||
"runtime"
|
||||
],
|
||||
"blacklist": []
|
||||
})
|
||||
],
|
||||
debug: true,
|
||||
watch: false,
|
||||
compression: null
|
||||
};
|
||||
|
||||
var Bundler = function(entry) {
|
||||
this.entry = entry
|
||||
this.compression = settings.compression
|
||||
this.build = this.build.bind(this);
|
||||
|
||||
this.bundler = browserify({
|
||||
entries: ['./src/' + entry],
|
||||
debug: settings.debug,
|
||||
cache: {},
|
||||
transform: settings.transform,
|
||||
plugin: settings.plugin
|
||||
});
|
||||
|
||||
this.watcher = settings.watch &&
|
||||
watchify(this.bundler)
|
||||
.on('update', this.build);
|
||||
}
|
||||
Bundler.prototype.bundle = function() {
|
||||
gutil.log(`Begin bundling: '${this.entry}'`);
|
||||
return this.watcher ? this.watcher.bundle() : this.bundler.bundle();
|
||||
}
|
||||
|
||||
Bundler.prototype.build = function() {
|
||||
var bundle = this
|
||||
.bundle()
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to browserify: '${this.entry}'`, error.message);
|
||||
})
|
||||
.pipe(source(this.entry + '.js'))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true}))
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to make source maps for: '${this.entry}'`,
|
||||
error.message);
|
||||
});
|
||||
|
||||
return (this.compression ? bundle.pipe(uglify(this.compression)) : bundle)
|
||||
.on('error', (error) => {
|
||||
gutil.beep();
|
||||
console.error(`Failed to bundle: '${this.entry}'`,
|
||||
error.message);
|
||||
})
|
||||
.pipe(sourcemaps.write('./'))
|
||||
.pipe(gulp.dest('./dist/'))
|
||||
.on('end', () => {
|
||||
gutil.log(`Completed bundling: '${this.entry}'`);
|
||||
});
|
||||
}
|
||||
|
||||
var bundler = function(entry) {
|
||||
return gulp.task(entry, function() {
|
||||
return new Bundler(entry).build();
|
||||
});
|
||||
}
|
||||
|
||||
// Starts a static http server that serves browser.html directory.
|
||||
gulp.task('server', function() {
|
||||
var server = http.createServer(ecstatic({
|
||||
root: path.join(module.filename, '../'),
|
||||
cache: 0
|
||||
}));
|
||||
server.listen(settings.port);
|
||||
});
|
||||
|
||||
gulp.task('compressor', function() {
|
||||
settings.compression = {
|
||||
mangle: true,
|
||||
compress: true,
|
||||
acorn: true
|
||||
};
|
||||
});
|
||||
|
||||
gulp.task('watcher', function() {
|
||||
settings.watch = true
|
||||
});
|
||||
|
||||
gulp.task('hotreload', function() {
|
||||
settings.plugin.push(hmr);
|
||||
settings.transform.push(hotify);
|
||||
});
|
||||
|
||||
bundler('index');
|
||||
|
||||
gulp.task('build', [
|
||||
'compressor',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('watch', [
|
||||
'watcher',
|
||||
'index'
|
||||
]);
|
||||
|
||||
gulp.task('develop', sequencial('watch', 'server'));
|
||||
gulp.task('live', ['hotreload', 'develop']);
|
||||
gulp.task('default', ['live']);
|
|
@ -0,0 +1,10 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Sample App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'>
|
||||
</div>
|
||||
<script src="./dist/index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "counter",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"test": "flow check",
|
||||
"start": "gulp live",
|
||||
"build": "NODE_ENV=production gulp build"
|
||||
},
|
||||
"dependencies": {
|
||||
"reflex": "latest",
|
||||
"reflex-virtual-dom-renderer": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "11.0.1",
|
||||
"watchify": "3.3.1",
|
||||
|
||||
"babelify": "6.1.3",
|
||||
"browserify-hmr": "0.3.0",
|
||||
"hotify": "0.0.1",
|
||||
|
||||
"babel-core": "5.8.23",
|
||||
"babel-runtime": "5.8.20",
|
||||
"ecstatic": "0.8.0",
|
||||
"flow-bin": "0.17.0",
|
||||
|
||||
"gulp": "3.9.0",
|
||||
"gulp-sequence": "0.4.1",
|
||||
"gulp-sourcemaps": "1.5.2",
|
||||
"gulp-uglify": "^1.2.0",
|
||||
"gulp-util": "^3.0.6",
|
||||
"vinyl-buffer": "1.0.0",
|
||||
"vinyl-source-stream": "1.1.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/* @flow */
|
||||
import {Record, Union} from "typed-immutable";
|
||||
import {html, forward} from "reflex";
|
||||
|
||||
/*::
|
||||
import type {Address} from "reflex/type/signal";
|
||||
import type {VirtualNode} from "reflex/type/renderer";
|
||||
|
||||
export type Model = {value:number};
|
||||
export type Increment = {$typeof: 'Increment'};
|
||||
export type Decrement = {$typeof: 'Decrement'};
|
||||
export type Action = Increment|Decrement;
|
||||
*/
|
||||
|
||||
|
||||
const set = /*::<T>*/(record/*:T*/, field/*:string*/, value/*:any*/)/*:T*/ => {
|
||||
const result = Object.assign({}, record)
|
||||
result[field] = value
|
||||
return result
|
||||
}
|
||||
|
||||
export const create = ({value}/*:Model*/)/*:Model*/ => ({value})
|
||||
export const Inc = ()/*:Increment*/ => ({$typeof: 'Increment'})
|
||||
export const Dec = ()/*:Decrement*/ => ({$typeof: 'Decrement'})
|
||||
|
||||
export const update = (model/*:Model*/, action/*:Action*/)/*:Model*/ =>
|
||||
action.$typeof === 'Increment' ?
|
||||
set(model, 'value', model.value + 1) :
|
||||
action.$typeof === 'Decrement' ?
|
||||
set(model, 'value', model.value - 1) :
|
||||
model
|
||||
|
||||
const counterStyle = {
|
||||
value: {
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}
|
||||
|
||||
// View
|
||||
export var view = (model/*:Model*/, address/*:Address<Action>*/)/*:VirtualNode*/ => {
|
||||
return html.span({key: 'counter'}, [
|
||||
html.button({
|
||||
key: 'decrement',
|
||||
onClick: forward(address, Dec)
|
||||
}, ["-"]),
|
||||
html.span({
|
||||
key: 'value',
|
||||
style: counterStyle.value,
|
||||
}, [String(model.value)]),
|
||||
html.button({
|
||||
key: 'increment',
|
||||
onClick: forward(address, Inc)
|
||||
}, ["+"])
|
||||
]);
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/* @flow */
|
||||
|
||||
import * as Counter from "./counter"
|
||||
import {start} from "reflex"
|
||||
import {Renderer} from "reflex-virtual-dom-renderer"
|
||||
|
||||
var app = start({
|
||||
initial: Counter.create(window.app != null ?
|
||||
window.app.model.value :
|
||||
{value: 0}),
|
||||
update: Counter.update,
|
||||
view: Counter.view
|
||||
});
|
||||
window.app = app
|
||||
|
||||
var renderer = new Renderer({target: document.body})
|
||||
|
||||
app.view.subscribe(renderer.address)
|
|
@ -0,0 +1,5 @@
|
|||
/*flow*/
|
||||
declare function requestAnimationFrame(callback: any): number;
|
||||
declare class performance {
|
||||
static now(): number;
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"name": "reflex-virtual-dom-renderer",
|
||||
"version": "0.0.0",
|
||||
"description": "React based renderer for reflex",
|
||||
"keywords": [
|
||||
"reflex",
|
||||
"react",
|
||||
"renderer"
|
||||
],
|
||||
"author": "Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)",
|
||||
"homepage": "https://github.com/Gozala/reflex-react-renderer",
|
||||
"main": "./lib/index.js",
|
||||
"dependencies": {
|
||||
"blanks": "0.0.2",
|
||||
"object-as-dictionary": "0.0.3",
|
||||
"react": "0.13.3",
|
||||
"virtual-dom": "2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel": "5.6.14",
|
||||
"babel-plugin-flow-comments": "1.0.9",
|
||||
"reflex": "0.0.40",
|
||||
"tap": "1.1.0",
|
||||
"tape": "2.3.2"
|
||||
},
|
||||
"babel": {
|
||||
"sourceMaps": "inline",
|
||||
"optional": [
|
||||
"spec.protoToAssign"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tap lib/test/test-*.js",
|
||||
"build-node": "babel ./src --out-dir ./lib --plugins flow-comments --blacklist flow",
|
||||
"build-browser": "babel ./src --out-dir ./dist --modules umdStrict",
|
||||
"build": "npm run build-node && npm run build-browser",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Gozala/reflex-react-renderer.git",
|
||||
"web": "https://github.com/Gozala/reflex-react-renderer"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/Gozala/reflex-react-renderer/issues/"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "https://github.com/Gozala/reflex-react-renderer/License.md"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/* @flow */
|
||||
|
||||
import {dictionary} from "object-as-dictionary"
|
||||
/*::
|
||||
import * as type from "../../type/index"
|
||||
*/
|
||||
|
||||
const nameOverrides/*:type.nameOverrides*/ = dictionary({
|
||||
className: 'class',
|
||||
htmlFor: 'for'
|
||||
})
|
||||
|
||||
export const supportedAttributes/*:type.supportedAttributes*/ =
|
||||
`accept acceptCharset accessKey action allowFullScreen allowTransparency alt
|
||||
async autoComplete autoFocus autoPlay capture cellPadding cellSpacing charSet
|
||||
challenge checked classID className cols colSpan content contentEditable contextMenu
|
||||
controls coords crossOrigin data dateTime defer dir disabled download draggable
|
||||
encType form formAction formEncType formMethod formNoValidate formTarget frameBorder
|
||||
headers height hidden high href hrefLang htmlFor httpEquiv icon id inputMode
|
||||
keyParams keyType label lang list loop low manifest marginHeight marginWidth max
|
||||
maxLength media mediaGroup method min minlength multiple muted name noValidate open
|
||||
optimum pattern placeholder poster preload radioGroup readOnly rel required role
|
||||
rows rowSpan sandbox scope scoped scrolling seamless selected shape size sizes
|
||||
span spellCheck src srcDoc srcSet start step summary tabIndex target title
|
||||
type useMap value width wmode wrap
|
||||
|
||||
autoCapitalize autoCorrect
|
||||
|
||||
property
|
||||
|
||||
itemProp itemScope itemType itemRef itemID
|
||||
|
||||
unselectable
|
||||
|
||||
results autoSave
|
||||
|
||||
clipPath cx cy d dx dy fill fillOpacity fontFamily
|
||||
fontSize fx fy gradientTransform gradientUnits markerEnd
|
||||
markerMid markerStart offset opacity patternContentUnits
|
||||
patternUnits points preserveAspectRatio r rx ry spreadMethod
|
||||
stopColor stopOpacity stroke strokeDasharray strokeLinecap
|
||||
strokeOpacity strokeWidth textAnchor transform version
|
||||
viewBox x1 x2 x xlinkActuate xlinkArcrole xlinkHref xlinkRole
|
||||
xlinkShow xlinkTitle xlinkType xmlBase xmlLang xmlSpace y1 y2 y`
|
||||
.split(/\s+/)
|
||||
.reduce((table, name) => {
|
||||
table[name] = nameOverrides[name] != null ?
|
||||
nameOverrides[name] :
|
||||
name.toLowerCase()
|
||||
return table
|
||||
}, dictionary())
|
|
@ -0,0 +1,165 @@
|
|||
/* @flow */
|
||||
|
||||
import {dictionary} from "object-as-dictionary"
|
||||
|
||||
/*::
|
||||
import * as type from "../../type"
|
||||
*/
|
||||
|
||||
const nameOverrides/*:type.nameOverrides*/ = dictionary({
|
||||
DoubleClick: 'dblclick',
|
||||
})
|
||||
|
||||
export const supportedEvents/*:type.supportedEvents*/ = [
|
||||
// Clipboard Events
|
||||
'Copy',
|
||||
'Cut',
|
||||
'Paste',
|
||||
// Keyboard Events
|
||||
'KeyDown',
|
||||
'KeyPress',
|
||||
'KeyUp',
|
||||
// Focus Events
|
||||
'Focus',
|
||||
'Blur',
|
||||
// Form Events
|
||||
'Change',
|
||||
'Input',
|
||||
'Submit',
|
||||
// Mouse Events
|
||||
'Click',
|
||||
'ContextMenu',
|
||||
'DoubleClick',
|
||||
'Drag',
|
||||
'DragEnd',
|
||||
'DragEnter',
|
||||
'DragExit',
|
||||
'DragLeave',
|
||||
'DragOver',
|
||||
'DragStart',
|
||||
'Drop',
|
||||
'MouseDown',
|
||||
'MouseEnter',
|
||||
'MouseLeave',
|
||||
'MouseMove',
|
||||
'MouseOut',
|
||||
'MouseOver',
|
||||
'MouseUp',
|
||||
// Touch events
|
||||
'TouchStart',
|
||||
'TouchMove',
|
||||
'TouchCancel',
|
||||
'TouchEnd',
|
||||
// UI Events
|
||||
'Scroll',
|
||||
// Wheel Events
|
||||
'Wheel',
|
||||
// Media Events
|
||||
'Abort',
|
||||
'CanPlay',
|
||||
'CanPlayThrough',
|
||||
'DurationChange',
|
||||
'Emptied',
|
||||
'Encrypted',
|
||||
'Ended',
|
||||
'Error',
|
||||
'LoadedData',
|
||||
'LoadedMetadata',
|
||||
'LoadStart',
|
||||
'Pause',
|
||||
'Play',
|
||||
'Playing',
|
||||
'Progress',
|
||||
'RateChange',
|
||||
'Seeked',
|
||||
'Seeking',
|
||||
'Stalled',
|
||||
'Suspend',
|
||||
'TimeUpdate',
|
||||
'VolumeChange',
|
||||
'Waiting',
|
||||
// Image Events
|
||||
'Load',
|
||||
'Error',
|
||||
// Composition ?
|
||||
|
||||
'BeforeInput',
|
||||
'CompositionEnd',
|
||||
'CompositionStart',
|
||||
'CompositionUpdate'
|
||||
].reduce((table, name) => {
|
||||
const type = nameOverrides[name] == null ? name.toLowerCase() :
|
||||
nameOverrides[name]
|
||||
|
||||
table[`on${name}`] = {type, capture:false}
|
||||
table[`on${name}Capture`] = {type, capture:true}
|
||||
|
||||
return table
|
||||
}, dictionary())
|
||||
|
||||
|
||||
const handleEvent/*:type.handleEvent*/ = phase => event => {
|
||||
const {currentTarget, type} = event
|
||||
const handler = currentTarget[`on${type}${phase}`]
|
||||
|
||||
if (typeof(handler) === 'function') {
|
||||
handler(event)
|
||||
}
|
||||
|
||||
if (handler != null && typeof(handler.handleEvent) === 'function') {
|
||||
handler.handleEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCapturing = handleEvent('capture')
|
||||
const handleBubbling = handleEvent('bubble')
|
||||
|
||||
export class EventHandler {
|
||||
/*::
|
||||
handler: type.EventListener;
|
||||
*/
|
||||
constructor(handler/*:type.EventListener*/) {
|
||||
this.handler = handler
|
||||
}
|
||||
hook(node/*:type.EventTarget*/, name/*:string*/, previous/*:any*/) {
|
||||
const config = supportedEvents[name]
|
||||
if (config != null) {
|
||||
const {type, capture} = config
|
||||
const phase = capture ? 'capture' : 'bubble'
|
||||
|
||||
if (!(previous instanceof EventHandler)) {
|
||||
const handler = capture ? handleCapturing : handleBubbling
|
||||
node.addEventListener(type, handler, capture)
|
||||
}
|
||||
|
||||
node[`on${type}${phase}`] = this.handler
|
||||
}
|
||||
}
|
||||
unhook(node/*:type.EventTarget*/, name/*:string*/, next/*:any*/) {
|
||||
const config = supportedEvents[name]
|
||||
|
||||
if (config != null) {
|
||||
if (!(next instanceof EventHandler)) {
|
||||
const {type, capture} = config
|
||||
const phase = capture ? 'capture' : 'bubble'
|
||||
const id = `on${type}${phase}`
|
||||
const handler = node[id]
|
||||
if (handler != null) {
|
||||
node.removeEventListener(type, handler, capture)
|
||||
}
|
||||
delete node[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const eventHandler/*:type.eventHandler*/ = address => {
|
||||
const handler = address.reflexEventListener
|
||||
if (handler == null) {
|
||||
const handler = new EventHandler(address)
|
||||
address.reflexEventListener = handler
|
||||
return handler
|
||||
} else {
|
||||
return handler
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/* @flow */
|
||||
|
||||
import diff from "virtual-dom/diff"
|
||||
import createElement from "virtual-dom/create-element"
|
||||
import patch from "virtual-dom/patch"
|
||||
import {TextNode, text} from "./text"
|
||||
import {VirtualNode, node} from "./node"
|
||||
import {ThunkNode, thunk} from "./thunk"
|
||||
|
||||
/*::
|
||||
import * as renderer from "reflex/type/renderer"
|
||||
import * as signal from "reflex/type/signal"
|
||||
*/
|
||||
|
||||
export class Renderer {
|
||||
/*::
|
||||
target: Element;
|
||||
mount: ?(Element & {reflexTree?: renderer.ChildNode});
|
||||
value: renderer.RootNode;
|
||||
isScheduled: boolean;
|
||||
version: number;
|
||||
address: signal.Address<renderer.ChildNode>;
|
||||
execute: () => void;
|
||||
|
||||
node: renderer.node;
|
||||
thunk: renderer.thunk;
|
||||
text: renderer.text;
|
||||
*/
|
||||
constructor({target}/*:{target: Element}*/) {
|
||||
this.target = target
|
||||
this.mount = (target.children.length === 1 &&
|
||||
target.children[0].reflexTree != null) ?
|
||||
target.children[0] :
|
||||
null
|
||||
|
||||
this.address = this.receive.bind(this)
|
||||
this.execute = this.execute.bind(this)
|
||||
|
||||
this.node = node
|
||||
this.thunk = thunk
|
||||
this.text = text
|
||||
}
|
||||
toString()/*:string*/{
|
||||
return `Renderer({target: ${this.target}})`
|
||||
}
|
||||
receive(value/*:renderer.RootNode*/) {
|
||||
this.value = value
|
||||
this.schedule()
|
||||
}
|
||||
schedule() {
|
||||
if (!this.isScheduled) {
|
||||
this.isScheduled = true
|
||||
this.version = requestAnimationFrame(this.execute)
|
||||
}
|
||||
}
|
||||
execute(_/*:number*/) {
|
||||
if (profile) {
|
||||
console.time('render')
|
||||
}
|
||||
|
||||
const start = performance.now()
|
||||
|
||||
// It is important to mark `isScheduled` as `false` before doing actual
|
||||
// rendering since state changes in effect of reflecting current state
|
||||
// won't be handled by this render cycle. For example rendering a state
|
||||
// with updated focus will cause `blur` & `focus` events to be dispatched
|
||||
// that happen synchronously, and there for another render cycle may be
|
||||
// scheduled for which `isScheduled` must be `false`. Attempt to render
|
||||
// this state may also cause a runtime exception but even then we would
|
||||
// rather attempt to render updated states that end up being blocked
|
||||
// forever.
|
||||
this.isScheduled = false
|
||||
if (profile) {
|
||||
console.time('render')
|
||||
}
|
||||
|
||||
this.value.renderWith(this)
|
||||
|
||||
const end = performance.now()
|
||||
const time = end - start
|
||||
|
||||
if (time > 16) {
|
||||
console.warn(`Render took ${time}ms & will cause frame drop`)
|
||||
}
|
||||
|
||||
if (profile) {
|
||||
console.timeEnd('render')
|
||||
}
|
||||
}
|
||||
render(tree/*:renderer.ChildNode*/) {
|
||||
const {mount, target} = this
|
||||
if (mount) {
|
||||
patch(mount, diff(mount.reflexTree, tree))
|
||||
mount.reflexTree = tree
|
||||
} else {
|
||||
const mount = createElement(tree)
|
||||
mount.reflexTree = tree
|
||||
target.innerHTML = ""
|
||||
this.mount = mount
|
||||
target.appendChild(mount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let profile = null
|
||||
export const time = (name/*:string*/)/*:void*/ =>
|
||||
void(profile = `${name == null ? "" : name} `)
|
||||
|
||||
export const timeEnd = () =>
|
||||
void(profile = null)
|
|
@ -0,0 +1,165 @@
|
|||
/* @flow */
|
||||
|
||||
/*::
|
||||
import * as type from "../type"
|
||||
*/
|
||||
|
||||
import isVirtualNode from "virtual-dom/vnode/is-vnode"
|
||||
import isWidget from "virtual-dom/vnode/is-widget"
|
||||
import isThunk from "virtual-dom/vnode/is-thunk"
|
||||
import isHook from "virtual-dom/vnode/is-vhook"
|
||||
import version from "virtual-dom/vnode/version"
|
||||
import SoftSetHook from "virtual-dom/virtual-hyperscript/hooks/soft-set-hook"
|
||||
import {TextNode} from "./text"
|
||||
import {empty} from "blanks/lib/array"
|
||||
import {blank} from "blanks/lib/object"
|
||||
|
||||
|
||||
import {supportedEvents, eventHandler} from "./hooks/event-handler"
|
||||
import {supportedAttributes} from "./hooks/attribute"
|
||||
|
||||
export class VirtualNode {
|
||||
/*::
|
||||
|
||||
$$typeof: "VirtualNode";
|
||||
type: "VirtualNode";
|
||||
version: number;
|
||||
|
||||
|
||||
tagName: type.TagName;
|
||||
namespace: ?string;
|
||||
key: ?string;
|
||||
properties: type.PropertyDictionary;
|
||||
children: Array<type.ChildNode>;
|
||||
count: number;
|
||||
descendants: number;
|
||||
hasWidgets: boolean;
|
||||
hasThunks: boolean;
|
||||
hooks: ?type.HookDictionary;
|
||||
*/
|
||||
constructor(tagName/*:string*/, namespace/*:?string*/, properties/*:type.PropertyDictionary*/, children/*:Array<type.ChildNode>*/) {
|
||||
this.tagName = tagName
|
||||
this.namespace = namespace
|
||||
this.key = properties.key != null ? String(properties.key) : null
|
||||
this.children = children
|
||||
|
||||
|
||||
let count = children.length || 0
|
||||
let descendants = 0
|
||||
let hasWidgets = false
|
||||
let hasThunks = false
|
||||
let descendantHooks = false
|
||||
|
||||
let hooks = null
|
||||
let attributes = properties.attributes != null ? properties.attributes : null
|
||||
|
||||
for (let key in properties) {
|
||||
if (properties.hasOwnProperty(key)) {
|
||||
const property = properties[key]
|
||||
if (isHook(property)) {
|
||||
if (hooks == null) {
|
||||
hooks = {}
|
||||
}
|
||||
|
||||
hooks[key] = property
|
||||
} else {
|
||||
// Event handlers
|
||||
if (supportedEvents[key] != null) {
|
||||
if (hooks == null) {
|
||||
hooks = {}
|
||||
}
|
||||
|
||||
const handler = eventHandler(property)
|
||||
hooks[key] = handler
|
||||
properties[key] = handler
|
||||
}
|
||||
// Special handlind of input.value
|
||||
else if (key === 'value' &&
|
||||
tagName.toLowerCase() === 'input' &&
|
||||
property != null) {
|
||||
if (hooks == null) {
|
||||
hooks = {}
|
||||
}
|
||||
|
||||
const hook = new SoftSetHook(property)
|
||||
hooks[key] = hook
|
||||
properties[key] = hook
|
||||
}
|
||||
// Attributes
|
||||
else if (supportedAttributes[key] != null) {
|
||||
if (attributes == null) {
|
||||
attributes = {}
|
||||
}
|
||||
|
||||
attributes[supportedAttributes[key]] = property
|
||||
delete properties[key]
|
||||
}
|
||||
else if (key.indexOf('data-') === 0 || key.indexOf('aria-') === 0) {
|
||||
if (attributes == null) {
|
||||
attributes = {}
|
||||
}
|
||||
|
||||
attributes[key] = property
|
||||
delete properties[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (attributes != null) {
|
||||
properties.attributes = attributes
|
||||
}
|
||||
|
||||
let index = 0
|
||||
while (index < count) {
|
||||
const child = children[index]
|
||||
|
||||
if (typeof(child) === "string") {
|
||||
children[index] = new TextNode(child)
|
||||
}
|
||||
else if (child.$$typeof === "OrphanNode") {
|
||||
children[index] = child.force()
|
||||
index = index - 1
|
||||
} else if (child instanceof VirtualNode) {
|
||||
descendants += child.count
|
||||
|
||||
if (!hasWidgets && child.hasWidgets) {
|
||||
hasWidgets = true
|
||||
}
|
||||
|
||||
if (!hasThunks && child.hasThunks) {
|
||||
hasThunks = true
|
||||
}
|
||||
|
||||
if (!descendantHooks && (child.hooks != null || child.descendantHooks)) {
|
||||
descendantHooks = true
|
||||
}
|
||||
}
|
||||
else if (!hasWidgets && isWidget(child)) {
|
||||
hasWidgets = true
|
||||
}
|
||||
else if (!hasThunks && isThunk(child)) {
|
||||
hasThunks = true
|
||||
}
|
||||
|
||||
index = index + 1
|
||||
}
|
||||
|
||||
this.count = count + descendants
|
||||
this.hasWidgets = hasWidgets
|
||||
this.hasThunks = hasThunks
|
||||
this.properties = properties
|
||||
this.hooks = hooks
|
||||
}
|
||||
}
|
||||
VirtualNode.prototype.$$typeof = "VirtualNode"
|
||||
VirtualNode.prototype.type = "VirtualNode"
|
||||
VirtualNode.prototype.version = version
|
||||
|
||||
|
||||
export const node/*:type.node*/ = (tagName, properties, children) =>
|
||||
new VirtualNode(tagName,
|
||||
null,
|
||||
properties == null ? blank : properties,
|
||||
children == null ? empty : children)
|
|
@ -0,0 +1,29 @@
|
|||
/* @flow */
|
||||
|
||||
import version from "virtual-dom/vnode/version"
|
||||
|
||||
/*::
|
||||
import * as type from "../type"
|
||||
*/
|
||||
|
||||
export class TextNode {
|
||||
/*::
|
||||
$$typeof: "TextNode";
|
||||
type: "VirtualText";
|
||||
version: string;
|
||||
text: string;
|
||||
*/
|
||||
|
||||
constructor(text/*:string*/) {
|
||||
this.text = text
|
||||
this.$$typeof = "TextNode"
|
||||
this.type = "VirtualText"
|
||||
this.version = version
|
||||
}
|
||||
}
|
||||
|
||||
TextNode.prototype.$$typeof = "TextNode"
|
||||
TextNode.prototype.type = "VirtualText"
|
||||
TextNode.prototype.version = version
|
||||
|
||||
export const text/*:type.text*/ = text => new TextNode(text)
|
|
@ -0,0 +1,129 @@
|
|||
/* @flow */
|
||||
|
||||
/*::
|
||||
import * as type from "../type"
|
||||
*/
|
||||
|
||||
const redirect/*:type.redirect*/ = (addressBook, index) =>
|
||||
action => addressBook[index](action);
|
||||
|
||||
export class ThunkNode {
|
||||
/*::
|
||||
$$typeof: "ThunkNode";
|
||||
type: "Thunk";
|
||||
key: type.Key;
|
||||
view: type.View;
|
||||
args: Array<any>;
|
||||
|
||||
addressBook: ?type.AddressBook<any>;
|
||||
value: ?type.ChildNode;
|
||||
*/
|
||||
constructor(key/*:type.Key*/, view/*:type.View*/, args/*:Array<any>*/) {
|
||||
this.key = key
|
||||
this.view = view
|
||||
this.args = args
|
||||
this.addressBook = null
|
||||
this.value = null
|
||||
}
|
||||
render(previous/*:?type.ChildNode*/) {
|
||||
if (previous instanceof ThunkNode && previous.value != null) {
|
||||
if (profile) {
|
||||
console.time(`${this.key}.receive`)
|
||||
}
|
||||
|
||||
const {view, args: passed, key} = this
|
||||
const {args, addressBook, value} = previous
|
||||
this.addressBook = addressBook
|
||||
this.args = args
|
||||
this.value = value
|
||||
|
||||
const count = passed.length
|
||||
let index = 0
|
||||
let isUpdated = view !== previous.view
|
||||
|| key !== previous.key
|
||||
|
||||
if (args.length !== count) {
|
||||
isUpdated = true
|
||||
args.length = count
|
||||
addressBook.length = count
|
||||
}
|
||||
|
||||
while (index < count) {
|
||||
const next = passed[index]
|
||||
const arg = args[index]
|
||||
|
||||
if (next !== arg) {
|
||||
const isNextAddress = typeof(next) === 'function'
|
||||
const isCurrentAddress = typeof(arg) === 'function'
|
||||
|
||||
if (isNextAddress && isCurrentAddress) {
|
||||
// Update adrress book with a new address.
|
||||
addressBook[index] = next
|
||||
} else {
|
||||
isUpdated = true
|
||||
|
||||
if (isNextAddress) {
|
||||
addressBook[index] = next
|
||||
args[index] = redirect(addressBook, index)
|
||||
} else {
|
||||
args[index] = next
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
index = index + 1
|
||||
}
|
||||
|
||||
if (profile) {
|
||||
console.timeEnd(`${key}.receive`)
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
if (profile) {
|
||||
console.time(`${key}.render`)
|
||||
}
|
||||
|
||||
this.value = view(...args)
|
||||
|
||||
if (profile) {
|
||||
console.timeEnd(`${key}.render`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (profile) {
|
||||
console.time(`${this.key}.render`)
|
||||
}
|
||||
|
||||
const addressBook = []
|
||||
const {args, view, key} = this
|
||||
const count = args.length
|
||||
|
||||
let index = 0
|
||||
while (index < count) {
|
||||
const arg = args[index]
|
||||
if (typeof(arg) === 'function') {
|
||||
addressBook[index] = arg
|
||||
args[index] = redirect(addressBook, index)
|
||||
} else {
|
||||
args[index] = arg
|
||||
}
|
||||
index = index + 1
|
||||
}
|
||||
|
||||
this.addressBook = addressBook
|
||||
this.value = view(...args)
|
||||
|
||||
if (profile) {
|
||||
console.timeEnd(`${key}.render`)
|
||||
}
|
||||
}
|
||||
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
ThunkNode.prototype.type = "Thunk"
|
||||
ThunkNode.prototype.$$typeof = "ThunkNode"
|
||||
|
||||
let profile = null
|
||||
export const thunk/*:type.thunk*/ = (key, view, ...args) =>
|
||||
new ThunkNode(key, view, args)
|
|
@ -0,0 +1,3 @@
|
|||
/* @flow */
|
||||
|
||||
export {EventListener, EventHandler, Event, EventTarget}
|
|
@ -0,0 +1,73 @@
|
|||
/* @flow */
|
||||
|
||||
import {Dictionary} from "object-as-dictionary"
|
||||
import * as Signal from "reflex/type/signal"
|
||||
import * as DOM from "./dom"
|
||||
import * as Renderer from "reflex/type/renderer"
|
||||
import * as VirtualDOM from "./virtual-dom"
|
||||
|
||||
// hooks
|
||||
|
||||
export type Hook <target> = {
|
||||
hook: (node:target, name:string, previous:any) => void,
|
||||
unhook: (node:target, name:string, next:any) => void
|
||||
}
|
||||
|
||||
|
||||
export type nameOverrides = Dictionary<string>
|
||||
export type supportedAttributes = Dictionary<string>
|
||||
export type HookDictionary = Dictionary<Hook<any>>
|
||||
|
||||
|
||||
// hooks/event-handler
|
||||
|
||||
export type EventConfig = {type:string, capture:boolean}
|
||||
export type supportedEvents = Dictionary<EventConfig>
|
||||
|
||||
export type EventHandler = DOM.EventHandler
|
||||
export type EventListener = DOM.EventListener
|
||||
|
||||
export type EventPhaseName
|
||||
= 'capture'
|
||||
| 'bubble'
|
||||
|
||||
export type EventTarget
|
||||
= DOM.EventTarget
|
||||
& {[key:string]: EventListener}
|
||||
|
||||
export type DOMEvent = Event
|
||||
export type Event
|
||||
= DOM.Event
|
||||
& {currentTarget: EventTarget}
|
||||
|
||||
export type handleEvent = (phase:EventPhaseName) =>
|
||||
(event:Event) => void
|
||||
|
||||
export type Address <message>
|
||||
= Signal.Address <message>
|
||||
& {reflexEventListener?: EventHandler}
|
||||
|
||||
|
||||
export type eventHandler = (address:Address) =>
|
||||
Hook<EventTarget>
|
||||
|
||||
export type AddressBook <message>
|
||||
= Array<Address<message>>
|
||||
|
||||
export type redirect <message>
|
||||
= (addressBook:AddressBook<message>, index:number) => Address<message>
|
||||
|
||||
export type Key = Renderer.Key
|
||||
export type TagName = Renderer.TagName
|
||||
export type AttributeDictionary = Renderer.AttributeDictionary
|
||||
export type StyleDictionary = Renderer.StyleDictionary
|
||||
export type PropertyDictionary = Renderer.PropertyDictionary
|
||||
export type VirtualNode = Renderer.VirtualNode
|
||||
export type TextNode = Renderer.TextNode
|
||||
export type Text = Renderer.Text
|
||||
export type ChildNode = Renderer.ChildNode
|
||||
export type ThunkNode = Renderer.ThunkNode
|
||||
export type View = Renderer.View
|
||||
export type text = Renderer.text
|
||||
export type node = Renderer.node
|
||||
export type thunk = Renderer.thunk
|
|
@ -0,0 +1,114 @@
|
|||
/* @flow */
|
||||
|
||||
// vtext
|
||||
|
||||
export type VirtualText = {
|
||||
type: "VirtualText",
|
||||
version: string,
|
||||
text: string
|
||||
}
|
||||
|
||||
|
||||
// vnode
|
||||
|
||||
export type HookTarget <extension>
|
||||
= Element
|
||||
& EventTarget
|
||||
& extension
|
||||
|
||||
export type Hook <extension> = {
|
||||
hook:(node:HookTarget<extension>, key:string, previous:any) => void,
|
||||
unhook:(node:HookTarget<extension>, key:string, next:any) => void
|
||||
}
|
||||
|
||||
export type HookDictionary <extension> = {[key:string]: Hook<extension>}
|
||||
|
||||
export type AttributeDictionary = {
|
||||
[key:string]: string|number|boolean
|
||||
}
|
||||
|
||||
export type StyleDictionary = {
|
||||
[key:string]: string|number|boolean
|
||||
}
|
||||
|
||||
export type VirtualProperties = {
|
||||
attributes: ?Hook<any>|AttributeDictionary,
|
||||
style: ?StyleDictionary,
|
||||
[key:string]: Hook<any>|Function|string|number|boolean|EventListener
|
||||
}
|
||||
|
||||
export type VirtualNode = {
|
||||
type: "VirtualNode",
|
||||
version: string,
|
||||
|
||||
tagName: string,
|
||||
properties: VirtualProperties,
|
||||
children: Array<Entity>,
|
||||
hooks: ?HookDictionary<any>,
|
||||
|
||||
count: number,
|
||||
hasWidgets: boolean,
|
||||
hasThunks: boolean,
|
||||
descendantHooks: boolean
|
||||
}
|
||||
|
||||
// thunk
|
||||
|
||||
export type Thunk = {
|
||||
type: "Thunk",
|
||||
vnode: ?VNode,
|
||||
render: (previous: ?Entity) => VNode
|
||||
}
|
||||
|
||||
// widget
|
||||
|
||||
export type Widget = {
|
||||
type: "Widget",
|
||||
init: () => HTMLElement,
|
||||
update: (previous:Widget, element:HTMLElement) => ?HTMLElement,
|
||||
destroy: (element:HTMLElement) => void
|
||||
}
|
||||
|
||||
export type VNode
|
||||
= VirtualText
|
||||
| VirtualNode
|
||||
| Widget
|
||||
|
||||
export type Entity
|
||||
= VirtualText
|
||||
| VirtualNode
|
||||
| Thunk
|
||||
| Widget
|
||||
|
||||
|
||||
|
||||
// virtual-dom/diff
|
||||
|
||||
export type NONE = 0
|
||||
export type VTEXT = 1
|
||||
export type VNODE = 2
|
||||
export type WIDGET = 3
|
||||
export type PROPS = 4
|
||||
export type ORDER = 5
|
||||
export type INSERT = 6
|
||||
export type REMOVE = 7
|
||||
export type THUNK = 8
|
||||
|
||||
export type VirtualPatch <type:NONE|VTEXT|VNODE|WIDGET|PROPS|ORDER|INSERT|REMOVE|THUNK>
|
||||
= {type: type, vnode: Entity, patch: any}
|
||||
|
||||
export type Delta = {
|
||||
a: Entity,
|
||||
[key:number]: VirtualPatch<any>|Array<VirtualPatch<any>>
|
||||
}
|
||||
|
||||
|
||||
export type diff = (left:Entity, right:Entity) => Delta
|
||||
|
||||
|
||||
// virtual-dom/create-element
|
||||
|
||||
export type createElement = (left:Entity) => HTMLElement
|
||||
|
||||
// virtual-dom/patch
|
||||
export type patch = (target:HTMLElement, delta:Delta) => void
|
Загрузка…
Ссылка в новой задаче