model-navigator-standalone
Version:
Standalone MDF graph model visualizer
227 lines (224 loc) • 8.98 kB
JavaScript
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
import "core-js/modules/es.array.includes.js";
import "core-js/modules/es.json.stringify.js";
import "core-js/modules/es.regexp.exec.js";
import "core-js/modules/es.string.includes.js";
import "core-js/modules/es.string.search.js";
import _ from 'lodash';
import Fuse from 'fuse.js';
const lc = str => "".concat(str).toLowerCase();
const searchHistoryLocalStorageKey = 'datadictionary:searchHistory';
const getType = prop => {
if (prop.type === 'value_set') {
return prop.valueSet();
} else {
return prop.type || 'UNDEFINED';
}
};
export const prepareSearchData = model => {
const searchData = model.nodes() // eslint-disable-line no-undef
.map(node => {
const properties = node.props().map(prop => {
let type = getType(prop);
if (type === 'UNDEFINED') type = undefined;
const propertyDescription = prop.desc;
const splitText = propertyDescription ? propertyDescription.split('<br>')[0] : propertyDescription;
return {
name: lc(prop.handle),
description: lc(splitText),
type
};
});
return {
id: node.handle,
title: lc(node.handle),
description: lc(node.desc),
properties
};
});
return searchData; // eslint-disable-line no-undef
};
export const ERR_KEYWORD_TOO_SHORT = 'Keyword too short, try longer keyword.';
export const filterMatches = (results, keyword) => {
if (results && results.length > 0) {
results.forEach(item => {
const {
matches
} = item;
if (matches.length > 0) {
matches.forEach(match => {
const highlightIndices = [];
const {
indices,
value
} = match;
if (match.indices.length > 0) {
indices.forEach(index => {
if (match.key !== 'title') {
const text = value.slice(index[0], index[1] + 1);
if ("".concat(text).toLowerCase().includes(keyword.toLowerCase())) {
const initIndex = "".concat(text).indexOf(keyword, 0);
const diff = index[1] - index[0];
if (diff >= keyword.length) {
index[0] += initIndex;
index[1] = index[0] + keyword.length - 1;
}
highlightIndices.push(index);
}
}
});
if (highlightIndices.length > 0) {
match.indices = _.cloneDeep(highlightIndices);
}
}
});
}
});
}
};
/**
* Call Fuse search and returns search result
* @params [Object] searchData - see prepareSearchData returns
* @params [string] keyword
* @returns [SearchResultItemShape[]] (see ../../utils).
*/
export const searchKeyword = (searchData, keyword) => {
if (!keyword || keyword.length < 2) {
return {
result: [],
errorMsg: ERR_KEYWORD_TOO_SHORT
};
}
// 32 is length limitation of Fuse search library
if (keyword.length > 32) {
keyword = keyword.substring(0, 32);
}
const halfLength = Math.round(keyword.length / 2);
const minMatchCharLength = Math.min(Math.max(halfLength, 10), keyword.length);
const options = {
keys: ['title', 'description', 'properties.name', 'properties.description', 'properties.type'],
includeMatches: true,
threshold: 0,
shouldSort: true,
includeScore: true,
minMatchCharLength,
ignoreLocation: true
};
const handler = new Fuse(searchData, options);
const result = handler.search(keyword).map(resItem => {
// A bug in Fuse sometimes returns wrong indices that end < start
const matches = resItem.matches.filter(matchItem => matchItem.indices[0][1] >= matchItem.indices[0][0]);
return _objectSpread(_objectSpread({}, resItem), {}, {
matches
});
}).map(resItem => {
// filter out matches that is too shorter than keyword
const matches = resItem.matches.filter(matchItem => {
const matchLen = matchItem.indices[0][1] - matchItem.indices[0][0] + 1;
return matchLen >= halfLength;
});
return _objectSpread(_objectSpread({}, resItem), {}, {
matches
});
}).filter(resItem => resItem.matches.length > 0);
const errorMsg = result && result.length > 0 ? '' : "No results found.";
filterMatches(result, keyword);
return {
result,
errorMsg
};
};
/**
* Prepare search items for Fuse.io library, call Fuse constructor
* and return a search instance handler.
* @params [SearchResultItemShape[]] search result (SearchResultItemShape from '../../utils')
* @returns [Object] summary
*/
export const getSearchSummary = result => {
const matchedNodeIDsInNameAndDescription = [];
const matchedNodeIDsInProperties = [];
const generalMatchedNodeIDs = [];
let matchedPropertiesCount = 0;
let matchedNodeNameAndDescriptionsCount = 0;
result.forEach(resItem => {
const nodeID = resItem.item.id;
resItem.matches.forEach(matchedItem => {
switch (matchedItem.key) {
case 'properties.type':
case 'properties.name':
case 'properties.description':
matchedPropertiesCount += matchedItem.indices && matchedItem.indices.length;
if (!matchedNodeIDsInProperties.includes(nodeID)) {
matchedNodeIDsInProperties.push(nodeID);
}
if (!generalMatchedNodeIDs.includes(nodeID)) {
generalMatchedNodeIDs.push(nodeID);
}
break;
case 'title':
case 'description':
matchedNodeNameAndDescriptionsCount += matchedItem.indices && matchedItem.indices.length;
if (!matchedNodeIDsInNameAndDescription.includes(nodeID)) {
matchedNodeIDsInNameAndDescription.push(nodeID);
}
if (!generalMatchedNodeIDs.includes(nodeID)) {
generalMatchedNodeIDs.push(nodeID);
}
break;
default:
break;
}
});
});
return {
matchedPropertiesCount,
matchedNodeNameAndDescriptionsCount,
matchedNodeIDsInNameAndDescription,
matchedNodeIDsInProperties,
generalMatchedNodeIDs
};
};
export const retrieveSearchHistoryItems = () => {
const items = JSON.parse(localStorage.getItem(searchHistoryLocalStorageKey));
return items;
};
export const storeSearchHistoryItem = searchHistoryItem => {
const {
keywordStr
} = searchHistoryItem;
if (!keywordStr || keywordStr.length === 0) return retrieveSearchHistoryItems();
const prevHistory = JSON.parse(localStorage.getItem(searchHistoryLocalStorageKey));
let newHistory = [];
if (prevHistory) newHistory = prevHistory.slice(0); // clone array
// if item already exists, need to remove item before adding to the beginning
if (prevHistory && prevHistory.find(item => item.keywordStr === keywordStr)) {
const index = prevHistory.findIndex(item => item.keywordStr === keywordStr);
newHistory = prevHistory.slice(0);
newHistory.splice(index, 1); // remove item
}
newHistory.unshift(searchHistoryItem); // add to the beginning
localStorage.setItem(searchHistoryLocalStorageKey, JSON.stringify(newHistory));
return newHistory;
};
export const clearSearchHistoryItems = () => {
const newHistory = [];
localStorage.setItem(searchHistoryLocalStorageKey, JSON.stringify(newHistory));
return newHistory;
};
export const calcMatchedStrings = result => {
const matchedStrings = {};
result.forEach(resItem => {
resItem.matches.forEach(matchItem => {
if (!matchedStrings[matchItem.value]) {
matchedStrings[matchItem.value] = {
matchedPieceIndices: matchItem.indices.map(arr => [arr[0], arr[1] + 1])
};
}
});
});
return matchedStrings;
};