UNPKG

rpd

Version:

RPD is a minimal framework for building Node-Based User Interfaces, powered by Reactive Programming

327 lines (295 loc) 11.6 kB
;(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);