aboutsummaryrefslogtreecommitdiff
path: root/src_js
diff options
context:
space:
mode:
authorbozokopic <bozo.kopic@gmail.com>2018-04-09 09:25:32 +0200
committerbozokopic <bozo.kopic@gmail.com>2018-04-09 09:25:32 +0200
commit234d2def50f630e54d4d184dcb3f3df63437f49e (patch)
tree23260ac1b12b89f5efb8819f0eb57628f5d21010 /src_js
parent1a7bbad8a6295db02d3e66a8d601b5e9da1e9057 (diff)
build refactoring
Diffstat (limited to 'src_js')
-rw-r--r--src_js/opcut/ev.js22
-rw-r--r--src_js/opcut/lenses.js21
-rw-r--r--src_js/opcut/main.js9
-rw-r--r--src_js/opcut/renderer.js306
-rw-r--r--src_js/opcut/util.js461
-rw-r--r--src_js/opcut/vt.js7
6 files changed, 672 insertions, 154 deletions
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<Any>} arr1
+ * @param {Array<Any>} arr2
+ * @return {Array<Array<Any>>}
+ */
+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<Array>}
+ */
+export function toPairs(obj) {
+ return Object.entries(obj);
+}
+
+/**
+ * Convert array of key, value pairs to object
+ * @param {Array<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'
+ ]
];
}