-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathscript.js
More file actions
585 lines (546 loc) · 19.2 KB
/
script.js
File metadata and controls
585 lines (546 loc) · 19.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
// gen.sh and append.sh replace DATA with the JSON of a connector
var connectorData = [
/*DATA*/
];
// gen.sh replaces COLS with a JSON object
const globalColumns = ///COLS///;
// gen.sh replaces PRINT_COLS with a JSON array
const globalPrintColumns = ///PRINT_COLS///;
// gen.sh replaces COLOR_COLS with a JSON array
const globalColorColumns = ///COLOR_COLS///;
// gen.sh replaces INFO_COL with a string
const globalInfoColumn = "///INFO_COL///";
// gen.sh replaces TEMPLATES with a JSON object
const templates = ///TEMPLATES///;
var possibleColors = [];
// We call this function after creating the main table, and when showing a pin in the info table.
function hideEmptyColumns(table) {
const rows = Array.from(table.querySelector("tbody").children);
const tableHeads = Array.from(table.querySelector("thead>tr").children);
// For every column, loop through all the rows and check if the column is empty
tableHeads.forEach((col, i) => {
const found = rows.find((r) => r.children[i] && r.children[i].textContent.length > 0);
// If the column was empty, we have to hide the header and the children,
// If not, we do the same procedure, but show instead of hide.
col.style.display = found ? "" : "none";
rows.forEach((r) => {
if (r.children[i]) {
r.children[i].style.display = found ? "" : "none";
}
});
});
}
// Add a row to a table, with an associated connector,
// so we can scroll the connector into view
function addRow(table, pin, c, click) {
click = typeof click !== 'undefined' ? click : true;
const clone = getRow(pin, connectorData[c]);
const row = clone.querySelector(".data");
// If we've been passed a reference to a pin on the connector view,
// make this row clickable.
// This will not be the case:
// - When no x/y coordinates were provided for the pin and couldn't be interpolated
// - When there is no image specified in the input YAML
if (pin.pdiv && click) {
row.addEventListener("click", () => {
// Find the closest container up the tree.
// We don't know how far it is, because the info and
// main tables are at different depths.
const container = table.closest(".container");
// Handle the click.
clickPin(container.querySelector(".info-table tbody"), pin, c);
// Scroll so the connector view is visible.
container.scrollIntoView();
});
}
table.appendChild(clone);
}
// Build a row to add to a table
function getRow(pin, connector) {
const template = document.getElementById("table-template");
const clone = template.content.cloneNode(true);
const row = clone.querySelector(".data");
// Loop through the columns and create a data element for each
const columns = (connector && connector.info.columns) || globalColumns;
for (const column in columns) {
const el = document.createElement("td")
// If we should print this column (We always print pins)
const printColumns = (connector && connector.info["print-columns"]) || globalPrintColumns;
if ( printColumns.indexOf(column) !== -1 || column == "pin" ) {
el.classList.add("print-column");
}
// Sometimes the data is an array instead of a string, so we might need to join it.
let text = Array.isArray(pin[column]) ? pin[column].join(", ") : (pin[column] ?? "");
if (typeof templates === "object" && typeof text === "string") {
for (const template in templates) {
text = text.replace(template, pin[templates[template]])
}
}
el.textContent = text;
el.dataset.field = column;
row.appendChild(el);
}
// Set the type of the pin so it will be colored in print view.
clone.querySelector('[data-field="pin"]').dataset.type = pin.type;
return clone;
}
// Called when we click on a pin, either in a table or in the connector view
// table is always the info table
function clickPin(table, pin, c) {
// Find the closest container up the tree.
// We don't know how far it is, because table rows and
// pins in the connector view are at different depths.
const container = table.closest(".container");
// Make sure the table is visible.
table.parentElement.style.display = "table";
// Clear the table, then add the row
table.innerHTML = "";
addRow(table, pin, c, false);
highlightMatches([["type", pin.type]])
// Select the clicked pin.
pin.pdiv.classList.add("selected");
// Hide empty columns from the table
hideEmptyColumns(table.parentElement);
// If there's a connector id for this pin, go to this pin's URL
if (typeof(c) != "undefined") {
const url = new URL(window.location);
url.searchParams.set("connector", connectorData[c].info.cid);
url.searchParams.set("pin", pin.pin);
url.searchParams.delete("highlight");
// Don't ruin the history if we're not going somewhere new.
if ( url.toString() != new URL(window.location).toString() ) {
window.history.pushState({}, "", url)
}
} else {
const url = new URL(window.location);
url.search = "";
url.searchParams.set("pin", pin.pin);
url.searchParams.delete("highlight");
// Don't ruin the history if we're not going somewhere new.
if ( url.toString() != new URL(window.location).toString() ) {
window.history.pushState({}, "", url)
}
}
container.scrollIntoView();
}
function checkFieldMatch(fieldmatches, pin) {
let found = false;
fieldmatches.forEach((fieldmatch) => {
const field = fieldmatch[0];
const match = fieldmatch[1];
const re = new RegExp("^" + match + "$")
if (pin[field] == match ||
re.test(pin[field]) ||
(Array.isArray(pin[field]) &&
(pin[field].indexOf(match) >= 0 ||
pin[field].findIndex((fieldx) => re.test(fieldx)) >= 0))) {
found = true;
}
});
return found;
}
function highlightMatches(fieldmatches) {
// Loop through the pins, and highlight those of the same type,
// remove highlights from any other previously highlighted pins,
// and remove selection from all pins.
connectorData.forEach((cdata) => {
cdata.pins.forEach((pin) => {
if (pin.pdiv) {
if (checkFieldMatch(fieldmatches, pin)) {
pin.pdiv.classList.add("highlight");
} else {
pin.pdiv.classList.remove("highlight");
}
pin.pdiv.classList.remove("selected");
}
});
});
}
function addMatchesToTable(fieldmatches) {
// Add pins matching type to their connector table
let found = false;
connectorData.forEach((cdata, i) => {
const table = document.querySelectorAll(".info-table tbody")[i];
cdata.pins.forEach((pin) => {
if (checkFieldMatch(fieldmatches, pin)) {
if (!found) {
// scroll to the connector of the first found matching pin
table.closest(".container").scrollIntoView();
found = true;
}
table.parentElement.style.display = "table";
addRow(table, pin, i);
}
});
});
}
// Check URL parameters for a selected pin
function checkparams() {
const params = new URLSearchParams(window.location.search);
const hl = params.get("highlight");
if (hl != null) {
// matching instead of splitting to allow escaping of , and ~
const fieldmatches = hl.match(/(?:\\.|[^,])+/g).map((p) => p.match(/(?:\\.|[^~])+/g));
highlightMatches(fieldmatches);
addMatchesToTable(fieldmatches);
return;
}
const connector = params.get("connector");
const pin = params.get("pin");
if (pin == null) {
return;
}
// Loop through the connectors and find if there's one that matches.
// If none match, default to the first connector.
let c = connectorData.findIndex((data) => (typeof(data.info.cid) != "undefined" && data.info.cid == connector)) || 0;
const cdata = connectorData[c];
const table = document.querySelectorAll(".info-table tbody")[c];
// Loop through the pins and find if there's one that matches.
cdata.pins.forEach((cpin) => {
if (cpin.pin == pin) {
// Just pretend we clicked on it
clickPin(table, cpin, c);
return;
}
});
}
// Keep track of how many images need to be loaded
var images = 0;
// If all images are loaded, check the params for a selected pin.
// We don't want to try to select a pin before the image is loaded.
function checkImagesLoaded() {
images -= 1;
if (images == 0) {
checkparams();
generateColorStyles();
}
}
// This is a butchery and I hate it and never want to touch it again.
function calcPinSize(pin, cdiv, connector, pinfo) {
// Find the closest pin, to maximize the pin size for best readability,
// without overlapping pins.
let closest = 1000000;
connector.info.image.pins.forEach((tinfo) => {
if (tinfo.pin == pin.pin) return;
const distance = Math.pow((tinfo.x - pinfo.x), 2) + Math.pow((tinfo.y - pinfo.y), 2);
if (distance < closest) {
closest = distance;
}
});
closest = Math.sqrt(closest);
// Set the pin's size
const divheight = cdiv.clientHeight;
const divwidth = cdiv.clientWidth;
const scale = divheight / cdiv.querySelector("img").naturalHeight;
const newheight = closest * scale;
// Default height is 8% of div
let pinsize = divheight * 0.08;
if (newheight < pinsize) {
pinsize = newheight;
}
// Use percent for scaling when printing
const height = (pinsize / divheight) * 100;
const width = (pinsize / divwidth) * 100;
pin.pdiv.style.height = height + "%";
pin.pdiv.style.width = width + "%";
// 0.5cqh achieves vertical centering, roughly
pin.pdiv.style.lineHeight = (height - 0.5) + "cqh";
// When using a percent for margins, the width value is used even for top and bottom
pin.pdiv.style.marginTop = "-" + (width / 2) + "%";
pin.pdiv.style.marginLeft = "-" + (width / 2) + "%";
pin.pdiv.style.fontSize = (height * 0.5) + "cqh";
}
function findBounds(pins, i) {
let d = i;
let u = i;
for (; d > -1; d--) {
if (typeof(pins[d].x) != "undefined" && typeof(pins[d].y) != "undefined") {
break;
}
}
for (; u < pins.length; u++) {
if (typeof(pins[u].x) != "undefined" && typeof(pins[u].y) != "undefined") {
break;
}
}
return [pins[d], pins[u]]
}
function findBetween(a, b, p) {
const a_ns = ("" + a.pin).match(/\d+/g)
const b_ns = ("" + b.pin).match(/\d+/g)
const n_ns = ("" + p.pin).match(/\d+/g)
let n = 0;
for (let i = 0; i < a_ns.length; i++) {
if (a_ns[i] != b_ns[i]) {
a.n = a_ns[i];
b.n = b_ns[i];
n = n_ns[i];
break
}
}
return brange(a, b, n);
}
function range(x1, x2, n1, n2, n) {
n1 = parseInt(n1);
n2 = parseInt(n2);
x1 = parseInt(x1);
x2 = parseInt(x2);
n = parseInt(n);
return x1 + (x2 - x1) * ((n - n1) / (n2 - n1));
}
function brange(p1, p2, n) {
return {
x: range(p1.x, p2.x, p1.n, p2.n, n),
y: range(p1.y, p2.y, p1.n, p2.n, n)
};
}
function selectCell(cell) {
Array.from(cell.parentElement.children).forEach((sibling) => {
sibling.classList.remove("selected");
});
cell.classList.add("selected");
}
function setPinColor(pin, col, el) {
if (col in pin) {
el.dataset.color = pin[col].replace(/\s/g, "")
} else {
el.dataset.color = "";
}
}
function setupColorToggle(sdiv, connector, columns) {
let hasColorColumn = false;
const colorColumns = (connector && connector.info["color-columns"]) || (globalColorColumns.length > 0 && globalColorColumns) || ["color"];
colorColumns.forEach((col) => {
if (col in columns) {
if (connector.pins.some((pin) => col in pin)) {
hasColorColumn = true;
}
const ctab = sdiv.querySelector(".color-table tbody tr");
let d = document.createElement("td");
d.innerText = columns[col];
d.addEventListener("click", () => {
selectCell(d);
let pinCells = sdiv.querySelectorAll(".general-table tbody tr td:first-child");
connector.pins.forEach((pin) => {
let cell = null;
pinCells.forEach((p) => {
if (p.innerText == pin.pin) cell = p;
});
if (cell) {
setPinColor(pin, col, cell);
}
if ('pdiv' in pin) {
setPinColor(pin, col, pin.pdiv)
}
});
});
ctab.append(d);
}
});
if (hasColorColumn) {
sdiv.querySelector(".switch-block").style.display = "block"
sdiv.querySelector(".toggle-label").style.display = "block"
const defaultColor = sdiv.querySelector(".default-color");
defaultColor.addEventListener('click', () => {
selectCell(defaultColor);
connector.pins.forEach((pin) => {
if ('pdiv' in pin) {
pin.pdiv.dataset.color = "";
}
});
sdiv.querySelectorAll(".general-table tbody tr td:first-child").forEach((p) => {
p.dataset.color = ""
});
});
}
}
function addPossibleColor(connector, pin) {
const colorColumns = (connector && connector.info["color-columns"]) || (globalColorColumns.length > 0 && globalColorColumns) || ["color"];
colorColumns.forEach((col) => {
if (col in pin && ! possibleColors.includes(pin[col])) {
possibleColors.push(pin[col].replace(/\s/g, ""));
}
});
}
function generateColorStyles() {
let styleSheet = document.createElement("style");
styleSheet.textContent = possibleColors.reduce((a, color) => {
let c = color.split("/");
const cm = new Option().style;
cm.color = c[0];
if (cm.color == "") return a;
if (c.length == 1) {
return a + '[data-color="' + cm.color + '"] {\n' +
'border-color: ' + cm.color + ' !important;\n}\n';
}
const cs = new Option().style;
cs.color = c[1];
if (cs.color == "") return a;
return a + '[data-color="' + cm.color + "/" + cs.color + '"] {\n' +
'border-color: ' + cm.color + ' !important;\n' +
'border-top-color: ' + cs.color + ' !important;\n}\n';
}, "");
document.head.appendChild(styleSheet);
}
function handleImageLoad(connector, c, sdiv, img, columns) {
const cdiv = sdiv.querySelector(".connector-div");
const cdc = sdiv.querySelector(".connector-div-container");
cdc.style.aspectRatio = img.naturalWidth / img.naturalHeight;
const ptemplate = document.getElementById("pin-template");
const pitemplate = document.getElementById("pin-info-template");
const imgHeight = img.naturalHeight;
const imgWidth = img.naturalWidth;
const infoTable = sdiv.querySelector(".info-table").querySelector("tbody");
const infoTableHeader = sdiv.querySelector(".info-table").querySelector("thead>tr");
const fullTable = sdiv.querySelector(".pinout-table").querySelector("tbody");
const fullTableHeader = sdiv.querySelector(".pinout-table").querySelector("thead>tr");
// Loop through our columns and add the headers for both tables
for (const column in columns) {
const el = document.createElement("th");
el.textContent = columns[column];
infoTableHeader.appendChild(el.cloneNode(true));
fullTableHeader.appendChild(el.cloneNode(true));
}
// For every pin...
connector.pins.forEach((pin) => {
if (!pin.pin) {
return;
}
addPossibleColor(connector, pin);
// Get the pin info from the info section (i.e. x/y coordinates)
const pinfoidx = connector.info.image.pins.findIndex((e) => e.pin == pin.pin);
const pinfo = connector.info.image.pins[pinfoidx];
// If we found a listing without coordinates
if (pinfo && !pinfo.x) {
const bounds = findBounds(connector.info.image.pins, pinfoidx)
if (typeof(bounds[0]) != "undefined" && typeof(bounds[1]) != "undefined") {
Object.assign(pinfo, findBetween(bounds[0], bounds[1], pinfo))
}
}
// If we didn't find a listing in the image section, just add to the table
if (!pinfo || !pinfo.x) {
addRow(fullTable, pin, c);
return;
}
// Create the pin element and set its position and type
const pclone = ptemplate.content.cloneNode(true);
const pdiv = pclone.querySelector("div");
pdiv.textContent = pinfo.pin;
const piclone = pitemplate.content.cloneNode(true);
const pidiv = piclone.querySelector(".pin-info");
pidiv.textContent = pin[connector.info["info-column"] || globalInfoColumn];
pdiv.appendChild(piclone);
pdiv.style.top = ((pinfo.y / imgHeight) * 100) + "%";
pdiv.style.left = ((pinfo.x / imgWidth) * 100) + "%";
pdiv.dataset.type = pin.type;
// Associate the pin's element with the pin object
pin.pdiv = pdiv;
pdiv.addEventListener("click", () => {
clickPin(infoTable, pin, c);
});
calcPinSize(pin, cdiv, connector, pinfo)
// Recalculate the size when the window is resized.
resizeHandlers.push(() => {
calcPinSize(pin, cdiv, connector, pinfo)
});
cdiv.appendChild(pdiv);
addRow(fullTable, pin, c);
});
hideEmptyColumns(sdiv.querySelector(".pinout-table"));
setupColorToggle(sdiv, connector, columns);
// Check if we have loaded all the images.
checkImagesLoaded();
}
function buildConnector(connector, c) {
const columns = (connector && connector.info.columns) || globalColumns;
const template = document.getElementById("connector-template");
const clone = template.content.cloneNode(true);
document.body.appendChild(clone);
const sdiv = document.body.lastElementChild;
const img = sdiv.querySelector(".connector-img");
// If there's info, use it.
if (typeof(connector.info) != "undefined") {
// Set the document title
if (document.title.length == 0 && typeof(connector.info.title) != "undefined") {
document.title = connector.info.title;
}
// bacwards compatability
if (typeof(connector.info.board_url) != "undefined") {
connector.info.url = connector.info.board_url;
}
// Add the board link
if (typeof(connector.info.url) != "undefined" && document.title.length > 0) {
document.getElementById("board-link").innerText = document.title;
document.getElementById("board-link").href = connector.info.url;
}
// Add the connector name
if (typeof(connector.info.name) != "undefined") {
sdiv.querySelector(".connector-name").innerText = connector.info.name;
}
}
// Check if there is an image
if (typeof(connector.info) != "undefined" && typeof(connector.info.image) != "undefined") {
// When the image is loaded, then handle the pins
img.addEventListener("load", () => handleImageLoad(connector, c, sdiv, img, columns));
// Add the image to load it
img.src = connector.info.image.file;
// Increment "images we need to load" counter
images += 1;
// If there's no image, just build the table.
} else {
img.parentElement.parentElement.parentElement.style.height = 0;
const fullTable = sdiv.querySelector(".pinout-table").querySelector("tbody");
const fullTableHeader = sdiv.querySelector(".pinout-table").querySelector("thead>tr");
connector.pins.forEach((pin) => {
if (!pin.pin) {
return;
}
addRow(fullTable, pin, c);
addPossibleColor(connector, pin);
});
// Loop through our columns and add the headers for the main table
for (const column in columns) {
const el = document.createElement("th");
el.textContent = columns[column];
fullTableHeader.appendChild(el.cloneNode(true));
}
hideEmptyColumns(sdiv.querySelector(".pinout-table"));
setupColorToggle(sdiv, connector, columns);
}
}
let resizeHandlers = [];
window.addEventListener("load", function() {
// Manage history navigation
window.onpopstate = () => {
checkparams();
};
// @ifdef DEBUG
if (debug) {
console.log(connectorData.length + " connectors")
}
// @endif
// Parse JSON strings
for (let c = 0; c < connectorData.length; c++) {
// Parse the JSON, add connector to document body
connectorData[c] = JSON.parse(connectorData[c]);
const connector = connectorData[c];
if (connector && connector.info) {
if (connector.info.columns) {
connector.info.columns = JSON.parse(connector.info.columns);
}
if (connector.info["print-columns"]) {
connector.info["print-columns"] = JSON.parse(connector.info["print-columns"]);
}
if (connector.info["color-columns"]) {
connector.info["color-columns"] = JSON.parse(connector.info["color-columns"]);
}
}
}
connectorData.forEach(buildConnector);
// Recalculate pin sizes when the window is resized.
window.addEventListener("resize", () => {
beforePrintHandlers = [];
afterPrintHandlers = [];
resizeHandlers.forEach((h) => h());
});
});