src/components/widgets/Tree/until.js
import React from 'react';
import {
forEach,
keys,
upperCase,
filter,
eq,
omit,
isFunction,
get,
has,
uniqueId,
find,
some,
} from 'lodash';
import { KEY_CODES } from './component/constants';
import { findDOMNode } from 'react-dom';
import cssAnimation from 'css-animation';
import Icon from '../../snippets/Icon/Icon';
/**
* Создаем коллекцию из дерева tree -> [{ id: ..., parentId: ... }, ...]
* @param tree
* @param parentFieldId
* @param valueFieldId
*/
export const treeToCollection = (
tree,
{ parentFieldId, valueFieldId, childrenFieldId }
) => {
let buf = [...tree];
buf.forEach(el => {
if (el[childrenFieldId]) {
const elems = el[childrenFieldId].map(v => ({
...v,
[parentFieldId]: el[valueFieldId],
}));
buf.push(...elems);
} else {
buf.push(...el);
}
});
return buf.map(v => omit(v, [childrenFieldId]));
};
export const FILTER_MODE = ['includes', 'startsWith', 'endsWith'];
export const createRegExp = (searchText, filter) => {
let regExp;
if (eq(filter, 'includes')) {
regExp = new RegExp(searchText, 'i');
}
if (eq(filter, 'startsWith')) {
regExp = new RegExp(`^${searchText}`, 'i');
}
if (eq(filter, 'endsWith')) {
regExp = new RegExp(`${searchText}$`, 'i');
}
return regExp;
};
/**
* Превращаем коллекцию в обьект с ключами id и value Element
* [{ id: 1, ...}, { id: 2, ... }] => { 1: {...}, 2: {...} }
*/
export const collectionToComponentObject = (Component, props) => {
let buf = {};
const valueFieldId = get(props, 'valueFieldId');
const datasource = get(props, 'datasource');
const iconFieldId = get(props, 'iconFieldId');
if (valueFieldId && datasource) {
datasource.forEach(data => {
buf[data[valueFieldId]] = {
...data,
icon: has(data, iconFieldId) && (
<Icon key={uniqueId('tree_icon_')} name={data[iconFieldId]} />
),
key: data[valueFieldId],
title: React.createElement(Component, { data, ...props }),
children: [],
};
});
}
return buf;
};
export const createTreeFn = Component => props => {
const itemsByID = collectionToComponentObject(Component, props);
const parentFieldId = get(props, 'parentFieldId');
keys(itemsByID).forEach(key => {
const elem = itemsByID[key];
if (elem[parentFieldId] && itemsByID[elem[parentFieldId]]) {
itemsByID[elem[parentFieldId]].children.push({ ...elem });
}
});
let buf = [];
keys(itemsByID).forEach(key => {
if (!itemsByID[key][parentFieldId]) {
buf.push(itemsByID[key]);
}
});
return buf;
};
export const takeKeysWhenSearching = props => {
const filter = get(props, 'filter');
const value = get(props, 'value');
const datasource = get(props, 'datasource', []);
const valueFieldId = get(props, 'valueFieldId');
const labelFieldId = get(props, 'labelFieldId');
if (filter && FILTER_MODE.includes(filter) && value) {
const regExp = createRegExp(value, filter);
const filterFunc = searchStr => searchStr.search(regExp) + 1;
const expandedKeys = datasource
.filter(item => filterFunc(item[labelFieldId]))
.map(v => v[valueFieldId]);
return expandedKeys;
}
return [];
};
/**
* Вспомогогательная функция для клавиатуры
* Определяет путь по которому будет двигаться клавиатура
* возвращает массив из id
* @param data
* @param expandedKeys - открытые ключи
* @param parentFieldId
* @param valueFieldId
* @returns {Array}
*/
export const getTreeLinerRoute = (
data,
expandedKeys,
{ parentFieldId, valueFieldId }
) => {
//берем всех родителей
const parenIds = filter(data, dt => !dt[parentFieldId] && !dt.disabled).map(
dt => dt[valueFieldId]
);
let buff = [];
// рекурсивно спускаемся вниз ко всем потомкам
// и если потомки есть в expandedKeys то добавляем в буфер
const recursionFn = ids =>
forEach(ids, id => {
buff.push(id);
if (expandedKeys.includes(id)) {
const childs = filter(data, dt => {
return dt[parentFieldId] === id && !dt.disabled;
}).map(dt => dt[valueFieldId]);
if (childs) {
recursionFn(childs);
}
}
});
recursionFn(parenIds);
return buff;
};
///Key base fns
const down = (focusedElement, route, node) => {
if (eq(focusedElement.className, 'hotkey') || focusedElement) {
const child = node.querySelector(`.cls-${route[0]}`);
child.focus();
}
if (focusedElement.dataset.id) {
const id = focusedElement.dataset.id;
const inx = route.indexOf(id);
if (route.length > inx + 1) {
const child = node.querySelector(`.cls-${route[inx + 1]}`);
child.focus();
}
}
};
const up = (focusedElement, route, node) => {
if (focusedElement.dataset.id) {
const id = focusedElement.dataset.id;
const inx = route.indexOf(id);
if (inx - 1 >= 0) {
const child = node.querySelector(`.cls-${route[inx - 1]}`);
child.focus();
}
}
};
const select = (focusedElement, node) => {
if (focusedElement.dataset.id) {
const id = focusedElement.dataset.id;
const child = node.querySelector(`.cls-${id}`);
child.click();
}
};
const toggle = (focusedElement, node, { prefixCls }) => {
if (focusedElement.dataset.id) {
const id = focusedElement.dataset.id;
const child = node
.querySelector(`.cls-${id}`)
.closest('li')
.querySelector(`.${prefixCls}-switcher`);
child.click();
}
};
const checked = (focusedElement, node, { prefixCls }) => {
if (focusedElement.dataset.id) {
const id = focusedElement.dataset.id;
const child = node
.querySelector(`.cls-${id}`)
.closest('li')
.querySelector(`.${prefixCls}-checkbox`);
child && child.click();
}
};
/// end base fns
export const customTreeActions = ({
key,
treeRef,
datasource,
expandedKeys,
prefixCls,
valueFieldId,
parentFieldId,
hasCheckboxes,
}) => {
const node = findDOMNode(treeRef.current);
const route = getTreeLinerRoute(datasource, expandedKeys, {
valueFieldId,
parentFieldId,
});
const focusedElement = document.activeElement;
const isParent = id => some(datasource, [parentFieldId, id]);
const isRootParent = id => {
const elem = find(datasource, [valueFieldId, id]);
if (
elem &&
!has(elem, parentFieldId) &&
!find(datasource, [parentFieldId, id])
) {
return true;
}
return false;
};
if (eq(key, KEY_CODES.KEY_DOWN)) {
down(focusedElement, route, node);
}
if (eq(key, KEY_CODES.KEY_UP)) {
up(focusedElement, route, node);
}
if (eq(key, KEY_CODES.KEY_SPACE)) {
select(focusedElement, node);
}
if (eq(key, KEY_CODES.CTRL_ENTER)) {
checked(focusedElement, node, { prefixCls });
}
if (eq(key, KEY_CODES.RIGHT)) {
const id = focusedElement.dataset.id;
if (
!expandedKeys.includes(id) &&
isParent(id) &&
!isRootParent(id) &&
route.includes(id)
) {
toggle(focusedElement, node, { prefixCls });
} else {
down(focusedElement, route, node);
}
}
if (eq(key, KEY_CODES.LEFT)) {
const id = focusedElement.dataset.id;
if (
expandedKeys.includes(id) &&
isParent(id) &&
!isRootParent(id) &&
route.includes(id)
) {
toggle(focusedElement, node, { prefixCls });
} else {
up(focusedElement, route, node);
}
}
if (eq(key, 'DB_CLICK')) {
toggle(focusedElement, node, { prefixCls });
}
return false;
};
export const splitSearchText = (text, searchText, filter) => {
if (FILTER_MODE.includes(filter)) {
const regExp = createRegExp(searchText, filter);
const html = text.replace(
regExp,
str => `<span class='search-text'>${str}</span>`
);
return <span dangerouslySetInnerHTML={{ __html: html }} />;
}
return text;
};
const animate = (node, show, done) => {
let height = node.offsetHeight;
return cssAnimation(node, 'collapse', {
start() {
if (!show) {
node.style.height = `${node.offsetHeight}px`;
} else {
height = node.offsetHeight;
node.style.height = 0;
}
},
active() {
node.style.height = `${show ? height : 0}px`;
},
end() {
node.style.height = '';
done();
},
});
};
export const animationTree = {
enter(node, done) {
return animate(node, true, done);
},
leave(node, done) {
return animate(node, false, done);
},
};
export const singleDoubleClickFilter = (
singleCallback,
doubleCallback,
timeout
) => {
let timer = 0;
return (...args) => {
timer++;
if (timer === 1) {
setTimeout(() => {
if (timer === 1) {
singleCallback && singleCallback(...args);
} else {
doubleCallback && doubleCallback(...args);
}
timer = 0;
}, timeout || 100);
}
};
};