Commit c98e96f2 authored by Samir Sadyhov's avatar Samir Sadyhov 🤔

новый компонент меню с кнопкой создать

parent 08f3df70
this.MenuSearch = class {
constructor(menuItems, options = {}) {
this.locale = options.locale || AS?.OPTIONS?.locale || 'ru';
this.mode = options.mode || 'OR';
this.delay = options.delay || 250;
this.indexedMenu = this.buildIndex(menuItems);
this.searchDebounced = this.debounce(
this.search.bind(this),
this.delay
);
}
set filterCondition(mode) {
if (mode === 'AND' || mode === 'OR') this.mode = mode;
}
get filterCondition() {
return this.mode;
}
normalize(str = '') {
return str
.toLowerCase()
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
.replace(/\s+/g, ' ')
.trim();
}
getLocalizedTitle(item) {
const t = item.translations?.find(t => t.locale === this.locale);
return t?.value || item.title || '';
}
buildIndex(items) {
return items.map(item => {
const indexed = {
...item,
_searchIndex: this.normalize(
`${this.getLocalizedTitle(item)} ${item.code || ''}`
)
};
if (item.is_group && item.items) {
indexed.items = this.buildIndex(item.items);
}
return indexed;
});
}
calcScore(index, words, mode) {
let score = 0;
for (const w of words) {
if (index.includes(` ${w} `)) score += 2;
if (index.startsWith(w) || index.includes(` ${w}`)) score += 3;
}
if (mode === 'AND' && words.every(w => index.includes(w))) {
score += 5;
}
if (index.length < 40) score += 1;
return score;
}
search(query, modeOverride) {
if (!query) return [];
const mode = modeOverride || this.mode;
const words = this.normalize(query).split(' ');
const result = [];
const match =
mode === 'AND'
? index => words.every(w => index.includes(w))
: index => words.some(w => index.includes(w));
const walk = (list) => {
for (const item of list) {
if (!item.is_group && match(item._searchIndex)) {
result.push({
...item,
_score: this.calcScore(item._searchIndex, words, mode)
});
}
if (item.is_group && item.items) {
walk(item.items);
}
}
};
walk(this.indexedMenu);
return result.sort((a, b) => b._score - a._score);
}
debounce(fn, delay) {
let t;
return (query, mode) => {
clearTimeout(t);
t = setTimeout(() => fn(query, mode), delay);
};
}
}
/*
const search = new MenuSearch(menuItems, { mode: 'OR' });
search.setMode('AND');
const result = search.search('заявление кжс');
search.search('заявление кжс', 'OR');
search.search('заявление кжс', 'AND');
modeToggle.addEventListener('change', e => {
search.setMode(e.target.value);
});
const result = search.search('отпуск учебный');
console.log(result);
search.searchDebounced = search.debounce((value) => {
const result = search.search(value);
console.log(result);
}, 250);
searchInput.addEventListener('input', e => {
search.searchDebounced(e.target.value);
});
*/
\ No newline at end of file
.qam_buttons_group {
display: flex;
}
.qam_buttons_group button {
border: 1px solid #1890FF;
background: #1890FF;
color: #fff;
cursor: pointer;
font-weight: bold;
transition: .3s;
}
.qam_buttons_group button:first-child {
border-radius: 6px 0 0 6px;
padding: 5px 20px;
}
.qam_buttons_group button:last-child {
border-radius: 0 6px 6px 0;
border-left: 1px solid #64abf3;
padding: 0 10px;
}
.qam_buttons_group button:hover {
background-color: #49a7ff;
border-color: #49a7ff;
}
.qam_menu_container {
position: fixed;
display: none;
background: rgba(255,255,255, 0.8);
width: 100vw;
height: 100vh;
z-index: 1;
left: 0;
top: 0;
overflow: hidden;
}
.qam_menu_container.active {
display: block;
}
.qam_menu_container_header {
border-bottom: 1px solid #c4c4c4;
background: #fff;
display: flex;
position: absolute;
width: 100%;
height: 50px;
left: 0;
top: 0;
justify-content: space-between;
align-items: center;
padding: 0 20px;
gap: 20px;
}
.qam_menu_container_header_left {
display: flex;
width: 100%;
height: 100%;
justify-content: flex-start;
align-items: center;
}
.qam_menu_container_header_right {
display: flex;
width: 100%;
height: 100%;
justify-content: flex-end;
align-items: center;
gap: 20px;
}
.qam_menu_container_content {
display: flex;
position: absolute;
width: 100%;
height: calc(100% - 50px);
left: 0;
top: 50px;
overflow: auto;
}
.qam_favorites_modal_container {
display: flex;
flex-direction: column;
gap: 10px;
margin: 15px 20px;
overflow: hidden;
}
.qam_favorites_items_container {
width: 100%;
min-height: 100px;
max-height: 400px;
border: 1px solid #cfcfcf;
overflow: auto;
}
.qam_favorites_buttons {
display: flex;
justify-content: center;
border-top: 1px solid #cfcfcf;
padding-top: 10px;
}
.qam_favorites_button {
display: flex;
justify-content: center;
border: 1px solid #cfcfcf;
padding: 10px 15px;
background: #fff;
border-radius: 6px;
cursor: pointer;
transition: .2s;
color: #222;
}
.qam_favorites_button:hover {
opacity: .7;
}
.qam_favorites_items_container>div {
width: 100%;
display: flex;
gap: 10px;
padding: 5px 10px;
user-select: none;
transition: 0.3s;
color: #333;
font-size: 14px;
}
.qam_favorites_items_container>div:nth-child(2n) {
background: #fbfbfb;
}
.qam_favorites_items_container>div:hover {
background: #E6F7FF;
}
.qam_favorites_items_container .favorites_group {
font-weight: bold;
padding-left: 40px;
}
.qam_favorites_items_container .favorites_item_container.selected {
background: #E6F7FF;
}
.favorites_icon {
cursor: pointer;
transition: .3s;
}
.favorites_icon[is_bookmark = "true"] {
color: #d8cb0f;
}
.favorites_icon[is_bookmark = "true"] polygon {
fill: #ffef00;
}
.favorites_icon[is_bookmark = "true"]:hover {
color: #333;
}
.favorites_icon[is_bookmark = "true"]:hover polygon {
fill: #fff;
}
.favorites_icon[is_bookmark = "false"]:hover {
color: #d8cb0f;
}
.favorites_icon[is_bookmark = "false"]:hover polygon {
fill: #d8cb0f;
}
.qam_menu_close_button {
width: 25px;
height: 25px;
cursor: pointer;
user-select: none;
transition: 0.3s ease-out 0s;
}
.qam_menu_close_button:hover {
transform: rotate(90deg);
}
.qam_menu_close_button:hover svg > line {
stroke: #f0506e;
}
#qam_input_search {
min-width: 450px;
border-radius: 3px;
}
.qam_filter_select_search {
border-radius: 3px;
}
.qam_item_list_container {
width: 100%;
background: #fff;
}
.qam_item_list_container .qam_item_list_item {
padding: 15px;
cursor: pointer;
transition: .2s;
}
.qam_item_list_container .qam_item_list_item:hover {
background: #E6F7FF;
}
.qam_menu_list {
min-width: 200px;
max-width: 350px;
border-right: 1px solid #eee;
background: #fff;
overflow-x: auto;
height: 100%;
}
.qam_menu_item {
padding: 8px 12px;
cursor: pointer;
user-select: none;
transition: .2s;
}
.qam_menu_item:hover {
background: #E6F7FF;
}
.qam_menu_item_group::after {
content: "›";
float: right;
opacity: .6;
}
.qam_menu_item_group.active {
background: #E6F7FF;
}
.uk-list-striped>li.qam_item_list_item:nth-child(even) {
background: #fff;
}
const createRegistryRow = async registryID => {
Cons.showLoader();
try {
const regInfo = await appAPI.getRegistryInfoByID(registryID);
if(!regInfo) throw new Error(i18n.tr('При создании записи произошла ошибка. Не удалось плучить информацию по реестру.'));
if(regInfo.rr_create != "Y") throw new Error(i18n.tr('У вас нет прав на создание записи'));
const doc = await appAPI.createDoc(regInfo.code);
if(!doc) throw new Error(i18n.tr('Произошла ошибка при создании данного типа документа'));
Cons.hideLoader();
const eventParam = {
type: 'document',
documentID: doc.documentID,
editable: true,
};
$('#root-panel').trigger({type: 'custom_open_document', eventParam});
} catch (err) {
showMessage(err.message, 'error');
Cons.hideLoader();
}
}
const checkEmpty = items => {
let result = true;
const s = arr => {
arr.forEach(x => {
if(x.is_bookmark == 1) result = false;
if(x.is_group == 1) s(x.items);
});
}
s(items);
return result;
}
const getItemTitle = item => {
const title = item.translations.find(x => x.locale == AS.OPTIONS.locale);
return title ? title?.value : item.title;
}
const getFavoritesModal = body => {
const dialog = $('<div class="uk-flex-top" uk-modal="bg-close: false; esc-close: false;">');
const md = $('<div class="uk-modal-dialog uk-margin-auto-vertical">');
md.append(`<button class="uk-modal-close-default modal-close-custom" type="button" uk-close></button>`,
`<div class="uk-modal-header"><h3>${i18n.tr("Избранное")}</h3></div>`, body);
dialog.append(md);
return dialog;
}
const getSearchComponent = () => {
const container = $('<div>', {class: "uk-inline", style: "width: 100%"});
const icon = $(`<a class="uk-form-icon uk-form-icon-flip" href="#" uk-icon="icon: search"></a>`);
const input = $(`<input class="uk-input" type="text">`);
container.append(icon, input)
return {container, icon, input};
}
const favoritesInit = (items, successHandler) => {
let selected = null;
let itemsIsChange = false;
const favoritesContainer = $('<div>', {class: "qam_favorites_modal_container"});
const search = getSearchComponent();
const itemsContainer = $('<div>', {class: "qam_favorites_items_container"});
const buttonsContainer = $('<div>', {class: "qam_favorites_buttons"});
const buttonApply = $('<button>', {
class: "uk-button uk-button-primary",
style: "border-radius: 3px;",
disabled: true
});
const modal = getFavoritesModal(favoritesContainer);
const renderFavoritesItem = listItems => {
for(let i = 0; i < listItems.length; i++) {
const item = listItems[i];
const itemTitle = getItemTitle(item);
if(item.is_group == 1) {
const d = $('<div>', {class: "favorites_group"});
d.text(itemTitle);
d.attr('menu_itemID', item.menu_itemID);
itemsContainer.append(d);
renderFavoritesItem(item.items);
} else {
if(itemsContainer.find(`[menu_itemid="${item.menu_itemID}"]`).length) continue;
const d = $('<div>', {class: "favorites_item_container"});
const itemIcon = $('<div>', {class: "favorites_icon"});
const itemTitle = $('<div>', {class: "favorites_title"});
if(item.is_bookmark == 1) {
itemIcon.attr('is_bookmark', true);
} else {
itemIcon.attr('is_bookmark', false);
}
d.attr('menu_itemID', item.menu_itemID);
itemTitle.text(itemTitle);
itemIcon.append(`<span uk-icon="icon: star"></span>`);
d.append(itemIcon, itemTitle);
itemsContainer.append(d);
d.on('click', e => {
buttonApply.attr('disabled', false);
$('.favorites_item_container').removeClass('selected');
d.addClass('selected');
selected = item;
});
itemIcon.on('click', async e => {
try {
const is_bookmark = item.is_bookmark == 0 ? 1 : 0;
const setResult = await appAPI.setCreateMenuItems([{
itemID: item.menu_itemID, is_bookmark
}]);
if(!setResult || setResult?.errorCode !== 0) throw new Error('Произошла ошибка изменения списка избранного');
itemsIsChange = true;
item.is_bookmark = is_bookmark;
if(is_bookmark == 1) {
itemIcon.attr('is_bookmark', true);
} else {
itemIcon.attr('is_bookmark', false);
}
} catch (err) {
showMessage(i18n.tr(err.message), 'error');
}
});
}
}
}
const searchItem = () => {
$.each(itemsContainer.find(".favorites_item_container"), function() {
if($(this).text().toLowerCase().indexOf(search.input.val().toLowerCase()) === -1) $(this).hide();
else $(this).show();
});
}
buttonApply.text(i18n.tr('Выбрать'));
buttonsContainer.append(buttonApply);
favoritesContainer.append(search.container, itemsContainer, buttonsContainer);
renderFavoritesItem(items);
search.input.on('keyup', e => {
searchItem();
});
search.icon.on('click', e => {
searchItem();
});
buttonApply.on('click', e => {
UIkit.modal(modal).hide();
if(selected) createRegistryRow(selected.registryID);
});
UIkit.modal(modal).show();
modal.on('hidden', () => {
if(itemsIsChange && successHandler && typeof successHandler === 'function') successHandler();
modal.remove();
});
$('#root-panel').on('change_locale', e => {
buttonApply.text(i18n.tr('Выбрать'));
});
}
this.QuickAccessMenu = class {
constructor(buttonContainerSelector, defaultHandler, defaultHandlerTitle, addMenuItems) {
this.buttonContainerSelector = buttonContainerSelector;
this.defaultHandler = defaultHandler;
this.defaultHandlerTitle = defaultHandlerTitle;
this.addMenuItems = addMenuItems;
this.init();
}
addButtonHandler(buttonCreate, buttonMenu){
buttonCreate.off().on('click', e => {
e.preventDefault();
if(this.defaultHandler && typeof this.defaultHandler === 'function') {
this.defaultHandler();
}
});
buttonMenu.off().on('click', e => {
e.preventDefault();
this.openMenuWindow();
console.log('show menu');
});
}
renderButton(){
const buttonsGroup = $('<div>', {class: 'qam_buttons_group'});
const buttonCreate = $('<button>', {class: 'qam_button_create'});
const buttonMenu = $('<button>', {class: 'qam_button_menu'});
buttonCreate.text(i18n.tr('Создать'));
buttonMenu.text('');
buttonsGroup.append(buttonCreate, buttonMenu);
this.buttonContainer.empty().append(buttonsGroup);
this.addButtonHandler(buttonCreate, buttonMenu);
$('#root-panel').on('change_locale', e => {
buttonCreate.text(i18n.tr('Создать'));
});
}
getCloseButton(){
const button = $('<div>', {class: 'qam_menu_close_button'});
button.append(`<svg width="25" height="25" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" data-svg="close-large"><line fill="none" stroke="#666" stroke-width="1.4" x1="1" y1="1" x2="19" y2="19"></line><line fill="none" stroke="#666" stroke-width="1.4" x1="19" y1="1" x2="1" y2="19"></line></svg>`);
return button;
}
openMenuWindow(){
this.menuContainer.addClass('active');
this.renderMenuItems();
}
closeMenuWindow(){
this.menuContainer.removeClass('active');
this.contentContainer.empty();
this.inputSearch.val(null);
}
getSearchInput(){
const container = $('<div>');
const inline = $('<div>', {class: 'uk-inline'});
const input = $('<input>', {class: 'uk-input', id: 'qam_input_search', type: 'text', placeholder: `${i18n.tr('Поиск')}...`});
const icon = $('<span>', {class: 'uk-form-icon uk-form-icon-flip', 'uk-icon': 'icon: search'});
inline.append(icon, input);
container.append(inline);
$('#root-panel').on('change_locale', e => {
input.attr('placeholder', `${i18n.tr('Поиск')}...`);
});
return {container, input};
}
renderSearchItems(items) {
this.contentContainer.empty();
const itemsContainer = $('<ul>', {class: 'qam_item_list_container uk-list uk-list-striped'});
items.forEach(item => {
const listItem = $('<li>', {class: 'qam_item_list_item'});
const title = item.translations.find(x => x.locale == AS.OPTIONS.locale);
listItem.text(getItemTitle(item));
listItem.on('click', () => {
this.closeMenuWindow();
createRegistryRow(item.registryID);
});
itemsContainer.append(listItem);
});
this.contentContainer.append(itemsContainer);
}
renderMenuItems() {
this.contentContainer.empty();
const render = (items, level = 0) => {
const menuList = $('<div>', {
class: 'qam_menu_list',
'data-level': level
});
items.forEach(item => {
const titleValue = getItemTitle(item);
if (item.is_group == 1 && !checkEmpty(item.items)) {
const groupItem = $('<div>', {
class: 'qam_menu_item qam_menu_item_group',
text: titleValue
});
groupItem.on('click', e => {
e.preventDefault();
e.stopPropagation();
const isActive = groupItem.hasClass('active');
menuList.find('.qam_menu_item_group').removeClass('active');
this.contentContainer
.find('.qam_menu_list')
.filter((_, el) => Number($(el).data('level')) > level)
.remove();
if (isActive) return;
groupItem.addClass('active');
render(item.items, level + 1);
});
menuList.append(groupItem);
} else if (item.is_group == 0 && item.is_bookmark == 1) {
const menuItem = $('<div>', {
class: 'qam_menu_item',
text: titleValue
});
menuItem.on('click', e => {
e.preventDefault();
e.stopPropagation();
this.closeMenuWindow();
createRegistryRow(item.registryID);
});
menuList.append(menuItem);
}
});
this.contentContainer.append(menuList);
};
render(this.items, 0);
}
renderMenuWindow(){
this.menuContainer = $('<div>', {class: 'qam_menu_container'});
this.contentContainer =$('<div>', {class: 'qam_menu_container_content'});
const header = $('<div>', {class: 'qam_menu_container_header'});
const headerLeftPanel = $('<div>', {class: 'qam_menu_container_header_left'});
const headerRightPanel = $('<div>', {class: 'qam_menu_container_header_right'});
const closeButton = this.getCloseButton();
const favoritesButton = $('<button>', {class: 'qam_favorites_button'});
favoritesButton.text(i18n.tr('Ещё...'));
const {container: containerSearch, input: inputSearch} = this.getSearchInput();
this.inputSearch = inputSearch;
inputSearch.on('input', e => {
const searchValue = e.target.value;
if(searchValue && searchValue != '') {
this._search.searchDebounced(searchValue);
} else {
this.renderMenuItems();
}
});
const searchFilterItems = [{title: 'Любое совпадение слов', value: 'OR'}, {title: 'Содержатся все слова', value: 'AND'}];
const selectFilterSearch = new CustomSelect({items: searchFilterItems, defaultValue: this._search.filterCondition, classList: 'qam_filter_select_search'});
const selectFilterLabel = $('<span>', {class: 'uk-text-bolder'});
selectFilterLabel.text(i18n.tr('Условие поиска:'));
selectFilterSearch.container.on('selected', e => {
this._search.filterCondition = e.eventParam.value;
const searchValue = inputSearch.val();
if(searchValue && searchValue != '') {
this._search.searchDebounced(searchValue);
}
});
favoritesButton.on('click', e => {
e.preventDefault();
this.closeMenuWindow();
favoritesInit(this.items, () => {
this.updateItems();
});
});
closeButton.on('click', e => {
e.preventDefault();
this.closeMenuWindow();
});
this.contentContainer.on('click', () => {
this.contentContainer
.find('.qam_menu_list')
.filter((_, el) => Number($(el).data('level')) > 0)
.remove();
this.contentContainer.find('.qam_menu_item_group.active').removeClass('active');
});
headerLeftPanel.append(favoritesButton);
headerRightPanel.append(containerSearch, selectFilterLabel, selectFilterSearch.container, `<hr class="uk-divider-vertical" style="height: 30px;">`, closeButton);
header.append(headerLeftPanel, headerRightPanel);
this.menuContainer.append(header, this.contentContainer);
$('body').append(this.menuContainer);
this.renderMenuItems();
$('#root-panel').on('change_locale', e => {
favoritesButton.text(i18n.tr('Еще...'));
selectFilterLabel.text(i18n.tr('Условие поиска:'));
});
}
async updateItems(){
this.items = await appAPI.getCreateMenuItems();
this._search = new MenuSearch(this.items);
}
async init(){
try {
if(!this.buttonContainerSelector) throw new Error('Не передан селектор контейнера для отрисовки кнопки "Создать"');
this.buttonContainer = $(this.buttonContainerSelector);
await this.updateItems();
this._search.searchDebounced = this._search.debounce((value) => {
const result = this._search.search(value);
this.renderSearchItems(result);
}, 500);
this.renderButton();
this.renderMenuWindow();
} catch (err) {
console.log(err.message);
}
}
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment