cables_dev/cables_ui/src/ui/components/opsearch.js
import { Events } from "cables-shared-client";
import defaultOps from "../defaultops.js";
/**
* search through opdocs, e.g. for opselect
*
* @export
* @class OpSearch
* @extends {Events}
*/
export default class OpSearch extends Events
{
constructor()
{
super();
this._list = null;
this._wordsDb = null;
this.numPatchops = 0;
}
get list()
{
return this._list;
}
resetList()
{
this._list = null;
}
_buildList()
{
const perf = CABLES.UI.uiProfiler.start("opsearch.getlist");
const codeOpNames = this._getOpsNamesFromCode([], "Ops", Ops, "");
let items = this._createListItemsByNames(codeOpNames);
const docOpName = gui.opDocs.getOpDocs().map((ext) => { return ext.name; });
items = items.concat(this._createListItemsByNames(docOpName, items));
const extensionNames = gui.opDocs.getExtensions().map((ext) => { return ext.name; });
items = items.concat(this._createListItemsByNames(extensionNames, items));
const teamNamespaces = gui.opDocs.getTeamNamespaces().map((ext) => { return ext.name; });
items = items.concat(this._createListItemsByNames(teamNamespaces, items));
const namespace = CABLES.platform.getPatchOpsNamespace();
const patchOpNames = gui.opDocs.getNamespaceDocs(namespace).map((ext) => { return ext.name; });
this.numPatchops = CABLES.uniqueArray(patchOpNames || []).length;
items = items.concat(this._createListItemsByNames(patchOpNames, items));
const newList = {};
items.forEach((item) =>
{
if (!newList.hasOwnProperty(item.opId))
{
newList[item.opId] = item;
}
});
this._list = Object.values(newList);
this._list.sort((a, b) => { return b.pop - a.pop; });
perf.finish();
/// --------------
let maxPop = 0;
for (let i = 0; i < this._list.length; i++)
{
if (!this._list[i].shortName) this._list[i].shortName = this._list[i].name;
maxPop = Math.max(this._list[i].pop || 0, maxPop);
this._list[i].id = i;
this._list[i].summary = this._list[i].summary || "";
this._list[i]._summary = this._list[i].summary.toLowerCase();
this._list[i]._shortName = this._list[i].shortName.toLowerCase();
this._list[i]._lowerCaseName = this._list[i].name.toLowerCase();
this._list[i]._nameSpace = this._list[i].nameSpace.toLowerCase() + ".";
this._list[i]._nameSpaceFull = this._list[i].nameSpace.toLowerCase() + "." + this._list[i].shortName.toLowerCase();
const opdoc = gui.opDocs.getOpDocByName(this._list[i].name);
if (defaultOps.isDeprecatedOp(this._list[i].name) || (opdoc && opdoc.oldVersion)) this._list[i].old = true;
}
// console.log("opselect build list...");
this._rebuildWordList();
CABLES.UI.OPSELECT.maxPop = maxPop;
}
_searchWord(wordIndex, orig, list, query)
{
if (!query || query === " " || query === "") return;
const perf = CABLES.UI.uiProfiler.start("opsearch._searchWord");
for (let i = 0; i < list.length; i++)
{
if (wordIndex > 0 && list[i].score === 0) continue; // when second word was found, but first was not
let scoreDebug = "<b>Query: " + query + " </b><br/>";
let found = false;
let points = 0;
if (list[i].lowercasename.indexOf(query) > -1)
{
if (list[i].name === "Ops.Gl.MainLoop")
{
found = true;
scoreDebug += "+2 vip op<br/>";
points += 2;
}
}
if (list[i].abbrev && list[i].abbrev.indexOf(orig) === 0)
{
found = true;
let p = 2;
if (orig.length === 2)p = 6;
if (orig.length === 3)p = 4;
scoreDebug += "+" + p + " abbreviation<br/>";
points += p;
}
if (list[i].userOp && this._hideUserOps)
{
continue;
}
if (list[i]._summary.indexOf(query) > -1)
{
found = true;
points += 1;
scoreDebug += "+1 found in summary (" + query + ")<br/>";
}
if (list[i]._nameSpace.indexOf(query) > -1)
{
found = true;
points += 1;
scoreDebug += "+1 found in namespace (" + query + ")<br/>";
}
if (list[i]._shortName.indexOf(query) > -1)
{
found = true;
points += 4;
scoreDebug += "+4 found in shortname (" + query + ")<br/>";
}
if (list[i]._shortName == query)
{
found = true;
points += 5;
scoreDebug += "+5 query quals shortname<br/>";
}
if (orig.length > 1 && list[i]._lowerCaseName.indexOf(orig) > -1)
{
found = true;
points += 2;
scoreDebug += "+2 found full namespace (" + query + ")<br/>";
}
if (points == 0)
{
if (list[i]._lowerCaseName.indexOf(query) > -1)
{
found = true;
points += 2;
scoreDebug += "+2 found full namespace (" + query + ")<br/>";
}
}
if (list[i].collectionOpNames && list[i].collectionOpNames.indexOf(orig) > -1)
{
found = true;
points += 1;
scoreDebug += "+1 is op in collection (" + query + ")<br/>";
}
if (found)
{
if (this._newOpOptions)
{
const firstportfitspoints = 3;
const firstportfitsText = "+3 First Port fits<br/>";
const docs = gui.opDocs.getOpDocByName(list[i].name);
if (docs && docs.hasOwnProperty("version"))
{
const p = docs.version * 0.01;
points += p;
scoreDebug += "+" + p + " version<br/>";
}
if (docs && docs.layout && docs.layout.portsIn && docs.layout.portsOut && docs.layout.portsIn.length > 0 && docs.layout.portsOut.length > 0)
{
// when inserting into link - find fitting ports
if (this._newOpOptions.linkNewLink)
{
let foundPortTypeIn = false;
for (let j = 0; j < docs.layout.portsIn.length; j++)
{
if (docs.layout.portsIn[j] &&
this._newOpOptions.linkNewLink.portIn &&
docs.layout.portsIn[j].type == this._newOpOptions.linkNewLink.portIn.type)
{
foundPortTypeIn = true;
break;
}
}
let foundPortTypeOut = false;
for (let j = 0; j < docs.layout.portsOut.length; j++)
{
if (docs.layout.portsOut[j].type == this._newOpOptions.linkNewLink.portOut.type)
{
foundPortTypeOut = true;
break;
}
}
if (
docs.layout.portsIn[0].type == this._newOpOptions.linkNewLink.portOut.type &&
docs.layout.portsOut[0].type == this._newOpOptions.linkNewLink.portIn.type
)
{
points += firstportfitspoints;
scoreDebug += firstportfitsText;
}
if (!foundPortTypeOut && !foundPortTypeIn)
{
points -= 5.0; // seems harsh, but is only used when dragging a port, so it should be fine...
scoreDebug += "-5.0 no compatible port found<br/>";
}
}
// when dragging a port - find fitting input/output port
if (this._newOpOptions.linkNewOpToPort)
{
let foundPortType = false;
if (this._newOpOptions.linkNewOpToPort.direction === CABLES.PORT_DIR_OUT)
{
if (docs.layout.portsIn[0].type == this._newOpOptions.linkNewOpToPort.type)
{
points += firstportfitspoints;
scoreDebug += firstportfitsText;
}
for (let j = 0; j < docs.layout.portsIn.length; j++)
{
if (docs.layout.portsIn[j].type == this._newOpOptions.linkNewOpToPort.type)
{
foundPortType = true;
break;
}
}
}
else
{
if (docs.layout.portsOut[0].type == this._newOpOptions.linkNewOpToPort.type)
{
points += firstportfitspoints;
scoreDebug += firstportfitsText;
}
for (let j = 0; j < docs.layout.portsOut.length; j++)
{
if (docs.layout.portsOut[j].type == this._newOpOptions.linkNewOpToPort.type)
{
foundPortType = true;
break;
}
}
}
if (!foundPortType)
{
points -= 10.0; // seems harsh, but is only used when dragging a port, so it should be fine...
scoreDebug += "-10.0 no comparible port found<br/>";
}
}
}
}
if (list[i]._shortName.indexOf(orig) === 0)
{
points += 2.5;
scoreDebug += "+2.5 found in shortname at beginning (" + query + ")<br/>";
if (list[i]._shortName == orig)
{
points += 2;
scoreDebug += "+2 exact name (" + query + ")<br/>";
}
}
if (list[i]._nameSpace.indexOf("ops.math") > -1)
{
points += 1;
scoreDebug += "+1 is math op (" + query + ")<br/>";
}
else if (list[i]._nameSpace.indexOf("ops.patch") > -1)
{
points += 3;
scoreDebug += "+1 is patch op (" + query + ")<br/>";
}
else if (list[i]._nameSpace.indexOf("ops.team") > -1)
{
points += 2;
scoreDebug += "+2 is team op (" + query + ")<br/>";
}
const shortnessPoints = 2 * Math.round((1.0 - Math.min(1, (list[i]._nameSpace + list[i]._shortName).length / 100)) * 100) / 100;
points += shortnessPoints;
scoreDebug += "+" + shortnessPoints + " shortness namespace<br/>";
}
if (found && this._list[i].old)
{
points -= 1;
scoreDebug += "-1 outdated<br/>";
}
if (found && list[i].pop > 0)
{
points += (list[i].pop || 2) / CABLES.UI.OPSELECT.maxPop || 1;
}
if (found && this._list[i].notUsable)
{
points = 0.1;
scoreDebug += "0.1 not usable<br/>";
}
if (!found) points = 0;
if (points === 0 && list[i].score > 0) list[i].score = 0;
else list[i].score += points;
list[i].scoreDebug = (list[i].scoreDebug || "") + scoreDebug + " (" + Math.round(points * 100) / 100 + " points)<br/><br/>";
}
perf.finish();
}
search(query)
{
document.getElementById("realsearch").innerHTML = "";
document.getElementById("opOptions").innerHTML = "";
if (!query) return;
const origQuery = query;
if (this._wordsDb) // search through word db
{
let q = query;
const queryParts = [];
let found = false;
do
{
found = false;
for (let i = 0; i < this._wordsDb.length; i++)
{
const idx = q.indexOf(this._wordsDb[i]);
if (idx > -1) // && queryParts.indexOf(this._wordsDb[i])==-1
{
found = true;
queryParts.push(this._wordsDb[i]);
q = q.substr(0, idx) + " " + q.substr(idx + this._wordsDb[i].length, q.length - idx);
break;
}
}
}
while (found);
if (queryParts.length > 0)
{
let nquery = queryParts.join(" ");
nquery += " " + q;
if (nquery.trim() !== query) document.getElementById("realsearch").innerHTML = "Searching for: <b>" + nquery + "</b>";
query = nquery;
}
else document.getElementById("realsearch").innerHTML = "";
}
if (query.length > 1 && this._list)
{
for (let i = 0; i < this._list.length; i++)
{
this._list[i].score = 0;
this._list[i].scoreDebug = "";
}
if (query.indexOf(" ") > -1)
{
const words = query.split(" ");
for (let i = 0; i < words.length; i++) { this._searchWord(i, origQuery, this._list, words[i]); }
}
else
{
this._searchWord(0, query, this._list, query);
}
}
}
_rebuildWordList()
{
if (!this._list) return;
const buildWordDB = {};
for (let i = 0; i < this._list.length; i++)
{
const res = this._list[i].name.split(/(?=[A-Z,0-9,/.])/);
for (let j = 0; j < res.length; j++)
{
if (res[j][res[j].length - 2] === "_") res[j] = res[j].substr(0, res[j].length - 2);
if (res[j][0] === ".") res[j] = res[j].substr(1);
if (res[j].length > 2) buildWordDB[res[j].toLowerCase()] = 1;
}
let shortName = "";
const ccParts = this._list[i].shortName.split(/(?=[A-Z,0-9,/.])/);
for (let j = 0; j < ccParts.length; j++)
shortName += ccParts[j].substr(0, 1);
this._list[i].abbrev = shortName.toLocaleLowerCase();
}
this._wordsDb = Object.keys(buildWordDB);
this._wordsDb.sort((a, b) => { return b.length - a.length; });
}
_getOpsNamesFromCode(opNames, ns, val, parentname)
{
if (Object.prototype.toString.call(val) === "[object Object]")
{
for (const propertyName in val)
{
if (val.hasOwnProperty(propertyName))
{
const opName = ns + "." + parentname + propertyName;
if (typeof (CABLES.Patch.getOpClass(opName)) === "function") opNames.push(opName);
opNames = this._getOpsNamesFromCode(opNames, ns, val[propertyName], parentname + propertyName + ".");
}
}
}
return opNames;
}
_createListItemsByNames(opNames, listItems = [])
{
if (!opNames) return;
const items = [];
for (let i = 0; i < opNames.length; i++)
{
const opName = opNames[i];
if (!opName) continue;
const parts = opName.split(".");
const lowerCaseName = opName.toLowerCase() + "_" + parts.join("").toLowerCase();
const opDoc = gui.opDocs.getOpDocByName(opName);
let shortName = parts[parts.length - 1];
let hidden = false;
let opDocHidden = false;
let opId = null;
if (opDoc)
{
opId = opDoc.id;
opDocHidden = opDoc.hidden;
hidden = opDoc.hidden;
if (defaultOps.isNonCoreOp(opName))
{
shortName = opDoc.shortName;
}
else
{
shortName = opDoc.shortNameDisplay;
}
}
if (hidden)
{
if (defaultOps.isAdminOp(opName) && !gui.user.isAdmin) hidden = true;
}
if (defaultOps.isDevOp(opName) && !CABLES.platform.isDevEnv()) hidden = true;
parts.length -= 1;
const nameSpace = parts.join(".");
if (defaultOps.isCollection(opName))
{
const inUse = listItems && listItems.some((op) => { return op.name.startsWith(opName); });
if (inUse)
{
hidden = true;
}
}
if (!hidden)
{
let oldState = "";
if (hidden)oldState = "OLD";
if (opDocHidden)oldState = "OLD";
if (defaultOps.isDeprecatedOp(opName)) oldState = "DEPREC";
if (defaultOps.isAdminOp(opName)) oldState = "ADMIN";
let popularity = -1;
let summary = gui.opDocs.getSummary(opName);
let type = "op";
if (defaultOps.isTeamNamespace(opName)) type = "team";
if (defaultOps.isExtension(opName)) type = "extension";
if (defaultOps.isPatchOp(opName)) type = "patchop";
const isTeamOp = defaultOps.isTeamOp(opName);
const isCollection = defaultOps.isCollection(opName);
let collectionOpNames = null;
if (isCollection)
{
const a = gui.opDocs.getNamespaceDocs(opName);
if (a && a.length > 0 && a[0].ops) collectionOpNames = a[0].ops.join(" ").toLowerCase();
}
const op = {
"opId": opId || CABLES.simpleId(),
"name": opName,
"summary": summary,
"collectionOpNames": collectionOpNames,
"nscolor": defaultOps.getNamespaceClassName(opName),
"isOp": !defaultOps.isCollection(opName),
"userOp": defaultOps.isUserOp(opName),
"devOp": defaultOps.isDevOp(opName),
"extensionOp": defaultOps.isExtensionOp(opName),
"teamOp": defaultOps.isTeamOp(opName),
"patchOp": defaultOps.isPatchOp(opName),
"isExtension": defaultOps.isExtension(opName),
"isTeamNamespace": isTeamOp,
"shortName": shortName,
"nameSpace": nameSpace,
"oldState": oldState,
"lowercasename": lowerCaseName,
"isCollection": isCollection,
"buttonText": isCollection ? "Load" : "Add",
"type": type,
"pop": popularity,
};
if (opDoc && opDoc.notUsable)
{
op.notUsable = true;
op.notUsableReasons = opDoc.notUsableReasons;
}
if (defaultOps.isCollection(opName))
{
op.isOp = false;
op.pop = 1;
if (opDoc)
{
op.summary = opDoc.summary;
op.description = opDoc.description;
op.teamName = opDoc.teamName;
op.teamLink = opDoc.teamLink;
op.numOps = opDoc.numOps;
op.ops = opDoc.ops || [];
}
}
items.push(op);
}
}
return items;
}
}