Path: blob/master/webroot/rsrc/js/application/diffusion/DiffusionLocateFileSource.js
12241 views
/**1* @provides javelin-diffusion-locate-file-source2* @requires javelin-install3* javelin-dom4* javelin-typeahead-preloaded-source5* javelin-util6* @javelin7*/89JX.install('DiffusionLocateFileSource', {1011extend: 'TypeaheadPreloadedSource',1213construct: function(uri) {14JX.TypeaheadPreloadedSource.call(this, uri);15this.cache = {};16},1718members: {19tree: null,20limit: 20,21cache: null,2223ondata: function(results) {24this.tree = results.tree;2526if (this.lastValue !== null) {27this.matchResults(this.lastValue);28}2930this.setReady(true);31},323334/**35* Match a query and show results in the typeahead.36*/37matchResults: function(value, partial) {38// For now, just pretend spaces don't exist.39var search = value.toLowerCase();40search = search.replace(' ', '');4142var paths = this.findResults(search);4344var nodes = [];45for (var ii = 0; ii < paths.length; ii++) {46var path = paths[ii];47var name = [];48name.push(path.path.substr(0, path.pos));49name.push(50JX.$N('strong', {}, path.path.substr(path.pos, path.score)));5152var pos = path.score;53var lower = path.path.toLowerCase();54for (var jj = path.pos + path.score; jj < path.path.length; jj++) {55if (lower.charAt(jj) == search.charAt(pos)) {56pos++;57name.push(JX.$N('strong', {}, path.path.charAt(jj)));58if (pos == search.length) {59break;60}61} else {62name.push(path.path.charAt(jj));63}64}6566if (jj < path.path.length - 1 ) {67name.push(path.path.substr(jj + 1));68}6970var attr = {71className: 'visual-only phui-icon-view phui-font-fa fa-file'72};73var icon = JX.$N('span', attr, '');7475nodes.push(76JX.$N(77'a',78{79sigil: 'typeahead-result',80className: 'jx-result diffusion-locate-file',81ref: path.path82},83[icon, name]));84}8586this.invoke('resultsready', nodes, value);87if (!partial) {88this.invoke('complete');89}90},919293/**94* Find the results matching a query.95*/96findResults: function(search) {97if (!search.length) {98return [];99}100101// We know that the results for "abc" are always a subset of the results102// for "a" and "ab" -- and there's a good chance we already computed103// those result sets. Find the longest cached result which is a prefix104// of the search query.105var best = 0;106var start = this.tree;107for (var k in this.cache) {108if ((k.length <= search.length) &&109(k.length > best) &&110(search.substr(0, k.length) == k)) {111best = k.length;112start = this.cache[k];113}114}115116var matches;117if (start === null) {118matches = null;119} else {120matches = this.matchTree(start, search, 0);121}122123// Save this tree in cache; throw the cache away after a few minutes.124if (!(search in this.cache)) {125this.cache[search] = matches;126setTimeout(127JX.bind(this, function() { delete this.cache[search]; }),1281000 * 60 * 5);129}130131if (!matches) {132return [];133}134135var paths = [];136this.buildPaths(matches, paths, '', search, []);137138paths.sort(139function(u, v) {140if (u.score != v.score) {141return (v.score - u.score);142}143144if (u.pos != v.pos) {145return (u.pos - v.pos);146}147148return ((u.path > v.path) ? 1 : -1);149});150151var num = Math.min(paths.length, this.limit);152var results = [];153for (var ii = 0; ii < num; ii++) {154results.push(paths[ii]);155}156157return results;158},159160161/**162* Select the subtree that matches a query.163*/164matchTree: function(tree, value, pos) {165var matches = null;166for (var k in tree) {167var p = pos;168169if (p != value.length) {170p = this.matchString(k, value, pos);171}172173var result;174if (p == value.length) {175result = tree[k];176} else {177if (tree == 1) {178continue;179} else {180result = this.matchTree(tree[k], value, p);181if (!result) {182continue;183}184}185}186187if (!matches) {188matches = {};189}190matches[k] = result;191}192193return matches;194},195196197/**198* Look for the needle in a string, returning how much of it was found.199*/200matchString: function(haystack, needle, pos) {201var str = haystack.toLowerCase();202var len = str.length;203for (var ii = 0; ii < len; ii++) {204if (str.charAt(ii) == needle.charAt(pos)) {205pos++;206if (pos == needle.length) {207break;208}209}210}211return pos;212},213214215/**216* Flatten a tree into paths.217*/218buildPaths: function(matches, paths, prefix, search) {219var first = search.charAt(0);220221for (var k in matches) {222if (matches[k] == 1) {223var path = prefix + k;224var lower = path.toLowerCase();225226var best = 0;227var pos = 0;228for (var jj = 0; jj < lower.length; jj++) {229if (lower.charAt(jj) != first) {230continue;231}232233var score = this.scoreMatch(lower, jj, search);234if (score == -1) {235break;236}237238if (score > best) {239best = score;240pos = jj;241if (best == search.length) {242break;243}244}245}246247paths.push({248path: path,249score: best,250pos: pos251});252253} else {254this.buildPaths(matches[k], paths, prefix + k, search);255}256}257},258259260/**261* Score a matching string by finding the longest prefix of the search262* query it contains contiguously.263*/264scoreMatch: function(haystack, haypos, search) {265var pos = 0;266for (var ii = haypos; ii < haystack.length; ii++) {267if (haystack.charAt(ii) == search.charAt(pos)) {268pos++;269if (pos == search.length) {270return pos;271}272} else {273ii++;274break;275}276}277278var rem = pos;279for (/* keep going */; ii < haystack.length; ii++) {280if (haystack.charAt(ii) == search.charAt(rem)) {281rem++;282if (rem == search.length) {283return pos;284}285}286}287288return -1;289}290291}292});293294295