search-filter component

This commit is contained in:
Artur
2025-12-04 14:50:52 +03:00
parent 90e57fff64
commit e07e696dff
8 changed files with 310 additions and 269 deletions

View File

@ -104,12 +104,6 @@
<button id="saveAsTextBtn" class="btn-text-default i18n" style="width: 100%;">Unlink citations</button>
</div>
<div id="mainState" class="flexCol flexSize hidden">
<div id="selectedWrapper">
<div class="flexCol flexSize flexCenter">
<div id="selectedHolder"></div>
</div>
<div id="selectedThumb" class="scrollThumb hidden"></div>
</div>
<label for="searchField" class="i18n">Search the references you want to cite in this document.</label>
<div id="searchWrapper">
<div id="librarySelectList"></div>
@ -121,7 +115,12 @@
</svg>
</button>
</div>
<div id="selectedWrapper">
<div class="flexCol flexSize flexCenter">
<div id="selectedHolder"></div>
</div>
<div id="selectedThumb" class="scrollThumb hidden"></div>
</div>
<div id="docsWrapper" class="flexSize">
<div class="flexCol flexSize">
<div id="docsHolder" class="flexCol"></div>

View File

@ -11,7 +11,7 @@ html {
--input-border-hover: #c0c0c0;
--input-border-focus: #848484;
--input-border-active: #c0c0c0;
--input-border-error: #f56c6c;
--input-border-error: #F62211;
--input-border-warning: #e6a23c;
--input-icon: #bdbdbd;
--input-icon-hover: #848484;
@ -31,7 +31,7 @@ html {
--input-border-hover-dark: #666666;
--input-border-focus-dark: rgb(204, 204, 204);
--input-border-active-dark: #666666;
--input-border-error-dark: #f56c6c;
--input-border-error-dark: #F62211;
--input-border-warning-dark: #e6a23c;
--input-icon-dark: rgba(255, 255, 255, 0.8);
--input-icon-hover-dark: white;

View File

@ -93,16 +93,18 @@
/** @type {CitationService} */
var citationService;
/** @type {{text: string, obj: SearchResult | null, groups: Array<SearchResult>}}} */
/** @type {{text: string, obj: SearchResult | null, groups: Array<SearchResult>, groupsHash: string}}} */
var lastSearch = {
text: "",
obj: null,
groups: [],
groupsHash: "",
};
/** @type {Object.<string, Button | InputField | SelectBox>} */
var customElements = {};
/** @type {SearchFilter} */
var searchFilter;
/** @type {Button} */
var saveAsTextBtn;
/** @type {Object.<string, HTMLElement | HTMLInputElement>} */
var elements = {};
function initElements() {
@ -225,23 +227,9 @@
if (!cslFileInput) {
throw new Error("cslFileInput not found");
}
customElements = {
searchField: new InputField("searchField", {
type: "text",
autofocus: true,
showClear: true,
}),
filterButton: new Button("filterButton", {
variant: "secondary-icon",
size: "small",
}),
librarySelectList: new SelectBox("librarySelectList", {
// TODO: add translation
placeholder: translate("No items selected"),
multiple: true,
}),
saveAsTextBtn: new Button("saveAsTextBtn"),
};
searchFilter = new SearchFilter();
saveAsTextBtn = new Button("saveAsTextBtn");
elements = {
loader: loader,
libLoader: libLoader,
@ -437,132 +425,10 @@
return sdk
.getUserGroups()
.then(function (/** @type {Array<UserGroupInfo>} */ groups) {
let selectedItem = localStorage.getItem("selectedGroup");
let hasSelected = false;
groups.forEach(function (group) {
group.id = String(group.id);
});
const customGroups = [
{ id: "my_library", name: translate("My Library") },
{
id: "group_libraries",
name: translate("Group Libraries"),
},
];
!hasSelected &&
customGroups.forEach(function (group) {
if (group.id === selectedItem) {
hasSelected = true;
}
});
!hasSelected &&
groups.forEach(function (group) {
if (group.id.toString() === selectedItem) {
hasSelected = true;
}
});
if (!hasSelected) {
selectedItem = "my_library";
hasSelected = true;
}
/**
* @param {string|number} id
* @param {string} name
*/
const addGroupToSelectBox = function (id, name) {
if (typeof id === "number") {
id = id.toString();
}
if (customElements.librarySelectList instanceof SelectBox)
customElements.librarySelectList.addItem(
id,
name,
id === selectedItem
);
};
for (var i = 0; i < customGroups.length; i++) {
const id = customGroups[i].id;
const name = customGroups[i].name;
addGroupToSelectBox(id, name);
}
if (groups.length === 0) {
return;
}
customElements.librarySelectList.addSeparator();
for (var i = 0; i < groups.length; i++) {
const id = groups[i].id;
const name = groups[i].name;
addGroupToSelectBox(id, name);
}
selectedGroupsWatcher(customGroups, groups);
searchFilter.addGroups(groups);
});
}
/**
* @param {Array<{id: string, name: string}>} customGroups
* @param {Array<UserGroupInfo>} groups
* @returns
*/
function selectedGroupsWatcher(customGroups, groups) {
if (customElements.librarySelectList instanceof SelectBox === false) {
return;
}
customElements.librarySelectList.subscribe(function (event) {
if (event.type !== "selectbox:change") {
return;
}
const values = event.detail.values;
const current = event.detail.current;
const bEnabled = event.detail.enabled;
const customIds = customGroups.map(function (group) {
return group.id;
});
let ids = groups.map(function (group) {
return group.id;
});
let bWasCustom = customIds.indexOf(String(current)) !== -1;
if (bWasCustom && current === "group_libraries") {
if (bEnabled) {
customElements.librarySelectList.selectItems(ids, true);
} else {
customElements.librarySelectList.unselectItems(ids, true);
}
} else if (!bWasCustom) {
let bAllGroupsSelected = ids.every(function (id) {
return values.indexOf(id) !== -1;
});
if (bAllGroupsSelected) {
customElements.librarySelectList.selectItems(
"group_libraries",
true
);
} else {
customElements.librarySelectList.unselectItems(
"group_libraries",
true
);
}
}
});
}
/**
* @return {number|"my_library"|"group_libraries"}
*/
function getSelectedGroup() {
const id = customElements.librarySelectList.getValue();
if (id === "my_library" || id === "group_libraries") {
return id;
}
return Number(id);
}
/**
* @param {Array<StyleInfo>} stylesInfo
*/
@ -678,50 +544,45 @@
/**
* @param {string} text
* @param {Array<string|"my_library"|"group_libraries">} selectedGroups
* @returns
*/
function searchFor(text) {
if (elements.mainState.classList.contains(displayNoneClass)) return;
function searchFor(text, selectedGroups) {
text = text.trim();
if (!text) return;
if (text == lastSearch.text) return;
lastSearch.text = text;
lastSearch.obj = null;
lastSearch.groups = [];
const groupsHash = selectedGroups.join(",");
if (
elements.mainState.classList.contains(displayNoneClass) ||
!text ||
(text == lastSearch.text &&
groupsHash === lastSearch.groupsHash) ||
selectedGroups.length === 0
)
return Promise.resolve();
clearLibrary();
/** @type {Array<Promise<void>>} */
const promises = [];
const selectedGroup = getSelectedGroup();
return sdk
.getUserGroups()
.then(function (
/** @type {Array<UserGroupInfo>} */ userGroups
) {
/** @type {Array<string|number>} */
let groups = [];
switch (selectedGroup) {
case "my_library":
groups = [];
break;
case "group_libraries":
groups = userGroups.map(function (group) {
return group.id;
});
break;
default:
groups = [selectedGroup];
break;
}
let groups = selectedGroups.filter(function (group) {
return (
group !== "my_library" &&
group !== "group_libraries"
);
});
const append = true;
let showLoader = true;
let hideLoader = !groups.length;
const bCount = true;
if (selectedGroup === "my_library") {
if (selectedGroups.indexOf("my_library") !== -1) {
promises.push(
loadLibrary(
sdk.getItems(text),
@ -739,7 +600,7 @@
hideLoader = i === groups.length - 1;
promises.push(
loadLibrary(
sdk.getGroupItems(lastSearch.text, groups[i]),
sdk.getGroupItems(text, groups[i]),
append,
showLoader,
hideLoader,
@ -750,31 +611,16 @@
}
})
.then(function () {
lastSearch.text = text;
lastSearch.obj = null;
lastSearch.groups = [];
lastSearch.groupsHash = groupsHash;
return Promise.all(promises);
});
}
if (customElements.searchField instanceof HTMLInputElement) {
customElements.searchField.subscribe(function (e) {
if (
e.type === "inputfield:blur" ||
e.type === "inputfield:submit"
) {
searchFor(e.detail.value);
}
});
}
if (customElements.filterButton instanceof Button) {
customElements.filterButton.subscribe(function (e) {
if (e.type === "button:click") {
if (!customElements.librarySelectList.isOpen) {
if (e.detail.originalEvent) {
e.detail.originalEvent.stopPropagation();
}
customElements.librarySelectList.openDropdown();
}
}
});
}
searchFilter.subscribe(function (text, selectedGroups) {
searchFor(text, selectedGroups);
});
elements.cancelBtn.onclick = function (e) {
var ids = [];
@ -885,17 +731,15 @@
});
};
if (customElements.saveAsTextBtn instanceof Button) {
customElements.saveAsTextBtn.subscribe(function (event) {
if (event.type !== "button:click") {
return;
}
showLoader(true);
citationService.saveAsText().then(function () {
showLoader(false);
});
saveAsTextBtn.subscribe(function (event) {
if (event.type !== "button:click") {
return;
}
showLoader(true);
citationService.saveAsText().then(function () {
showLoader(false);
});
}
});
elements.locatorLabelsList.addEventListener("click", function (e) {
const target = e.target;

View File

@ -0,0 +1,195 @@
// @ts-check
/// <reference path="../../types-global.js" />
/// <reference path="../ui/input.js" />
/// <reference path="../ui/selectbox.js" />
/// <reference path="../ui/button.js" />
function SearchFilter() {
this._searchField = new InputField("searchField", {
type: "text",
autofocus: true,
showClear: true,
});
this._filterButton = new Button("filterButton", {
variant: "secondary-icon",
size: "small",
});
this._librarySelectList = new SelectBox("librarySelectList", {
// TODO: add translation
placeholder: translate("No items selected"),
multiple: true,
});
/** @type {Function[]} */
this._subscribers = [];
}
SearchFilter.prototype._addEventListeners = function () {
const self = this;
this._searchField.subscribe(function (e) {
if (e.type === "inputfield:blur" || e.type === "inputfield:submit") {
const selectedGroups = self._getSelectedGroups();
self._subscribers.forEach(function (cb) {
cb(e.detail.value, selectedGroups);
});
}
});
this._filterButton.subscribe(function (e) {
if (e.type === "button:click") {
if (!self._librarySelectList.isOpen) {
if (e.detail.originalEvent) {
e.detail.originalEvent.stopPropagation();
}
self._librarySelectList.openDropdown();
}
}
});
};
/**
* @param {Array<UserGroupInfo>} groups
*/
SearchFilter.prototype.addGroups = function (groups) {
const self = this;
let selectedItem = localStorage.getItem("selectedGroup");
let hasSelected = false;
groups.forEach(function (group) {
group.id = String(group.id);
});
const customGroups = [
{ id: "my_library", name: translate("My Library") },
{
id: "group_libraries",
name: translate("Group Libraries"),
},
];
!hasSelected &&
customGroups.forEach(function (group) {
if (group.id === selectedItem) {
hasSelected = true;
}
});
!hasSelected &&
groups.forEach(function (group) {
if (group.id.toString() === selectedItem) {
hasSelected = true;
}
});
if (!hasSelected) {
selectedItem = "my_library";
hasSelected = true;
}
/**
* @param {string|number} id
* @param {string} name
*/
const addGroupToSelectBox = function (id, name) {
if (typeof id === "number") {
id = id.toString();
}
if (self._librarySelectList instanceof SelectBox)
self._librarySelectList.addItem(id, name, id === selectedItem);
};
for (var i = 0; i < customGroups.length; i++) {
const id = customGroups[i].id;
const name = customGroups[i].name;
addGroupToSelectBox(id, name);
}
if (groups.length === 0) {
return;
}
this._librarySelectList.addSeparator();
for (var i = 0; i < groups.length; i++) {
const id = groups[i].id;
const name = groups[i].name;
addGroupToSelectBox(id, name);
}
this._selectedGroupsWatcher(customGroups, groups);
};
/**
* @return {Array<string|"my_library"|"group_libraries">}
*/
SearchFilter.prototype._getSelectedGroups = function () {
const self = this;
const ids = this._librarySelectList.getSelectedValues();
if (Array.isArray(ids) === false || ids.length === 0) {
setTimeout(function () {
self._librarySelectList.openDropdown();
}, 500);
}
if (ids === null || typeof ids === "string") {
return [];
}
return ids;
};
/**
* @param {function(string, Array<string|"my_library"|"group_libraries">): void} callback
* @returns {Object}
*/
SearchFilter.prototype.subscribe = function (callback) {
var self = this;
this._subscribers.push(callback);
return {
unsubscribe: function () {
self._subscribers = self._subscribers.filter(function (cb) {
return cb !== callback;
});
},
};
};
/**
* @param {Array<{id: string, name: string}>} customGroups
* @param {Array<UserGroupInfo>} groups
* @returns
*/
SearchFilter.prototype._selectedGroupsWatcher = function (
customGroups,
groups
) {
const self = this;
if (this._librarySelectList instanceof SelectBox === false) {
return;
}
this._librarySelectList.subscribe(function (event) {
if (event.type !== "selectbox:change") {
return;
}
const values = event.detail.values;
const current = event.detail.current;
const bEnabled = event.detail.enabled;
const customIds = customGroups.map(function (group) {
return group.id;
});
/** @type {Array<string>} */
let ids = groups.map(function (group) {
return group.id.toString();
});
let bWasCustom = customIds.indexOf(String(current)) !== -1;
if (bWasCustom && current === "group_libraries") {
if (bEnabled) {
self._librarySelectList.selectItems(ids, true);
} else {
self._librarySelectList.unselectItems(ids, true);
}
} else if (!bWasCustom) {
let bAllGroupsSelected = ids.every(function (id) {
return values.indexOf(id) !== -1;
});
if (bAllGroupsSelected) {
self._librarySelectList.selectItems("group_libraries", true);
} else {
self._librarySelectList.unselectItems("group_libraries", true);
}
}
});
};

View File

@ -107,7 +107,8 @@ InputField.prototype._createDOM = function () {
var fragment = document.createDocumentFragment();
fragment.appendChild(this._container);
this._container.className += " input-field-container input-field-container-" + this._id;
this._container.className +=
" input-field-container input-field-container-" + this._id;
var inputField = document.createElement("div");
this._container.appendChild(inputField);
@ -350,48 +351,45 @@ InputField.prototype.validate = function () {
};
InputField.prototype.updateValidationState = function () {
if (this._validationElement) {
if (!this.isValid) {
this._validationElement.textContent = this._validationMessage;
this._validationElement.style.display = "block";
if (!this.isValid) {
this._validationElement.textContent = this._validationMessage;
this._validationElement.style.display = "block";
var containerClasses = this._container.className.split(" ");
if (containerClasses.indexOf("input-field-invalid") === -1) {
this._container.className += " input-field-invalid";
}
this._container.className = this._container.className
.split(" ")
.filter(function (cls) {
return cls !== "input-field-valid";
})
.join(" ");
} else if (this.input.value.length > 0) {
this._validationElement.style.display = "none";
var containerClasses = this._container.className.split(" ");
if (containerClasses.indexOf("input-field-valid") === -1) {
this._container.className += " input-field-valid";
}
this._container.className = this._container.className
.split(" ")
.filter(function (cls) {
return cls !== "input-field-invalid";
})
.join(" ");
} else {
this._validationElement.style.display = "none";
this._container.className = this._container.className
.split(" ")
.filter(function (cls) {
return (
cls !== "input-field-valid" &&
cls !== "input-field-invalid"
);
})
.join(" ");
var containerClasses = this._container.className.split(" ");
if (containerClasses.indexOf("input-field-invalid") === -1) {
this._container.className += " input-field-invalid";
}
this._container.className = this._container.className
.split(" ")
.filter(function (cls) {
return cls !== "input-field-valid";
})
.join(" ");
} else if (this.input.value.length > 0) {
this._validationElement.style.display = "none";
var containerClasses = this._container.className.split(" ");
if (containerClasses.indexOf("input-field-valid") === -1) {
this._container.className += " input-field-valid";
}
this._container.className = this._container.className
.split(" ")
.filter(function (cls) {
return cls !== "input-field-invalid";
})
.join(" ");
} else {
this._validationElement.style.display = "none";
this._container.className = this._container.className
.split(" ")
.filter(function (cls) {
return (
cls !== "input-field-valid" && cls !== "input-field-invalid"
);
})
.join(" ");
}
};
@ -457,7 +455,7 @@ InputField.prototype.disable = function () {
};
/**
* @param {function(InputEventType): void} callback
* @param {function(InputEventType): void} callback
* @returns {Object}
*/
InputField.prototype.subscribe = function (callback) {

View File

@ -556,7 +556,10 @@ SelectBox.prototype.removeItem = function (value) {
this._updateSelectedText();
};
SelectBox.prototype.getValue = function () {
/**
* @return {null | string | Array<string>}
*/
SelectBox.prototype.getSelectedValues = function () {
if (this._options.multiple) {
return Array.from(this._selectedValues);
} else {
@ -568,7 +571,7 @@ SelectBox.prototype.getValue = function () {
/**
* @param {string | Array<string>} value
*/
SelectBox.prototype.setValue = function (value) {
SelectBox.prototype.setSelectedValues = function (value) {
if (this._options.multiple && Array.isArray(value)) {
this._selectedValues = new Set(value);
} else {

View File

@ -256,3 +256,20 @@ var Api = window.Api;
/**
* @typedef {Promise<FetchResponse>} FetchPromise
*/
/** ------------------------------------------------ */
/**
* @typedef {Object} ZoteroGroupInfo
* @property {number} id
* @property {number} version
* @property {CslJsonObjectLinks} meta
* @property {{created: string, lastModified: string, numItems: number}} links
* @property {{name: string, description: string, id: number, owner: number, type: string}} data
*/
/**
* @typedef {Object} UserGroupInfo
* @property {number|string} id
* @property {string} name
*/

View File

@ -21,21 +21,6 @@
/// <reference path="../types-global.js" />
/// <reference path="./zotero-environment.js" />
/**
* @typedef {Object} ZoteroGroupInfo
* @property {number} id
* @property {number} version
* @property {CslJsonObjectLinks} meta
* @property {{created: string, lastModified: string, numItems: number}} links
* @property {{name: string, description: string, id: number, owner: number, type: string}} data
*/
/**
* @typedef {Object} UserGroupInfo
* @property {number|string} id
* @property {string} name
*/
const ZoteroSdk = function () {
this._apiKey = null;
this._userId = 0;