rpd
Version:
RPD is a minimal framework for building Node-Based User Interfaces, powered by Reactive Programming
223 lines (186 loc) • 9.04 kB
JavaScript
;(function(global) {
"use strict";
var RpdUtils = (function() {
function adaptToState(state, value) {
return Math.floor((state.min + ((state.max - state.min) * value)) * 100) / 100;
}
function numberToHex(num) { return (num > 15) ? num.toString(16) : '0' + num.toString(16); }
function toHexColor(color) {
return '#' + numberToHex(color.r || 0)
+ numberToHex(color.g || 0)
+ numberToHex(color.b || 0);
}
function getNodeTypesByToolkit(nodeTypes) {
return Object.keys(nodeTypes).reduce(function(byToolkit, nodeType) {
var slashPos = nodeType.indexOf('/');
var toolkit = (slashPos < 0) ? toolkit : nodeType.substring(0, slashPos);
var typeName = (slashPos < 0) ? '' : nodeType.substring(slashPos + 1);
if (!byToolkit[toolkit]) byToolkit[toolkit] = { icon: '', types: [] };
byToolkit[toolkit].types.push({ toolkit: toolkit,
fullName: nodeType, name: typeName,
data: nodeTypes[nodeType] });
return byToolkit;
}, {})
}
function NodeList(conf) {
this.listElements = [];
this.selected = null;
this.markSelected = conf.markSelected;
this.markDeselected = conf.markDeselected;
this.markAdding = conf.markAdding;
this.markAdded = conf.markAdded;
this.recalculateSize = conf.recalculateSize;
this.setVisible = conf.setVisible;
this.setInvisible = conf.setInvisible;
this.getPatch = conf.getPatch;
// create text input, search field, to let user filter results
this.searchInput = conf.createSearchInput();
// create a button which is able to clear this field
this.clearSearchButton = conf.createClearSearchButton();
var listElements = conf.buildList();
var search = this.searchInput,
clearSearch = this.clearSearchButton;
this.clearingEvents = Kefir.fromEvents(this.clearSearchButton.node(), 'click')
.onValue(function() {
this.selectNothing();
conf.clearSearchInput(this.searchInput);
}.bind(this));
// make the list of elements double-linked and looped,
// so easy navigation with up/down arrow keys will be possible
for (var i = 0; i < listElements.length; i++) {
listElements[i].visible = true;
listElements[i].prev = (i > 0) ? listElements[i - 1] : listElements[listElements.length - 1];
listElements[i].next = (i < listElements.length - 1) ? listElements[i + 1] : listElements[0];
}
this.listElements = listElements;
this.currentlyVisible = listElements.length;
}
NodeList.prototype.select = function(elmData) {
if (this.selected) this.markDeselected(this.selected);
this.selected = elmData;
if (this.selected) this.markSelected(elmData);
}
NodeList.prototype.selectNothing = function() {
this.select(null);
}
NodeList.prototype.addOnClick = function() {
var nodeList = this;
var listElements = this.listElements;
listElements.forEach(function(elmData) {
var li = elmData.element;
Kefir.fromEvents(li.node(), 'click')
.onValue(function() {
// add the node when corresponding element was clicked with mouse
nodeList.select(elmData);
nodeList.addNode(elmData);
});
});
}
NodeList.prototype.addNode = function(elmData) {
this.markAdding(elmData);
setTimeout(function() {
this.markAdded(elmData);
}.bind(this), 1000);
this.getPatch().addNode(elmData.def.fullName);
}
NodeList.prototype.addSearch = function() {
var nodeList = this;
var search = this.searchInput;
// make seach field hide filtered results when user changes search request
Kefir.fromEvents(search.node(), 'input')
.merge(this.clearingEvents)
.throttle(500)
.map(function() { return search.node().value; })
.onValue(function(searchString) {
var visibleBefore = nodeList.currentlyVisible;
nodeList.currentlyVisible = 0;
nodeList.listElements.forEach(function(elmData) {
var index = elmData.def.fullName.indexOf(searchString);
if (index >= 0) { nodeList.setVisible(elmData); }
else { nodeList.setInvisible(elmData); };
elmData.visible = (index >= 0);
if (elmData.visible) nodeList.currentlyVisible++;
});
nodeList.recalculateSize(nodeList.listElements,
visibleBefore, nodeList.currentlyVisible);
});
}
NodeList.prototype.addCtrlSpaceAndArrows = function() {
var nodeList = this;
var listElements = this.listElements;
var search = this.searchInput,
clearSearch = this.clearSearchButton;
var clearingEvents = this.clearingEvents;
// ctrl+space should focus on the search field and up/down arrows should
// work for selection
Kefir.merge([
Kefir.fromEvents(document.body, 'keyup')
.filter(function(evt) {
// (control / alt / cmd) + space // TODO: double space (#63)?
return (evt.which == 32 || evt.keyCode == 32) && (evt.altKey || evt.metaKey || evt.ctrlKey);
}),
Kefir.fromEvents(search.node(), 'click')
]).flatMap(function(switchedOn) {
//console.log('start!');
search.node().focus();
if (listElements.length > 0) nodeList.select(listElements[0]);
return Kefir.fromEvents(document.body, 'keyup')
.map(function(evt) { return evt.which || evt.keyCode; })
.filter(function(key) { return (key === 38) || (key === 40); })
.map(function(key) { return (key === 38) ? 'up' : 'down'; })
.takeUntilBy(Kefir.merge([
Kefir.fromEvents(document.body, 'keyup')
.filter(function(evt) {
return (evt.which == 13 || evt.keyCode == 13); // key == enter
}).map(function() { return true; }),
Kefir.fromEvents(document.body, 'keyup').filter(function(evt) {
return (evt.which == 27 || evt.keyCode == 27); // key === escape
}).map(function() { return false; }),
Kefir.fromEvents(document.body, 'click').filter(function(evt) {
return evt.target !== search.node();
}).map(function() { return false; }),
nodeList.clearingEvents.map(function() { return false; })/*,
Kefir.fromEvents(search.node(), 'click')*/
]).take(1).onValue(function(doAdd) {
if (doAdd && nodeList.selected) {
nodeList.addNode(nodeList.selected);
}
search.node().blur();
nodeList.selectNothing();
}))
.onValue(function(key) {
if (nodeList.currentlyVisible == 0) return;
search.node().blur();
if (key === 'up') {
var current = nodeList.selected ? nodeList.selected.prev : listElements[listElements.length - 1];
while (current && !current.visible) {
current = current.prev;
}
} else if (key === 'down') {
var current = nodeList.selected ? nodeList.selected.next : listElements[0];
while (current && !current.visible) {
current = current.next;
}
}
if (current) nodeList.select(current);
});
}).onValue(function() {});
}
return {
'adaptToState': adaptToState,
'numberToHex': numberToHex,
'toHexColor': toHexColor,
'getNodeTypesByToolkit': getNodeTypesByToolkit,
'NodeList': NodeList
};
})();
if (typeof define === 'function' && define.amd) {
define([], function() { return RpdUtils; });
global.RpdUtils = RpdUtils;
} else if (typeof module === 'object' && typeof exports === 'object') {
module.exports = RpdUtils;
if (typeof Rpd !== 'undefined') Rpd.RpdUtils = RpdUtils;
} else {
global.RpdUtils = RpdUtils;
}
}(this));