mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-12 19:49:59 +05:00
Drive "search-as-you-type" resulting in slow and cumbersome experience #2129
Implementation of asyncSearch and async search result display to avoid bad experience on large drive. Includes also search time display and debug
This commit is contained in:
parent
2f9f752a59
commit
cc1c160233
@ -233,6 +233,7 @@
|
||||
font-size: 30px;
|
||||
margin: 15px;
|
||||
height: 2.5rem;
|
||||
position: relative;
|
||||
input {
|
||||
background: transparent;
|
||||
color: @cp_drive-fg;
|
||||
@ -262,6 +263,13 @@
|
||||
.cp-app-drive-tree-search-icon, .cp-app-drive-search-cancel {
|
||||
color: @cp_drive-fg;
|
||||
}
|
||||
.cp-app-drive-search-timing {
|
||||
font-size: 0.5em;
|
||||
position: absolute;
|
||||
right: 60px;
|
||||
top: 100%;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
.cp-app-drive-search-spinner {
|
||||
display: inline-flex;
|
||||
@ -271,7 +279,8 @@
|
||||
svg {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: -1rem;
|
||||
}
|
||||
.cp-spinner svg {
|
||||
animation: spin 10s linear infinite;
|
||||
@ -297,7 +306,8 @@
|
||||
}
|
||||
.cp-app-drive-search-spinner svg {
|
||||
width: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.3rem;
|
||||
margin-left: -0.75rem;
|
||||
}
|
||||
.cp-app-drive-search-noresult {
|
||||
font-size: 22px;
|
||||
@ -1146,4 +1156,3 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -1656,6 +1656,35 @@ const factory = (UserObject, Util, Hash,
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
var searchAsync = function (Env, value, cb, opts) {
|
||||
cb = Util.once(cb || function () {});
|
||||
var ret = [];
|
||||
var userObjects = _getUserObjects(Env);
|
||||
var index = 0;
|
||||
var next = function () {
|
||||
if (index >= userObjects.length) { return void cb(ret); }
|
||||
var uo = userObjects[index++];
|
||||
var fPath = _getUserObjectPath(Env, uo);
|
||||
var handleResults = function (results) {
|
||||
if (!results || !results.length) { return void next(); }
|
||||
if (fPath) {
|
||||
results.forEach(function (r) {
|
||||
r.inSharedFolder = true;
|
||||
r.paths.forEach(function (p) {
|
||||
Array.prototype.unshift.apply(p, fPath);
|
||||
});
|
||||
});
|
||||
}
|
||||
Array.prototype.push.apply(ret, results);
|
||||
next();
|
||||
};
|
||||
if (uo.searchAsync) {
|
||||
return void uo.searchAsync(value, handleResults, opts);
|
||||
}
|
||||
handleResults(uo.search(value));
|
||||
};
|
||||
next();
|
||||
};
|
||||
|
||||
var getRecentPads = function (Env) {
|
||||
var files = [];
|
||||
@ -1799,6 +1828,7 @@ const factory = (UserObject, Util, Hash,
|
||||
isStaticFile: callWithEnv(isStaticFile),
|
||||
getFiles: callWithEnv(getFiles),
|
||||
search: callWithEnv(search),
|
||||
searchAsync: callWithEnv(searchAsync),
|
||||
getRecentPads: callWithEnv(getRecentPads),
|
||||
getOwnedPads: callWithEnv(getOwnedPads),
|
||||
getTagsList: callWithEnv(getTagsList),
|
||||
|
||||
@ -782,6 +782,109 @@ const factory = (Util, Hash,
|
||||
|
||||
return ret;
|
||||
};
|
||||
exp.searchAsync = function (value, cb, opts) {
|
||||
cb = Util.once(cb || function () {});
|
||||
if (typeof(value) !== "string") { return void cb([]); }
|
||||
value = value.trim();
|
||||
var res = [];
|
||||
var allFilesList = files[FILES_DATA];
|
||||
var allSFList = files[SHARED_FOLDERS];
|
||||
var lValue = value.toLowerCase();
|
||||
var tags;
|
||||
if (/^#/.test(lValue)) {
|
||||
tags = [lValue.slice(1).trim()];
|
||||
}
|
||||
|
||||
var containsSearchedTag = function (T) {
|
||||
if (!tags) { return false; }
|
||||
if (!T.length) { return false; }
|
||||
T = T.map(function (t) { return t.toLowerCase(); });
|
||||
return tags.some(function (tag) {
|
||||
return T.some(function (t) {
|
||||
return t === tag;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var ids = getFiles([FILES_DATA, SHARED_FOLDERS]);
|
||||
var index = 0;
|
||||
var batchSize = opts && opts.batchSize ? opts.batchSize : 250;
|
||||
|
||||
var processFilesBatch = function () {
|
||||
console.log("SEARCH: " + new Date().toISOString() + " searchAsync files batch start");
|
||||
var end = Math.min(index + batchSize, ids.length);
|
||||
for (; index < end; index++) {
|
||||
var id = ids[index];
|
||||
var data = allFilesList[id] || allSFList[id];
|
||||
if (!data) { continue; }
|
||||
if (Array.isArray(data.tags) && containsSearchedTag(data.tags)) {
|
||||
res.push(id);
|
||||
continue;
|
||||
}
|
||||
var title = data.title || data.lastTitle;
|
||||
if ((title && title.toLowerCase().indexOf(lValue) !== -1) ||
|
||||
(data.filename && data.filename.toLowerCase().indexOf(lValue) !== -1)) {
|
||||
res.push(id);
|
||||
}
|
||||
}
|
||||
if (index < ids.length) {
|
||||
return void setTimeout(processFilesBatch, 0);
|
||||
}
|
||||
var href = Hash.getRelativeHref(value);
|
||||
if (href) {
|
||||
var hrefId = getIdFromHref(href);
|
||||
if (hrefId) { res.push(hrefId); }
|
||||
}
|
||||
res = Util.deduplicateString(res);
|
||||
|
||||
var ret = [];
|
||||
res.forEach(function (l) {
|
||||
ret.push({
|
||||
id: l,
|
||||
paths: findFile(l),
|
||||
data: exp.getFileData(l)
|
||||
});
|
||||
});
|
||||
|
||||
var resFolders = [];
|
||||
var stack = [{ folder: files[ROOT], path: [ROOT] }];
|
||||
var folderBatchSize = opts && opts.folderBatchSize ? opts.folderBatchSize : 50;
|
||||
var processFoldersBatch = function () {
|
||||
console.log("SEARCH: " + new Date().toISOString() + " searchAsync folders batch start");
|
||||
var processed = 0;
|
||||
while (stack.length && processed < folderBatchSize) {
|
||||
var current = stack.pop();
|
||||
var folder = current.folder;
|
||||
var path = current.path;
|
||||
for (var key in folder) {
|
||||
if (isFolder(folder[key]) && !isSharedFolder(folder[key])) {
|
||||
if (key.toLowerCase().indexOf(lValue) !== -1) {
|
||||
resFolders.push({
|
||||
id: null,
|
||||
paths: [path.concat(key)],
|
||||
data: {
|
||||
title: key
|
||||
}
|
||||
});
|
||||
}
|
||||
stack.push({ folder: folder[key], path: path.concat(key) });
|
||||
}
|
||||
}
|
||||
processed++;
|
||||
}
|
||||
if (stack.length) {
|
||||
return void setTimeout(processFoldersBatch, 0);
|
||||
}
|
||||
resFolders = resFolders.sort(function (a, b) {
|
||||
return a.data.title.toLowerCase() > b.data.title.toLowerCase();
|
||||
});
|
||||
ret = resFolders.concat(ret);
|
||||
cb(ret);
|
||||
};
|
||||
processFoldersBatch();
|
||||
};
|
||||
processFilesBatch();
|
||||
};
|
||||
exp.getRecentPads = function () {
|
||||
var allFiles = files[FILES_DATA];
|
||||
var sorted = Object.keys(allFiles).filter(function (a) { return allFiles[a]; })
|
||||
|
||||
@ -3931,6 +3931,9 @@ define([
|
||||
APP.Search = {};
|
||||
var displaySearch = function ($list, value) {
|
||||
var search = APP.Search;
|
||||
var logSearch = function (message) {
|
||||
console.log("SEARCH: " + new Date().toISOString() + " " + message);
|
||||
};
|
||||
var $div = $('<div>', {'id': 'cp-app-drive-search', 'class': 'cp-unselectable'});
|
||||
|
||||
$searchIcon.clone().appendTo($div);
|
||||
@ -3945,56 +3948,56 @@ define([
|
||||
draggable: false,
|
||||
tabindex: 1,
|
||||
}).keyup(function (e) {
|
||||
if (searching) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
logSearch("new text detected");
|
||||
var currentValue = $input.val().trim();
|
||||
if (search.to) { window.clearTimeout(search.to); }
|
||||
if (e.which === 13) {
|
||||
spinner.spin();
|
||||
var newLocation = [SEARCH, $input.val()];
|
||||
search.cursor = $input[0].selectionStart;
|
||||
if (!manager.comparePath(newLocation, currentPath.slice())) {
|
||||
searching = true;
|
||||
APP.displayDirectory(newLocation);
|
||||
}
|
||||
logSearch("search launched via enter");
|
||||
renderSearchResults($input.val());
|
||||
return;
|
||||
}
|
||||
if (e.which === 27) {
|
||||
$input.val('');
|
||||
logSearch("text field updated (escape clear)");
|
||||
search.cursor = 0;
|
||||
searching = true;
|
||||
APP.displayDirectory([SEARCH]);
|
||||
renderSearchResults('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentValue === "") {
|
||||
search.cursor = 0;
|
||||
APP.displayDirectory([SEARCH]);
|
||||
renderSearchResults('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentValue.length < 2) { return; } // Don't autosearch 1 character
|
||||
search.inputVersion = (search.inputVersion || 0) + 1;
|
||||
var inputVersion = search.inputVersion;
|
||||
search.to = window.setTimeout(function () {
|
||||
var newLocation = [SEARCH, $input.val()];
|
||||
if (inputVersion !== search.inputVersion) { return; }
|
||||
search.cursor = $input[0].selectionStart;
|
||||
if (currentValue === search.value) { return; }
|
||||
if (!manager.comparePath(newLocation, currentPath.slice())) {
|
||||
searching = true;
|
||||
APP.displayDirectory(newLocation);
|
||||
}
|
||||
logSearch("search launched via debounce");
|
||||
renderSearchResults($input.val());
|
||||
}, 500);
|
||||
}).on('click mousedown mouseup', function (e) {
|
||||
e.stopPropagation();
|
||||
}).on('focus', function () {
|
||||
logSearch("input focus");
|
||||
}).on('blur', function () {
|
||||
logSearch("input blur");
|
||||
}).val(value || '').appendTo($div);
|
||||
logSearch("text field updated (initial value)");
|
||||
search.value = typeof(value) === "string" ? value : '';
|
||||
$input[0].selectionStart = search.cursor || 0;
|
||||
$input[0].selectionEnd = search.cursor || 0;
|
||||
|
||||
var cancel = Icons.get('close', {class: 'cp-app-drive-search-cancel',title:Messages.cancel}); // not visible
|
||||
cancel.addEventListener('click', function () {
|
||||
$input.val('');
|
||||
logSearch("text field updated (cancel click)");
|
||||
search.cursor = 0;
|
||||
APP.displayDirectory([SEARCH]);
|
||||
});
|
||||
@ -4002,90 +4005,226 @@ define([
|
||||
|
||||
$div.append($spinnerContainer);
|
||||
$list.append($div);
|
||||
var $timing = $('<div>', {
|
||||
'class': 'cp-app-drive-search-timing'
|
||||
}).appendTo($div);
|
||||
setTimeout(function () {
|
||||
$input.focus();
|
||||
});
|
||||
|
||||
if (typeof(value) === "string" && value.trim()) {
|
||||
spinner.spin();
|
||||
} else {
|
||||
searching = false;
|
||||
return;
|
||||
}
|
||||
var clearSearchResults = function () {
|
||||
$list.children().not('#cp-app-drive-search').remove();
|
||||
};
|
||||
|
||||
setTimeout(function () {
|
||||
//$list.closest('#cp-app-drive-content-folder').addClass('cp-app-drive-content-list');
|
||||
var filesList = manager.search(value);
|
||||
if (!filesList.length) {
|
||||
$list.append(h('div.cp-app-drive-search-noresult', Messages.fm_noResult));
|
||||
var renderSearchResults = function (nextValue) {
|
||||
logSearch("launching search for value " + nextValue);
|
||||
if (searching==true) {
|
||||
logSearch("search already in progress");
|
||||
setTimeout(function () {
|
||||
logSearch("retrying search for value " + nextValue);
|
||||
renderSearchResults(nextValue);
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
var searchValue = typeof(nextValue) === "string" ? nextValue : '';
|
||||
search.value = searchValue;
|
||||
clearSearchResults();
|
||||
$timing.text('');
|
||||
if (!searchValue.trim()) {
|
||||
spinner.hide();
|
||||
searching = false;
|
||||
logSearch("finished search (empty query)");
|
||||
return;
|
||||
}
|
||||
var sortable = {};
|
||||
var sortableFolders = [];
|
||||
filesList.forEach(function (r) {
|
||||
// if r.id === null, then it's a folder, not a file
|
||||
r.paths.forEach(function (path) {
|
||||
if (!r.inSharedFolder &&
|
||||
APP.hideDuplicateOwned && manager.isDuplicateOwned(path)) { return; }
|
||||
var _path = path.slice();
|
||||
var key = path.pop();
|
||||
var root = manager.find(path);
|
||||
var obj = {
|
||||
path: path,
|
||||
_path: _path,
|
||||
key: key,
|
||||
root: root,
|
||||
data: r.data
|
||||
};
|
||||
if (manager.isFolder(root[key])) {
|
||||
sortableFolders.push(obj);
|
||||
var searchStartedAt = Date.now();
|
||||
spinner.spin();
|
||||
searching = true;
|
||||
search.displayVersion = (search.displayVersion || 0) + 1;
|
||||
var displayVersion = search.displayVersion;
|
||||
setTimeout(function () {
|
||||
if (displayVersion !== search.displayVersion) {
|
||||
spinner.hide();
|
||||
searching = false;
|
||||
return;
|
||||
}
|
||||
var latestValue = $input.val();
|
||||
if (latestValue !== searchValue) {
|
||||
spinner.hide();
|
||||
searching = false;
|
||||
return;
|
||||
}
|
||||
//$list.closest('#cp-app-drive-content-folder').addClass('cp-app-drive-content-list');
|
||||
logSearch("search execution scheduled for value " + searchValue);
|
||||
setTimeout(function () {
|
||||
if (displayVersion !== search.displayVersion) {
|
||||
spinner.hide();
|
||||
searching = false;
|
||||
return;
|
||||
}
|
||||
sortable[root[key]] = obj;
|
||||
});
|
||||
});
|
||||
var _folders = sortElements(true, [ROOT], sortableFolders, null, !getSortFolderDesc(), true);
|
||||
var sortableKeys = Object.keys(sortable).map(Number);
|
||||
var _files = sortElements(false, [ROOT], sortableKeys, APP.store[SORT_FILE_BY], !getSortFileDesc(), true);
|
||||
if ($input.val() !== searchValue) {
|
||||
spinner.hide();
|
||||
searching = false;
|
||||
return;
|
||||
}
|
||||
logSearch("search execution started for value " + searchValue);
|
||||
var searchExecStartedAt = Date.now();
|
||||
var handleSearchResults = function (filesList) {
|
||||
if (displayVersion !== search.displayVersion) {
|
||||
spinner.hide();
|
||||
searching = false;
|
||||
return;
|
||||
}
|
||||
if ($input.val() !== searchValue) {
|
||||
spinner.hide();
|
||||
searching = false;
|
||||
return;
|
||||
}
|
||||
var searchExecElapsed = Date.now() - searchExecStartedAt;
|
||||
logSearch("search execution done in " + searchExecElapsed + "ms");
|
||||
logSearch("starting render search results");
|
||||
var sortStartedAt = Date.now();
|
||||
if (!filesList.length) {
|
||||
$list.append(h('div.cp-app-drive-search-noresult', Messages.fm_noResult));
|
||||
spinner.hide();
|
||||
searching = false;
|
||||
var totalElapsed = Date.now() - searchStartedAt;
|
||||
logSearch("sort time 0ms (no results)");
|
||||
logSearch("render time 0ms (no results)");
|
||||
logSearch("total time " + totalElapsed + "ms");
|
||||
$timing.text(totalElapsed + 'ms');
|
||||
logSearch("finished render (no results)");
|
||||
return;
|
||||
}
|
||||
var sortable = {};
|
||||
var sortableFolders = [];
|
||||
filesList.forEach(function (r) {
|
||||
// if r.id === null, then it's a folder, not a file
|
||||
r.paths.forEach(function (path) {
|
||||
if (!r.inSharedFolder &&
|
||||
APP.hideDuplicateOwned && manager.isDuplicateOwned(path)) { return; }
|
||||
var _path = path.slice();
|
||||
var key = path.pop();
|
||||
var root = manager.find(path);
|
||||
var obj = {
|
||||
path: path,
|
||||
_path: _path,
|
||||
key: key,
|
||||
root: root,
|
||||
data: r.data
|
||||
};
|
||||
if (manager.isFolder(root[key])) {
|
||||
sortableFolders.push(obj);
|
||||
return;
|
||||
}
|
||||
sortable[root[key]] = obj;
|
||||
});
|
||||
});
|
||||
var _folders = sortElements(true, [ROOT], sortableFolders, null, !getSortFolderDesc(), true);
|
||||
var sortableKeys = Object.keys(sortable).map(Number);
|
||||
var _files = sortElements(false, [ROOT], sortableKeys, APP.store[SORT_FILE_BY], !getSortFileDesc(), true);
|
||||
var sortElapsed = Date.now() - sortStartedAt;
|
||||
|
||||
var addEl = function (obj, folder) {
|
||||
var $element = createElement(obj.path, obj.key, obj.root, folder);
|
||||
$element.addClass('cp-app-drive-element-notrash cp-app-drive-search-result');
|
||||
$element.off('contextmenu');
|
||||
$element.contextmenu(openContextMenu('default'));
|
||||
$element.data('context', 'default');
|
||||
if (folder) {
|
||||
$element.find('.cp-app-drive-element-list').css({
|
||||
visibility: 'hidden'
|
||||
}).text('');
|
||||
}
|
||||
if (manager.isPathIn(obj._path, ['hrefArray'])) {
|
||||
obj._path.pop();
|
||||
obj._path.push(obj.data.title);
|
||||
}
|
||||
var $path = $('<span>', {
|
||||
'class': 'cp-app-drive-search-path'
|
||||
}).appendTo($element.find('.cp-app-drive-element-name'));
|
||||
createTitle($path, obj._path);
|
||||
var addEl = function (obj, folder) {
|
||||
var $element = createElement(obj.path, obj.key, obj.root, folder);
|
||||
$element.addClass('cp-app-drive-element-notrash cp-app-drive-search-result');
|
||||
$element.off('contextmenu');
|
||||
$element.contextmenu(openContextMenu('default'));
|
||||
$element.data('context', 'default');
|
||||
if (folder) {
|
||||
$element.find('.cp-app-drive-element-list').css({
|
||||
visibility: 'hidden'
|
||||
}).text('');
|
||||
}
|
||||
if (manager.isPathIn(obj._path, ['hrefArray'])) {
|
||||
obj._path.pop();
|
||||
obj._path.push(obj.data.title);
|
||||
}
|
||||
var $path = $('<span>', {
|
||||
'class': 'cp-app-drive-search-path'
|
||||
}).appendTo($element.find('.cp-app-drive-element-name'));
|
||||
createTitle($path, obj._path);
|
||||
|
||||
$list.append($element);
|
||||
};
|
||||
if (_folders.length) { getFolderListHeader(true, true).appendTo($list); }
|
||||
_folders.forEach(function (el) {
|
||||
var obj = el;
|
||||
addEl(obj, true);
|
||||
$list.append($element);
|
||||
};
|
||||
|
||||
var renderStartedAt = Date.now();
|
||||
var appendQueue = [];
|
||||
if (_folders.length) {
|
||||
appendQueue.push(function () {
|
||||
getFolderListHeader(true, true).appendTo($list);
|
||||
});
|
||||
_folders.forEach(function (el) {
|
||||
var obj = el;
|
||||
appendQueue.push(function () {
|
||||
addEl(obj, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (_files.length) {
|
||||
appendQueue.push(function () {
|
||||
getFileListHeader(true).appendTo($list);
|
||||
});
|
||||
_files.forEach(function (el) {
|
||||
var obj = sortable[el];
|
||||
appendQueue.push(function () {
|
||||
addEl(obj, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var appendIndex = 0;
|
||||
var appendBatch = function () {
|
||||
var batchStartedAt = Date.now();
|
||||
if (displayVersion !== search.displayVersion) {
|
||||
spinner.hide();
|
||||
searching = false;
|
||||
return;
|
||||
}
|
||||
if ($input.val() !== searchValue) {
|
||||
spinner.hide();
|
||||
searching = false;
|
||||
return;
|
||||
}
|
||||
var end = Math.min(appendIndex + 30, appendQueue.length);
|
||||
for (; appendIndex < end; appendIndex++) {
|
||||
appendQueue[appendIndex]();
|
||||
}
|
||||
var batchElapsed = Date.now() - batchStartedAt;
|
||||
logSearch("render batch time " + batchElapsed + "ms (" + appendIndex + "/" + appendQueue.length + ")");
|
||||
if (appendIndex < appendQueue.length) {
|
||||
window.requestAnimationFrame(appendBatch);
|
||||
return;
|
||||
}
|
||||
var renderElapsed = Date.now() - renderStartedAt;
|
||||
setTimeout(collapseDrivePath);
|
||||
spinner.hide();
|
||||
searching = false;
|
||||
var totalElapsed = Date.now() - searchStartedAt;
|
||||
logSearch("sort time " + sortElapsed + "ms");
|
||||
logSearch("render time " + renderElapsed + "ms");
|
||||
logSearch("total time " + totalElapsed + "ms");
|
||||
$timing.text(totalElapsed + 'ms');
|
||||
logSearch("finished render (results displayed)");
|
||||
};
|
||||
|
||||
appendBatch();
|
||||
};
|
||||
|
||||
if (manager.searchAsync) {
|
||||
manager.searchAsync(searchValue, handleSearchResults);
|
||||
} else {
|
||||
handleSearchResults(manager.search(searchValue));
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
if (_files.length) { getFileListHeader(true).appendTo($list); }
|
||||
_files.forEach(function (el) {
|
||||
var obj = sortable[el];
|
||||
addEl(obj, false);
|
||||
});
|
||||
setTimeout(collapseDrivePath);
|
||||
spinner.hide();
|
||||
};
|
||||
|
||||
if (typeof(value) === "string") {
|
||||
renderSearchResults(value);
|
||||
} else {
|
||||
searching = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var displayRecent = function ($list, typeFilter) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user