|
@@ -30,6 +30,7 @@
|
|
|
var dark = false;
|
|
var dark = false;
|
|
|
var showTerm = false;
|
|
var showTerm = false;
|
|
|
var onlyOff = false;
|
|
var onlyOff = false;
|
|
|
|
|
+ var pinnedTerm = null;
|
|
|
var paused = false;
|
|
var paused = false;
|
|
|
var countdown = 0;
|
|
var countdown = 0;
|
|
|
var timer = null;
|
|
var timer = null;
|
|
@@ -455,7 +456,8 @@
|
|
|
var FLOOR_LEFT = 1130, FLOOR_RIGHT = 1525;
|
|
var FLOOR_LEFT = 1130, FLOOR_RIGHT = 1525;
|
|
|
var CHART_W = 1560;
|
|
var CHART_W = 1560;
|
|
|
var SEC_HEAD = 46, CORE_ROW = 92, WORK_TOP = 148;
|
|
var SEC_HEAD = 46, CORE_ROW = 92, WORK_TOP = 148;
|
|
|
- var ROW_H = 24, ROW_GAP = 3, WS_GAP = 10, SEC_GAP = 34;
|
|
|
|
|
|
|
+ var ROW_H = 24, ROW_GAP = 8, WS_GAP = 20, SEC_GAP = 40;
|
|
|
|
|
+ var FLOOR_GAP = 18;
|
|
|
|
|
|
|
|
function makeNetworks() {
|
|
function makeNetworks() {
|
|
|
var raw = CFG.networks;
|
|
var raw = CFG.networks;
|
|
@@ -654,7 +656,7 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function layoutWorkshop(w, top) {
|
|
function layoutWorkshop(w, top) {
|
|
|
- var rowH = ROW_H + (showTerm ? 24 : 0);
|
|
|
|
|
|
|
+ var rowH = ROW_H + ROW_GAP + (showTerm ? 24 : 0);
|
|
|
var floorMap = {}, floors = [];
|
|
var floorMap = {}, floors = [];
|
|
|
w.access.forEach(function (n) {
|
|
w.access.forEach(function (n) {
|
|
|
var fi = floorInfo(n);
|
|
var fi = floorInfo(n);
|
|
@@ -666,10 +668,10 @@
|
|
|
var accessH = 0;
|
|
var accessH = 0;
|
|
|
floors.forEach(function (f) {
|
|
floors.forEach(function (f) {
|
|
|
f.nodes.sort(function (a, b) { return String(a.name).localeCompare(String(b.name)); });
|
|
f.nodes.sort(function (a, b) { return String(a.name).localeCompare(String(b.name)); });
|
|
|
- f.h = 22 + f.nodes.length * rowH + 6;
|
|
|
|
|
|
|
+ f.h = 24 + f.nodes.length * rowH + 8;
|
|
|
accessH += f.h;
|
|
accessH += f.h;
|
|
|
});
|
|
});
|
|
|
- if (floors.length > 1) accessH += (floors.length - 1) * 5;
|
|
|
|
|
|
|
+ if (floors.length > 1) accessH += (floors.length - 1) * FLOOR_GAP;
|
|
|
var distH = w.dists.length ? (w.dists.length * 64 + (w.dists.length - 1) * 8) : 0;
|
|
var distH = w.dists.length ? (w.dists.length * 64 + (w.dists.length - 1) * 8) : 0;
|
|
|
var inner = Math.max(accessH, distH, 54);
|
|
var inner = Math.max(accessH, distH, 54);
|
|
|
var h = 38 + inner + 10;
|
|
var h = 38 + inner + 10;
|
|
@@ -682,7 +684,7 @@
|
|
|
f.nodes.forEach(function (n, i) {
|
|
f.nodes.forEach(function (n, i) {
|
|
|
f.rows.push({id: n.id, x: ACCESS_X, y: cursor + 22 + rowH / 2 + i * rowH});
|
|
f.rows.push({id: n.id, x: ACCESS_X, y: cursor + 22 + rowH / 2 + i * rowH});
|
|
|
});
|
|
});
|
|
|
- cursor -= 5;
|
|
|
|
|
|
|
+ cursor -= FLOOR_GAP;
|
|
|
});
|
|
});
|
|
|
var dstart = top + 38 + (inner - distH) / 2;
|
|
var dstart = top + 38 + (inner - distH) / 2;
|
|
|
w.distPos = w.dists.map(function (n, i) {
|
|
w.distPos = w.dists.map(function (n, i) {
|
|
@@ -819,6 +821,12 @@
|
|
|
if (s >= 1000) return (s / 1000) + 'G';
|
|
if (s >= 1000) return (s / 1000) + 'G';
|
|
|
return s + 'M';
|
|
return s + 'M';
|
|
|
}
|
|
}
|
|
|
|
|
+ function terminalOnline(tm) {
|
|
|
|
|
+ return tm && (tm.online === true || tm.online === 1 || tm.online === '1' || tm.online === 'true');
|
|
|
|
|
+ }
|
|
|
|
|
+ function terminalText(tm) {
|
|
|
|
|
+ return tm.mac + '\n' + speedText(tm.speed);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
/* ---------------- style ---------------- */
|
|
/* ---------------- style ---------------- */
|
|
|
function styleFor(d) {
|
|
function styleFor(d) {
|
|
@@ -918,7 +926,13 @@
|
|
|
}},
|
|
}},
|
|
|
{selector: 'node[type="term"]', style: {
|
|
{selector: 'node[type="term"]', style: {
|
|
|
'width': 7, 'height': 7, 'shape': 'ellipse', 'background-color': 'data(bg)',
|
|
'width': 7, 'height': 7, 'shape': 'ellipse', 'background-color': 'data(bg)',
|
|
|
- 'border-width': 1, 'border-color': d ? '#2a3648' : '#cfd8e3', 'label': '',
|
|
|
|
|
|
|
+ 'border-width': 1, 'border-color': d ? '#2a3648' : '#cfd8e3',
|
|
|
|
|
+ 'label': 'data(label)', 'font-size': 9, 'font-weight': 700,
|
|
|
|
|
+ 'color': d ? '#eaf1fb' : '#182c44',
|
|
|
|
|
+ 'text-valign': 'bottom', 'text-halign': 'center', 'text-margin-y': 4,
|
|
|
|
|
+ 'text-wrap': 'wrap', 'text-max-width': 160,
|
|
|
|
|
+ 'text-background-color': d ? '#0d1b2a' : '#ffffff', 'text-background-opacity': 0.9,
|
|
|
|
|
+ 'text-background-padding': '2px',
|
|
|
'z-index-compare': 'manual', 'z-index': 30
|
|
'z-index-compare': 'manual', 'z-index': 30
|
|
|
}},
|
|
}},
|
|
|
{selector: 'edge[type="tilelink"]', style: {
|
|
{selector: 'edge[type="tilelink"]', style: {
|
|
@@ -1037,6 +1051,7 @@
|
|
|
buildLegend();
|
|
buildLegend();
|
|
|
buildAlarms(snap);
|
|
buildAlarms(snap);
|
|
|
buildOffline(snap);
|
|
buildOffline(snap);
|
|
|
|
|
+ buildTerminals(snap);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function renderFull(snap) {
|
|
function renderFull(snap) {
|
|
@@ -1084,15 +1099,21 @@
|
|
|
}, position: {x: p.x, y: p.y}, classes: cls.join(' ')});
|
|
}, position: {x: p.x, y: p.y}, classes: cls.join(' ')});
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // Every protocol-discovered link = one straight segment. Endpoints sit at
|
|
|
|
|
- // the label left-edge midpoint; the labels paint on top, so a parent line
|
|
|
|
|
- // starts under the parent label and ends exactly at the child's left edge.
|
|
|
|
|
|
|
+ // Every protocol-discovered link = one straight segment. Cytoscape's
|
|
|
|
|
+ // percentage endpoints are unreliable with preset labels, so use two
|
|
|
|
|
+ // invisible anchor nodes placed exactly on the label left-edge midpoint.
|
|
|
|
|
+ // The labels paint on top, so a parent line starts under the parent label
|
|
|
|
|
+ // and ends exactly at the child's left edge.
|
|
|
pathParent = buildParents(snap, L);
|
|
pathParent = buildParents(snap, L);
|
|
|
(snap.edges || []).forEach(function (e, i) {
|
|
(snap.edges || []).forEach(function (e, i) {
|
|
|
- if (!byId[e.source] || !byId[e.target]) return;
|
|
|
|
|
|
|
+ var ps = byId[e.source], pt = byId[e.target];
|
|
|
|
|
+ if (!ps || !pt) return;
|
|
|
|
|
+ var sp = 'sp_' + i, tp = 'tp_' + i;
|
|
|
|
|
+ els.push({group: 'nodes', data: {id: sp, type: 'edgepoint'}, position: {x: ps.x - ps.w / 2, y: ps.y}, selectable: false, grabbable: false});
|
|
|
|
|
+ els.push({group: 'nodes', data: {id: tp, type: 'edgepoint'}, position: {x: pt.x - pt.w / 2, y: pt.y}, selectable: false, grabbable: false});
|
|
|
var col = speedColor(e.speed, e.state);
|
|
var col = speedColor(e.speed, e.state);
|
|
|
els.push({group: 'edges', data: {
|
|
els.push({group: 'edges', data: {
|
|
|
- id: 'e_' + i, type: 'link', source: e.source, target: e.target, p: e.source, c: e.target,
|
|
|
|
|
|
|
+ id: 'e_' + i, type: 'link', source: sp, target: tp, p: e.source, c: e.target,
|
|
|
speed: e.speed, state: e.state, a_port: e.a_port, b_port: e.b_port,
|
|
speed: e.speed, state: e.state, a_port: e.a_port, b_port: e.b_port,
|
|
|
color: col, ls: (e.state && e.state !== 'up') ? 'dashed' : 'solid', w: Math.min(4, speedWidth(e.speed))
|
|
color: col, ls: (e.state && e.state !== 'up') ? 'dashed' : 'solid', w: Math.min(4, speedWidth(e.speed))
|
|
|
}});
|
|
}});
|
|
@@ -1110,18 +1131,31 @@
|
|
|
var bySw = {};
|
|
var bySw = {};
|
|
|
((lastSnap && lastSnap.terminals) || []).forEach(function (tm) {
|
|
((lastSnap && lastSnap.terminals) || []).forEach(function (tm) {
|
|
|
if (!byId[tm.sw_ip]) return;
|
|
if (!byId[tm.sw_ip]) return;
|
|
|
- if (onlyOff && tm.online) return;
|
|
|
|
|
|
|
+ if (onlyOff && terminalOnline(tm)) return;
|
|
|
(bySw[tm.sw_ip] = bySw[tm.sw_ip] || []).push(tm);
|
|
(bySw[tm.sw_ip] = bySw[tm.sw_ip] || []).push(tm);
|
|
|
});
|
|
});
|
|
|
Object.keys(bySw).forEach(function (sw) {
|
|
Object.keys(bySw).forEach(function (sw) {
|
|
|
var p = byId[sw]; if (!p || p.role === 'core' || p.role === 'main') return;
|
|
var p = byId[sw]; if (!p || p.role === 'core' || p.role === 'main') return;
|
|
|
- bySw[sw].sort(function (a, b) { return (a.online === b.online) ? 0 : (a.online ? -1 : 1); });
|
|
|
|
|
|
|
+ bySw[sw].sort(function (a, b) { return (terminalOnline(a) === terminalOnline(b)) ? 0 : (terminalOnline(a) ? -1 : 1); });
|
|
|
|
|
+ // A search hit is pinned into the visible 24 so any MAC in the snapshot
|
|
|
|
|
+ // can be located even when its switch has more than 24 terminals.
|
|
|
|
|
+ if (pinnedTerm) {
|
|
|
|
|
+ for (var pi = 0; pi < bySw[sw].length; pi++) {
|
|
|
|
|
+ if (bySw[sw][pi].mac === pinnedTerm) {
|
|
|
|
|
+ if (pi > 0) { var pin = bySw[sw].splice(pi, 1)[0]; bySw[sw].unshift(pin); }
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
bySw[sw].slice(0, 24).forEach(function (tm, k) {
|
|
bySw[sw].slice(0, 24).forEach(function (tm, k) {
|
|
|
var col = k % 12, row = Math.floor(k / 12);
|
|
var col = k % 12, row = Math.floor(k / 12);
|
|
|
var x = p.x - p.w / 2 + 16 + col * 9;
|
|
var x = p.x - p.w / 2 + 16 + col * 9;
|
|
|
var y = p.y + 16 + row * 9;
|
|
var y = p.y + 16 + row * 9;
|
|
|
- els.push({group: 'nodes', data: {id: 't_' + tm.mac, type: 'term', bg: tm.online ? '#2ECC71' : '#95a5a6', mac: tm.mac, ip: tm.ip, online: tm.online},
|
|
|
|
|
- position: {x: x, y: y}, selectable: false});
|
|
|
|
|
|
|
+ els.push({group: 'nodes', data: {
|
|
|
|
|
+ id: 't_' + tm.mac, type: 'term', bg: terminalOnline(tm) ? '#2ECC71' : '#95a5a6',
|
|
|
|
|
+ mac: tm.mac, ip: tm.ip, online: terminalOnline(tm), speed: tm.speed,
|
|
|
|
|
+ sw_name: tm.sw_name, port: tm.port, label: '', termText: terminalText(tm)
|
|
|
|
|
+ }, position: {x: x, y: y}, selectable: false});
|
|
|
els.push({group: 'edges', data: {id: 'te_' + tm.mac, type: 'term', source: sw, target: 't_' + tm.mac}});
|
|
els.push({group: 'edges', data: {id: 'te_' + tm.mac, type: 'term', source: sw, target: 't_' + tm.mac}});
|
|
|
});
|
|
});
|
|
|
});
|
|
});
|
|
@@ -1339,14 +1373,14 @@
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function buildOffline(snap) {
|
|
function buildOffline(snap) {
|
|
|
- var off = (snap.terminals || []).filter(function (tm) { return !tm.online; });
|
|
|
|
|
|
|
+ var off = (snap.terminals || []).filter(function (tm) { return !terminalOnline(tm); });
|
|
|
el('l2-offline-count').textContent = off.length;
|
|
el('l2-offline-count').textContent = off.length;
|
|
|
if (!off.length) { el('l2-offline-list').innerHTML = '<div class="panel-empty">' + t('l2.offline_empty', 'No offline terminals.') + '</div>'; return; }
|
|
if (!off.length) { el('l2-offline-list').innerHTML = '<div class="panel-empty">' + t('l2.offline_empty', 'No offline terminals.') + '</div>'; return; }
|
|
|
var h = '';
|
|
var h = '';
|
|
|
off.forEach(function (tm) {
|
|
off.forEach(function (tm) {
|
|
|
h += '<div class="off-row" data-mac="' + esc(tm.mac) + '">' +
|
|
h += '<div class="off-row" data-mac="' + esc(tm.mac) + '">' +
|
|
|
'<div class="off-main"><div class="off-mac">' + esc(tm.mac) + '</div>' +
|
|
'<div class="off-main"><div class="off-mac">' + esc(tm.mac) + '</div>' +
|
|
|
- '<div class="off-sub">' + esc(tm.sw_name || tm.sw_ip) + ' · ' + esc(tm.port || '') + '</div></div></div>';
|
|
|
|
|
|
|
+ '<div class="off-sub">' + esc(speedText(tm.speed)) + ' · ' + esc(tm.sw_name || tm.sw_ip) + ' · ' + esc(tm.port || '') + '</div></div></div>';
|
|
|
});
|
|
});
|
|
|
var box = el('l2-offline-list');
|
|
var box = el('l2-offline-list');
|
|
|
box.innerHTML = h;
|
|
box.innerHTML = h;
|
|
@@ -1358,6 +1392,39 @@
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function buildTerminals(snap) {
|
|
|
|
|
+ var list = (snap.terminals || []).filter(function (tm) { return !onlyOff || !terminalOnline(tm); });
|
|
|
|
|
+ if (el('l2-term-count')) el('l2-term-count').textContent = list.length;
|
|
|
|
|
+ var box = el('l2-term-list');
|
|
|
|
|
+ if (!box) return;
|
|
|
|
|
+ if (!showTerm) {
|
|
|
|
|
+ box.innerHTML = '<div class="panel-empty">' + t('l2.show_term', 'Show terminals') + '</div>';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!list.length) {
|
|
|
|
|
+ box.innerHTML = '<div class="panel-empty">' + t('l2.no_data', 'No data') + '</div>';
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ list.sort(function (a, b) {
|
|
|
|
|
+ var x = String(a.sw_name || a.sw_ip || '').localeCompare(String(b.sw_name || b.sw_ip || ''));
|
|
|
|
|
+ return x || String(a.mac).localeCompare(String(b.mac));
|
|
|
|
|
+ });
|
|
|
|
|
+ var h = '';
|
|
|
|
|
+ list.forEach(function (tm) {
|
|
|
|
|
+ var on = terminalOnline(tm);
|
|
|
|
|
+ h += '<div class="off-row term-row" data-mac="' + esc(tm.mac) + '">' +
|
|
|
|
|
+ '<span class="term-dot" style="background:' + (on ? '#2ECC71' : '#95a5a6') + '"></span>' +
|
|
|
|
|
+ '<div class="off-main"><div class="off-mac">' + esc(tm.mac) + '</div>' +
|
|
|
|
|
+ '<div class="off-sub"><span class="term-rate">' + esc(speedText(tm.speed)) + '</span> · ' +
|
|
|
|
|
+ (on ? t('l2.term_on', 'online') : t('l2.term_off', 'offline')) + ' · ' +
|
|
|
|
|
+ esc(tm.sw_name || tm.sw_ip || '') + ' · ' + esc(tm.port || '') + '</div></div></div>';
|
|
|
|
|
+ });
|
|
|
|
|
+ box.innerHTML = h;
|
|
|
|
|
+ box.querySelectorAll('.term-row').forEach(function (r) {
|
|
|
|
|
+ r.addEventListener('click', function () { revealById('t_' + r.getAttribute('data-mac')); });
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]; }); }
|
|
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]; }); }
|
|
|
|
|
|
|
|
/* ---------------- interactions ---------------- */
|
|
/* ---------------- interactions ---------------- */
|
|
@@ -1383,10 +1450,25 @@
|
|
|
if (id.indexOf('t_') === 0) {
|
|
if (id.indexOf('t_') === 0) {
|
|
|
var tm = (lastSnap.terminals || []).filter(function (x) { return 't_' + x.mac === id; })[0];
|
|
var tm = (lastSnap.terminals || []).filter(function (x) { return 't_' + x.mac === id; })[0];
|
|
|
if (!tm) return;
|
|
if (!tm) return;
|
|
|
|
|
+ pinnedTerm = tm.mac;
|
|
|
showTerm = true;
|
|
showTerm = true;
|
|
|
if (el('l2-showterm')) el('l2-showterm').checked = true;
|
|
if (el('l2-showterm')) el('l2-showterm').checked = true;
|
|
|
|
|
+ // A search hit must be visible even when the offline-only filter is on.
|
|
|
|
|
+ if (onlyOff && terminalOnline(tm)) {
|
|
|
|
|
+ onlyOff = false;
|
|
|
|
|
+ if (el('l2-onlyoff')) el('l2-onlyoff').checked = false;
|
|
|
|
|
+ }
|
|
|
if (!cy || cy.getElementById(id).empty()) render(lastSnap);
|
|
if (!cy || cy.getElementById(id).empty()) render(lastSnap);
|
|
|
- setTimeout(function () { focusNode(id); }, 90);
|
|
|
|
|
|
|
+ setTimeout(function () {
|
|
|
|
|
+ // Center the switch that the terminal hangs under, then flash the
|
|
|
|
|
+ // terminal dot so the exact port location is obvious.
|
|
|
|
|
+ focusNode(tm.sw_ip || id);
|
|
|
|
|
+ var tn = cy && cy.getElementById(id);
|
|
|
|
|
+ if (tn && tn.length) {
|
|
|
|
|
+ tn.addClass('hl');
|
|
|
|
|
+ setTimeout(function () { tn.removeClass('hl'); }, 2600);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 90);
|
|
|
} else {
|
|
} else {
|
|
|
focusNode(id);
|
|
focusNode(id);
|
|
|
}
|
|
}
|
|
@@ -1443,6 +1525,20 @@
|
|
|
showTip(e, 'Link ' + esc(d.a_port || '') + ' ↔ ' + esc(d.b_port || '') + '<br>' + sp + ' · ' + esc(d.state || ''));
|
|
showTip(e, 'Link ' + esc(d.a_port || '') + ' ↔ ' + esc(d.b_port || '') + '<br>' + sp + ' · ' + esc(d.state || ''));
|
|
|
});
|
|
});
|
|
|
cy.on('mouseout', 'edge', hideTip);
|
|
cy.on('mouseout', 'edge', hideTip);
|
|
|
|
|
+ cy.on('mouseover', 'node[type="term"]', function (e) {
|
|
|
|
|
+ var d = e.target.data();
|
|
|
|
|
+ e.target.data('label', d.termText || '');
|
|
|
|
|
+ showTip(e, '<b>' + esc(t('l2.term_title', 'Terminal')) + ' ' + esc(d.mac || '') + '</b><br>' +
|
|
|
|
|
+ esc(t('l2.term_mac', 'MAC address')) + ': ' + esc(d.mac || '') + '<br>' +
|
|
|
|
|
+ esc(t('l2.term_ip', 'IP address')) + ': ' + esc(d.ip || '—') + '<br>' +
|
|
|
|
|
+ esc(t('l2.term_rate', 'Link rate')) + ': ' + esc(speedText(d.speed)) + '<br>' +
|
|
|
|
|
+ esc(t('l2.term_switch', 'Switch')) + ': ' + esc(d.sw_name || '—') + '<br>' +
|
|
|
|
|
+ esc(t('l2.term_port', 'Port')) + ': ' + esc(d.port || '—'));
|
|
|
|
|
+ });
|
|
|
|
|
+ cy.on('mouseout', 'node[type="term"]', function (e) {
|
|
|
|
|
+ e.target.data('label', '');
|
|
|
|
|
+ hideTip();
|
|
|
|
|
+ });
|
|
|
cy.on('tap', 'node[type="switch"]', function (e) {
|
|
cy.on('tap', 'node[type="switch"]', function (e) {
|
|
|
lockedPath = e.target.id();
|
|
lockedPath = e.target.id();
|
|
|
highlightPath(lockedPath);
|
|
highlightPath(lockedPath);
|
|
@@ -1476,7 +1572,9 @@
|
|
|
function buildIndex(snap) {
|
|
function buildIndex(snap) {
|
|
|
var idx = [];
|
|
var idx = [];
|
|
|
(snap.nodes || []).forEach(function (n) { idx.push({id: n.id, label: n.name, sub: n.ip, kind: 'sw'}); });
|
|
(snap.nodes || []).forEach(function (n) { idx.push({id: n.id, label: n.name, sub: n.ip, kind: 'sw'}); });
|
|
|
- (snap.terminals || []).forEach(function (tm) { idx.push({id: 't_' + tm.mac, label: tm.ip || tm.mac, sub: tm.mac + ' · ' + (tm.sw_name || ''), kind: 'term', mac: tm.mac}); });
|
|
|
|
|
|
|
+ (snap.terminals || []).forEach(function (tm) {
|
|
|
|
|
+ idx.push({id: 't_' + tm.mac, label: tm.mac, sub: (tm.ip || '') + ' · ' + speedText(tm.speed) + ' · ' + (tm.sw_name || ''), kind: 'term', mac: tm.mac});
|
|
|
|
|
+ });
|
|
|
return idx;
|
|
return idx;
|
|
|
}
|
|
}
|
|
|
var INDEX = [];
|
|
var INDEX = [];
|
|
@@ -1485,9 +1583,12 @@
|
|
|
q = (q || '').trim().toLowerCase();
|
|
q = (q || '').trim().toLowerCase();
|
|
|
if (!q) { box.style.display = 'none'; box.innerHTML = ''; return; }
|
|
if (!q) { box.style.display = 'none'; box.innerHTML = ''; return; }
|
|
|
var hits = [];
|
|
var hits = [];
|
|
|
|
|
+ var qn = q.replace(/[^a-z0-9]/g, '');
|
|
|
for (var i = 0; i < INDEX.length && hits.length < 14; i++) {
|
|
for (var i = 0; i < INDEX.length && hits.length < 14; i++) {
|
|
|
var it = INDEX[i];
|
|
var it = INDEX[i];
|
|
|
- if ((it.label && it.label.toLowerCase().indexOf(q) >= 0) || (it.sub && it.sub.toLowerCase().indexOf(q) >= 0)) hits.push(it);
|
|
|
|
|
|
|
+ var hay = ((it.label || '') + ' ' + (it.sub || '')).toLowerCase();
|
|
|
|
|
+ var hayN = hay.replace(/[^a-z0-9]/g, '');
|
|
|
|
|
+ if (hay.indexOf(q) >= 0 || (qn && hayN.indexOf(qn) >= 0)) hits.push(it);
|
|
|
}
|
|
}
|
|
|
if (!hits.length) { box.innerHTML = '<div class="sug-empty">—</div>'; box.style.display = 'block'; return; }
|
|
if (!hits.length) { box.innerHTML = '<div class="sug-empty">—</div>'; box.style.display = 'block'; return; }
|
|
|
box.innerHTML = hits.map(function (h) {
|
|
box.innerHTML = hits.map(function (h) {
|