",
12 | "license": "ISC",
13 | "dependencies": {
14 | "base64-image-loader": "^1.2.1",
15 | "ejs": "^2.6.1",
16 | "express": "^4.16.3",
17 | "osc": "^2.2.3",
18 | "rangeslider-pure": "^0.4.5-1",
19 | "vanilla-picker": "^2.2.1",
20 | "whatwg-fetch": "^2.0.4"
21 | },
22 | "devDependencies": {
23 | "jsdom": "^11.11.0",
24 | "jsdom-global": "^3.0.2",
25 | "mocha": "^5.2.0",
26 | "mocha-webpack": "^2.0.0-beta.0",
27 | "osc-js": "^1.2.2",
28 | "svg-inline-loader": "^0.8.0",
29 | "webpack": "^4.16.1",
30 | "webpack-cli": "^2.1.5"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/builder.js:
--------------------------------------------------------------------------------
1 | const toggleMinusSvg = require("svg-inline-loader?classPrefix=_minus!../assets/img/toggle-minus.svg");
2 | const togglePlusSvg = require("svg-inline-loader?classPrefix=_plus!../assets/img/toggle-plus.svg");
3 |
4 | const types = require('./types.js');
5 |
6 | const DEFAULT_COLOR_ELEM_VALUE = '#4466ff';
7 |
8 | // Build controls recursively based upon the json, and append to the parent.
9 | function buildContentsAddToContainer(contents, parentContainer, cfg) {
10 | let dirNames = Object.keys(contents);
11 | dirNames.sort();
12 | for (let j = 0; j < dirNames.length; j++) {
13 | let nodeName = dirNames[j];
14 | let dirObj = contents[dirNames[j]];
15 | // Container for this node.
16 | let directoryElem = document.createElement('div');
17 | let id = generateId();
18 | let html = '';
19 | // If this has CONTENTS, build a directory node with toggle controls.
20 | // If it has neither CONTENTS nor TYPE, treat it as an empty directory.
21 | if (dirObj.CONTENTS || (!dirObj.CONTENTS && !dirObj.TYPE)) {
22 | html += createTogglerHtml(id, dirNames[j]);
23 | directoryElem.className = 'dir-container';
24 | }
25 | html += ' ';
26 | // Main body. This is the element that toggle shows or hides.
27 | html += '';
29 | html += '
';
30 | directoryElem.innerHTML = html;
31 | let nodeContainer = directoryElem.querySelector('#control_body_' + id);
32 | // If this has TYPE, build control(s) from the details.
33 | if (dirObj.TYPE) {
34 | directoryElem.classList.add('node');
35 | buildControlElements(nodeContainer, nodeName, dirObj);
36 | }
37 | // If this has CONTENTS, recursively handle the inner contents.
38 | if (dirObj.CONTENTS) {
39 | buildContentsAddToContainer(dirObj.CONTENTS, nodeContainer);
40 | directoryElem.appendChild(nodeContainer);
41 | }
42 | parentContainer.appendChild(directoryElem);
43 | }
44 | }
45 |
46 | // Create html for toggle controls that show and hide directory contents.
47 | function createTogglerHtml(id, name) {
48 | let html = '';
49 | // Toggle button when this is collapsed, will show the node.
50 | html += '';
52 | html += '' + togglePlusSvg + ' ';
53 | html += ' ' + E(name) + ' ';
54 | html += '
';
55 | // Toggle button when this is expanded, will hide the node.
56 | html += '';
57 | html += '' + toggleMinusSvg + ' ';
58 | html += '' + E(name) + ' ';
59 | html += '
';
60 | return html;
61 | }
62 |
63 | function shortDisplay(text) {
64 | if (text.length > 28) {
65 | return (text.substring(0, 20) + '...' +
66 | text.substring(text.length - 6, text.length));
67 | }
68 | return text;
69 | }
70 |
71 | // Add control nodes. Iterate the type field, adding one node per kind of type.
72 | function buildControlElements(containerElem, name, details, cfg) {
73 | // Handle the case where a directory is also a control.
74 | let existingName = containerElem.parentNode.querySelector('.dir-name');
75 | if (!existingName) {
76 | createAppendElem(containerElem, 'span', 'control-name',
77 | shortDisplay(name));
78 | }
79 | createAppendElem(containerElem, 'span', 'full-path',
80 | shortDisplay(details.FULL_PATH));
81 | createAppendElem(containerElem, 'span', 'description', details.DESCRIPTION);
82 | let groupElem = document.createElement('div');
83 | groupElem.className = 'group';
84 | // Traverse the input.
85 | let selector = [0];
86 | let pos = 0;
87 | for (let i = 0; i < details.TYPE.length; i++) {
88 | let type = details.TYPE[i];
89 | if (type == '[') {
90 | selector.push(0);
91 | continue;
92 | } else if (type == ']') {
93 | selector.pop();
94 | } else {
95 | details.name = name;
96 | let html = buildSingleControl(details, type, selector, pos, cfg);
97 | if (html) {
98 | let id = generateId();
99 | let kind = types.extractControlKind(type, html);
100 | let elem = document.createElement('div');
101 | elem.id = 'control_body_' + id;
102 | elem.className = 'control kind_' + kind;
103 | elem.innerHTML = html;
104 | groupElem.appendChild(elem);
105 | }
106 | pos += 1;
107 | }
108 | selector[selector.length - 1]++;
109 | }
110 | containerElem.appendChild(groupElem);
111 | }
112 |
113 | function buildSingleControl(details, type, selector, pos, cfg) {
114 | var html = '';
115 | var getter = null;
116 | var setter = null;
117 | cfg = cfg || {};
118 | if (type == 'c') {
119 | // Char
120 | if (details.RANGE && applySelector(details.RANGE, selector, 'VALS')) {
121 | var options = applySelector(details.RANGE, selector, 'VALS');
122 | var value = applyPos(details.VALUE, pos) || '';
123 | html += '';
124 | for (let i = 0; i < options.length; i++) {
125 | let v = options[i];
126 | html += '' + E(v) + ' ';
131 | }
132 | html += ' ';
133 | getter = 'value';
134 | } else {
135 | var value = applyPos(details.VALUE, pos) || '';
136 | html += ' ';
138 | getter = 'value';
139 | }
140 | } else if (type == 'r') {
141 | // Color
142 | var value = applyPos(details.VALUE, pos);
143 | if (value && value[0] === '#') {
144 | // Remove alpha channel to just get '#rrggbb'.
145 | value = value.substr(0, 7);
146 | } else if (value) {
147 | value = convertOSCColorToHex(value);
148 | } else {
149 | value = DEFAULT_COLOR_ELEM_VALUE;
150 | }
151 | if (cfg.supportHtml5Color) {
152 | html += ' ';
153 | } else {
154 | html += ('
');
156 | }
157 | getter = 'color';
158 | setter = 'color';
159 | } else if (type == 'd') {
160 | // Double
161 | if (details.RANGE && applySelector(details.RANGE, selector, 'VALS')) {
162 | var options = applySelector(details.RANGE, selector, 'VALS');
163 | var value = applyPos(details.VALUE, pos) || 0;
164 | html += '';
165 | for (let i = 0; i < options.length; i++) {
166 | let v = options[i];
167 | html += '' + E(v) + ' ';
172 | }
173 | html += ' ';
174 | getter = 'value';
175 | } else if (details.RANGE) {
176 | var min = applySelector(details.RANGE, selector, 'MIN') || 0;
177 | var max = applySelector(details.RANGE, selector, 'MAX') || 1;
178 | var value = applyPos(details.VALUE, pos) || 0;
179 | html += buildCurrRangeValue(value, min, max);
180 | html += ' ';
182 | getter = 'parseFloat';
183 | setter = 'float';
184 | } else {
185 | var value = applyPos(details.VALUE, pos) || 0;
186 | html += buildCurrRangeValue(value);
187 | html += ' ';
188 | getter = 'parseFloat';
189 | setter = 'float';
190 | }
191 | } else if (type == 'F') {
192 | // False
193 | var value = applyPos(details.VALUE, pos) || false;
194 | html += ' ';
200 | getter = 'boolToggle';
201 | setter = 'setToggle';
202 | } else if (type == 'f') {
203 | // Float
204 | if (details.RANGE && applySelector(details.RANGE, selector, 'VALS')) {
205 | var options = applySelector(details.RANGE, selector, 'VALS');
206 | var value = applyPos(details.VALUE, pos) || 0;
207 | html += '';
208 | for (let i = 0; i < options.length; i++) {
209 | let v = options[i];
210 | html += '' + E(v) + ' ';
215 | }
216 | html += ' ';
217 | getter = 'value';
218 | } else if (details.RANGE) {
219 | var min = applySelector(details.RANGE, selector, 'MIN') || 0;
220 | var max = applySelector(details.RANGE, selector, 'MAX') || 1;
221 | var value = applyPos(details.VALUE, pos) || 0;
222 | html += buildCurrRangeValue(value, min, max);
223 | html += ' ';
225 | getter = 'parseFloat';
226 | setter = 'float';
227 | } else {
228 | var value = applyPos(details.VALUE, pos) || 0;
229 | html += buildCurrRangeValue(value);
230 | html += ' ';
231 | getter = 'parseFloat';
232 | setter = 'float';
233 | }
234 | } else if (type == 'I') {
235 | // Infinity
236 | html += ' ';
237 | setter = 'button';
238 | } else if (type == 'i') {
239 | // Integer
240 | if (details.RANGE && applySelector(details.RANGE, selector, 'VALS')) {
241 | var options = applySelector(details.RANGE, selector, 'VALS');
242 | var value = applyPos(details.VALUE, pos) || 0;
243 | if (options.length == 1) {
244 | value = options[0];
245 | html += ' ';
247 | getter = 'parseSingle';
248 | setter = 'button';
249 | } else if (options.length == 2) {
250 | first = options[0];
251 | second = options[1];
252 | value = applyPos(details.VALUE, pos) || first;
253 | html += ' ';
260 | getter = 'parseIntToggle';
261 | setter = 'setToggle';
262 | } else {
263 | html += '';
264 | for (let i = 0; i < options.length; i++) {
265 | let v = options[i];
266 | html += '' + E(v) + ' ';
271 | }
272 | html += ' ';
273 | getter = 'value';
274 | }
275 | } else if (details.RANGE) {
276 | var min = applySelector(details.RANGE, selector, 'MIN');
277 | var max = applySelector(details.RANGE, selector, 'MAX');
278 | if (min == null || max == null) {
279 | return ('Invalid node: RANGE needs ' +
280 | 'MIN,MAX or VALS ');
281 | }
282 | var value = applyPos(details.VALUE, pos) || 0;
283 | if (max - min == 0) {
284 | value = min;
285 | html += ' ';
287 | getter = 'parseSingle';
288 | setter = 'button';
289 | } else if (max - min == 1) {
290 | value = applyPos(details.VALUE, pos) || min;
291 | html += ' ';
298 | getter = 'parseIntToggle';
299 | setter = 'setToggle';
300 | } else {
301 | html += buildCurrRangeValue(value, min, max);
302 | html += ' ';
304 | getter = 'parseInt';
305 | setter = 'int';
306 | }
307 | } else {
308 | var value = applyPos(details.VALUE, pos) || 0;
309 | html += buildCurrRangeValue(value);
310 | html += ' ';
311 | getter = 'parseInt';
312 | setter = 'int';
313 | }
314 | } else if (type == 'h') {
315 | // Longlong
316 | if (details.RANGE && applySelector(details.RANGE, selector, 'VALS')) {
317 | var options = applySelector(details.RANGE, selector, 'VALS');
318 | var value = applyPos(details.VALUE, pos) || 0;
319 | html += '';
320 | for (let i = 0; i < options.length; i++) {
321 | let v = options[i];
322 | html += '' + E(v) + ' ';
327 | }
328 | html += ' ';
329 | getter = 'parseInt64';
330 | setter = 'int64';
331 | } else if (details.RANGE) {
332 | var min = applySelector(details.RANGE, selector, 'MIN') || 0;
333 | var max = applySelector(details.RANGE, selector, 'MAX') || 1;
334 | var value = applyPos(details.VALUE, pos) || 0;
335 | html += buildCurrRangeValue(value, min, max);
336 | html += ' ';
338 | getter = 'parseInt64';
339 | setter = 'int64';
340 | } else {
341 | var value = applyPos(details.VALUE, pos) || 0;
342 | html += buildCurrRangeValue(value);
343 | html += ' ';
344 | getter = 'parseInt64';
345 | setter = 'int64';
346 | }
347 | } else if (type == 'm') {
348 | // MIDI
349 | return null;
350 | } else if (type == 'N') {
351 | // Null
352 | html += ' ';
353 | setter = 'button';
354 | } else if (type == 's') {
355 | // String
356 | if (details.RANGE && applySelector(details.RANGE, selector, 'VALS')) {
357 | var options = applySelector(details.RANGE, selector, 'VALS');
358 | var value = applyPos(details.VALUE, pos) || '';
359 | html += '';
360 | for (let i = 0; i < options.length; i++) {
361 | let v = options[i];
362 | html += '' + E(v) + ' ';
367 | }
368 | html += ' ';
369 | getter = 'value';
370 | } else {
371 | var value = applyPos(details.VALUE, pos) || '';
372 | html += ' ';
373 | getter = 'value';
374 | }
375 | } else if (type == 'T') {
376 | // True
377 | var value = applyPos(details.VALUE, pos);
378 | // NOTE: Can't use `func() || true` because `false` is possible value.
379 | if (value === null) { value = true; }
380 | html += ' ';
386 | getter = 'boolToggle';
387 | setter = 'setToggle';
388 | } else if (type == 't') {
389 | // Timetag
390 | return null;
391 | } else {
392 | html += 'UNKNOWN (' + E(type) + ') ';
393 | }
394 | html += ' ';
403 | return html;
404 | }
405 |
406 | function buildCurrRangeValue(value, min, max) {
407 | let html = '';
408 | if (min === undefined && max === undefined) {
409 | html += '' + E(value) + ' ';
410 | } else {
411 | html += '' + E(value) + ' ';
412 | html += ' (' + E(min) + '-' +
413 | E(max) + ') ';
414 | }
415 | html += ' ';
416 | return html;
417 | }
418 |
419 | // Add an element to the parent, with the tag, class, and text content.
420 | function createAppendElem(parentElem, tagName, className, text) {
421 | let elem = document.createElement(tagName);
422 | elem.className = className;
423 | elem.textContent = text;
424 | parentElem.appendChild(elem);
425 | }
426 |
427 | var g_idGen = 0;
428 |
429 | function generateId() {
430 | let result = g_idGen;
431 | g_idGen++;
432 | return result;
433 | }
434 |
435 | function applySelector(obj, selector, key) {
436 | if (!obj) {
437 | return null;
438 | }
439 | if (!Array.isArray(obj)) {
440 | return obj[key];
441 | }
442 | for (let n = 0; n < selector.length; n++) {
443 | let i = selector[n];
444 | obj = obj[i];
445 | if (!obj) {
446 | return null;
447 | }
448 | }
449 | return obj[key];
450 | }
451 |
452 | function applyPos(obj, pos) {
453 | if (!obj) {
454 | return null;
455 | }
456 | if (Array.isArray(obj)) {
457 | return obj[pos];
458 | }
459 | return obj;
460 | }
461 |
462 | function textToHexColor(elem) {
463 | return '#' + num2Hex(elem['r']) + num2Hex(elem['g']) + num2Hex(elem['b']);
464 | }
465 |
466 | function num2Hex(num) {
467 | let hex = Number(Math.floor(num)).toString(16);
468 | if (hex.length < 2) {
469 | hex = '0' + hex;
470 | }
471 | return hex;
472 | }
473 |
474 | function convertOSCColorToHex(c) {
475 | return '#' + num2Hex(c[0]*255) + num2Hex(c[1]*255) + num2Hex(c[2]*255);
476 | }
477 |
478 | function E(text) {
479 | if (text === 0) {
480 | return "0";
481 | }
482 | if (!text) {
483 | return "";
484 | }
485 | return text.toString().replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
487 | }
488 |
489 | function E_bool(bool) {
490 | return bool ? "true" : "false";
491 | }
492 |
493 | module.exports = {
494 | buildContentsAddToContainer: buildContentsAddToContainer,
495 | buildControlElements: buildControlElements,
496 | buildSingleControl: buildSingleControl,
497 | textToHexColor: textToHexColor,
498 | createTogglerHtml: createTogglerHtml,
499 | shortDisplay: shortDisplay,
500 | generateId: generateId,
501 | }
502 |
--------------------------------------------------------------------------------
/src/colorpicker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * ColorPicker - pure JavaScript color picker without using images, external CSS or 1px divs.
3 | * Copyright © 2011 David Durman, All rights reserved.
4 | */
5 | (function(window, document, undefined) {
6 |
7 | var type = (window.SVGAngle || document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1") ? "SVG" : "VML"),
8 | picker, slide, hueOffset = 15, svgNS = 'http://www.w3.org/2000/svg';
9 |
10 | // This HTML snippet is inserted into the innerHTML property of the passed color picker element
11 | // when the no-hassle call to ColorPicker() is used, i.e. ColorPicker(function(hex, hsv, rgb) { ... });
12 |
13 | var colorpickerHTMLSnippet = [
14 |
15 | '',
16 | '
',
17 | '
',
18 | '
',
19 | '',
20 | '
',
21 | '
',
22 | '
'
23 |
24 | ].join('');
25 |
26 | /**
27 | * Return mouse position relative to the element el.
28 | */
29 | function mousePosition(evt) {
30 | // IE:
31 | if (window.event && window.event.contentOverflow !== undefined) {
32 | return { x: window.event.offsetX, y: window.event.offsetY };
33 | }
34 | // Webkit:
35 | if (evt.offsetX !== undefined && evt.offsetY !== undefined) {
36 | return { x: evt.offsetX, y: evt.offsetY };
37 | }
38 | // Firefox:
39 | var wrapper = evt.target.parentNode.parentNode;
40 | return { x: evt.layerX - wrapper.offsetLeft, y: evt.layerY - wrapper.offsetTop };
41 | }
42 |
43 | /**
44 | * Create SVG element.
45 | */
46 | function $(el, attrs, children) {
47 | el = document.createElementNS(svgNS, el);
48 | for (var key in attrs)
49 | el.setAttribute(key, attrs[key]);
50 | if (Object.prototype.toString.call(children) != '[object Array]') children = [children];
51 | var i = 0, len = (children[0] && children.length) || 0;
52 | for (; i < len; i++)
53 | el.appendChild(children[i]);
54 | return el;
55 | }
56 |
57 | /**
58 | * Create slide and picker markup depending on the supported technology.
59 | */
60 | if (type == 'SVG') {
61 |
62 | slide = $('svg', { xmlns: 'http://www.w3.org/2000/svg', version: '1.1', width: '100%', height: '100%' },
63 | [
64 | $('defs', {},
65 | $('linearGradient', { id: 'gradient-hsv', x1: '0%', y1: '100%', x2: '0%', y2: '0%'},
66 | [
67 | $('stop', { offset: '0%', 'stop-color': '#FF0000', 'stop-opacity': '1' }),
68 | $('stop', { offset: '13%', 'stop-color': '#FF00FF', 'stop-opacity': '1' }),
69 | $('stop', { offset: '25%', 'stop-color': '#8000FF', 'stop-opacity': '1' }),
70 | $('stop', { offset: '38%', 'stop-color': '#0040FF', 'stop-opacity': '1' }),
71 | $('stop', { offset: '50%', 'stop-color': '#00FFFF', 'stop-opacity': '1' }),
72 | $('stop', { offset: '63%', 'stop-color': '#00FF40', 'stop-opacity': '1' }),
73 | $('stop', { offset: '75%', 'stop-color': '#0BED00', 'stop-opacity': '1' }),
74 | $('stop', { offset: '88%', 'stop-color': '#FFFF00', 'stop-opacity': '1' }),
75 | $('stop', { offset: '100%', 'stop-color': '#FF0000', 'stop-opacity': '1' })
76 | ]
77 | )
78 | ),
79 | $('rect', { x: '0', y: '0', width: '100%', height: '100%', fill: 'url(#gradient-hsv)'})
80 | ]
81 | );
82 |
83 | picker = $('svg', { xmlns: 'http://www.w3.org/2000/svg', version: '1.1', width: '100%', height: '100%' },
84 | [
85 | $('defs', {},
86 | [
87 | $('linearGradient', { id: 'gradient-black', x1: '0%', y1: '100%', x2: '0%', y2: '0%'},
88 | [
89 | $('stop', { offset: '0%', 'stop-color': '#000000', 'stop-opacity': '1' }),
90 | $('stop', { offset: '100%', 'stop-color': '#CC9A81', 'stop-opacity': '0' })
91 | ]
92 | ),
93 | $('linearGradient', { id: 'gradient-white', x1: '0%', y1: '100%', x2: '100%', y2: '100%'},
94 | [
95 | $('stop', { offset: '0%', 'stop-color': '#FFFFFF', 'stop-opacity': '1' }),
96 | $('stop', { offset: '100%', 'stop-color': '#CC9A81', 'stop-opacity': '0' })
97 | ]
98 | )
99 | ]
100 | ),
101 | $('rect', { x: '0', y: '0', width: '100%', height: '100%', fill: 'url(#gradient-white)'}),
102 | $('rect', { x: '0', y: '0', width: '100%', height: '100%', fill: 'url(#gradient-black)'})
103 | ]
104 | );
105 |
106 | } else if (type == 'VML') {
107 | slide = [
108 | '',
109 | '',
110 | ' ',
111 | ' ',
112 | '
'
113 | ].join('');
114 |
115 | picker = [
116 | '',
117 | '',
118 | ' ',
119 | ' ',
120 | '',
121 | ' ',
122 | ' ',
123 | '
'
124 | ].join('');
125 |
126 | if (!document.namespaces['v'])
127 | document.namespaces.add('v', 'urn:schemas-microsoft-com:vml', '#default#VML');
128 | }
129 |
130 | /**
131 | * Convert HSV representation to RGB HEX string.
132 | * Credits to http://www.raphaeljs.com
133 | */
134 | function hsv2rgb(hsv) {
135 | var R, G, B, X, C;
136 | var h = (hsv.h % 360) / 60;
137 |
138 | C = hsv.v * hsv.s;
139 | X = C * (1 - Math.abs(h % 2 - 1));
140 | R = G = B = hsv.v - C;
141 |
142 | h = ~~h;
143 | R += [C, X, 0, 0, X, C][h];
144 | G += [X, C, C, X, 0, 0][h];
145 | B += [0, 0, X, C, C, X][h];
146 |
147 | var r = Math.floor(R * 255);
148 | var g = Math.floor(G * 255);
149 | var b = Math.floor(B * 255);
150 | return { r: r, g: g, b: b, hex: "#" + (16777216 | b | (g << 8) | (r << 16)).toString(16).slice(1) };
151 | }
152 |
153 | /**
154 | * Convert RGB representation to HSV.
155 | * r, g, b can be either in <0,1> range or <0,255> range.
156 | * Credits to http://www.raphaeljs.com
157 | */
158 | function rgb2hsv(rgb) {
159 |
160 | var r = rgb.r;
161 | var g = rgb.g;
162 | var b = rgb.b;
163 |
164 | if (rgb.r > 1 || rgb.g > 1 || rgb.b > 1) {
165 | r /= 255;
166 | g /= 255;
167 | b /= 255;
168 | }
169 |
170 | var H, S, V, C;
171 | V = Math.max(r, g, b);
172 | C = V - Math.min(r, g, b);
173 | H = (C == 0 ? null :
174 | V == r ? (g - b) / C + (g < b ? 6 : 0) :
175 | V == g ? (b - r) / C + 2 :
176 | (r - g) / C + 4);
177 | H = (H % 6) * 60;
178 | S = C == 0 ? 0 : C / V;
179 | return { h: H, s: S, v: V };
180 | }
181 |
182 | /**
183 | * Return click event handler for the slider.
184 | * Sets picker background color and calls ctx.callback if provided.
185 | */
186 | function slideListener(ctx, slideElement, pickerElement) {
187 | return function(evt) {
188 | evt = evt || window.event;
189 | var mouse = mousePosition(evt);
190 | ctx.h = mouse.y / slideElement.offsetHeight * 360 + hueOffset;
191 | var pickerColor = hsv2rgb({ h: ctx.h, s: 1, v: 1 });
192 | var c = hsv2rgb({ h: ctx.h, s: ctx.s, v: ctx.v });
193 | pickerElement.style.backgroundColor = pickerColor.hex;
194 | ctx.callback && ctx.callback(c.hex, { h: ctx.h - hueOffset, s: ctx.s, v: ctx.v }, { r: c.r, g: c.g, b: c.b }, undefined, mouse);
195 | }
196 | };
197 |
198 | /**
199 | * Return click event handler for the picker.
200 | * Calls ctx.callback if provided.
201 | */
202 | function pickerListener(ctx, pickerElement) {
203 | return function(evt) {
204 | evt = evt || window.event;
205 | var mouse = mousePosition(evt),
206 | width = pickerElement.offsetWidth,
207 | height = pickerElement.offsetHeight;
208 |
209 | ctx.s = mouse.x / width;
210 | ctx.v = (height - mouse.y) / height;
211 | var c = hsv2rgb(ctx);
212 | ctx.callback && ctx.callback(c.hex, { h: ctx.h - hueOffset, s: ctx.s, v: ctx.v }, { r: c.r, g: c.g, b: c.b }, mouse);
213 | }
214 | };
215 |
216 | var uniqID = 0;
217 |
218 | /**
219 | * ColorPicker.
220 | * @param {DOMElement} slideElement HSV slide element.
221 | * @param {DOMElement} pickerElement HSV picker element.
222 | * @param {Function} callback Called whenever the color is changed provided chosen color in RGB HEX format as the only argument.
223 | */
224 | function ColorPicker(slideElement, pickerElement, callback) {
225 |
226 | if (!(this instanceof ColorPicker)) return new ColorPicker(slideElement, pickerElement, callback);
227 |
228 | this.h = 0;
229 | this.s = 1;
230 | this.v = 1;
231 |
232 | if (!callback) {
233 | // call of the form ColorPicker(element, funtion(hex, hsv, rgb) { ... }), i.e. the no-hassle call.
234 |
235 | var element = slideElement;
236 | element.innerHTML = colorpickerHTMLSnippet;
237 |
238 | this.slideElement = element.getElementsByClassName('slide')[0];
239 | this.pickerElement = element.getElementsByClassName('picker')[0];
240 | var slideIndicator = element.getElementsByClassName('slide-indicator')[0];
241 | var pickerIndicator = element.getElementsByClassName('picker-indicator')[0];
242 |
243 | ColorPicker.fixIndicators(slideIndicator, pickerIndicator);
244 |
245 | this.callback = function(hex, hsv, rgb, pickerCoordinate, slideCoordinate) {
246 |
247 | ColorPicker.positionIndicators(slideIndicator, pickerIndicator, slideCoordinate, pickerCoordinate);
248 |
249 | pickerElement(hex, hsv, rgb);
250 | };
251 |
252 | } else {
253 |
254 | this.callback = callback;
255 | this.pickerElement = pickerElement;
256 | this.slideElement = slideElement;
257 | }
258 |
259 | if (type == 'SVG') {
260 |
261 | // Generate uniq IDs for linearGradients so that we don't have the same IDs within one document.
262 | // Then reference those gradients in the associated rectangles.
263 |
264 | var slideClone = slide.cloneNode(true);
265 | var pickerClone = picker.cloneNode(true);
266 |
267 | var hsvGradient = slideClone.getElementsByTagName('linearGradient')[0];
268 |
269 | var hsvRect = slideClone.getElementsByTagName('rect')[0];
270 |
271 | hsvGradient.id = 'gradient-hsv-' + uniqID;
272 | hsvRect.setAttribute('fill', 'url(#' + hsvGradient.id + ')');
273 |
274 | var blackAndWhiteGradients = [pickerClone.getElementsByTagName('linearGradient')[0], pickerClone.getElementsByTagName('linearGradient')[1]];
275 | var whiteAndBlackRects = pickerClone.getElementsByTagName('rect');
276 |
277 | blackAndWhiteGradients[0].id = 'gradient-black-' + uniqID;
278 | blackAndWhiteGradients[1].id = 'gradient-white-' + uniqID;
279 |
280 | whiteAndBlackRects[0].setAttribute('fill', 'url(#' + blackAndWhiteGradients[1].id + ')');
281 | whiteAndBlackRects[1].setAttribute('fill', 'url(#' + blackAndWhiteGradients[0].id + ')');
282 |
283 | this.slideElement.appendChild(slideClone);
284 | this.pickerElement.appendChild(pickerClone);
285 |
286 | uniqID++;
287 |
288 | } else {
289 |
290 | this.slideElement.innerHTML = slide;
291 | this.pickerElement.innerHTML = picker;
292 | }
293 |
294 | addEventListener(this.slideElement, 'click', slideListener(this, this.slideElement, this.pickerElement));
295 | addEventListener(this.pickerElement, 'click', pickerListener(this, this.pickerElement));
296 |
297 | enableDragging(this, this.slideElement, slideListener(this, this.slideElement, this.pickerElement));
298 | enableDragging(this, this.pickerElement, pickerListener(this, this.pickerElement));
299 | };
300 |
301 | function addEventListener(element, event, listener) {
302 |
303 | if (element.attachEvent) {
304 |
305 | element.attachEvent('on' + event, listener);
306 |
307 | } else if (element.addEventListener) {
308 |
309 | element.addEventListener(event, listener, false);
310 | }
311 | }
312 |
313 | /**
314 | * Enable drag&drop color selection.
315 | * @param {object} ctx ColorPicker instance.
316 | * @param {DOMElement} element HSV slide element or HSV picker element.
317 | * @param {Function} listener Function that will be called whenever mouse is dragged over the element with event object as argument.
318 | */
319 | function enableDragging(ctx, element, listener) {
320 |
321 | var mousedown = false;
322 |
323 | addEventListener(element, 'mousedown', function(evt) { mousedown = true; });
324 | addEventListener(element, 'mouseup', function(evt) { mousedown = false; });
325 | addEventListener(element, 'mouseout', function(evt) { mousedown = false; });
326 | addEventListener(element, 'mousemove', function(evt) {
327 |
328 | if (mousedown) {
329 |
330 | listener(evt);
331 | }
332 | });
333 | }
334 |
335 |
336 | ColorPicker.hsv2rgb = function(hsv) {
337 | var rgbHex = hsv2rgb(hsv);
338 | delete rgbHex.hex;
339 | return rgbHex;
340 | };
341 |
342 | ColorPicker.hsv2hex = function(hsv) {
343 | return hsv2rgb(hsv).hex;
344 | };
345 |
346 | ColorPicker.rgb2hsv = rgb2hsv;
347 |
348 | ColorPicker.rgb2hex = function(rgb) {
349 | return hsv2rgb(rgb2hsv(rgb)).hex;
350 | };
351 |
352 | ColorPicker.hex2hsv = function(hex) {
353 | return rgb2hsv(ColorPicker.hex2rgb(hex));
354 | };
355 |
356 | ColorPicker.hex2rgb = function(hex) {
357 | return { r: parseInt(hex.substr(1, 2), 16), g: parseInt(hex.substr(3, 2), 16), b: parseInt(hex.substr(5, 2), 16) };
358 | };
359 |
360 | /**
361 | * Sets color of the picker in hsv/rgb/hex format.
362 | * @param {object} ctx ColorPicker instance.
363 | * @param {object} hsv Object of the form: { h: , s: , v: }.
364 | * @param {object} rgb Object of the form: { r: , g: , b: }.
365 | * @param {string} hex String of the form: #RRGGBB.
366 | */
367 | function setColor(ctx, hsv, rgb, hex) {
368 | ctx.h = hsv.h % 360;
369 | ctx.s = hsv.s;
370 | ctx.v = hsv.v;
371 |
372 | var c = hsv2rgb(ctx);
373 |
374 | var mouseSlide = {
375 | y: (ctx.h * ctx.slideElement.offsetHeight) / 360,
376 | x: 0 // not important
377 | };
378 |
379 | var pickerHeight = ctx.pickerElement.offsetHeight;
380 |
381 | var mousePicker = {
382 | x: ctx.s * ctx.pickerElement.offsetWidth,
383 | y: pickerHeight - ctx.v * pickerHeight
384 | };
385 |
386 | ctx.pickerElement.style.backgroundColor = hsv2rgb({ h: ctx.h, s: 1, v: 1 }).hex;
387 | ctx.callback && ctx.callback(hex || c.hex, { h: ctx.h, s: ctx.s, v: ctx.v }, rgb || { r: c.r, g: c.g, b: c.b }, mousePicker, mouseSlide);
388 |
389 | return ctx;
390 | };
391 |
392 | /**
393 | * Sets color of the picker in hsv format.
394 | * @param {object} hsv Object of the form: { h: , s: , v: }.
395 | */
396 | ColorPicker.prototype.setHsv = function(hsv) {
397 | return setColor(this, hsv);
398 | };
399 |
400 | /**
401 | * Sets color of the picker in rgb format.
402 | * @param {object} rgb Object of the form: { r: , g: , b: }.
403 | */
404 | ColorPicker.prototype.setRgb = function(rgb) {
405 | return setColor(this, rgb2hsv(rgb), rgb);
406 | };
407 |
408 | /**
409 | * Sets color of the picker in hex format.
410 | * @param {string} hex Hex color format #RRGGBB.
411 | */
412 | ColorPicker.prototype.setHex = function(hex) {
413 | return setColor(this, ColorPicker.hex2hsv(hex), undefined, hex);
414 | };
415 |
416 | /**
417 | * Helper to position indicators.
418 | * @param {HTMLElement} slideIndicator DOM element representing the indicator of the slide area.
419 | * @param {HTMLElement} pickerIndicator DOM element representing the indicator of the picker area.
420 | * @param {object} mouseSlide Coordinates of the mouse cursor in the slide area.
421 | * @param {object} mousePicker Coordinates of the mouse cursor in the picker area.
422 | */
423 | ColorPicker.positionIndicators = function(slideIndicator, pickerIndicator, mouseSlide, mousePicker) {
424 |
425 | if (mouseSlide) {
426 | slideIndicator.style.top = (mouseSlide.y - slideIndicator.offsetHeight/2) + 'px';
427 | }
428 | if (mousePicker) {
429 | pickerIndicator.style.top = (mousePicker.y - pickerIndicator.offsetHeight/2) + 'px';
430 | pickerIndicator.style.left = (mousePicker.x - pickerIndicator.offsetWidth/2) + 'px';
431 | }
432 | };
433 |
434 | /**
435 | * Helper to fix indicators - this is recommended (and needed) for dragable color selection (see enabledDragging()).
436 | */
437 | ColorPicker.fixIndicators = function(slideIndicator, pickerIndicator) {
438 |
439 | pickerIndicator.style.pointerEvents = 'none';
440 | slideIndicator.style.pointerEvents = 'none';
441 | };
442 |
443 | window.ColorPicker = ColorPicker;
444 |
445 | })(window, window.document);
446 |
--------------------------------------------------------------------------------
/src/controls.js:
--------------------------------------------------------------------------------
1 | // Get the value of an html control, and return it as an OSC argument.
2 | function getControlArg(controlElem) {
3 | let inputElem = controlElem.querySelector('input');
4 | if (!inputElem) {
5 | inputElem = controlElem.querySelector('select');
6 | }
7 | let detailsElem = controlElem.querySelector('.details');
8 | let fullPath = detailsElem.attributes['data-full-path'].value;
9 | let dataType = detailsElem.attributes['data-type'].value;
10 | let getter = detailsElem.attributes['data-getter'];
11 | let arg = null;
12 | if (!getter) {
13 | return {type: dataType};
14 | } else if (getter.value == 'value') {
15 | return {type: dataType, value: inputElem.value };
16 | } else if (getter.value == 'parseInt') {
17 | return {type: dataType, value: parseInt(inputElem.value, 10) };
18 | } else if (getter.value == 'parseInt64') {
19 | let num = parseInt(inputElem.value);
20 | let radix = 0x100000000;
21 | let high = Math.floor(num / radix);
22 | let low = num % radix;
23 | return {type: dataType, value: {high: high, low: low}};
24 | } else if (getter.value == 'parseFloat') {
25 | return {type: dataType, value: parseFloat(inputElem.value) };
26 | } else if (getter.value == 'parseSingle') {
27 | let first = inputElem.attributes['data-first'].value;
28 | return {type: dataType, value: parseInt(first, 10) };
29 | } else if (getter.value == 'boolToggle') {
30 | return {type: inputElem.value == 'true' ? 'T' : 'F'};
31 | } else if (getter.value == 'parseIntToggle') {
32 | let value = null;
33 | let dataFirst = inputElem.attributes['data-first']
34 | let dataSecond = inputElem.attributes['data-second']
35 | if (dataFirst.value == inputElem.value) {
36 | value = dataFirst.value;
37 | } else {
38 | value = dataSecond.value;
39 | }
40 | return {type: dataType, value: parseInt(value, 10) };
41 | } else if (getter.value == 'sendCheckbox') {
42 | let value;
43 | if (inputElem.checked) {
44 | value = parseInt(inputElem.attributes['data-second'].value, 10);
45 | } else {
46 | value = parseInt(inputElem.attributes['data-first'].value, 10);
47 | }
48 | return {type: dataType, value: value};
49 | } else if (getter.value == 'color') {
50 | if (!inputElem) {
51 | // Only for color elements in browsers that don't support the
52 | // html5 color input.
53 | inputElem = controlElem.querySelector('.color-control');
54 | }
55 | var color = inputElem.value;
56 | var r = parseInt(color.substr(1, 2), 16);
57 | var g = parseInt(color.substr(3, 2), 16);
58 | var b = parseInt(color.substr(5, 2), 16);
59 | return {type: dataType, value: {r:r, g:g, b:b, a:1} };
60 | }
61 | }
62 |
63 | // Set the value of an html control, based upon the type it represents.
64 | function runSetter(controlElem, type, value) {
65 | if (type == 'int') {
66 | let currValElem = controlElem.querySelector('.curr-val');
67 | currValElem.textContent = value;
68 | } else if (type == 'int64') {
69 | let currValElem = controlElem.querySelector('.curr-val');
70 | if (value.hasOwnProperty('high') && value.hasOwnProperty('low')) {
71 | let radix = 0x100000000;
72 | value = value.high * radix + value.low;
73 | }
74 | currValElem.textContent = value;
75 | } else if (type == 'float') {
76 | let currValElem = controlElem.querySelector('.curr-val');
77 | currValElem.textContent = Math.round(value * 1000) / 1000;
78 | } else if (type == 'setToggleBeforeGetControlArg') {
79 | let buttonElem = controlElem.querySelector('input');
80 | let dataFirst = buttonElem.attributes['data-first']
81 | let dataSecond = buttonElem.attributes['data-second']
82 | let isEnabled;
83 | if (dataFirst && dataSecond) {
84 | if (dataFirst.value == value) {
85 | value = dataSecond.value;
86 | isEnabled = false;
87 | } else {
88 | value = dataFirst.value;
89 | isEnabled = true;
90 | }
91 | } else {
92 | if (value === false || value == 'false') {
93 | value = 'true';
94 | isEnabled = true;
95 | } else {
96 | value = 'false';
97 | isEnabled = false;
98 | }
99 | }
100 | buttonElem.value = value;
101 | if (isEnabled) {
102 | buttonElem.classList.add('enabled');
103 | } else {
104 | buttonElem.classList.remove('enabled');
105 | }
106 | } else if (type == 'setToggle') {
107 | // do nothing
108 | } else if (type == 'button') {
109 | // do nothing
110 | }
111 | }
112 |
113 | module.exports = {
114 | getControlArg: getControlArg,
115 | runSetter: runSetter,
116 | }
117 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const osc = require('osc');
2 | const oscTransports = require('osc-transports');
3 | const oscWebsocketClient = require('osc-websocket-client');
4 |
5 | const builder = require('./builder.js');
6 | const controls = require('./controls.js');
7 | const settings = require('./settings.js');
8 | const userinput = require('./userinput.js');
9 | const types = require('./types.js');
10 | const retrieve = require('./retrieve.js');
11 |
12 | // Polyfilled controls.
13 | const vanillaColorPicker = require('vanilla-picker');
14 | const rangeSlider = require('rangeslider-pure');
15 |
16 | // Image assets.
17 | const logoBase64 = require("base64-image-loader!../assets/img/icon.png");
18 | const listenButtonSvg = require("svg-inline-loader?classPrefix=_listen!../assets/img/listen.svg");
19 | const ignoreButtonSvg = require("svg-inline-loader?classPrefix=_ignore!../assets/img/pressed.svg");
20 |
21 | global.g_allControlStruct = null;
22 | global.g_hostInfo = {};
23 | global.g_extensions = null;
24 | global.g_isListenEnabled = false;
25 | global.g_serverUrl = null;
26 |
27 | function $(selector) {
28 | return document.querySelector(selector);
29 | }
30 |
31 | function objectGetValue(obj, i) {
32 | return obj[Object.keys(obj)[i]];
33 | }
34 |
35 | function storeHostInfo(hostInfo) {
36 | global.g_hostInfo = hostInfo;
37 | global.g_extensions = hostInfo.EXTENSIONS;
38 | }
39 |
40 | var g_supportHtml5Color = false;
41 |
42 | function detectColorPicker() {
43 | let input = document.createElement('input');
44 | input.setAttribute('type', 'color');
45 | input.setAttribute('value', '$');
46 | // Currently, always use third-party control.
47 | g_supportHtml5Color = false;
48 | }
49 |
50 | // Build all controls from json object, from the top-level.
51 | function buildFromQueryResult(result) {
52 | let mainContentsElem = $('#mainContents');
53 | {
54 | let logoHolderElem = document.createElement('div');
55 | let logoElem = document.createElement('img');
56 | logoElem.className = 'logo';
57 | logoElem.src = logoBase64;
58 | logoHolderElem.appendChild(logoElem);
59 | mainContents.appendChild(logoHolderElem);
60 | }
61 | {
62 | let refreshMessageElem = document.createElement('div');
63 | refreshMessageElem.id = 'refresh-butter';
64 | refreshMessageElem.style.display = 'none';
65 | refreshMessageElem.style.backgroundColor = '#ffff88';
66 | refreshMessageElem.style.position = 'absolute';
67 | refreshMessageElem.style.top = '2px';
68 | refreshMessageElem.style.left = '2px';
69 | refreshMessageElem.style.textAlign = 'center';
70 | refreshMessageElem.style.width = '40%';
71 | refreshMessageElem.textContent = 'Changes found, refreshing...';
72 | document.body.appendChild(refreshMessageElem);
73 | }
74 | let contents = result.CONTENTS;
75 | if (!contents) {
76 | let noControlsElem = document.createElement('div');
77 | noControlsElem.textContent = 'No controls detected';
78 | mainContentsElem.appendChild(noControlsElem);
79 | return;
80 | }
81 | if (global.g_extensions.LISTEN) {
82 | // Label for listen button.
83 | let labelDivElem = document.createElement('div');
84 | labelDivElem.className = 'listen-label';
85 | labelDivElem.textContent = 'Listen for OSC: ';
86 | mainContentsElem.appendChild(labelDivElem);
87 | // Listen and ignore buttons.
88 | let listenSpanElem = document.createElement('span');
89 | listenSpanElem.className = 'svg-listen';
90 | listenSpanElem.style.display = 'none';
91 | listenSpanElem.innerHTML = listenButtonSvg;
92 | let ignoreSpanElem = document.createElement('span');
93 | ignoreSpanElem.className = 'svg-ignore';
94 | ignoreSpanElem.style.display = 'inline-block';
95 | ignoreSpanElem.innerHTML = ignoreButtonSvg;
96 | mainContentsElem.appendChild(listenSpanElem);
97 | mainContentsElem.appendChild(ignoreSpanElem);
98 | // Set listening state.
99 | setTimeout(settings.enableInitialListenState, 0);
100 | }
101 | {
102 | // Create style links, dark and light.
103 | let styleDarkElem = document.createElement('div');
104 | styleDarkElem.id = 'choice-dark-mode';
105 | styleDarkElem.innerHTML = 'dark light ';
106 | let styleLightElem = document.createElement('div');
107 | styleLightElem.id = 'choice-light-mode';
108 | styleLightElem.innerHTML = 'dark light ';
109 | styleLightElem.style.display = 'none';
110 | mainContentsElem.appendChild(styleDarkElem);
111 | mainContentsElem.appendChild(styleLightElem);
112 | // Create css to bold the in-use style, underline the unused style.
113 | let styleElem = document.createElement('style');
114 | styleElem.textContent = '.curr_mode { font-weight: bold; } #set_light {text-decoration: underline; cursor: pointer;} #set_dark {text-decoration: underline; cursor: pointer;}'
115 | mainContentsElem.appendChild(styleElem);
116 | let setLightLink = $('#set_light');
117 | setLightLink.addEventListener('click', function() {
118 | settings.setStyleMode('light');
119 | }, false);
120 | let setDarkLink = $('#set_dark');
121 | setDarkLink.addEventListener('click', function() {
122 | settings.setStyleMode('dark');
123 | }, false);
124 | // Set beginning style based upon the user's cookie.
125 | if (document.cookie.includes('style=light')) {
126 | settings.setStyleMode('light');
127 | }
128 | }
129 | // Configuration for building.
130 | let cfg = {supportHtml5Color: g_supportHtml5Color};
131 | // Root node.
132 | let rootElem = document.createElement('div');
133 | rootElem.id = 'control_body_' + builder.generateId();
134 | rootElem.setAttribute('data-dir-path', '/');
135 | mainContentsElem.appendChild(rootElem);
136 | // Build contents for the main container.
137 | builder.buildContentsAddToContainer(contents, rootElem, cfg)
138 | }
139 |
140 | function extractControlPaths(obj) {
141 | var paths = [];
142 | if (obj.CONTENTS) {
143 | let dirNames = Object.keys(obj.CONTENTS);
144 | dirNames.sort();
145 | for (let j = 0; j < dirNames.length; j++) {
146 | let name = dirNames[j];
147 | let dirObj = obj.CONTENTS[dirNames[j]];
148 | paths = paths.concat(extractControlPaths(dirObj));
149 | }
150 | }
151 | if (obj.TYPE && obj.FULL_PATH) {
152 | paths.push(obj.FULL_PATH);
153 | }
154 | return paths;
155 | }
156 |
157 | function storeControlStructure(data) {
158 | g_allControlStruct = extractControlPaths(data);
159 | }
160 |
161 | function nullFunction() {}
162 |
163 | global.isOscReady = false;
164 | global.oscPort = null;
165 |
166 | function initWebSocket(url) {
167 | global.oscPort = new osc.WebSocketPort({
168 | url: url,
169 | metadata: true
170 | });
171 | global.oscPort.open();
172 | oscPort.on('ready', function() {
173 | global.isOscReady = true;
174 | global.oscPort.socket.onmessage = function(e) {
175 | // Check if message was a JSON command.
176 | let msg = null;
177 | try {
178 | msg = JSON.parse(e.data);
179 | } catch (e) {
180 | // pass
181 | }
182 | if (msg) {
183 | processCommandMessage(msg);
184 | return;
185 | }
186 | // Non-JSON data, assume it's a binary OSC packet.
187 | let packet = osc.readPacket(new Uint8Array(e.data), {});
188 | console.log('***** Got packet <' + JSON.stringify(packet) + '>');
189 | let address = packet.address;
190 | // TODO: Validate address contains allowed characters.
191 | let query = '[data-full-path="' + address + '"]';
192 | let detailsElem = document.querySelector(query);
193 | let groupElem = detailsElem.parentNode.parentNode;
194 | for (let i = 0; i < packet.args.length; i++) {
195 | let value = packet.args[i];
196 | let controlElem = groupElem.children[i];
197 | applyOSCMessageValue(controlElem, value);
198 | }
199 | }
200 | });
201 | }
202 |
203 | // Apply OSC packet's single value by setting control state, update UI.
204 | function applyOSCMessageValue(controlElem, value) {
205 | // Get input or select tag, which needs to have value changed.
206 | let targetElem = controlElem.querySelector('input');
207 | if (!targetElem) {
208 | targetElem = controlElem.querySelector('select');
209 | }
210 | if (!targetElem) {
211 | return;
212 | }
213 | // Update position of slider polyfill. NOTE: Kind of a hack to
214 | // put this code here, it's a one-off.
215 | if (targetElem.attributes.type &&
216 | targetElem.attributes.type.value == 'range') {
217 | if (global.g_numRangeMessagePending > 0) {
218 | global.g_numRangeMessagePending--;
219 | return;
220 | }
221 | targetElem.rangeSlider.update({value: value}, false);
222 | return;
223 | }
224 | let detailsElem = controlElem.querySelector('[class="details"]');
225 | let setter = detailsElem.attributes['data-setter'];
226 | if (setter) {
227 | if (setter.value == 'color') {
228 | // If the html5 color control is being dragged around,
229 | // and LISTEN is enabled, the messages sent from this
230 | // control will be routed back to it, and subtly decrease
231 | // the lightness due to rounding errors. So, while the
232 | // control is being changed, wait a short amount of time
233 | // before accepting new updates.
234 | if (global.g_numColorMessagePending > 0) {
235 | global.g_numColorMessagePending--;
236 | return;
237 | }
238 | if (!global.g_supportHtml5Color) {
239 | // Polyfill control, update the color.
240 | value = builder.textToHexColor(value);
241 | let colorClass = '.color-control';
242 | targetElem = controlElem.querySelector(colorClass);
243 | // Change the picker's color, but don't send events.
244 | let picker = targetElem.picker;
245 | let preserveHandler = picker.onChange;
246 | picker.onChange = nullFunction;
247 | picker.setColor(value);
248 | picker.onChange = preserveHandler;
249 | }
250 | } else if (setter.value == 'setCheckbox') {
251 | // If the control is a checkbox, there should only be
252 | // two possible values. Either check or uncheck the box,
253 | // but only if it matches one of the two known values.
254 | let first = targetElem.attributes['data-first'].value;
255 | let second = targetElem.attributes['data-second'].value;
256 | if (value == first) {
257 | targetElem.checked = false;
258 | } else if (value == second) {
259 | targetElem.checked = true;
260 | }
261 | return;
262 | } else if (setter.value == 'setToggle') {
263 | controls.runSetter(controlElem, setter.value, value);
264 | return;
265 | } else if (setter.value == 'button') {
266 | // do nothing
267 | return;
268 | } else {
269 | controls.runSetter(controlElem, setter.value, value);
270 | }
271 | }
272 | targetElem.value = value;
273 | }
274 |
275 | function processCommandMessage(msg) {
276 | if (msg.COMMAND == 'PATH_CHANGED') {
277 | if (global.g_extensions.PATH_CHANGED) {
278 | let refreshElem = document.getElementById('refresh-butter');
279 | refreshElem.style.display = 'inline';
280 | global.location.reload(true);
281 | }
282 | } else if (msg.COMMAND == 'PATH_ADDED') {
283 | if (global.g_extensions.PATH_ADDED) {
284 | let nodePath = msg.DATA;
285 | let pathParts = nodePath.split('/');
286 | let numParts = pathParts.length - 1;
287 | let nodeName = pathParts[numParts];
288 | let nodeUrl = global.g_serverUrl + nodePath;
289 | retrieve.retrieveJson(nodeUrl, (err, contents) => {
290 | // Get the directory container for where the newly created
291 | // node should go, creating new elements as needed.
292 | let targetElem = getOrMakeDirNode(pathParts.slice(0, numParts));
293 | // Node container for the new element.
294 | let containerElem = document.createElement('div')
295 | containerElem.className = 'node';
296 | let headerElem = document.createElement('header');
297 | containerElem.appendChild(headerElem);
298 | targetElem.appendChild(containerElem);
299 | // Build the new node control, insert it into the container.
300 | let newElem = document.createElement('div');
301 | newElem.id = 'control_body_' + builder.generateId();
302 | newElem.setAttribute('data-dir-path', nodePath);
303 | containerElem.appendChild(newElem);
304 | builder.buildControlElements(newElem, nodeName, contents);
305 | // Add event listeners to new elements.
306 | addToggleEventHandlers();
307 | maybeAddPolyfill(newElem);
308 | });
309 | }
310 | } else if (msg.COMMAND == 'PATH_RENAMED') {
311 | if (global.g_extensions.PATH_RENAMED) {
312 | let oldPath = msg.DATA.OLD;
313 | let newPath = msg.DATA.NEW;
314 | let targetElem = document.querySelector(
315 | '[data-dir-path="' + oldPath + '"]');
316 | if (!targetElem) {
317 | return;
318 | }
319 | targetElem.setAttribute('data-dir-path', newPath);
320 | let controlDetail = targetElem.querySelector('.control-name');
321 | if (controlDetail) {
322 | let newParts = newPath.split('/');
323 | let newName = newParts[newParts.length - 1];
324 | controlDetail.textContent = builder.shortDisplay(newName);
325 | }
326 | let fullPathDetail = targetElem.querySelector('.full-path');
327 | if (fullPathDetail) {
328 | fullPathDetail.textContent = builder.shortDisplay(newPath);
329 | }
330 | targetElem = document.querySelector(
331 | '[data-full-path="' + oldPath + '"]');
332 | if (!targetElem) {
333 | return;
334 | }
335 | targetElem.setAttribute('data-full-path', newPath);
336 | }
337 | } else if (msg.COMMAND == 'PATH_REMOVED') {
338 | if (global.g_extensions.PATH_REMOVED) {
339 | let nodePath = msg.DATA;
340 | let targetElem = document.querySelector(
341 | '[data-dir-path="' + nodePath + '"]');
342 | // Remove the parent, with either class "dir-container" or "node".
343 | let removeElem = targetElem.parentNode;
344 | if (removeElem) {
345 | removeElem.parentNode.removeChild(removeElem);
346 | }
347 | }
348 | } else {
349 | console.log('??????????');
350 | console.log('Unknown message: ' + e.data);
351 | }
352 | }
353 |
354 | function getOrMakeDirNode(pathParts) {
355 | let result = document.querySelector('[data-dir-path="/"]');
356 | for (let i = 1; i < pathParts.length; i++) {
357 | let path = pathParts.slice(0, i + 1).join('/');
358 | let elem = result.querySelector(
359 | '[data-dir-path="' + path + '"]');
360 | if (!elem) {
361 | let id = builder.generateId();
362 | elem = document.createElement('div');
363 | elem.id = 'control_body_' + id;
364 | elem.setAttribute('data-dir-path', path);
365 | // Directory container for the new element.
366 | let containerElem = document.createElement('div')
367 | containerElem.className = 'dir-container';
368 | let headerElem = document.createElement('header');
369 | headerElem.innerHTML = builder.createTogglerHtml(id, pathParts[i]);
370 | containerElem.appendChild(headerElem);
371 | // Add the container to the parent.
372 | containerElem.appendChild(elem);
373 | result.appendChild(containerElem);
374 | }
375 | result = elem;
376 | }
377 | return result;
378 | }
379 |
380 | function getDataEvent(element) {
381 | if (element.attributes['data-event']) {
382 | return element.attributes['data-event'].value;
383 | }
384 | return null;
385 | }
386 |
387 | function addInputEventHandlers() {
388 | let inputs = document.getElementsByTagName("input");
389 | for (let i = 0; i < inputs.length; i++) {
390 | let input = inputs[i];
391 | if (getDataEvent(input) == 'keypress') {
392 | input.addEventListener('keypress',
393 | userinput.charKeyPressEvent, false);
394 | } else if (input.type == "button" && input.attributes['data-toggle']) {
395 | input.addEventListener('click', userinput.toggleEvent, false);
396 | } else if (input.type == "button") {
397 | input.addEventListener('click', userinput.controlEvent, false);
398 | } else if (input.type == "range") {
399 | input.addEventListener('input', userinput.rangeModifyEvent, false);
400 | input.addEventListener('change', userinput.rangeModifyEvent, false);
401 | } else if (input.type == "color") {
402 | input.addEventListener('change', userinput.colorModifyEvent, false);
403 | } else {
404 | input.addEventListener('change', userinput.controlEvent, false);
405 | }
406 | }
407 | let selects = document.getElementsByTagName("select");
408 | for (let i = 0; i < selects.length; i++) {
409 | let select = selects[i];
410 | select.addEventListener('change', userinput.controlEvent, false);
411 | }
412 | let listenButtons = document.getElementsByClassName('svg-listen');
413 | for (let i = 0; i < listenButtons.length; i++) {
414 | let listenBtn = listenButtons[i];
415 | listenBtn.addEventListener('click', settings.listenClick, false);
416 | }
417 | let ignoreButtons = document.getElementsByClassName('svg-ignore');
418 | for (let i = 0; i < ignoreButtons.length; i++) {
419 | let ignoreBtn = ignoreButtons[i];
420 | ignoreBtn.addEventListener('click', settings.ignoreClick, false);
421 | }
422 | addToggleEventHandlers();
423 | }
424 |
425 | function addToggleEventHandlers() {
426 | let toggleHideElems = document.getElementsByClassName('toggle-hide');
427 | for (let i = 0; i < toggleHideElems.length; i++) {
428 | let elem = toggleHideElems[i];
429 | if (!elem.hasListener) {
430 | elem.addEventListener('click', settings.toggleHide, false);
431 | elem.hasListener = true;
432 | }
433 | }
434 | let toggleShowElems = document.getElementsByClassName('toggle-show');
435 | for (let i = 0; i < toggleShowElems.length; i++) {
436 | let elem = toggleShowElems[i];
437 | if (!elem.hasListener) {
438 | elem.addEventListener('click', settings.toggleShow, false);
439 | elem.hasListener = true;
440 | }
441 | }
442 | }
443 |
444 | function maybeAddPolyfill(elem) {
445 | // Check if the added node needs a color polyfill.
446 | let colorElem = elem.querySelector('[class="color-control"]');
447 | if (colorElem) {
448 | createRangeSliderPolyfill(colorElem);
449 | colorElem.addEventListener('change', userinput.colorModifyEvent, false);
450 | return;
451 | }
452 | // Check if the added node needs a range-slider polyfill.
453 | let rangeElem = elem.querySelector('input[type="range"]');
454 | if (rangeElem) {
455 | createRangeSliderPolyfill(rangeElem);
456 | rangeElem.addEventListener('input', userinput.rangeModifyEvent, false);
457 | rangeElem.addEventListener('change', userinput.rangeModifyEvent, false);
458 | return;
459 | }
460 | }
461 |
462 | function addColorPickerPolyfills() {
463 | // If this browser does not support the built-in html5 color picker
464 | // element, create polyfill controls for each element.
465 | if (global.g_supportHtml5Color) {
466 | return;
467 | }
468 | let colorElems = document.getElementsByClassName('color-control');
469 | for (let i = 0; i < colorElems.length; i++) {
470 | createColorPickerPolyfill(colorElems[i]);
471 | }
472 | }
473 |
474 | function createColorPickerPolyfill(colorElem) {
475 | let initValue = colorElem.attributes['data-value'].value;
476 | colorElem.picker = new vanillaColorPicker({
477 | parent: colorElem,
478 | popup: false,
479 | alpha: false,
480 | onChange: function(color) {
481 | colorElem.value = color;
482 | userinput.controlEvent({target: colorElem});
483 | },
484 | });
485 | colorElem.picker.setColor(initValue);
486 | }
487 |
488 | function addRangeSliderPolyfills() {
489 | let sliderList = document.querySelectorAll('input[type="range"]');
490 | for (let i = 0; i < sliderList.length; i++) {
491 | createRangeSliderPolyfill(sliderList[i]);
492 | }
493 | }
494 |
495 | function createRangeSliderPolyfill(rangeElem) {
496 | let options = {polyfill: true};
497 | if (rangeElem.attributes.min) {
498 | options.min = rangeElem.attributes.min;
499 | }
500 | if (rangeElem.attributes.max) {
501 | options.max = rangeElem.attributes.max;
502 | }
503 | if (rangeElem.attributes.step && rangeElem.attributes.step.value == 'any') {
504 | options.step = 0.001;
505 | } else if (rangeElem.attributes.step) {
506 | options.step = rangeElem.attributes.step;
507 | }
508 | rangeSlider.create(rangeElem, options);
509 | }
510 |
511 | function createApp(serverUrl) {
512 | global.g_serverUrl = serverUrl;
513 | initWebSocket(serverUrl.replace("http", "ws"));
514 | retrieve.retrieveHostInfo(serverUrl, (err, hostInfo) => {
515 | if (hostInfo) {
516 | storeHostInfo(hostInfo);
517 | }
518 | retrieve.retrieveJson(serverUrl, (err, result) => {
519 | if (err) {
520 | let mainContentsElem = $('#mainContents');
521 | let errorElem = document.createElement('div');
522 | errorElem.innerHTML = '' + err + ' ';
524 | let firstElem = mainContentsElem.children[0];
525 | mainContentsElem.insertBefore(errorElem, firstElem);
526 | return;
527 | }
528 | detectColorPicker();
529 | buildFromQueryResult(result);
530 | storeControlStructure(result);
531 | addInputEventHandlers();
532 | addColorPickerPolyfills();
533 | addRangeSliderPolyfills();
534 | });
535 | });
536 | }
537 |
538 | module.exports = {
539 | createApp: createApp,
540 | getDataEvent: getDataEvent,
541 | extractControlPaths: extractControlPaths,
542 | };
543 |
--------------------------------------------------------------------------------
/src/retrieve.js:
--------------------------------------------------------------------------------
1 | import 'whatwg-fetch';
2 |
3 | function retrieveHostInfo(url, cb) {
4 | var hostInfoUrl = url + '/?HOST_INFO';
5 | fetch(hostInfoUrl)
6 | .then(function(response) {
7 | return response.json();
8 | })
9 | .then(function(json) {
10 | cb(null, json);
11 | }).catch(function(err) {
12 | cb('No HOST_INFO "' + err + '"', null);
13 | });
14 | }
15 |
16 | function retrieveJson(url, cb) {
17 | fetch(url)
18 | .then(function(response) {
19 | return response.json();
20 | })
21 | .then(function(json) {
22 | cb(null, json);
23 | }).catch(function(err) {
24 | cb('Failed to process JSON: ' + err, null);
25 | });
26 | }
27 |
28 | export {retrieveJson, retrieveHostInfo};
29 |
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | // Configuration changes initiated by user: LISTEN, TOGGLE, and STYLE.
2 |
3 | // TODO: Common file.
4 | function $(selector) {
5 | return document.querySelector(selector);
6 | }
7 |
8 | // If OSC is ready (connected), start listening, otherwise wait 10 ms.
9 | function enableInitialListenState() {
10 | if (global.isOscReady) {
11 | listenIgnoreChange(true);
12 | } else {
13 | setTimeout(enableInitialListenState, 10);
14 | }
15 | }
16 |
17 | function listenClick(e) {
18 | listenIgnoreChange(true);
19 | }
20 |
21 | function ignoreClick(e) {
22 | listenIgnoreChange(false);
23 | }
24 |
25 | // Change the listen / ignore state, and send commands.
26 | function listenIgnoreChange(state) {
27 | global.g_isListenEnabled = state;
28 | let command = null;
29 | if (state) {
30 | $('.svg-listen').style.display = 'none';
31 | $('.svg-ignore').style.display = 'inline-block';
32 | command = 'LISTEN';
33 | } else {
34 | $('.svg-listen').style.display = 'inline-block';
35 | $('.svg-ignore').style.display = 'none';
36 | command = 'IGNORE';
37 | }
38 | if (global.isOscReady) {
39 | for (let i = 0; i < global.g_allControlStruct.length; i++) {
40 | var path = global.g_allControlStruct[i];
41 | var msg = JSON.stringify(
42 | {
43 | 'COMMAND': command,
44 | 'DATA': path
45 | });
46 | console.log('***** Sending WS: ' + msg);
47 | global.oscPort.socket.send(msg);
48 | }
49 | }
50 | }
51 |
52 | const TOGGLE_SHOW_DISPLAY = 'grid';
53 |
54 | // Hide some directory.
55 | function toggleHide(e) {
56 | e.preventDefault();
57 | let elem = e.target;
58 | for (let i = 0; i < 6; i++) {
59 | if (elem.tagName.toLowerCase() == 'div' && elem.id) {
60 | break;
61 | }
62 | elem = elem.parentNode;
63 | }
64 | let text = elem.id;
65 | let id = text.substr(12);
66 | $('#control_body_' + id).style.display = 'none';
67 | $('#toggle_show_' + id).style.display = TOGGLE_SHOW_DISPLAY;
68 | $('#toggle_hide_' + id).style.display = 'none';
69 | if (e.altKey) {
70 | let controlBody = $('#control_body_' + id);
71 | if (!controlBody) {
72 | return;
73 | }
74 | let dirContainerElems = controlBody.querySelectorAll('.dir-container');
75 | for (let i = 0; i < dirContainerElems.length; i++) {
76 | let toggleElem = dirContainerElems[i].querySelector('.toggle-hide');
77 | toggleHide({target: toggleElem, altKey: true});
78 | }
79 | }
80 | }
81 |
82 | // Show some directory.
83 | function toggleShow(e) {
84 | e.preventDefault();
85 | let elem = e.target;
86 | for (let i = 0; i < 6; i++) {
87 | if (elem.tagName.toLowerCase() == 'div' && elem.id) {
88 | break;
89 | }
90 | elem = elem.parentNode;
91 | }
92 | let text = elem.id;
93 | let id = text.substr(12);
94 | $('#control_body_' + id).style.display = TOGGLE_SHOW_DISPLAY;
95 | $('#toggle_show_' + id).style.display = 'none';
96 | $('#toggle_hide_' + id).style.display = TOGGLE_SHOW_DISPLAY;
97 | if (e.altKey) {
98 | let controlBody = $('#control_body_' + id);
99 | if (!controlBody) {
100 | return;
101 | }
102 | let dirContainerElems = controlBody.querySelectorAll('.dir-container');
103 | for (let i = 0; i < dirContainerElems.length; i++) {
104 | let toggleElem = dirContainerElems[i].querySelector('.toggle-show');
105 | toggleShow({target: toggleElem, altKey: true});
106 | }
107 | }
108 | }
109 |
110 | // Set the visual style to light or dark.
111 | function setStyleMode(mode) {
112 | if (mode == 'light') {
113 | $('#choice-dark-mode').style.display = 'none';
114 | $('#choice-light-mode').style.display = 'block';
115 | $('body').classList.add('light');
116 | document.cookie = 'style=light';
117 | } else if (mode == 'dark') {
118 | $('#choice-light-mode').style.display = 'none';
119 | $('#choice-dark-mode').style.display = 'block';
120 | $('body').classList.remove('light');
121 | document.cookie = 'style=dark';
122 | }
123 | }
124 |
125 | module.exports = {
126 | enableInitialListenState: enableInitialListenState,
127 | listenClick: listenClick,
128 | ignoreClick: ignoreClick,
129 | toggleHide: toggleHide,
130 | toggleShow: toggleShow,
131 | setStyleMode: setStyleMode,
132 | }
133 |
--------------------------------------------------------------------------------
/src/types.js:
--------------------------------------------------------------------------------
1 | function extractControlKind(type, html) {
2 | if (type == 'c') {
3 | if (html.includes('')) {
4 | return 'dropdown';
5 | } else {
6 | return 'char';
7 | }
8 | } else if (type == 'r') {
9 | return 'color';
10 | } else if (type == 'd' || type == 'f' || type == 'i' || type == 'h') {
11 | if (html.includes('')) {
12 | return 'dropdown';
13 | } else if (html.includes(' ')) {
22 | return 'dropdown';
23 | } else {
24 | return 'text';
25 | }
26 | } else if (type == 'F' || type == 'T') {
27 | return 'toggle';
28 | } else if (type == 'I' || type == 'N') {
29 | return 'button';
30 | } else if (type == 'm' || type == 't') {
31 | return 'none';
32 | } else {
33 | return 'unknown';
34 | }
35 | }
36 |
37 | function typeToControlName(type) {
38 | if (type == 'c') {
39 | return 'char';
40 | } else if (type == 'r') {
41 | return 'color';
42 | } else if (type == 'd') {
43 | return 'double';
44 | } else if (type == 'F') {
45 | return 'false';
46 | } else if (type == 'f') {
47 | return 'float';
48 | } else if (type == 'I') {
49 | return 'infinity';
50 | } else if (type == 'i') {
51 | return 'integer';
52 | } else if (type == 'h') {
53 | return 'longlong';
54 | } else if (type == 'm') {
55 | return 'midi';
56 | } else if (type == 'N') {
57 | return 'null';
58 | } else if (type == 'a') {
59 | return 'string';
60 | } else if (type == 'T') {
61 | return 'true';
62 | } else if (type == 't') {
63 | return 'timetag';
64 | } else {
65 | return 'unknown';
66 | }
67 | }
68 |
69 | module.exports = {
70 | extractControlKind: extractControlKind,
71 | }
72 |
--------------------------------------------------------------------------------
/src/userinput.js:
--------------------------------------------------------------------------------
1 | const controls = require('./controls.js');
2 |
3 | function toggleEvent(e) {
4 | // Control that was modified.
5 | let controlElem = e.target.parentNode;
6 | let detailsElem = controlElem.querySelector('.details');
7 | let setter = detailsElem.attributes['data-setter'];
8 | // Special hook for toggles because we need to modify the value before
9 | // calling `getControlArg` in the main `controlEvent` handler.
10 | if (setter) {
11 | controls.runSetter(controlElem, 'setToggleBeforeGetControlArg',
12 | e.target.value);
13 | }
14 | return controlEvent(e);
15 | }
16 |
17 | // Handle an event on a control, return whether an OSC message was sent.
18 | function controlEvent(e) {
19 | // Control that was modified.
20 | let controlElem = e.target.parentNode;
21 | let detailsElem = controlElem.querySelector('.details');
22 | let fullPath = detailsElem.attributes['data-full-path'].value;
23 | let setter = detailsElem.attributes['data-setter'];
24 | // Group that contains this control (in case the node has multiple types).
25 | let groupElem = controlElem.parentNode;
26 | let args = [];
27 | for (let i = 0; i < groupElem.children.length; i++) {
28 | let c = groupElem.children[i];
29 | if (c.tagName.toLowerCase() == 'div' &&
30 | c.classList.contains('control')) {
31 | args.push(controls.getControlArg(c));
32 | }
33 | }
34 | if (setter) {
35 | controls.runSetter(controlElem, setter.value, e.target.value);
36 | }
37 | var message = {
38 | address: fullPath,
39 | args: args,
40 | };
41 | console.log('***** Sending value: ' + JSON.stringify(message));
42 | if (window.isOscReady) {
43 | oscPort.send(message);
44 | return true;
45 | }
46 | return false;
47 | }
48 |
49 | function charKeyPressEvent(e) {
50 | e.target.value = String.fromCharCode(e.keyCode);
51 | return controlEvent(e);
52 | }
53 |
54 | global.g_numRangeMessagePending = 0;
55 | global.g_lastRangeMessageSent = null;
56 |
57 | function rangeModifyEvent(e) {
58 | let value = e.target.value;
59 | // Cache value so that it won't send twice in a row.
60 | if (e.target.cacheValue === value) {
61 | return;
62 | }
63 | e.target.cacheValue = value
64 | if (controlEvent(e)) {
65 | if (global.g_isListenEnabled) {
66 | global.g_numRangeMessagePending++;
67 | global.g_lastRangeMessageSent = new Date();
68 | }
69 | }
70 | }
71 |
72 | global.g_numColorMessagePending = 0;
73 | global.g_lastColorMessageSent = null;
74 |
75 | function colorModifyEvent(e) {
76 | if (controlEvent(e)) {
77 | if (global.g_isListenEnabled) {
78 | global.g_numColorMessagePending++;
79 | global.g_lastColorMessageSent = new Date();
80 | }
81 | }
82 | }
83 |
84 | setInterval(function() {
85 | let now = new Date();
86 | if (global.g_lastRangeMessageSent) {
87 | if (now - global.g_lastRangeMessageSent > 1000) {
88 | global.g_numRangeMessagePending = 0;
89 | global.g_lastRangeMessageSent = null;
90 | }
91 | }
92 | if (global.g_lastColorMessageSent) {
93 | if (now - global.g_lastColorMessageSent > 1000) {
94 | global.g_numColorMessagePending = 0;
95 | global.g_lastColorMessageSent = null;
96 | }
97 | }
98 | }, 400);
99 |
100 | module.exports = {
101 | toggleEvent: toggleEvent,
102 | controlEvent: controlEvent,
103 | charKeyPressEvent: charKeyPressEvent,
104 | rangeModifyEvent: rangeModifyEvent,
105 | colorModifyEvent: colorModifyEvent,
106 | }
107 |
--------------------------------------------------------------------------------
/test/builder.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const index = require('../src/index.js');
3 | const builder = require('../src/builder.js');
4 |
5 | describe('util', () => {
6 | describe('getDataEvent', () => {
7 | it('returns data-event value', () => {
8 | let obj = {'attributes': {'data-event': {'value': 'keypress'}}};
9 | assert.equal(index.getDataEvent(obj), 'keypress');
10 | obj = {'attributes': {'class': 'container'}}
11 | assert.equal(index.getDataEvent(obj), null);
12 | });
13 | });
14 | });
15 |
16 | var g_supportHtml5Color;
17 |
18 | describe('buildSingleControl', () => {
19 | describe('i', () => {
20 | it('returns int control', () => {
21 | let data = {'TYPE': 'i', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p'};
22 | let html = builder.buildSingleControl(data, 'i', [0], 0);
23 | assert.equal(html,
24 | '' +
25 | '0 ' +
26 | ' ' +
27 | ' ' +
28 | ' ');
31 | });
32 | });
33 | describe('i with min,max', () => {
34 | it('returns int control with min and max', () => {
35 | let data = {'TYPE': 'i', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p',
36 | 'RANGE': [{'MIN': 0, 'MAX': 10}]};
37 | let html = builder.buildSingleControl(data, 'i', [0], 0);
38 | assert.equal(html,
39 | '' +
40 | '0 ' +
41 | ' (0-10) ' +
42 | ' ' +
43 | ' ' +
44 | ' ');
47 | });
48 | });
49 | describe('i with vals', () => {
50 | it('returns int control with values in a dropdown', () => {
51 | let data = {'TYPE': 'i', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p',
52 | 'RANGE': [{'VALS': [3,4,5,6]}]};
53 | let html = builder.buildSingleControl(data, 'i', [0], 0);
54 | assert.equal(html,
55 | '3 ' +
56 | '4 ' +
57 | '5 ' +
58 | '6 ' +
59 | ' ' +
60 | ' ');
62 | });
63 | });
64 | describe('i with min,max almost the same', () => {
65 | it('returns int control with checkbox', () => {
66 | let data = {'TYPE': 'i', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p',
67 | 'RANGE': [{'MIN': 8, 'MAX': 9}]};
68 | let html = builder.buildSingleControl(data, 'i', [0], 0);
69 | assert.equal(html,
70 | ' ' +
72 | ' ');
75 | });
76 | });
77 | describe('i with only 2 vals', () => {
78 | it('returns int control with checkbox', () => {
79 | let data = {'TYPE': 'i', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p',
80 | 'RANGE': [{'VALS': [5, 10]}]};
81 | let html = builder.buildSingleControl(data, 'i', [0], 0);
82 | assert.equal(html,
83 | ' ' +
85 | ' ');
88 | });
89 | });
90 | describe('i with min,max exactly the same', () => {
91 | it('returns int control with button', () => {
92 | let data = {'TYPE': 'i', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p',
93 | 'RANGE': [{'MIN': 20, 'MAX': 20}]};
94 | let html = builder.buildSingleControl(data, 'i', [0], 0);
95 | assert.equal(html,
96 | ' ' +
97 | ' ');
100 | });
101 | });
102 | describe('i with only 1 val', () => {
103 | it('returns int control with button', () => {
104 | let data = {'TYPE': 'i', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p',
105 | 'RANGE': [{'VALS': [32]}]};
106 | let html = builder.buildSingleControl(data, 'i', [0], 0);
107 | assert.equal(html,
108 | ' ' +
109 | ' ');
112 | });
113 | });
114 | describe('i with invalid size of RANGE', () => {
115 | it('returns a message about invalid node', () => {
116 | let data = {'TYPE': 'i', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p',
117 | 'RANGE': []};
118 | let html = builder.buildSingleControl(data, 'i', [0], 0);
119 | assert.equal(html,
120 | 'Invalid node: RANGE needs ' +
121 | 'MIN,MAX or VALS ');
122 | });
123 | });
124 | describe('h', () => {
125 | it('returns longlong control', () => {
126 | let data = {'TYPE': 'h', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p'};
127 | let html = builder.buildSingleControl(data, 'h', [0], 0);
128 | assert.equal(html,
129 | '' +
130 | '0 ' +
131 | ' ' +
132 | ' ' +
133 | ' ');
136 | });
137 | });
138 | describe('T', () => {
139 | it('returns true control', () => {
140 | let data = {'TYPE': 'T', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p'};
141 | let html = builder.buildSingleControl(data, 'T', [0], 0);
142 | assert.equal(html,
143 | ' ' +
145 | ' ');
148 | });
149 | });
150 | describe('F', () => {
151 | it('returns false control', () => {
152 | let data = {'TYPE': 'F', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p'};
153 | let html = builder.buildSingleControl(data, 'F', [0], 0);
154 | assert.equal(html,
155 | ' ' +
157 | ' ');
160 | });
161 | });
162 | describe('r creates colorPicker', () => {
163 | it('returns an html5 color control', () => {
164 | let cfg = {supportHtml5Color: true};
165 | let data = {'TYPE': 'r', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p',
166 | 'RANGE': []};
167 | let html = builder.buildSingleControl(data, 'r', [0], 0, cfg);
168 | assert.equal(html,
169 | ' ' +
170 | ' ');
173 | });
174 | it('returns a third-party color control', () => {
175 | let data = {'TYPE': 'r', 'DESCRIPTION': 'desc', 'FULL_PATH': '/p',
176 | 'RANGE': []};
177 | let html = builder.buildSingleControl(data, 'r', [0], 0);
178 | assert.equal(html,
179 | '' +
180 | '
' +
181 | ' ');
184 | });
185 | });
186 | });
187 |
188 |
--------------------------------------------------------------------------------
/test/extract.js:
--------------------------------------------------------------------------------
1 | const assert = require('assert');
2 | const index = require('../src/index.js');
3 |
4 | describe('extractControlPaths', () => {
5 | describe('two controls', () => {
6 | it('returns two paths', () => {
7 | let data = {
8 | 'FULL_PATH': '/',
9 | 'CONTENTS' : {
10 | 'dir' : {
11 | 'FULL_PATH': '/dir',
12 | 'CONTENTS' : {
13 | 'a_control' : {
14 | 'FULL_PATH': '/dir/a_control',
15 | 'TYPE' : 'c',
16 | },
17 | 'b_control' : {
18 | 'FULL_PATH': '/dir/b_control',
19 | 'TYPE' : 'r',
20 | }
21 | }
22 | }
23 | }
24 | };
25 | let paths = index.extractControlPaths(data);
26 | assert.deepEqual(paths, ['/dir/a_control', '/dir/b_control']);
27 | });
28 | });
29 | describe('has contents and type', () => {
30 | it('returns two paths', () => {
31 | let data = {
32 | 'FULL_PATH': '/',
33 | 'CONTENTS' : {
34 | 'dir' : {
35 | 'FULL_PATH': '/dir',
36 | 'CONTENTS' : {
37 | 'a_control' : {
38 | 'FULL_PATH': '/dir/a_control',
39 | 'TYPE' : 'c',
40 | },
41 | },
42 | 'TYPE': 'i'
43 | }
44 | }
45 | };
46 | let paths = index.extractControlPaths(data);
47 | assert.deepEqual(paths, ['/dir/a_control', '/dir']);
48 | });
49 | });
50 | });
51 |
52 |
--------------------------------------------------------------------------------
/web/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const app = express();
3 |
4 | const PORT = 5050;
5 |
6 | app.set('view engine', 'ejs');
7 | app.set('views', __dirname + '/views');
8 | app.use(express.static('../assets/'));
9 | app.use(express.static('../output'));
10 |
11 | app.get('/', (req, res) => {
12 | const url = process.env['SERVER_URL'];
13 | if (!url) {
14 | throw 'SERVER_URL not set, cannot connect to OSC Server'
15 | }
16 | let context = {hostUrl: url};
17 | res.render('pages/index', context);
18 | });
19 |
20 | app.listen(PORT, () => {
21 | console.log(`Listening on port ${PORT}`);
22 | });
23 |
--------------------------------------------------------------------------------
/web/views/pages/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | OSC Query Tool
5 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/web/views/pages/range-slider.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Vidvox/oscqueryhtml/ca2504114c59569bee665fde473c22f6d98a273c/web/views/pages/range-slider.css
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: ['whatwg-fetch', './src/index.js'],
6 | devtool: 'source-map',
7 | output: {
8 | path: path.resolve(__dirname, 'output'),
9 | filename: 'bundle.js',
10 | library: 'oscQuery',
11 | libraryTarget: 'window',
12 | },
13 | resolve: {
14 | alias: {
15 | 'osc': path.join(__dirname, 'node_modules/osc/src/osc.js'),
16 | 'osc-transports': path.join(__dirname, 'node_modules/osc/src/' +
17 | 'osc-transports.js'),
18 | 'osc-websocket-client': path.join(__dirname, 'node_modules/osc/src/' +
19 | 'platforms/osc-websocket-client.js'),
20 | 'vanilla-picker': path.join(__dirname, 'node_modules/vanilla-picker/' +
21 | 'dist/vanilla-picker.js'),
22 | }
23 | },
24 | node: {
25 | fs: 'empty',
26 | tls: 'empty',
27 | },
28 | externals: {
29 | ws: 'ws'
30 | },
31 | optimization: {
32 | minimizer: [
33 | new UglifyJsPlugin({
34 | sourceMap: true,
35 | uglifyOptions:{safari10:true}
36 | })
37 | ]
38 | }
39 | };
40 |
--------------------------------------------------------------------------------