rpd
Version:
RPD is a minimal framework for building Node-Based User Interfaces, powered by Reactive Programming
327 lines (295 loc) • 11.6 kB
JavaScript
;(function(global) {
"use strict";
var Rpd = global.Rpd;
if (typeof Rpd === "undefined" && typeof require !== "undefined") {
Rpd = require('rpd');
}
Rpd.Render = (function() {
var ƒ = Rpd.unit;
// =============================================================================
// ============================= Placing =======================================
// =============================================================================
function GridPlacing(style) {
this.nodeRects = [];
this.edgePadding = style.edgePadding || { horizontal: 30, vertical: 20 };
this.boxPadding = style.boxPadding || { horizontal: 20, vertical: 30 };
}
GridPlacing.DEFAULT_LIMITS = [ 1000, 1000 ]; // in pixels
GridPlacing.prototype.nextPosition = function(node, size, limits) {
limits = limits || GridPlacing.DEFAULT_LIMITS;
var nodeRects = this.nodeRects,
boxPadding = this.boxPadding, edgePadding = this.edgePadding;
var width = size.width, height = size.height;
var lastRect = (nodeRects.length ? nodeRects[nodeRects.length-1] : null);
var newRect = { x: lastRect ? lastRect.x : edgePadding.horizontal,
y: lastRect ? (lastRect.y + lastRect.height + boxPadding.vertical)
: edgePadding.vertical,
width: width, height: height };
if ((newRect.y + height + edgePadding.vertical) > limits.height) {
newRect.x = newRect.x + width + boxPadding.horizontal;
newRect.y = edgePadding.vertical;
}
nodeRects.push(newRect);
return { x: newRect.x, y: newRect.y };
}
// =============================================================================
// ============================= DragAndDrop ===================================
// =============================================================================
function DragAndDrop(canvas, style) {
this.canvas = canvas;
this.style = style;
}
DragAndDrop.prototype.add = function(handle, spec) {
var canvas = this.canvas; var style = this.style;
var start = spec.start, end = spec.end, drag = spec.drag;
Kefir.fromEvents(handle.node(), 'mousedown').map(extractPos)
.map(style.getLocalPos)
.flatMap(function(pos) {
var initPos = start(),
diffPos = { x: pos.x - initPos.x,
y: pos.y - initPos.y };
var moveStream = Kefir.fromEvents(canvas.node(), 'mousemove')
.map(stopPropagation)
.takeUntilBy(Kefir.merge([
Kefir.fromEvents(canvas.node(), 'mouseup'),
Kefir.fromEvents(handle.node(), 'mouseup')
]))
.map(extractPos)
.map(style.getLocalPos)
.map(function(absPos) {
return { x: absPos.x - diffPos.x,
y: absPos.y - diffPos.y };
}).toProperty(function() { return initPos; });
moveStream.last().onValue(end);
return moveStream;
}).onValue(drag);
}
// =============================================================================
// ================================ Links ======================================
// =============================================================================
function VLink(link, style) { // visual representation of the link
this.link = link; // may be null, if it's a ghost
this.style = style;
this.styledLink = null;
this.element = null;
}
VLink.prototype.construct = function(width) {
if (this.styledLink) throw new Error('VLink is already constructed');
var styledLink = this.style.createLink(this.link);
this.styledLink = styledLink;
this.element = d3.select(styledLink.element);
// html: this.element.style('z-index', LINK_LAYER);
return this;
}
VLink.prototype.rotate = function(x0, y0, x1, y1) {
var style = this.style;
var sourcePos = style.getLocalPos({ x: x0, y: y0 });
var targetPos = style.getLocalPos({ x: x1, y: y1 });
this.styledLink.rotate(sourcePos.x, sourcePos.y,
targetPos.x, targetPos.y);
return this;
}
VLink.prototype.rotateI = function(x0, y0, inlet) {
var style = this.style;
var inletPos = style.getInletPos(inlet);
return this.rotate(x0, y0, inletPos.x, inlet.y);
}
VLink.prototype.rotateO = function(outlet, x1, y1) {
var style = this.style;
var outletPos = style.getOutletPos(outlet);
return this.rotate(outletPos.x, outletPos.y, x1, y1);
}
VLink.prototype.rotateOI = function(outlet, inlet) {
var style = this.style;
var outletPos = style.getOutletPos(outlet),
inletPos = style.getInletPos(inlet);
return this.rotate(outletPos.x, outletPos.y, inletPos.x, inletPos.y);
}
VLink.prototype.update = function() {
if (!this.link) return;
var link = this.link;
this.rotateOI(link.outlet, link.inlet);
return this;
}
VLink.prototype.appendTo = function(target) {
target.append(ƒ(this.element.node()));
return this;
}
VLink.prototype.removeFrom = function(target) {
this.element.remove();
return this;
}
VLink.prototype.noPointerEvents = function() {
if (this.styledLink.noPointerEvents) {
this.styledLink.noPointerEvents();
}
return this;
}
VLink.prototype.listenForClicks = function() {
var link = this.link;
addClickSwitch(this.element.node(),
function() { link.enable(); },
function() { link.disable(); });
return this;
}
VLink.prototype.enable = function() {
this.element.classed('rpd-disabled', false);
}
VLink.prototype.disable = function() {
this.element.classed('rpd-disabled', true);
}
VLink.prototype.get = function() {
return this.link;
}
VLink.prototype.getElement = function() {
return this.element.node();
}
function VLinks() {
this.vlinks = [];
}
VLinks.prototype.clear = function() { this.vlinks = []; }
VLinks.prototype.add = function(vlink) {
this.vlinks.push(vlink);
return vlink;
}
VLinks.prototype.remove = function(vlink) {
this.vlinks = this.vlinks.filter(function(my_vlink) {
return my_vlink !== vlink;
});
}
VLinks.prototype.forEach = function(f) {
this.vlinks.forEach(f);
}
VLinks.prototype.updateAll = function() {
this.forEach(function(vlink) { vlink.update(); });
}
VLinks.prototype.count = function() {
return this.vlinks.length;
}
VLinks.prototype.getLast = function() {
return this.count() ? this.vlinks[this.vlinks.length - 1] : null;
}
// =============================================================================
// =============================== helpers =====================================
// =============================================================================
function mergeConfig(user_conf, defaults) {
if (user_conf) {
var merged = {};
Object.keys(defaults).forEach(function(prop) { merged[prop] = defaults[prop]; });
Object.keys(user_conf).forEach(function(prop) { merged[prop] = user_conf[prop]; });
return merged;
} else return defaults;
}
function preventDefault(evt) { evt.preventDefault(); return evt; };
function stopPropagation(evt) { evt.stopPropagation(); return evt; };
function extractPos(evt) { return { x: evt.clientX,
y: evt.clientY }; };
function addTarget(target) {
return function(pos) {
return { pos: pos, target: target };
}
};
function addClickSwitch(elm, on_true, on_false, initial) {
Kefir.fromEvents(elm, 'click')
.map(stopPropagation)
.map(ƒ(initial || false))
.scan(Rpd.not) // will toggle between `true` and `false`
.onValue(function(val) {
if (val) { on_true(); }
else { on_false(); }
})
}
var addValueErrorEffect = (function() {
var errorEffects = {};
return function(key, target, duration) {
target.classed('rpd-error', true);
if (errorEffects[key]) clearTimeout(errorEffects[key]);
errorEffects[key] = setTimeout(function() {
target.classed('rpd-error', false);
errorEffects[key] = null;
}, duration || 1);
}
})();
var addValueUpdateEffect = (function() {
var updateEffects = {};
return function(key, target, duration) {
target.classed('rpd-stale', false);
target.classed('rpd-fresh', true);
if (updateEffects[key]) clearTimeout(updateEffects[key]);
updateEffects[key] = setTimeout(function() {
target.classed('rpd-fresh', false);
target.classed('rpd-stale', true);
updateEffects[key] = null;
}, duration || 1);
}
})();
function subscribeUpdates(node, subscriptions) {
if (!subscriptions) return;
Object.keys(subscriptions).forEach(function(alias) {
(function(subscription, alias) {
node.event['node/add-inlet']
.filter(function(inlet) { return inlet.alias === alias; })
.onValue(function(inlet) {
node.event['node/is-ready'].onValue(function() {
if (subscription.default) {
inlet.receive(
(typeof subscription.default === 'function')
? subscription.default() : subscription.default
);
}
if (subscription.valueOut) {
subscription.valueOut.onValue(function(value) {
inlet.receive(value);
});
}
});
});
})(subscriptions[alias], alias);
});
}
// Should be called once when renderer registered and Rpd.events is ready
function reportErrorsToConsole(config) {
Rpd.events.onError(function(error) {
if (!config.logErrors) return;
if (error.silent) return;
var subjectStringified;
try {
subjectStringified = Rpd.autoStringify(error.subject);
} catch (e) {
subjectStringified = '<Unable-to-Stringify>';
}
if (error.system) {
console.error(new Error(error.type + ' — ' + error.message + '. ' +
'Subject: ' + subjectStringified));
} else {
console.log('Error:', error.type, '—', error.message + '. ',
'Subject: ' + subjectStringified, error.subject);
}
});
}
function _data(selection, data) {
// FIXME: should use d3Selection.datum() eventually
// see issue https://github.com/shamansir/rpd/issues/442
if (data) selection.node().__rpd_data__ = data;
else return selection.node().__rpd_data__;
};
return {
data: _data,
Placing: GridPlacing,
DragAndDrop: DragAndDrop,
//Connectivity: Connectivity,
VLink: VLink,
VLinks: VLinks,
mergeConfig: mergeConfig,
preventDefault: preventDefault,
stopPropagation: stopPropagation,
extractPos: extractPos,
addTarget: addTarget,
addClickSwitch: addClickSwitch,
addValueErrorEffect: addValueErrorEffect,
addValueUpdateEffect: addValueUpdateEffect,
subscribeUpdates: subscribeUpdates,
reportErrorsToConsole: reportErrorsToConsole
};
})();
})(this);