From 234d2def50f630e54d4d184dcb3f3df63437f49e Mon Sep 17 00:00:00 2001 From: bozokopic Date: Mon, 9 Apr 2018 09:25:32 +0200 Subject: build refactoring --- src_js/opcut/ev.js | 22 +++ src_js/opcut/lenses.js | 21 --- src_js/opcut/main.js | 9 +- src_js/opcut/renderer.js | 306 ++++++++++++++++++------------- src_js/opcut/util.js | 461 +++++++++++++++++++++++++++++++++++++++++++++++ src_js/opcut/vt.js | 7 +- 6 files changed, 672 insertions(+), 154 deletions(-) create mode 100644 src_js/opcut/ev.js delete mode 100644 src_js/opcut/lenses.js create mode 100644 src_js/opcut/util.js (limited to 'src_js') diff --git a/src_js/opcut/ev.js b/src_js/opcut/ev.js new file mode 100644 index 0000000..f619998 --- /dev/null +++ b/src_js/opcut/ev.js @@ -0,0 +1,22 @@ +/** @module opcut/ev */ + +import bean from 'bean'; + +import * as u from 'opcut/util'; + + +export function on(element, eventType, selector, handler, args) { + bean.on(element, eventType, selector, handler, args); +} + +export function one(element, eventType, selector, handler, args) { + bean.one(element, eventType, selector, handler, args); +} + +export function off(element, eventType, handler) { + bean.off(element, eventType, handler); +} + +export function fire(element, eventType, args) { + u.delay(bean.fire, 0, element, eventType, args); +} diff --git a/src_js/opcut/lenses.js b/src_js/opcut/lenses.js deleted file mode 100644 index 39da314..0000000 --- a/src_js/opcut/lenses.js +++ /dev/null @@ -1,21 +0,0 @@ -import R from 'ramda'; - - -export const index = R.lensIndex; - -export const prop = R.lensProp; - -export function path(...xs) { - return R.reduce((acc, i) => R.compose(acc, pathParamToLens(i)), - R.identity, xs); -} - -function pathParamToLens(x) { - switch (typeof(x)) { - case 'function': return x; - case 'number': return index(x); - case 'string': return prop(x); - case 'object': if (Array.isArray(x)) return R.apply(path, x); - } - throw 'Invalid path parameter'; -} diff --git a/src_js/opcut/main.js b/src_js/opcut/main.js index b4da90b..06ca4a0 100644 --- a/src_js/opcut/main.js +++ b/src_js/opcut/main.js @@ -1,12 +1,9 @@ -import bean from 'bean'; -import R from 'ramda'; - import r from 'opcut/renderer'; -import * as l from 'opcut/lenses'; +import * as u from 'opcut/util'; +import * as ev from 'opcut/ev'; import * as common from 'opcut/common'; import * as vt from 'opcut/vt'; -import 'static!static/index.html'; import 'style/main.scss'; @@ -16,4 +13,4 @@ function main() { } -bean.on(window, 'load', main); +ev.on(window, 'load', main); diff --git a/src_js/opcut/renderer.js b/src_js/opcut/renderer.js index 6738141..451d6cf 100644 --- a/src_js/opcut/renderer.js +++ b/src_js/opcut/renderer.js @@ -1,126 +1,180 @@ -import Delegator from 'dom-delegator'; -import R from 'ramda'; -import bean from 'bean'; -import vh from 'virtual-dom/h'; -import diff from 'virtual-dom/diff'; -import patch from 'virtual-dom/patch'; -import createElement from 'virtual-dom/create-element'; - -import * as l from 'opcut/lenses'; - - -const delegator = Delegator(); -const vhTypes = ['VirtualNode', 'Widget']; - - -function vhFromArray(node) { - if (!node) - return []; - if (typeof node == 'string' || vhTypes.includes(node.type)) - return node; - if (!Array.isArray(node)) - throw 'Invalid node structure'; - if (node.length < 1) - return []; - if (typeof node[0] != 'string') - return node.map(vhFromArray); - let hasProps = (node.length > 1 && - typeof node[1] == 'object' && - !Array.isArray(node[1]) && - !vhTypes.includes(node[1].type)); - let children = R.flatten(node.slice(hasProps ? 2 : 1).map(vhFromArray)); - let result = hasProps ? vh(node[0], node[1], children) : - vh(node[0], children); - - // disable SoftSetHook for input - if (result.tagName == 'INPUT' && - result.properties && - result.properties.value && - typeof(result.properties.value) === 'object') { - result.properties.value = result.properties.value.value; - } - - return result; -} - - -class VTreeRenderer { - - constructor(el) { - this._el = el; - this._vtree = null; - } - - render(vtree) { - let vt = vhFromArray(vtree); - if (vt.type == 'VirtualNode') { - if (this._vtree) { - let d = diff(this._vtree, vt); - patch(this._el.firstChild, d); - } else { - while (this._el.firstChild) - this._el.removeChild(this._el.firstChild); - this._el.appendChild(createElement(vt)); - } - this._vtree = vt; - } else { - this._vtree = null; - while (this._el.firstChild) - this._el.removeChild(this._el.firstChild); - } - } - -} - - -export class Renderer { - - constructor(el, initState, vtCb) { - this.init(el, initState, vtCb); - } - - init(el, initState, vtCb) { - this._state = null; - this._changeCbs = []; - this._vtCb = vtCb; - this._r = new VTreeRenderer(el || document.querySelector('body')); - if (initState) - this.change(R.identity, _ => initState); - } - - view(...lenses) { - return R.view(R.apply(l.path, lenses), this._state); - } - - set(lens, value) { - if (arguments.length < 2) { - value = lens; - lens = R.identity; - } - this.change(lens, _ => value); - } - - change(lens, cb) { - if (arguments.length < 2) { - cb = lens; - lens = R.identity; - } - if (this._changeCbs.push(cb) > 1) - return; - let startingSubState = this.view(lens); - while (this._changeCbs.length > 0) { - this._state = R.over(l.path(lens), this._changeCbs[0], this._state); - this._changeCbs.shift(); - } - if (!this._vtCb || - (this._state && R.equals(startingSubState, this.view(lens)))) - return; - this._r.render(this._vtCb(this._state)); - bean.fire(this, 'render', this._state); - } - -} - - -const defaultRenderer = new Renderer(); -export default defaultRenderer; +/** @module opcut/renderer2 */ + +import * as snabbdom from 'snabbdom/es/snabbdom'; +import snabbdomAttributes from 'snabbdom/es/modules/attributes'; +import snabbdomClass from 'snabbdom/es/modules/class'; +import snabbdomProps from 'snabbdom/es/modules/props'; +// import snabbdomStyle from 'snabbdom/es/modules/style'; +import snabbdomDataset from 'snabbdom/es/modules/dataset'; +import snabbdomEvent from 'snabbdom/es/modules/eventlisteners'; + +import * as u from 'opcut/util'; +import * as ev from 'opcut/ev'; + + +const patch = snabbdom.init([ + snabbdomAttributes, + snabbdomClass, + snabbdomProps, + // snabbdomStyle, + snabbdomDataset, + snabbdomEvent +]); + + +function vhFromArray(node) { + if (!node) + return []; + if (u.isString(node)) + return node; + if (!u.isArray(node)) + throw 'Invalid node structure'; + if (node.length < 1) + return []; + if (typeof node[0] != 'string') + return node.map(vhFromArray); + const hasData = node.length > 1 && u.isObject(node[1]); + const children = u.pipe( + u.map(vhFromArray), + u.flatten, + Array.from + )(node.slice(hasData ? 2 : 1)); + const result = hasData ? + snabbdom.h(node[0], node[1], children) : + snabbdom.h(node[0], children); + return result; +} + +/** + * Virtual DOM renderer + */ +export class Renderer { + + /** + * Calls `init` method + * @param {HTMLElement} [el=document.body] + * @param {Any} [initState=null] + * @param {Function} [vtCb=null] + * @param {Number} [maxFps=30] + */ + constructor(el, initState, vtCb, maxFps) { + this.init(el, initState, vtCb, maxFps); + } + + /** + * Initialize renderer + * @param {HTMLElement} [el=document.body] + * @param {Any} [initState=null] + * @param {Function} [vtCb=null] + * @param {Number} [maxFps=30] + * @return {Promise} + */ + init(el, initState, vtCb, maxFps) { + this._state = null; + this._changes = []; + this._promise = null; + this._timeout = null; + this._lastRender = null; + this._vtCb = vtCb; + this._maxFps = u.isNumber(maxFps) ? maxFps : 30; + this._vNode = el || document.querySelector('body'); + if (initState) + return this.change(_ => initState); + return new Promise(resolve => { resolve(); }); + } + + /** + * Get current state value referenced by `paths` + * @param {...Path} paths + * @return {Any} + */ + get(...paths) { + return u.get(paths, this._state); + } + + /** + * Change current state value referenced by `path` + * @param {Path} path + * @param {Any} value + * @return {Promise} + */ + set(path, value) { + if (arguments.length < 2) { + value = path; + path = []; + } + return this.change(path, _ => value); + } + + /** + * Change current state value referenced by `path` + * @param {Path} path + * @param {Function} cb + * @return {Promise} + */ + change(path, cb) { + if (arguments.length < 2) { + cb = path; + path = []; + } + this._changes.push([path, cb]); + if (this._promise) + return this._promise; + this._promise = new Promise((resolve, reject) => { + setTimeout(() => { + try { + this._change(); + } catch(e) { + this._promise = null; + reject(e); + throw e; + } + this._promise = null; + resolve(); + }, 0); + }); + return this._promise; + } + + _change() { + let change = false; + while (this._changes.length > 0) { + const [path, cb] = this._changes.shift(); + const view = u.get(path); + const oldState = this._state; + this._state = u.change(path, cb, this._state); + if (this._state && u.equals(view(oldState), + view(this._state))) + continue; + change = true; + if (!this._vtCb || this._timeout) + continue; + const delay = (!this._lastRender || !this._maxFps ? + 0 : + (1000 / this._maxFps) - + (performance.now() - this._lastRender)); + this._timeout = setTimeout(() => { + this._timeout = null; + this._lastRender = performance.now(); + const vNode = vhFromArray(this._vtCb(this)); + patch(this._vNode, vNode); + this._vNode = vNode; + ev.fire(this, 'render', [this._state]); + }, (delay > 0 ? delay : 0)); + } + if (change) + ev.fire(this, 'change', [this._state]); + } + +} +// Renderer.prototype.set = u.curry(Renderer.prototype.set); +// Renderer.prototype.change = u.curry(Renderer.prototype.change); + + +/** + * Default renderer + * @static + * @type {Renderer} + */ +const defaultRenderer = new Renderer(); +export default defaultRenderer; diff --git a/src_js/opcut/util.js b/src_js/opcut/util.js new file mode 100644 index 0000000..6f5526a --- /dev/null +++ b/src_js/opcut/util.js @@ -0,0 +1,461 @@ +/** @module opcut/util */ + +/** + * Identity function + * @function + * @param {Any} obj input object + * @return {Any} same object as input + */ +export const identity = obj => obj; + +/** + * Check if value is Array (wrapper for Array.isArray) + * @function + * @param {Any} arr input object + * @return {Boolean} + */ +export const isArray = Array.isArray; + +/** + * Check if value is Object (not `true` from Array or `null`) + * @function + * @param {Any} obj input object + * @return {Boolean} + */ +export const isObject = obj => obj !== null && + typeof(obj) == 'object' && + !isArray(obj); + +/** + * Check if value is number + * @function + * @param {Any} n input object + * @return {Boolean} + */ +export const isNumber = n => typeof(n) == 'number'; + +/** + * Check if value is integer + * @function + * @param {Any} n input object + * @type {Boolean} + */ +export const isInteger = Number.isInteger; + +/** + * Check if value is string + * @function + * @param {Any} str input object + * @type {Boolean} + */ +export const isString = str => typeof(str) == 'string'; + +/** + * Strictly parse integer from string + * @param {String} value + * @return {Number} + */ +export function strictParseInt(value) { + if (/^(-|\+)?([0-9]+)$/.test(value)) + return Number(value); + return NaN; +} + +/** + * Strictly parse float from string + * @param {String} value + * @return {Number} + */ +export function strictParseFloat(value) { + if (/^(-|\+)?([0-9]+(\.[0-9]+)?)$/.test(value)) + return Number(value); + return NaN; +} + +/** + * Create new deep copy of input value + * @param {Any} value + * @return {Any} copy of value + */ +export function clone(obj) { + if (isArray(obj)) + return Array.from(obj, clone); + if (isObject(obj)) { + let ret = {}; + for (let i in obj) + ret[i] = clone(obj[i]); + return ret; + } + return obj; +} + +/** + * Combine two arrays in single array of pairs + * @param {Array} arr1 + * @param {Array} arr2 + * @return {Array>} + */ +export function zip(arr1, arr2) { + return Array.from((function*() { + for (let i = 0; i < arr1.length || i < arr2.length; ++i) + yield [arr1[i], arr2[i]]; + })()); +} + +/** + * Convert object to array of key, value pairs + * @param {Object} obj + * @return {Array} + */ +export function toPairs(obj) { + return Object.entries(obj); +} + +/** + * Convert array of key, value pairs to object + * @param {Array} arr + * @return {Object} + */ +export function fromPairs(arr) { + let ret = {}; + for (let [k, v] of arr) + ret[k] = v; + return ret; +} + +/** + * Flatten nested arrays + * @param {Array} arr + * @return {Generator} + */ +export function* flatten(arr) { + if (isArray(arr)) { + for (let i of arr) + if (isArray(i)) + yield* flatten(i); + else + yield i; + } else { + yield arr; + } +} + +/** + * Pipe function calls (functional composition with reversed order) + * @param {...Function} fns functions + * @return {Function} + */ +export function pipe(...fns) { + if (fns.length < 1) + throw 'no functions'; + return function (...args) { + let ret = fns[0].apply(this, args); + for (let fn of fns.slice(1)) + ret = fn(ret); + return ret; + }; +} + +/** + * Curry function with fixed arguments lenth + * @param {Function} fn + * @return {Function} + */ +export function curry(fn) { + let wrapper = function(oldArgs) { + return function(...args) { + args = oldArgs.concat(args); + if (args.length >= fn.length) + return fn(...args); + return wrapper(args); + }; + }; + return wrapper([]); +} + +/** + * Deep object equality + * (curried function) + * @function + * @param {Any} x + * @param {Any} y + * @return {Boolean} + */ +export const equals = curry((x, y) => { + if (x === y) + return true; + if (typeof(x) != 'object' || + typeof(y) != 'object' || + x === null || + y === null) + return false; + if (Array.isArray(x) || Array.isArray(y)) { + if (!Array.isArray(x) || !Array.isArray(y) || x.length != y.length) + return false; + } + for (let i in x) + if (!equals(x[i], y[i])) + return false; + for (let i in y) + if (!equals(x[i], y[i])) + return false; + return true; +}); + +/** + * Get value from `obj` referenced by `path` + * (curried function) + * @function + * @param {Path} path + * @param {Any} obj + * @return {Any} + */ +export const get = curry((path, obj) => { + let ret = obj; + for (let i of flatten(path)) { + if (ret === null || typeof(ret) != 'object') + return undefined; + ret = ret[i]; + } + return ret; +}); + +/** + * Change `obj` by appling function `fn` to value referenced by `path` + * (curried function) + * @function + * @param {Path} path + * @param {Function} fn + * @param {Any} obj + * @return {Any} changed `obj` + */ +export const change = curry((path, fn, obj) => { + function _change(path, obj) { + if (isInteger(path[0])) { + obj = (isArray(obj) ? Array.from(obj) : []); + } else if (isString(path[0])) { + obj = (isObject(obj) ? Object.assign({}, obj) : {}); + } else { + throw 'invalid path'; + } + if (path.length > 1) { + obj[path[0]] = _change(path.slice(1), obj[path[0]]); + } else { + obj[path[0]] = fn(obj[path[0]]); + } + return obj; + } + path = Array.from(flatten(path)); + if (path.length < 1) + return fn(obj); + return _change(path, obj); +}); + +/** + * Change `obj` by setting value referenced by `path` to `val` + * (curried function) + * @function + * @param {Path} path + * @param {Any} val + * @param {Any} obj + * @return {Any} changed `obj` + */ +export const set = curry((path, val, obj) => change(path, _ => val, obj)); + +/** + * Change `obj` by omitting value referenced by `path` + * (curried function) + * @function + * @param {Path} path + * @param {Any} obj + * @return {Any} changed `obj` + */ +export const omit = curry((path, obj) => { + function _omit(path, obj) { + if (isInteger(path[0])) { + obj = (isArray(obj) ? Array.from(obj) : []); + } else if (isString(path[0])) { + obj = (isObject(obj) ? Object.assign({}, obj) : {}); + } else { + throw 'invalid path'; + } + if (path.length > 1) { + obj[path[0]] = _omit(path.slice(1), obj[path[0]]); + } else if (isInteger(path[0])) { + obj.splice(path[0], 1); + } else { + delete obj[path[0]]; + } + return obj; + } + path = Array.from(flatten(path)); + if (path.length < 1) + return undefined; + return _omit(path, obj); +}); + +/** + * Sort `arr` by with comparison function `fn` + * (curried function) + * @function + * @param {Function} fn + * @param {Array} arr + * @return {Array} sorted `arr` + */ +export const sortBy = curry((fn, arr) => Array.from(arr).sort((x, y) => { + let xVal = fn(x); + let yVal = fn(y); + if (xVal < yVal) + return -1; + if (xVal > yVal) + return 1; + return 0; +})); + +/** + * Create object which is subset `obj` containing only properties defined by + * `arr` + * (curried function) + * @function + * @param {Array} arr + * @param {Object} obj + * @return {Object} subset of `obj` + */ +export const pick = curry((arr, obj) => { + const ret = {}; + for (let i of arr) + if (i in obj) + ret[i] = obj[i]; + return ret; +}); + +/** + * Change `arr` by appling function `fn` to it's elements + * (curried function) + * @function + * @param {Function} fn + * @param {Array} arr + * @return {Array} modified `arr` + */ +export const map = curry((fn, arr) => isArray(arr) ? + arr.map(fn) : + pipe(toPairs, + x => x.map(([k, v]) => [k, fn(v)]), + fromPairs)(arr)); + +/** + * Change `arr` to contain only elements fow which function `fn` returns `true` + * (curried function) + * @function + * @param {Function} fn + * @param {Array} arr + * @return {Array} filtered `arr` + */ +export const filter = curry((fn, arr) => arr.filter(fn)); + +/** + * Append `val` to end of `arr` + * (curried function) + * @function + * @param {Any} val + * @param {Array} arr + * @return {Array} `arr` with appended `val` + */ +export const append = curry((val, arr) => arr.concat([val])); + +/** + * Reduce `arr` values by appling function `fn` + * (curried function) + * @function + * @param {Function} fn + * @param {Any} val initial accumulator value + * @param {Array} arr + * @return {Any} reduced value + */ +export const reduce = curry((fn, val, arr) => arr.reduce(fn, val)); + +/** + * Merge two objects + * (curried function) + * @function + * @param {Object} obj1 + * @param {Object} obj2 + * @return {Object} combined `obj1` and `obj2` + */ +export const merge = curry((obj1, obj2) => Object.assign({}, obj1, obj2)); + +/** + * Merge multiple objects + * (curried function) + * @function + * @param {...Object} objs + * @return {Object} combined `objs` + */ +export const mergeAll = reduce(merge, {}); + +/** + * Find element in `arr` for which function `fn` returns `true` + * (curried function) + * @function + * @param {Function} fn + * @param {Array} arr + * @return {Any} + */ +export const find = curry((fn, arr) => arr.find(fn)); + +/** + * Concatenate two arrays + * (curried function) + * @function + * @param {Array} arr1 + * @param {Array} arr2 + * @return {Array} concatenated `arr1` and `arr2` + */ +export const concat = curry((arr1, arr2) => arr1.concat(arr2)); + +/** + * Check if `arr` contains `val` + * (curried function) + * @function + * @param {Any} val + * @param {Array} arr + * @return {Boolean} + */ +export const contains = curry((val, arr) => arr.includes(val)); + +/** + * Insert `val` into `arr` on index `idx` + * (curried function) + * @function + * @param {Number} idx + * @param {Any} val + * @param {Array} arr + * @return {Array} + */ +// TODO: Array.from(arr).splice(idx, 0, val) not working? +export const insert = curry((idx, val, arr) => + arr.slice(0, idx).concat([val], arr.slice(idx))); + +/** + * Create promise that resolves in `t` milliseconds + * @param {Number} t + * @return {Promise} + */ +export function sleep(t) { + return new Promise(resolve => { + setTimeout(() => { resolve(); }, t); + }); +} + +/** + * Delay function call `fn(...args)` for `t` milliseconds + * @param {Function} fn + * @param {Number} [t=0] + * @param {...Any} args + * @return {Promise} + */ +export function delay(fn, t, ...args) { + return new Promise(resolve => { + setTimeout(() => { resolve(fn(...args)); }, t || 0); + }); +} diff --git a/src_js/opcut/vt.js b/src_js/opcut/vt.js index 2754877..f67b23f 100644 --- a/src_js/opcut/vt.js +++ b/src_js/opcut/vt.js @@ -3,6 +3,11 @@ export function main() { return ['div', 'application in development - ', - ['a', {href: 'https://github.com/bozokopic/opcut'}, 'Github page'] + ['a', { + props: { + href: 'https://github.com/bozokopic/opcut' + }}, + 'Github page' + ] ]; } -- cgit v1.2.3-70-g09d2